loyalty_levels 0.1.0
loyalty_levels: ^0.1.0 copied to clipboard
A DDD-based loyalty, streak, and levels package modeling the relationship system between customers and business.
Loyalty Levels #
A production-grade Dart package for modeling loyalty relationships using Domain-Driven Design (DDD) principles.
Overview #
loyalty_levels models the relationship system between customers and business through three distinct concepts:
- Level = Lifetime trust (NEVER resets)
- Streak = Current momentum (CAN reset or pause)
- Rewards = Tied to STREAK count, not level
This is NOT a gamification system. This is a relationship system built on fairness.
Core Philosophy #
1. Level ≠ Streak ≠ Reward #
These are three independent but coordinated concepts:
-
Level: Represents lifetime customer trust. Upgrades monotonically (identity → habit → legend). Can only downgrade by ONE zone at a time. Never resets completely.
-
Streak: Tracks current momentum through consecutive successful cycles. Can be active, paused, or broken. Subject to fair pause rules.
-
Rewards: Calculated from streak count, NOT from level. Level only determines WHICH reward policy applies.
2. Fairness Over Aggression #
Loyal customers must never feel cheated:
- Pause ≤15 days: Streak frozen (count preserved)
- Pause 16-45 days: Streak reset to 0, level unchanged
- Pause >45 days: Streak reset to 0, level downgraded by ONE zone
No hard resets unless fraud. Punishment is soft, gradual, and explainable.
Installation #
Add to your pubspec.yaml:
dependencies:
loyalty_levels: ^0.1.0
Then run:
dart pub get
Usage #
Creating a Loyalty Account #
import 'package:loyalty_levels/loyalty_levels.dart';
final clock = SystemClock();
final account = LoyaltyAccount.create(
accountId: 'customer-123',
createdAt: clock.now(),
);
print(account.level.current); // LoyaltyLevel.identity
print(account.streak.count); // 0
Recording Activity (Extends Streak) #
final service = LoyaltyEvaluationService(clock: clock);
var account = LoyaltyAccount.create(
accountId: 'customer-123',
createdAt: clock.now(),
);
// Customer completes an order
account = service.processActivity(account);
print(account.streak.count); // 1
// Another order next day
account = service.processActivity(account);
print(account.streak.count); // 2
Pause and Resume (Fair Rules) #
// Customer pauses subscription
account = service.pauseAccount(account);
// ... 10 days pass ...
// Resume after short pause (≤15 days)
account = service.resumeAccount(account);
print(account.streak.count); // FROZEN - count preserved
// Resume after medium pause (16-45 days)
// Streak resets to 0, but level unchanged
// Resume after long pause (>45 days)
// Streak resets to 0, level downgrades by ONE zone
Calculating Rewards #
// Rewards based on STREAK, not level
final rewards = service.calculateRewards(account);
// Level determines WHICH policy applies:
// - Identity: streak × baseReward
// - Habit: streak × baseReward × 1.5
// - Legend: (streak × baseReward × 2.0) + bonus
Upgrading Levels #
account = service.upgradeAccountLevel(account);
print(account.level.current); // LoyaltyLevel.habit
// Can upgrade: identity → habit → legend
// Streak count is preserved during upgrade
Domain Rules #
Level Rules #
- Level NEVER resets completely
- Upgrades are monotonic (identity → habit → legend)
- Downgrade limited to ONE zone only
- All level changes emit
LevelUpgradedevent
Streak Rules #
- Streak count is always ≥ 0
- Can be active, paused, or broken
- Pause ≤15 days → frozen
- Pause 16-45 days → reset
- Pause >45 days → reset + level downgrade
- All state changes emit appropriate events
Reward Rules #
- Rewards tied to STREAK count, not LEVEL
- Level only influences WHICH policy applies
- Policies are interchangeable (Liskov Substitution)
Time Safety #
- ABSOLUTE BAN on
DateTime.now()in domain code - All time comes from injected
Clock - Production: use
SystemClock - Tests: use
FakeClockfor determinism
Testing #
This package includes FakeClock for deterministic testing:
import 'package:loyalty_levels/loyalty_levels.dart';
import 'package:test/test.dart';
void main() {
test('deterministic time-based testing', () {
final clock = FakeClock(DateTime(2026, 1, 1));
final service = LoyaltyEvaluationService(clock: clock);
var account = LoyaltyAccount.create(
accountId: 'test-123',
createdAt: clock.now(),
);
clock.advanceDays(1);
account = service.processActivity(account);
expect(account.streak.count, 1);
});
}
Architecture #
Built with strict DDD principles:
- Entities:
LoyaltyAccount(aggregate root),Level,Streak - Value Objects:
LoyaltyLevelenum - Events:
StreakStarted,StreakBroken,LevelUpgraded - Policies:
RewardPolicy(interface),IdentityRewardPolicy,HabitRewardPolicy,LegendRewardPolicy - Ports:
Clockabstraction for time safety - Services:
LoyaltyEvaluationServicefor thin orchestration
Bounded Context #
This package has ZERO dependencies on:
- Ordering
- Delivery
- Inventory
- Payments
It ONLY consumes:
- Signals (events/inputs)
- Time (via
Clock)
License #
MIT License - see LICENSE file for details.
Philosophy #
If a rule feels unfair, redesign it.
If a rule can be abused, block it.
If logic is ambiguous, THROW.
Safety > Convenience.