update
This commit is contained in:
169
lib/features/home/domain/entities/member_card.dart
Normal file
169
lib/features/home/domain/entities/member_card.dart
Normal 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)';
|
||||
}
|
||||
}
|
||||
176
lib/features/home/domain/entities/promotion.dart
Normal file
176
lib/features/home/domain/entities/promotion.dart
Normal 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)';
|
||||
}
|
||||
}
|
||||
49
lib/features/home/domain/repositories/home_repository.dart
Normal file
49
lib/features/home/domain/repositories/home_repository.dart
Normal 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();
|
||||
}
|
||||
62
lib/features/home/domain/usecases/get_member_card.dart
Normal file
62
lib/features/home/domain/usecases/get_member_card.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
84
lib/features/home/domain/usecases/get_promotions.dart
Normal file
84
lib/features/home/domain/usecases/get_promotions.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user