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,70 @@
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
import 'package:worker/core/database/models/enums.dart';
part 'gift_catalog_model.g.dart';
@HiveType(typeId: HiveTypeIds.giftCatalogModel)
class GiftCatalogModel extends HiveObject {
GiftCatalogModel({required this.catalogId, required this.name, required this.description, this.imageUrl, required this.category, required this.pointsCost, required this.cashValue, required this.quantityAvailable, required this.quantityRedeemed, this.termsConditions, required this.isActive, this.validFrom, this.validUntil, required this.createdAt, this.updatedAt});
@HiveField(0) final String catalogId;
@HiveField(1) final String name;
@HiveField(2) final String description;
@HiveField(3) final String? imageUrl;
@HiveField(4) final GiftCategory category;
@HiveField(5) final int pointsCost;
@HiveField(6) final double cashValue;
@HiveField(7) final int quantityAvailable;
@HiveField(8) final int quantityRedeemed;
@HiveField(9) final String? termsConditions;
@HiveField(10) final bool isActive;
@HiveField(11) final DateTime? validFrom;
@HiveField(12) final DateTime? validUntil;
@HiveField(13) final DateTime createdAt;
@HiveField(14) final DateTime? updatedAt;
factory GiftCatalogModel.fromJson(Map<String, dynamic> json) => GiftCatalogModel(
catalogId: json['catalog_id'] as String,
name: json['name'] as String,
description: json['description'] as String,
imageUrl: json['image_url'] as String?,
category: GiftCategory.values.firstWhere((e) => e.name == json['category']),
pointsCost: json['points_cost'] as int,
cashValue: (json['cash_value'] as num).toDouble(),
quantityAvailable: json['quantity_available'] as int,
quantityRedeemed: json['quantity_redeemed'] as int? ?? 0,
termsConditions: json['terms_conditions'] as String?,
isActive: json['is_active'] as bool? ?? true,
validFrom: json['valid_from'] != null ? DateTime.parse(json['valid_from']?.toString() ?? '') : null,
validUntil: json['valid_until'] != null ? DateTime.parse(json['valid_until']?.toString() ?? '') : null,
createdAt: DateTime.parse(json['created_at']?.toString() ?? ''),
updatedAt: json['updated_at'] != null ? DateTime.parse(json['updated_at']?.toString() ?? '') : null,
);
Map<String, dynamic> toJson() => {
'catalog_id': catalogId,
'name': name,
'description': description,
'image_url': imageUrl,
'category': category.name,
'points_cost': pointsCost,
'cash_value': cashValue,
'quantity_available': quantityAvailable,
'quantity_redeemed': quantityRedeemed,
'terms_conditions': termsConditions,
'is_active': isActive,
'valid_from': validFrom?.toIso8601String(),
'valid_until': validUntil?.toIso8601String(),
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt?.toIso8601String(),
};
bool get isAvailable => isActive && quantityAvailable > quantityRedeemed;
bool get isValid {
final now = DateTime.now();
if (validFrom != null && now.isBefore(validFrom!)) return false;
if (validUntil != null && now.isAfter(validUntil!)) return false;
return true;
}
}

View File

@@ -0,0 +1,83 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'gift_catalog_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class GiftCatalogModelAdapter extends TypeAdapter<GiftCatalogModel> {
@override
final typeId = 11;
@override
GiftCatalogModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return GiftCatalogModel(
catalogId: fields[0] as String,
name: fields[1] as String,
description: fields[2] as String,
imageUrl: fields[3] as String?,
category: fields[4] as GiftCategory,
pointsCost: (fields[5] as num).toInt(),
cashValue: (fields[6] as num).toDouble(),
quantityAvailable: (fields[7] as num).toInt(),
quantityRedeemed: (fields[8] as num).toInt(),
termsConditions: fields[9] as String?,
isActive: fields[10] as bool,
validFrom: fields[11] as DateTime?,
validUntil: fields[12] as DateTime?,
createdAt: fields[13] as DateTime,
updatedAt: fields[14] as DateTime?,
);
}
@override
void write(BinaryWriter writer, GiftCatalogModel obj) {
writer
..writeByte(15)
..writeByte(0)
..write(obj.catalogId)
..writeByte(1)
..write(obj.name)
..writeByte(2)
..write(obj.description)
..writeByte(3)
..write(obj.imageUrl)
..writeByte(4)
..write(obj.category)
..writeByte(5)
..write(obj.pointsCost)
..writeByte(6)
..write(obj.cashValue)
..writeByte(7)
..write(obj.quantityAvailable)
..writeByte(8)
..write(obj.quantityRedeemed)
..writeByte(9)
..write(obj.termsConditions)
..writeByte(10)
..write(obj.isActive)
..writeByte(11)
..write(obj.validFrom)
..writeByte(12)
..write(obj.validUntil)
..writeByte(13)
..write(obj.createdAt)
..writeByte(14)
..write(obj.updatedAt);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is GiftCatalogModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,72 @@
import 'dart:convert';
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
import 'package:worker/core/database/models/enums.dart';
part 'loyalty_point_entry_model.g.dart';
@HiveType(typeId: HiveTypeIds.loyaltyPointEntryModel)
class LoyaltyPointEntryModel extends HiveObject {
LoyaltyPointEntryModel({required this.entryId, required this.userId, required this.points, required this.entryType, required this.source, required this.description, this.referenceId, this.referenceType, this.complaint, required this.complaintStatus, required this.balanceAfter, this.expiryDate, required this.timestamp, this.erpnextEntryId});
@HiveField(0) final String entryId;
@HiveField(1) final String userId;
@HiveField(2) final int points;
@HiveField(3) final EntryType entryType;
@HiveField(4) final EntrySource source;
@HiveField(5) final String description;
@HiveField(6) final String? referenceId;
@HiveField(7) final String? referenceType;
@HiveField(8) final String? complaint;
@HiveField(9) final ComplaintStatus complaintStatus;
@HiveField(10) final int balanceAfter;
@HiveField(11) final DateTime? expiryDate;
@HiveField(12) final DateTime timestamp;
@HiveField(13) final String? erpnextEntryId;
factory LoyaltyPointEntryModel.fromJson(Map<String, dynamic> json) => LoyaltyPointEntryModel(
entryId: json['entry_id'] as String,
userId: json['user_id'] as String,
points: json['points'] as int,
entryType: EntryType.values.firstWhere((e) => e.name == json['entry_type']),
source: EntrySource.values.firstWhere((e) => e.name == json['source']),
description: json['description'] as String,
referenceId: json['reference_id'] as String?,
referenceType: json['reference_type'] as String?,
complaint: json['complaint'] != null ? jsonEncode(json['complaint']) : null,
complaintStatus: ComplaintStatus.values.firstWhere((e) => e.name == (json['complaint_status'] ?? 'none')),
balanceAfter: json['balance_after'] as int,
expiryDate: json['expiry_date'] != null ? DateTime.parse(json['expiry_date']?.toString() ?? '') : null,
timestamp: DateTime.parse(json['timestamp']?.toString() ?? ''),
erpnextEntryId: json['erpnext_entry_id'] as String?,
);
Map<String, dynamic> toJson() => {
'entry_id': entryId,
'user_id': userId,
'points': points,
'entry_type': entryType.name,
'source': source.name,
'description': description,
'reference_id': referenceId,
'reference_type': referenceType,
'complaint': complaint != null ? jsonDecode(complaint!) : null,
'complaint_status': complaintStatus.name,
'balance_after': balanceAfter,
'expiry_date': expiryDate?.toIso8601String(),
'timestamp': timestamp.toIso8601String(),
'erpnext_entry_id': erpnextEntryId,
};
Map<String, dynamic>? get complaintMap {
if (complaint == null) return null;
try {
return jsonDecode(complaint!) as Map<String, dynamic>;
} catch (e) {
return null;
}
}
bool get isExpired => expiryDate != null && DateTime.now().isAfter(expiryDate!);
bool get hasComplaint => complaintStatus != ComplaintStatus.none;
}

View File

@@ -0,0 +1,81 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'loyalty_point_entry_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class LoyaltyPointEntryModelAdapter
extends TypeAdapter<LoyaltyPointEntryModel> {
@override
final typeId = 10;
@override
LoyaltyPointEntryModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return LoyaltyPointEntryModel(
entryId: fields[0] as String,
userId: fields[1] as String,
points: (fields[2] as num).toInt(),
entryType: fields[3] as EntryType,
source: fields[4] as EntrySource,
description: fields[5] as String,
referenceId: fields[6] as String?,
referenceType: fields[7] as String?,
complaint: fields[8] as String?,
complaintStatus: fields[9] as ComplaintStatus,
balanceAfter: (fields[10] as num).toInt(),
expiryDate: fields[11] as DateTime?,
timestamp: fields[12] as DateTime,
erpnextEntryId: fields[13] as String?,
);
}
@override
void write(BinaryWriter writer, LoyaltyPointEntryModel obj) {
writer
..writeByte(14)
..writeByte(0)
..write(obj.entryId)
..writeByte(1)
..write(obj.userId)
..writeByte(2)
..write(obj.points)
..writeByte(3)
..write(obj.entryType)
..writeByte(4)
..write(obj.source)
..writeByte(5)
..write(obj.description)
..writeByte(6)
..write(obj.referenceId)
..writeByte(7)
..write(obj.referenceType)
..writeByte(8)
..write(obj.complaint)
..writeByte(9)
..write(obj.complaintStatus)
..writeByte(10)
..write(obj.balanceAfter)
..writeByte(11)
..write(obj.expiryDate)
..writeByte(12)
..write(obj.timestamp)
..writeByte(13)
..write(obj.erpnextEntryId);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is LoyaltyPointEntryModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,70 @@
import 'dart:convert';
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
import 'package:worker/core/database/models/enums.dart';
part 'points_record_model.g.dart';
@HiveType(typeId: HiveTypeIds.pointsRecordModel)
class PointsRecordModel extends HiveObject {
PointsRecordModel({required this.recordId, required this.userId, required this.invoiceNumber, required this.storeName, required this.transactionDate, required this.invoiceAmount, this.notes, this.attachments, required this.status, this.rejectReason, this.pointsEarned, required this.submittedAt, this.processedAt, this.processedBy});
@HiveField(0) final String recordId;
@HiveField(1) final String userId;
@HiveField(2) final String invoiceNumber;
@HiveField(3) final String storeName;
@HiveField(4) final DateTime transactionDate;
@HiveField(5) final double invoiceAmount;
@HiveField(6) final String? notes;
@HiveField(7) final String? attachments;
@HiveField(8) final PointsStatus status;
@HiveField(9) final String? rejectReason;
@HiveField(10) final int? pointsEarned;
@HiveField(11) final DateTime submittedAt;
@HiveField(12) final DateTime? processedAt;
@HiveField(13) final String? processedBy;
factory PointsRecordModel.fromJson(Map<String, dynamic> json) => PointsRecordModel(
recordId: json['record_id'] as String,
userId: json['user_id'] as String,
invoiceNumber: json['invoice_number'] as String,
storeName: json['store_name'] as String,
transactionDate: DateTime.parse(json['transaction_date']?.toString() ?? ''),
invoiceAmount: (json['invoice_amount'] as num).toDouble(),
notes: json['notes'] as String?,
attachments: json['attachments'] != null ? jsonEncode(json['attachments']) : null,
status: PointsStatus.values.firstWhere((e) => e.name == json['status']),
rejectReason: json['reject_reason'] as String?,
pointsEarned: json['points_earned'] as int?,
submittedAt: DateTime.parse(json['submitted_at']?.toString() ?? ''),
processedAt: json['processed_at'] != null ? DateTime.parse(json['processed_at']?.toString() ?? '') : null,
processedBy: json['processed_by'] as String?,
);
Map<String, dynamic> toJson() => {
'record_id': recordId,
'user_id': userId,
'invoice_number': invoiceNumber,
'store_name': storeName,
'transaction_date': transactionDate.toIso8601String(),
'invoice_amount': invoiceAmount,
'notes': notes,
'attachments': attachments != null ? jsonDecode(attachments!) : null,
'status': status.name,
'reject_reason': rejectReason,
'points_earned': pointsEarned,
'submitted_at': submittedAt.toIso8601String(),
'processed_at': processedAt?.toIso8601String(),
'processed_by': processedBy,
};
List<String>? get attachmentsList {
if (attachments == null) return null;
try {
final decoded = jsonDecode(attachments!) as List;
return decoded.map((e) => e.toString()).toList();
} catch (e) {
return null;
}
}
}

View File

@@ -0,0 +1,80 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'points_record_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class PointsRecordModelAdapter extends TypeAdapter<PointsRecordModel> {
@override
final typeId = 13;
@override
PointsRecordModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return PointsRecordModel(
recordId: fields[0] as String,
userId: fields[1] as String,
invoiceNumber: fields[2] as String,
storeName: fields[3] as String,
transactionDate: fields[4] as DateTime,
invoiceAmount: (fields[5] as num).toDouble(),
notes: fields[6] as String?,
attachments: fields[7] as String?,
status: fields[8] as PointsStatus,
rejectReason: fields[9] as String?,
pointsEarned: (fields[10] as num?)?.toInt(),
submittedAt: fields[11] as DateTime,
processedAt: fields[12] as DateTime?,
processedBy: fields[13] as String?,
);
}
@override
void write(BinaryWriter writer, PointsRecordModel obj) {
writer
..writeByte(14)
..writeByte(0)
..write(obj.recordId)
..writeByte(1)
..write(obj.userId)
..writeByte(2)
..write(obj.invoiceNumber)
..writeByte(3)
..write(obj.storeName)
..writeByte(4)
..write(obj.transactionDate)
..writeByte(5)
..write(obj.invoiceAmount)
..writeByte(6)
..write(obj.notes)
..writeByte(7)
..write(obj.attachments)
..writeByte(8)
..write(obj.status)
..writeByte(9)
..write(obj.rejectReason)
..writeByte(10)
..write(obj.pointsEarned)
..writeByte(11)
..write(obj.submittedAt)
..writeByte(12)
..write(obj.processedAt)
..writeByte(13)
..write(obj.processedBy);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is PointsRecordModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,69 @@
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
import 'package:worker/core/database/models/enums.dart';
part 'redeemed_gift_model.g.dart';
@HiveType(typeId: HiveTypeIds.redeemedGiftModel)
class RedeemedGiftModel extends HiveObject {
RedeemedGiftModel({required this.giftId, required this.userId, required this.catalogId, required this.name, required this.description, this.voucherCode, this.qrCodeImage, required this.giftType, required this.pointsCost, required this.cashValue, this.expiryDate, required this.status, required this.redeemedAt, this.usedAt, this.usedLocation, this.usedReference});
@HiveField(0) final String giftId;
@HiveField(1) final String userId;
@HiveField(2) final String catalogId;
@HiveField(3) final String name;
@HiveField(4) final String description;
@HiveField(5) final String? voucherCode;
@HiveField(6) final String? qrCodeImage;
@HiveField(7) final GiftCategory giftType;
@HiveField(8) final int pointsCost;
@HiveField(9) final double cashValue;
@HiveField(10) final DateTime? expiryDate;
@HiveField(11) final GiftStatus status;
@HiveField(12) final DateTime redeemedAt;
@HiveField(13) final DateTime? usedAt;
@HiveField(14) final String? usedLocation;
@HiveField(15) final String? usedReference;
factory RedeemedGiftModel.fromJson(Map<String, dynamic> json) => RedeemedGiftModel(
giftId: json['gift_id'] as String,
userId: json['user_id'] as String,
catalogId: json['catalog_id'] as String,
name: json['name'] as String,
description: json['description'] as String,
voucherCode: json['voucher_code'] as String?,
qrCodeImage: json['qr_code_image'] as String?,
giftType: GiftCategory.values.firstWhere((e) => e.name == json['gift_type']),
pointsCost: json['points_cost'] as int,
cashValue: (json['cash_value'] as num).toDouble(),
expiryDate: json['expiry_date'] != null ? DateTime.parse(json['expiry_date']?.toString() ?? '') : null,
status: GiftStatus.values.firstWhere((e) => e.name == json['status']),
redeemedAt: DateTime.parse(json['redeemed_at']?.toString() ?? ''),
usedAt: json['used_at'] != null ? DateTime.parse(json['used_at']?.toString() ?? '') : null,
usedLocation: json['used_location'] as String?,
usedReference: json['used_reference'] as String?,
);
Map<String, dynamic> toJson() => {
'gift_id': giftId,
'user_id': userId,
'catalog_id': catalogId,
'name': name,
'description': description,
'voucher_code': voucherCode,
'qr_code_image': qrCodeImage,
'gift_type': giftType.name,
'points_cost': pointsCost,
'cash_value': cashValue,
'expiry_date': expiryDate?.toIso8601String(),
'status': status.name,
'redeemed_at': redeemedAt.toIso8601String(),
'used_at': usedAt?.toIso8601String(),
'used_location': usedLocation,
'used_reference': usedReference,
};
bool get isExpired => expiryDate != null && DateTime.now().isAfter(expiryDate!);
bool get isUsed => status == GiftStatus.used;
bool get isActive => status == GiftStatus.active && !isExpired;
}

View File

@@ -0,0 +1,86 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'redeemed_gift_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class RedeemedGiftModelAdapter extends TypeAdapter<RedeemedGiftModel> {
@override
final typeId = 12;
@override
RedeemedGiftModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return RedeemedGiftModel(
giftId: fields[0] as String,
userId: fields[1] as String,
catalogId: fields[2] as String,
name: fields[3] as String,
description: fields[4] as String,
voucherCode: fields[5] as String?,
qrCodeImage: fields[6] as String?,
giftType: fields[7] as GiftCategory,
pointsCost: (fields[8] as num).toInt(),
cashValue: (fields[9] as num).toDouble(),
expiryDate: fields[10] as DateTime?,
status: fields[11] as GiftStatus,
redeemedAt: fields[12] as DateTime,
usedAt: fields[13] as DateTime?,
usedLocation: fields[14] as String?,
usedReference: fields[15] as String?,
);
}
@override
void write(BinaryWriter writer, RedeemedGiftModel obj) {
writer
..writeByte(16)
..writeByte(0)
..write(obj.giftId)
..writeByte(1)
..write(obj.userId)
..writeByte(2)
..write(obj.catalogId)
..writeByte(3)
..write(obj.name)
..writeByte(4)
..write(obj.description)
..writeByte(5)
..write(obj.voucherCode)
..writeByte(6)
..write(obj.qrCodeImage)
..writeByte(7)
..write(obj.giftType)
..writeByte(8)
..write(obj.pointsCost)
..writeByte(9)
..write(obj.cashValue)
..writeByte(10)
..write(obj.expiryDate)
..writeByte(11)
..write(obj.status)
..writeByte(12)
..write(obj.redeemedAt)
..writeByte(13)
..write(obj.usedAt)
..writeByte(14)
..write(obj.usedLocation)
..writeByte(15)
..write(obj.usedReference);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RedeemedGiftModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

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)';
}
}