import 'package:equatable/equatable.dart'; import '../../core/constants/app_constants.dart'; import '../../core/utils/extensions.dart'; enum Gender { male('Male'), female('Female'), other('Other'), unspecified('Prefer not to say'); const Gender(this.label); final String label; } /// Loyalty tier, derived from lifetime spend. enum MembershipTier { bronze('Bronze', 0, 0.0), silver('Silver', 10000, 0.02), gold('Gold', 50000, 0.05), platinum('Platinum', 150000, 0.08); const MembershipTier(this.label, this.threshold, this.discountRate); final String label; /// Lifetime spend in rupees required to reach this tier. final double threshold; /// Automatic bill discount granted to members of this tier. final double discountRate; static MembershipTier forSpend(double lifetimeSpend) { return MembershipTier.values.lastWhere( (t) => lifetimeSpend >= t.threshold, orElse: () => MembershipTier.bronze, ); } MembershipTier? get next { final i = index; return i < MembershipTier.values.length - 1 ? MembershipTier.values[i + 1] : null; } } /// A registered shopper. A `null` customer on a sale means walk-in. class Customer extends Equatable { const Customer({ required this.id, required this.name, required this.mobile, this.email, this.gender = Gender.unspecified, this.dateOfBirth, this.loyaltyPoints = 0, this.lifetimeSpend = 0, this.visitCount = 0, this.createdAt, this.lastVisitAt, }); final String id; final String name; final String mobile; final String? email; final Gender gender; final DateTime? dateOfBirth; final int loyaltyPoints; final double lifetimeSpend; final int visitCount; final DateTime? createdAt; final DateTime? lastVisitAt; MembershipTier get tier => MembershipTier.forSpend(lifetimeSpend); /// Cash value of the points currently held. double get redeemableValue => loyaltyPoints * AppConstants.loyaltyPointValue; /// Rupees of additional spend needed to reach the next tier. double? get spendToNextTier { final next = tier.next; if (next == null) return null; return (next.threshold - lifetimeSpend).clamp(0, double.infinity); } bool get isBirthdayToday { final dob = dateOfBirth; if (dob == null) return false; final now = DateTime.now(); return dob.month == now.month && dob.day == now.day; } /// The shopper as they stand after a completed sale. /// /// Pure, so the caller can compute the new row and persist it in the same /// transaction as the bill rather than as a separate write that might fail /// on its own. Customer applySale({ required double amount, required int pointsEarned, required int pointsRedeemed, DateTime? at, }) { return copyWith( loyaltyPoints: (loyaltyPoints - pointsRedeemed + pointsEarned).clamp(0, 1 << 31), lifetimeSpend: (lifetimeSpend + amount).asMoney, visitCount: visitCount + 1, lastVisitAt: at ?? DateTime.now(), ); } Customer copyWith({ String? name, String? email, Gender? gender, DateTime? dateOfBirth, int? loyaltyPoints, double? lifetimeSpend, int? visitCount, DateTime? lastVisitAt, }) { return Customer( id: id, name: name ?? this.name, mobile: mobile, email: email ?? this.email, gender: gender ?? this.gender, dateOfBirth: dateOfBirth ?? this.dateOfBirth, loyaltyPoints: loyaltyPoints ?? this.loyaltyPoints, lifetimeSpend: lifetimeSpend ?? this.lifetimeSpend, visitCount: visitCount ?? this.visitCount, createdAt: createdAt, lastVisitAt: lastVisitAt ?? this.lastVisitAt, ); } @override List get props => [id, name, mobile, loyaltyPoints, lifetimeSpend]; }