update database

This commit is contained in:
Phuoc Nguyen
2025-10-24 11:31:48 +07:00
parent f95fa9d0a6
commit c4272f9a21
126 changed files with 23528 additions and 2234 deletions

View File

@@ -0,0 +1,212 @@
/// Domain Entity: Gift Catalog
///
/// Represents a redeemable gift in the loyalty program catalog.
library;
/// Gift category enum
enum GiftCategory {
/// Voucher gift
voucher,
/// Physical product
product,
/// Service
service,
/// Discount coupon
discount,
/// Other type
other;
/// Get display name for category
String get displayName {
switch (this) {
case GiftCategory.voucher:
return 'Voucher';
case GiftCategory.product:
return 'Product';
case GiftCategory.service:
return 'Service';
case GiftCategory.discount:
return 'Discount';
case GiftCategory.other:
return 'Other';
}
}
}
/// Gift Catalog Entity
///
/// Contains information about a redeemable gift:
/// - Gift details (name, description, image)
/// - Pricing in points
/// - Availability
/// - Terms and conditions
class GiftCatalog {
/// Unique catalog item identifier
final String catalogId;
/// Gift name
final String name;
/// Gift description
final String? description;
/// Gift image URL
final String? imageUrl;
/// Gift category
final GiftCategory category;
/// Points cost to redeem
final int pointsCost;
/// Cash value equivalent
final double? cashValue;
/// Quantity available for redemption
final int quantityAvailable;
/// Quantity already redeemed
final int quantityRedeemed;
/// Terms and conditions
final String? termsConditions;
/// Gift is active and available
final bool isActive;
/// Valid from date
final DateTime? validFrom;
/// Valid until date
final DateTime? validUntil;
/// Creation timestamp
final DateTime createdAt;
/// Last update timestamp
final DateTime updatedAt;
const GiftCatalog({
required this.catalogId,
required this.name,
this.description,
this.imageUrl,
required this.category,
required this.pointsCost,
this.cashValue,
required this.quantityAvailable,
required this.quantityRedeemed,
this.termsConditions,
required this.isActive,
this.validFrom,
this.validUntil,
required this.createdAt,
required this.updatedAt,
});
/// Check if gift is available for redemption
bool get isAvailable => isActive && quantityRemaining > 0 && isCurrentlyValid;
/// Get remaining quantity
int get quantityRemaining => quantityAvailable - quantityRedeemed;
/// Check if gift is in stock
bool get isInStock => quantityRemaining > 0;
/// Check if gift is currently valid (date range)
bool get isCurrentlyValid {
final now = DateTime.now();
if (validFrom != null && now.isBefore(validFrom!)) return false;
if (validUntil != null && now.isAfter(validUntil!)) return false;
return true;
}
/// Check if gift is coming soon
bool get isComingSoon {
if (validFrom == null) return false;
return DateTime.now().isBefore(validFrom!);
}
/// Check if gift has expired
bool get hasExpired {
if (validUntil == null) return false;
return DateTime.now().isAfter(validUntil!);
}
/// Get redemption percentage
double get redemptionPercentage {
if (quantityAvailable == 0) return 0;
return (quantityRedeemed / quantityAvailable) * 100;
}
/// Copy with method for immutability
GiftCatalog copyWith({
String? catalogId,
String? name,
String? description,
String? imageUrl,
GiftCategory? category,
int? pointsCost,
double? cashValue,
int? quantityAvailable,
int? quantityRedeemed,
String? termsConditions,
bool? isActive,
DateTime? validFrom,
DateTime? validUntil,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return GiftCatalog(
catalogId: catalogId ?? this.catalogId,
name: name ?? this.name,
description: description ?? this.description,
imageUrl: imageUrl ?? this.imageUrl,
category: category ?? this.category,
pointsCost: pointsCost ?? this.pointsCost,
cashValue: cashValue ?? this.cashValue,
quantityAvailable: quantityAvailable ?? this.quantityAvailable,
quantityRedeemed: quantityRedeemed ?? this.quantityRedeemed,
termsConditions: termsConditions ?? this.termsConditions,
isActive: isActive ?? this.isActive,
validFrom: validFrom ?? this.validFrom,
validUntil: validUntil ?? this.validUntil,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is GiftCatalog &&
other.catalogId == catalogId &&
other.name == name &&
other.category == category &&
other.pointsCost == pointsCost &&
other.isActive == isActive;
}
@override
int get hashCode {
return Object.hash(
catalogId,
name,
category,
pointsCost,
isActive,
);
}
@override
String toString() {
return 'GiftCatalog(catalogId: $catalogId, name: $name, category: $category, '
'pointsCost: $pointsCost, quantityRemaining: $quantityRemaining, '
'isAvailable: $isAvailable)';
}
}

View File

@@ -0,0 +1,232 @@
/// Domain Entity: Loyalty Point Entry
///
/// Represents a single loyalty points transaction.
library;
/// Entry type enum
enum EntryType {
/// Points earned
earn,
/// Points spent/redeemed
redeem,
/// Points adjusted by admin
adjustment,
/// Points expired
expiry;
}
/// Entry source enum
enum EntrySource {
/// Points from order purchase
order,
/// Points from referral
referral,
/// Points from gift redemption
redemption,
/// Points from project submission
project,
/// Points from points record
pointsRecord,
/// Manual adjustment by admin
manual,
/// Birthday bonus
birthday,
/// Welcome bonus
welcome,
/// Other source
other;
}
/// Complaint status enum
enum ComplaintStatus {
/// No complaint
none,
/// Complaint submitted
submitted,
/// Complaint under review
reviewing,
/// Complaint approved
approved,
/// Complaint rejected
rejected;
}
/// Loyalty Point Entry Entity
///
/// Contains information about a single points transaction:
/// - Points amount (positive for earn, negative for redeem)
/// - Transaction type and source
/// - Reference to related entity
/// - Complaint handling
class LoyaltyPointEntry {
/// Unique entry identifier
final String entryId;
/// User ID
final String userId;
/// Points amount (positive for earn, negative for redeem)
final int points;
/// Entry type
final EntryType entryType;
/// Source of the points
final EntrySource source;
/// Description of the transaction
final String? description;
/// Reference ID to related entity (order ID, gift ID, etc.)
final String? referenceId;
/// Reference type (order, gift, project, etc.)
final String? referenceType;
/// Complaint details (if any)
final Map<String, dynamic>? complaint;
/// Complaint status
final ComplaintStatus complaintStatus;
/// Balance after this transaction
final int balanceAfter;
/// Points expiry date
final DateTime? expiryDate;
/// Transaction timestamp
final DateTime timestamp;
/// ERPNext entry ID
final String? erpnextEntryId;
const LoyaltyPointEntry({
required this.entryId,
required this.userId,
required this.points,
required this.entryType,
required this.source,
this.description,
this.referenceId,
this.referenceType,
this.complaint,
required this.complaintStatus,
required this.balanceAfter,
this.expiryDate,
required this.timestamp,
this.erpnextEntryId,
});
/// Check if points are earned (positive)
bool get isEarn => points > 0 && entryType == EntryType.earn;
/// Check if points are spent (negative)
bool get isRedeem => points < 0 && entryType == EntryType.redeem;
/// Check if entry has complaint
bool get hasComplaint => complaintStatus != ComplaintStatus.none;
/// Check if complaint is pending
bool get isComplaintPending =>
complaintStatus == ComplaintStatus.submitted ||
complaintStatus == ComplaintStatus.reviewing;
/// Check if points are expired
bool get isExpired {
if (expiryDate == null) return false;
return DateTime.now().isAfter(expiryDate!);
}
/// Check if points are expiring soon (within 30 days)
bool get isExpiringSoon {
if (expiryDate == null) return false;
final daysUntilExpiry = expiryDate!.difference(DateTime.now()).inDays;
return daysUntilExpiry > 0 && daysUntilExpiry <= 30;
}
/// Get absolute points value
int get absolutePoints => points.abs();
/// Copy with method for immutability
LoyaltyPointEntry copyWith({
String? entryId,
String? userId,
int? points,
EntryType? entryType,
EntrySource? source,
String? description,
String? referenceId,
String? referenceType,
Map<String, dynamic>? complaint,
ComplaintStatus? complaintStatus,
int? balanceAfter,
DateTime? expiryDate,
DateTime? timestamp,
String? erpnextEntryId,
}) {
return LoyaltyPointEntry(
entryId: entryId ?? this.entryId,
userId: userId ?? this.userId,
points: points ?? this.points,
entryType: entryType ?? this.entryType,
source: source ?? this.source,
description: description ?? this.description,
referenceId: referenceId ?? this.referenceId,
referenceType: referenceType ?? this.referenceType,
complaint: complaint ?? this.complaint,
complaintStatus: complaintStatus ?? this.complaintStatus,
balanceAfter: balanceAfter ?? this.balanceAfter,
expiryDate: expiryDate ?? this.expiryDate,
timestamp: timestamp ?? this.timestamp,
erpnextEntryId: erpnextEntryId ?? this.erpnextEntryId,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is LoyaltyPointEntry &&
other.entryId == entryId &&
other.userId == userId &&
other.points == points &&
other.entryType == entryType &&
other.source == source &&
other.balanceAfter == balanceAfter;
}
@override
int get hashCode {
return Object.hash(
entryId,
userId,
points,
entryType,
source,
balanceAfter,
);
}
@override
String toString() {
return 'LoyaltyPointEntry(entryId: $entryId, userId: $userId, points: $points, '
'entryType: $entryType, source: $source, balanceAfter: $balanceAfter, '
'timestamp: $timestamp)';
}
}

View File

@@ -0,0 +1,182 @@
/// Domain Entity: Points Record
///
/// Represents a user-submitted invoice for points earning.
library;
/// Points record status enum
enum PointsStatus {
/// Record submitted, pending review
pending,
/// Record approved, points awarded
approved,
/// Record rejected
rejected;
/// Get display name for status
String get displayName {
switch (this) {
case PointsStatus.pending:
return 'Pending';
case PointsStatus.approved:
return 'Approved';
case PointsStatus.rejected:
return 'Rejected';
}
}
}
/// Points Record Entity
///
/// Contains information about a user-submitted invoice:
/// - Invoice details
/// - Submission files/attachments
/// - Review status
/// - Points calculation
class PointsRecord {
/// Unique record identifier
final String recordId;
/// User ID who submitted
final String userId;
/// Invoice number
final String invoiceNumber;
/// Store/vendor name
final String storeName;
/// Transaction date
final DateTime transactionDate;
/// Invoice amount
final double invoiceAmount;
/// Additional notes
final String? notes;
/// Attachment URLs (invoice photos, receipts)
final List<String> attachments;
/// Record status
final PointsStatus status;
/// Rejection reason (if rejected)
final String? rejectReason;
/// Points earned (if approved)
final int? pointsEarned;
/// Submission timestamp
final DateTime submittedAt;
/// Processing timestamp
final DateTime? processedAt;
/// ID of admin who processed
final String? processedBy;
const PointsRecord({
required this.recordId,
required this.userId,
required this.invoiceNumber,
required this.storeName,
required this.transactionDate,
required this.invoiceAmount,
this.notes,
required this.attachments,
required this.status,
this.rejectReason,
this.pointsEarned,
required this.submittedAt,
this.processedAt,
this.processedBy,
});
/// Check if record is pending review
bool get isPending => status == PointsStatus.pending;
/// Check if record is approved
bool get isApproved => status == PointsStatus.approved;
/// Check if record is rejected
bool get isRejected => status == PointsStatus.rejected;
/// Check if record has been processed
bool get isProcessed => status != PointsStatus.pending;
/// Check if record has attachments
bool get hasAttachments => attachments.isNotEmpty;
/// Get processing time duration
Duration? get processingDuration {
if (processedAt == null) return null;
return processedAt!.difference(submittedAt);
}
/// Copy with method for immutability
PointsRecord copyWith({
String? recordId,
String? userId,
String? invoiceNumber,
String? storeName,
DateTime? transactionDate,
double? invoiceAmount,
String? notes,
List<String>? attachments,
PointsStatus? status,
String? rejectReason,
int? pointsEarned,
DateTime? submittedAt,
DateTime? processedAt,
String? processedBy,
}) {
return PointsRecord(
recordId: recordId ?? this.recordId,
userId: userId ?? this.userId,
invoiceNumber: invoiceNumber ?? this.invoiceNumber,
storeName: storeName ?? this.storeName,
transactionDate: transactionDate ?? this.transactionDate,
invoiceAmount: invoiceAmount ?? this.invoiceAmount,
notes: notes ?? this.notes,
attachments: attachments ?? this.attachments,
status: status ?? this.status,
rejectReason: rejectReason ?? this.rejectReason,
pointsEarned: pointsEarned ?? this.pointsEarned,
submittedAt: submittedAt ?? this.submittedAt,
processedAt: processedAt ?? this.processedAt,
processedBy: processedBy ?? this.processedBy,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is PointsRecord &&
other.recordId == recordId &&
other.userId == userId &&
other.invoiceNumber == invoiceNumber &&
other.invoiceAmount == invoiceAmount &&
other.status == status;
}
@override
int get hashCode {
return Object.hash(
recordId,
userId,
invoiceNumber,
invoiceAmount,
status,
);
}
@override
String toString() {
return 'PointsRecord(recordId: $recordId, invoiceNumber: $invoiceNumber, '
'storeName: $storeName, invoiceAmount: $invoiceAmount, status: $status, '
'pointsEarned: $pointsEarned)';
}
}

View File

@@ -0,0 +1,207 @@
/// Domain Entity: Redeemed Gift
///
/// Represents a gift that has been redeemed by a user.
library;
import 'gift_catalog.dart';
/// Gift status enum
enum GiftStatus {
/// Gift is active and can be used
active,
/// Gift has been used
used,
/// Gift has expired
expired,
/// Gift has been cancelled
cancelled;
/// Get display name for status
String get displayName {
switch (this) {
case GiftStatus.active:
return 'Active';
case GiftStatus.used:
return 'Used';
case GiftStatus.expired:
return 'Expired';
case GiftStatus.cancelled:
return 'Cancelled';
}
}
}
/// Redeemed Gift Entity
///
/// Contains information about a redeemed gift:
/// - Gift details
/// - Voucher code and QR code
/// - Usage tracking
/// - Expiry dates
class RedeemedGift {
/// Unique gift identifier
final String giftId;
/// User ID who redeemed the gift
final String userId;
/// Catalog ID of the gift
final String catalogId;
/// Gift name (snapshot at redemption time)
final String name;
/// Gift description
final String? description;
/// Voucher code
final String? voucherCode;
/// QR code image URL
final String? qrCodeImage;
/// Gift type/category
final GiftCategory giftType;
/// Points cost (snapshot at redemption time)
final int pointsCost;
/// Cash value (snapshot at redemption time)
final double? cashValue;
/// Expiry date
final DateTime? expiryDate;
/// Gift status
final GiftStatus status;
/// Redemption timestamp
final DateTime redeemedAt;
/// Usage timestamp
final DateTime? usedAt;
/// Location where gift was used
final String? usedLocation;
/// Reference number when used (e.g., order ID)
final String? usedReference;
const RedeemedGift({
required this.giftId,
required this.userId,
required this.catalogId,
required this.name,
this.description,
this.voucherCode,
this.qrCodeImage,
required this.giftType,
required this.pointsCost,
this.cashValue,
this.expiryDate,
required this.status,
required this.redeemedAt,
this.usedAt,
this.usedLocation,
this.usedReference,
});
/// Check if gift is active
bool get isActive => status == GiftStatus.active;
/// Check if gift is used
bool get isUsed => status == GiftStatus.used;
/// Check if gift is expired
bool get isExpired =>
status == GiftStatus.expired ||
(expiryDate != null && DateTime.now().isAfter(expiryDate!));
/// Check if gift can be used
bool get canBeUsed => isActive && !isExpired;
/// Check if gift is expiring soon (within 7 days)
bool get isExpiringSoon {
if (expiryDate == null || isExpired) return false;
final daysUntilExpiry = expiryDate!.difference(DateTime.now()).inDays;
return daysUntilExpiry > 0 && daysUntilExpiry <= 7;
}
/// Get days until expiry
int? get daysUntilExpiry {
if (expiryDate == null) return null;
final days = expiryDate!.difference(DateTime.now()).inDays;
return days > 0 ? days : 0;
}
/// Copy with method for immutability
RedeemedGift copyWith({
String? giftId,
String? userId,
String? catalogId,
String? name,
String? description,
String? voucherCode,
String? qrCodeImage,
GiftCategory? giftType,
int? pointsCost,
double? cashValue,
DateTime? expiryDate,
GiftStatus? status,
DateTime? redeemedAt,
DateTime? usedAt,
String? usedLocation,
String? usedReference,
}) {
return RedeemedGift(
giftId: giftId ?? this.giftId,
userId: userId ?? this.userId,
catalogId: catalogId ?? this.catalogId,
name: name ?? this.name,
description: description ?? this.description,
voucherCode: voucherCode ?? this.voucherCode,
qrCodeImage: qrCodeImage ?? this.qrCodeImage,
giftType: giftType ?? this.giftType,
pointsCost: pointsCost ?? this.pointsCost,
cashValue: cashValue ?? this.cashValue,
expiryDate: expiryDate ?? this.expiryDate,
status: status ?? this.status,
redeemedAt: redeemedAt ?? this.redeemedAt,
usedAt: usedAt ?? this.usedAt,
usedLocation: usedLocation ?? this.usedLocation,
usedReference: usedReference ?? this.usedReference,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is RedeemedGift &&
other.giftId == giftId &&
other.userId == userId &&
other.catalogId == catalogId &&
other.voucherCode == voucherCode &&
other.status == status;
}
@override
int get hashCode {
return Object.hash(
giftId,
userId,
catalogId,
voucherCode,
status,
);
}
@override
String toString() {
return 'RedeemedGift(giftId: $giftId, name: $name, voucherCode: $voucherCode, '
'status: $status, redeemedAt: $redeemedAt)';
}
}