This commit is contained in:
Phuoc Nguyen
2025-10-17 17:49:01 +07:00
parent 628c81ce13
commit 57bf73e4d1
23 changed files with 2655 additions and 87 deletions

View File

@@ -0,0 +1,169 @@
/// Domain Entity: Member Card
///
/// Represents a member card with tier information, points, and QR code data.
/// This entity is used across the home feature to display membership information.
///
/// Tiers: Diamond, Platinum, Gold
///
/// This is a pure domain entity with no external dependencies.
library;
/// Member tier enum
enum MemberTier {
/// Diamond tier - highest tier
diamond,
/// Platinum tier - middle tier
platinum,
/// Gold tier - entry tier
gold;
/// Get display name for tier
String get displayName {
switch (this) {
case MemberTier.diamond:
return 'DIAMOND';
case MemberTier.platinum:
return 'PLATINUM';
case MemberTier.gold:
return 'GOLD';
}
}
}
/// Member type enum
enum MemberType {
/// Architect member type
architect,
/// Contractor member type
contractor,
/// Distributor member type
distributor,
/// Broker member type
broker;
/// Get display name for member type
String get displayName {
switch (this) {
case MemberType.architect:
return 'ARCHITECT MEMBERSHIP';
case MemberType.contractor:
return 'CONTRACTOR MEMBERSHIP';
case MemberType.distributor:
return 'DISTRIBUTOR MEMBERSHIP';
case MemberType.broker:
return 'BROKER MEMBERSHIP';
}
}
}
/// Member Card Entity
///
/// Contains all information needed to display a member card:
/// - Personal info (name, member ID)
/// - Membership details (tier, type, valid until)
/// - Loyalty points
/// - QR code data for scanning
class MemberCard {
/// Unique member ID
final String memberId;
/// Member's full name
final String name;
/// Member type (Architect, Contractor, etc.)
final MemberType memberType;
/// Current membership tier
final MemberTier tier;
/// Current loyalty points balance
final int points;
/// Card expiration date
final DateTime validUntil;
/// QR code data (typically member ID or encoded data)
final String qrData;
/// Constructor
const MemberCard({
required this.memberId,
required this.name,
required this.memberType,
required this.tier,
required this.points,
required this.validUntil,
required this.qrData,
});
/// Check if card is expired
bool get isExpired => DateTime.now().isAfter(validUntil);
/// Check if card is expiring soon (within 30 days)
bool get isExpiringSoon {
final daysUntilExpiry = validUntil.difference(DateTime.now()).inDays;
return daysUntilExpiry > 0 && daysUntilExpiry <= 30;
}
/// Copy with method for immutability
MemberCard copyWith({
String? memberId,
String? name,
MemberType? memberType,
MemberTier? tier,
int? points,
DateTime? validUntil,
String? qrData,
}) {
return MemberCard(
memberId: memberId ?? this.memberId,
name: name ?? this.name,
memberType: memberType ?? this.memberType,
tier: tier ?? this.tier,
points: points ?? this.points,
validUntil: validUntil ?? this.validUntil,
qrData: qrData ?? this.qrData,
);
}
/// Equality operator
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is MemberCard &&
other.memberId == memberId &&
other.name == name &&
other.memberType == memberType &&
other.tier == tier &&
other.points == points &&
other.validUntil == validUntil &&
other.qrData == qrData;
}
/// Hash code
@override
int get hashCode {
return Object.hash(
memberId,
name,
memberType,
tier,
points,
validUntil,
qrData,
);
}
/// String representation
@override
String toString() {
return 'MemberCard(memberId: $memberId, name: $name, memberType: $memberType, '
'tier: $tier, points: $points, validUntil: $validUntil, qrData: $qrData)';
}
}

View File

@@ -0,0 +1,176 @@
/// Domain Entity: Promotion
///
/// Represents a promotional offer or campaign displayed on the home screen.
/// This entity contains all information needed to display promotion banners.
///
/// This is a pure domain entity with no external dependencies.
library;
/// Promotion status enum
enum PromotionStatus {
/// Currently active promotion
active,
/// Promotion starting in the future
upcoming,
/// Expired promotion
expired;
}
/// Promotion Entity
///
/// Contains all information needed to display a promotion:
/// - Basic info (title, description)
/// - Visual assets (image URL)
/// - Validity period
/// - Optional discount information
class Promotion {
/// Unique promotion ID
final String id;
/// Promotion title
final String title;
/// Detailed description
final String description;
/// Banner/cover image URL
final String imageUrl;
/// Promotion start date
final DateTime startDate;
/// Promotion end date
final DateTime endDate;
/// Optional discount percentage (e.g., 30 for 30%)
final int? discountPercentage;
/// Optional discount amount
final double? discountAmount;
/// Optional terms and conditions
final String? terms;
/// Optional link to detailed page
final String? detailsUrl;
/// Constructor
const Promotion({
required this.id,
required this.title,
required this.description,
required this.imageUrl,
required this.startDate,
required this.endDate,
this.discountPercentage,
this.discountAmount,
this.terms,
this.detailsUrl,
});
/// Get current promotion status
PromotionStatus get status {
final now = DateTime.now();
if (now.isBefore(startDate)) {
return PromotionStatus.upcoming;
} else if (now.isAfter(endDate)) {
return PromotionStatus.expired;
} else {
return PromotionStatus.active;
}
}
/// Check if promotion is currently active
bool get isActive => status == PromotionStatus.active;
/// Get days remaining until expiry (null if expired or upcoming)
int? get daysRemaining {
if (status != PromotionStatus.active) return null;
return endDate.difference(DateTime.now()).inDays;
}
/// Format discount display text
String? get discountText {
if (discountPercentage != null) {
return 'Giảm $discountPercentage%';
} else if (discountAmount != null) {
return 'Giảm ${discountAmount?.toStringAsFixed(0) ?? '0'}';
}
return null;
}
/// Copy with method for immutability
Promotion copyWith({
String? id,
String? title,
String? description,
String? imageUrl,
DateTime? startDate,
DateTime? endDate,
int? discountPercentage,
double? discountAmount,
String? terms,
String? detailsUrl,
}) {
return Promotion(
id: id ?? this.id,
title: title ?? this.title,
description: description ?? this.description,
imageUrl: imageUrl ?? this.imageUrl,
startDate: startDate ?? this.startDate,
endDate: endDate ?? this.endDate,
discountPercentage: discountPercentage ?? this.discountPercentage,
discountAmount: discountAmount ?? this.discountAmount,
terms: terms ?? this.terms,
detailsUrl: detailsUrl ?? this.detailsUrl,
);
}
/// Equality operator
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is Promotion &&
other.id == id &&
other.title == title &&
other.description == description &&
other.imageUrl == imageUrl &&
other.startDate == startDate &&
other.endDate == endDate &&
other.discountPercentage == discountPercentage &&
other.discountAmount == discountAmount &&
other.terms == terms &&
other.detailsUrl == detailsUrl;
}
/// Hash code
@override
int get hashCode {
return Object.hash(
id,
title,
description,
imageUrl,
startDate,
endDate,
discountPercentage,
discountAmount,
terms,
detailsUrl,
);
}
/// String representation
@override
String toString() {
return 'Promotion(id: $id, title: $title, description: $description, '
'imageUrl: $imageUrl, startDate: $startDate, endDate: $endDate, '
'discountPercentage: $discountPercentage, discountAmount: $discountAmount, '
'terms: $terms, detailsUrl: $detailsUrl)';
}
}

View File

@@ -0,0 +1,49 @@
/// Domain Repository Interface: Home Repository
///
/// Defines the contract for home-related data operations.
/// This is an abstract interface following the Repository Pattern.
///
/// The actual implementation will be in the data layer.
/// This allows for dependency inversion and easier testing.
library;
import 'package:worker/features/home/domain/entities/member_card.dart';
import 'package:worker/features/home/domain/entities/promotion.dart';
/// Home Repository Interface
///
/// Provides methods to:
/// - Get member card information
/// - Fetch active promotions
///
/// Implementation will be in data/repositories/home_repository_impl.dart
abstract class HomeRepository {
/// Get the current user's member card
///
/// Returns [MemberCard] with user's membership information.
/// Throws exception if user not authenticated or data unavailable.
///
/// This should fetch from local cache first (Hive), then sync with server.
Future<MemberCard> getMemberCard();
/// Get list of active promotions
///
/// Returns list of [Promotion] objects that are currently active.
/// Returns empty list if no promotions available.
///
/// This should fetch from local cache first, then sync with server.
/// Promotions should be ordered by priority/start date.
Future<List<Promotion>> getPromotions();
/// Refresh member card data from server
///
/// Force refresh member card information from remote source.
/// Updates local cache after successful fetch.
Future<MemberCard> refreshMemberCard();
/// Refresh promotions from server
///
/// Force refresh promotions from remote source.
/// Updates local cache after successful fetch.
Future<List<Promotion>> refreshPromotions();
}

View File

@@ -0,0 +1,62 @@
/// Use Case: Get Member Card
///
/// Retrieves the current user's member card information.
/// This use case encapsulates the business logic for fetching member card data.
///
/// Follows the Single Responsibility Principle - one use case, one operation.
library;
import 'package:worker/features/home/domain/entities/member_card.dart';
import 'package:worker/features/home/domain/repositories/home_repository.dart';
/// Get Member Card Use Case
///
/// Fetches the current authenticated user's member card.
///
/// Usage:
/// ```dart
/// final memberCard = await getMemberCard();
/// ```
///
/// This use case:
/// - Retrieves member card from repository (which handles caching)
/// - Can add business logic if needed (e.g., validation, transformation)
/// - Returns MemberCard entity
class GetMemberCard {
/// Home repository instance
final HomeRepository repository;
/// Constructor
const GetMemberCard(this.repository);
/// Execute the use case
///
/// Returns [MemberCard] with user's membership information.
///
/// Throws:
/// - [UnauthorizedException] if user not authenticated
/// - [NetworkException] if network error occurs
/// - [CacheException] if local data corrupted
Future<MemberCard> call() async {
// TODO: Add business logic here if needed
// For example:
// - Validate user authentication
// - Check if card is expired and handle accordingly
// - Transform data if needed
// - Log analytics events
return await repository.getMemberCard();
}
/// Execute with force refresh
///
/// Forces a refresh from the server instead of using cached data.
///
/// Use this when:
/// - User explicitly pulls to refresh
/// - Cached data is known to be stale
/// - After points redemption or other updates
Future<MemberCard> refresh() async {
return await repository.refreshMemberCard();
}
}

View File

@@ -0,0 +1,84 @@
/// Use Case: Get Promotions
///
/// Retrieves the list of active promotional offers.
/// This use case encapsulates the business logic for fetching promotions.
///
/// Follows the Single Responsibility Principle - one use case, one operation.
library;
import 'package:worker/features/home/domain/entities/promotion.dart';
import 'package:worker/features/home/domain/repositories/home_repository.dart';
/// Get Promotions Use Case
///
/// Fetches active promotional offers to display on home screen.
///
/// Usage:
/// ```dart
/// final promotions = await getPromotions();
/// ```
///
/// This use case:
/// - Retrieves promotions from repository (which handles caching)
/// - Filters only active promotions
/// - Sorts by priority/date
/// - Returns list of Promotion entities
class GetPromotions {
/// Home repository instance
final HomeRepository repository;
/// Constructor
const GetPromotions(this.repository);
/// Execute the use case
///
/// Returns list of active [Promotion] objects.
/// Returns empty list if no promotions available.
///
/// Throws:
/// - [NetworkException] if network error occurs
/// - [CacheException] if local data corrupted
Future<List<Promotion>> call() async {
// Fetch promotions from repository
final promotions = await repository.getPromotions();
// TODO: Add business logic here if needed
// For example:
// - Filter only active promotions
// - Sort by priority or start date
// - Filter by user tier (Diamond exclusive promotions)
// - Add personalization logic
// - Log analytics events
// Filter only active promotions
final activePromotions = promotions
.where((promotion) => promotion.isActive)
.toList();
// Sort by start date (newest first)
activePromotions.sort((a, b) => b.startDate.compareTo(a.startDate));
return activePromotions;
}
/// Execute with force refresh
///
/// Forces a refresh from the server instead of using cached data.
///
/// Use this when:
/// - User explicitly pulls to refresh
/// - Cached data is known to be stale
/// - Need to show latest promotions immediately
Future<List<Promotion>> refresh() async {
final promotions = await repository.refreshPromotions();
// Apply same filtering logic
final activePromotions = promotions
.where((promotion) => promotion.isActive)
.toList();
activePromotions.sort((a, b) => b.startDate.compareTo(a.startDate));
return activePromotions;
}
}