loyalty_levels 0.1.1 copy "loyalty_levels: ^0.1.1" to clipboard
loyalty_levels: ^0.1.1 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 LevelUpgraded event

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 FakeClock for 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: LoyaltyLevel enum
  • Events: StreakStarted, StreakBroken, LevelUpgraded
  • Policies: RewardPolicy (interface), IdentityRewardPolicy, HabitRewardPolicy, LegendRewardPolicy
  • Ports: Clock abstraction for time safety
  • Services: LoyaltyEvaluationService for 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.

0
likes
145
points
21
downloads

Publisher

unverified uploader

Weekly Downloads

A DDD-based loyalty, streak, and levels package modeling the relationship system between customers and business.

Homepage

Documentation

API reference

License

MIT (license)

More

Packages that depend on loyalty_levels