@higher.archi/boe 1.0.28 → 1.0.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/engines/decay/engine.d.ts +32 -1
  2. package/dist/engines/decay/engine.d.ts.map +1 -1
  3. package/dist/engines/decay/engine.js +69 -0
  4. package/dist/engines/decay/engine.js.map +1 -1
  5. package/dist/engines/decay/index.d.ts +1 -1
  6. package/dist/engines/decay/index.d.ts.map +1 -1
  7. package/dist/engines/decay/index.js.map +1 -1
  8. package/dist/engines/decay/strategy.d.ts +4 -2
  9. package/dist/engines/decay/strategy.d.ts.map +1 -1
  10. package/dist/engines/decay/strategy.js +24 -0
  11. package/dist/engines/decay/strategy.js.map +1 -1
  12. package/dist/engines/decay/types.d.ts +9 -0
  13. package/dist/engines/decay/types.d.ts.map +1 -1
  14. package/dist/engines/loyalty/compiler.d.ts +11 -0
  15. package/dist/engines/loyalty/compiler.d.ts.map +1 -0
  16. package/dist/engines/loyalty/compiler.js +144 -0
  17. package/dist/engines/loyalty/compiler.js.map +1 -0
  18. package/dist/engines/loyalty/engine.d.ts +76 -0
  19. package/dist/engines/loyalty/engine.d.ts.map +1 -0
  20. package/dist/engines/loyalty/engine.js +132 -0
  21. package/dist/engines/loyalty/engine.js.map +1 -0
  22. package/dist/engines/loyalty/index.d.ts +8 -0
  23. package/dist/engines/loyalty/index.d.ts.map +1 -0
  24. package/dist/engines/loyalty/index.js +17 -0
  25. package/dist/engines/loyalty/index.js.map +1 -0
  26. package/dist/engines/loyalty/strategy.d.ts +35 -0
  27. package/dist/engines/loyalty/strategy.d.ts.map +1 -0
  28. package/dist/engines/loyalty/strategy.js +405 -0
  29. package/dist/engines/loyalty/strategy.js.map +1 -0
  30. package/dist/engines/loyalty/types.d.ts +190 -0
  31. package/dist/engines/loyalty/types.d.ts.map +1 -0
  32. package/dist/engines/loyalty/types.js +11 -0
  33. package/dist/engines/loyalty/types.js.map +1 -0
  34. package/dist/index.d.ts +4 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +10 -3
  37. package/dist/index.js.map +1 -1
  38. package/package.json +1 -1
  39. package/src/engines/decay/engine.ts +93 -1
  40. package/src/engines/decay/index.ts +2 -1
  41. package/src/engines/decay/strategy.ts +33 -0
  42. package/src/engines/decay/types.ts +10 -0
  43. package/src/engines/loyalty/compiler.ts +174 -0
  44. package/src/engines/loyalty/engine.ts +174 -0
  45. package/src/engines/loyalty/index.ts +43 -0
  46. package/src/engines/loyalty/strategy.ts +532 -0
  47. package/src/engines/loyalty/types.ts +252 -0
  48. package/src/index.ts +39 -1
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Loyalty Engine Compiler
3
+ *
4
+ * Validates loyalty rulesets and resolves defaults.
5
+ */
6
+
7
+ import { CompilationError } from '../../core/errors';
8
+
9
+ import type {
10
+ LoyaltyRuleSet,
11
+ CompiledLoyaltyRuleSet,
12
+ CompiledEarningRuleSet,
13
+ CompiledRedemptionRuleSet,
14
+ CompiledTierEvaluationRuleSet
15
+ } from './types';
16
+
17
+ /**
18
+ * Compile and validate a loyalty ruleset.
19
+ */
20
+ export function compileLoyaltyRuleSet(
21
+ ruleSet: LoyaltyRuleSet
22
+ ): CompiledLoyaltyRuleSet {
23
+ if (!ruleSet.id) {
24
+ throw new CompilationError('Loyalty ruleset requires an id');
25
+ }
26
+
27
+ if (ruleSet.mode !== 'loyalty') {
28
+ throw new CompilationError(`Expected mode 'loyalty', got '${ruleSet.mode}'`);
29
+ }
30
+
31
+ switch (ruleSet.strategy) {
32
+ case 'earning':
33
+ return compileEarning(ruleSet);
34
+ case 'redemption':
35
+ return compileRedemption(ruleSet);
36
+ case 'tier-evaluation':
37
+ return compileTierEvaluation(ruleSet);
38
+ default:
39
+ throw new CompilationError(`Unknown loyalty strategy: '${(ruleSet as any).strategy}'`);
40
+ }
41
+ }
42
+
43
+ function compileEarning(
44
+ ruleSet: LoyaltyRuleSet & { strategy: 'earning' }
45
+ ): CompiledEarningRuleSet {
46
+ if (!ruleSet.earningRules || ruleSet.earningRules.length === 0) {
47
+ throw new CompilationError('Earning strategy requires at least one earning rule');
48
+ }
49
+
50
+ // Validate unique rule IDs
51
+ const ruleIds = new Set<string>();
52
+ for (const rule of ruleSet.earningRules) {
53
+ if (!rule.id) {
54
+ throw new CompilationError('Each earning rule requires an id');
55
+ }
56
+ if (ruleIds.has(rule.id)) {
57
+ throw new CompilationError(`Duplicate earning rule id: '${rule.id}'`);
58
+ }
59
+ if (rule.baseRate < 0) {
60
+ throw new CompilationError(`Earning rule '${rule.id}' baseRate must be non-negative`);
61
+ }
62
+ ruleIds.add(rule.id);
63
+ }
64
+
65
+ // Validate tier IDs are unique
66
+ validateTierIds(ruleSet.tiers);
67
+
68
+ // Validate promotions
69
+ validatePromotions(ruleSet.promotions);
70
+
71
+ return {
72
+ id: ruleSet.id,
73
+ name: ruleSet.name,
74
+ mode: 'loyalty',
75
+ strategy: 'earning',
76
+ earningRules: ruleSet.earningRules,
77
+ defaultRate: ruleSet.defaultRate ?? 1,
78
+ tiers: ruleSet.tiers ?? [],
79
+ expirationPolicy: ruleSet.expirationPolicy ?? {},
80
+ promotions: ruleSet.promotions ?? []
81
+ };
82
+ }
83
+
84
+ function compileRedemption(
85
+ ruleSet: LoyaltyRuleSet & { strategy: 'redemption' }
86
+ ): CompiledRedemptionRuleSet {
87
+ if (!ruleSet.redemptionOptions || ruleSet.redemptionOptions.length === 0) {
88
+ throw new CompilationError('Redemption strategy requires at least one redemption option');
89
+ }
90
+
91
+ // Validate unique option IDs
92
+ const optionIds = new Set<string>();
93
+ for (const option of ruleSet.redemptionOptions) {
94
+ if (!option.id) {
95
+ throw new CompilationError('Each redemption option requires an id');
96
+ }
97
+ if (optionIds.has(option.id)) {
98
+ throw new CompilationError(`Duplicate redemption option id: '${option.id}'`);
99
+ }
100
+ if (option.pointsRequired <= 0) {
101
+ throw new CompilationError(`Redemption option '${option.id}' pointsRequired must be positive`);
102
+ }
103
+ optionIds.add(option.id);
104
+ }
105
+
106
+ return {
107
+ id: ruleSet.id,
108
+ name: ruleSet.name,
109
+ mode: 'loyalty',
110
+ strategy: 'redemption',
111
+ redemptionOptions: ruleSet.redemptionOptions,
112
+ tiers: ruleSet.tiers ?? [],
113
+ expirationPolicy: ruleSet.expirationPolicy ?? {},
114
+ promotions: ruleSet.promotions ?? []
115
+ };
116
+ }
117
+
118
+ function compileTierEvaluation(
119
+ ruleSet: LoyaltyRuleSet & { strategy: 'tier-evaluation' }
120
+ ): CompiledTierEvaluationRuleSet {
121
+ if (!ruleSet.tiers || ruleSet.tiers.length === 0) {
122
+ throw new CompilationError('Tier-evaluation strategy requires at least one tier definition');
123
+ }
124
+
125
+ // Validate tier IDs are unique
126
+ validateTierIds(ruleSet.tiers);
127
+
128
+ // Validate tiers are sorted by threshold ascending
129
+ for (let i = 1; i < ruleSet.tiers.length; i++) {
130
+ if (ruleSet.tiers[i].qualifyingThreshold < ruleSet.tiers[i - 1].qualifyingThreshold) {
131
+ throw new CompilationError(
132
+ `Tier '${ruleSet.tiers[i].id}' threshold (${ruleSet.tiers[i].qualifyingThreshold}) ` +
133
+ `is less than previous tier '${ruleSet.tiers[i - 1].id}' (${ruleSet.tiers[i - 1].qualifyingThreshold}). ` +
134
+ `Tiers must be ordered by ascending threshold.`
135
+ );
136
+ }
137
+ }
138
+
139
+ return {
140
+ id: ruleSet.id,
141
+ name: ruleSet.name,
142
+ mode: 'loyalty',
143
+ strategy: 'tier-evaluation',
144
+ evaluationPeriod: ruleSet.evaluationPeriod ?? 'rolling-12-months',
145
+ tiers: ruleSet.tiers,
146
+ expirationPolicy: ruleSet.expirationPolicy ?? {},
147
+ promotions: ruleSet.promotions ?? []
148
+ };
149
+ }
150
+
151
+ function validateTierIds(tiers?: { id: string }[]): void {
152
+ if (!tiers) return;
153
+ const tierIds = new Set<string>();
154
+ for (const tier of tiers) {
155
+ if (tierIds.has(tier.id)) {
156
+ throw new CompilationError(`Duplicate tier id: '${tier.id}'`);
157
+ }
158
+ tierIds.add(tier.id);
159
+ }
160
+ }
161
+
162
+ function validatePromotions(promotions?: { id: string }[]): void {
163
+ if (!promotions) return;
164
+ const promoIds = new Set<string>();
165
+ for (const promo of promotions) {
166
+ if (!promo.id) {
167
+ throw new CompilationError('Each promotion requires an id');
168
+ }
169
+ if (promoIds.has(promo.id)) {
170
+ throw new CompilationError(`Duplicate promotion id: '${promo.id}'`);
171
+ }
172
+ promoIds.add(promo.id);
173
+ }
174
+ }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Loyalty Engine
3
+ *
4
+ * Point ledger engine that manages earning rules with category multipliers,
5
+ * point transactions, tier-qualified balances, and promotion stacking.
6
+ * Unlike other BOE engines, the Loyalty engine maintains a running balance
7
+ * ledger across operations.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * const engine = new LoyaltyEngine();
12
+ * const compiled = compileLoyaltyRuleSet({ ... });
13
+ *
14
+ * // Stream purchases as they arrive
15
+ * const r1 = engine.ingest({ memberId: 'M001', amount: 250, category: 'dining' }, compiled);
16
+ * // r1.pointsEarned: 500, r1.newBalance: 500
17
+ *
18
+ * engine.getBalance('M001'); // { currentBalance: 500, ... }
19
+ * ```
20
+ */
21
+
22
+ import {
23
+ WorkingMemory,
24
+ Fact,
25
+ FactInput,
26
+ FactChange
27
+ } from '../../core';
28
+
29
+ import type {
30
+ CompiledLoyaltyRuleSet,
31
+ CompiledEarningRuleSet,
32
+ LoyaltyOptions,
33
+ LoyaltyResult,
34
+ LoyaltyIngestResult,
35
+ MemberBalance
36
+ } from './types';
37
+
38
+ import { LoyaltyExecutor } from './strategy';
39
+
40
+ export class LoyaltyEngine {
41
+ private wm: WorkingMemory;
42
+ private strategy: LoyaltyExecutor;
43
+ private _ledger: Map<string, MemberBalance> = new Map();
44
+
45
+ constructor(workingMemory?: WorkingMemory) {
46
+ this.wm = workingMemory ?? new WorkingMemory();
47
+ this.strategy = new LoyaltyExecutor();
48
+ }
49
+
50
+ // ========================================
51
+ // IWorkingMemory Implementation
52
+ // ========================================
53
+
54
+ add<T = Record<string, any>>(input: FactInput<T>): Fact<T> {
55
+ return this.wm.add(input);
56
+ }
57
+
58
+ remove(factId: string): Fact | undefined {
59
+ return this.wm.remove(factId);
60
+ }
61
+
62
+ update<T = Record<string, any>>(input: FactInput<T>): Fact<T> {
63
+ return this.wm.update(input);
64
+ }
65
+
66
+ get(factId: string): Fact | undefined {
67
+ return this.wm.get(factId);
68
+ }
69
+
70
+ getByType(type: string): Fact[] {
71
+ return this.wm.getByType(type);
72
+ }
73
+
74
+ getAll(): Fact[] {
75
+ return this.wm.getAll();
76
+ }
77
+
78
+ has(factId: string): boolean {
79
+ return this.wm.has(factId);
80
+ }
81
+
82
+ size(): number {
83
+ return this.wm.size();
84
+ }
85
+
86
+ clear(): void {
87
+ this.wm.clear();
88
+ }
89
+
90
+ getChanges(): FactChange[] {
91
+ return this.wm.getChanges();
92
+ }
93
+
94
+ clearChanges(): void {
95
+ this.wm.clearChanges();
96
+ }
97
+
98
+ // ========================================
99
+ // Engine Execution
100
+ // ========================================
101
+
102
+ /**
103
+ * Execute a loyalty ruleset against all facts in working memory.
104
+ *
105
+ * @param ruleSet - Compiled loyalty ruleset
106
+ * @param options - Runtime options (asOf date)
107
+ * @returns Loyalty result (earning, redemption, or tier-evaluation)
108
+ */
109
+ execute(
110
+ ruleSet: CompiledLoyaltyRuleSet,
111
+ options: LoyaltyOptions = {}
112
+ ): LoyaltyResult {
113
+ return this.strategy.run(ruleSet, this.wm, this._ledger, options);
114
+ }
115
+
116
+ // ========================================
117
+ // Streaming Ingest
118
+ // ========================================
119
+
120
+ /**
121
+ * Process a single purchase/activity event incrementally.
122
+ *
123
+ * Each call computes points earned, updates the internal ledger,
124
+ * evaluates tier status, and returns the result with the new balance.
125
+ * Only works with earning strategy rulesets.
126
+ *
127
+ * @param eventData - Event data with memberId, amount, and optional category
128
+ * @param ruleSet - Compiled earning ruleset
129
+ * @param asOf - Reference time (default: now)
130
+ * @returns Earning result with updated balance
131
+ */
132
+ ingest(
133
+ eventData: Record<string, any>,
134
+ ruleSet: CompiledEarningRuleSet,
135
+ asOf: Date = new Date()
136
+ ): LoyaltyIngestResult {
137
+ return this.strategy.earnSingle(eventData, ruleSet, this._ledger, asOf);
138
+ }
139
+
140
+ // ========================================
141
+ // Ledger Operations
142
+ // ========================================
143
+
144
+ /**
145
+ * Get the current balance for a member.
146
+ * Returns undefined if the member has no transactions.
147
+ */
148
+ getBalance(memberId: string): MemberBalance | undefined {
149
+ return this._ledger.get(memberId);
150
+ }
151
+
152
+ /**
153
+ * Get the full ledger state (all member balances).
154
+ */
155
+ getLedger(): Map<string, MemberBalance> {
156
+ return new Map(this._ledger);
157
+ }
158
+
159
+ /**
160
+ * Reset the ledger, clearing all member balances.
161
+ * Does not affect working memory.
162
+ */
163
+ resetLedger(): void {
164
+ this._ledger.clear();
165
+ }
166
+
167
+ // ========================================
168
+ // Utility Methods
169
+ // ========================================
170
+
171
+ getWorkingMemory(): WorkingMemory {
172
+ return this.wm;
173
+ }
174
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Loyalty Engine -- Point Ledger Management
3
+ */
4
+
5
+ // Types
6
+ export type {
7
+ LoyaltyStrategy,
8
+ PointTransactionType,
9
+ TierStatus,
10
+ QualifyingMetric,
11
+ EvaluationPeriod,
12
+ EarningRule,
13
+ RedemptionOption,
14
+ LoyaltyTierDefinition,
15
+ PromotionRule,
16
+ ExpirationPolicy,
17
+ PointTransaction,
18
+ MemberBalance,
19
+ EarningRuleSet,
20
+ RedemptionRuleSet,
21
+ TierEvaluationRuleSet,
22
+ LoyaltyRuleSet,
23
+ CompiledEarningRuleSet,
24
+ CompiledRedemptionRuleSet,
25
+ CompiledTierEvaluationRuleSet,
26
+ CompiledLoyaltyRuleSet,
27
+ EarningResult,
28
+ RedemptionResult,
29
+ MemberTierResult,
30
+ TierEvaluationResult,
31
+ LoyaltyResult,
32
+ LoyaltyOptions,
33
+ LoyaltyIngestResult
34
+ } from './types';
35
+
36
+ // Compiler
37
+ export { compileLoyaltyRuleSet } from './compiler';
38
+
39
+ // Strategy
40
+ export { LoyaltyExecutor, loyaltyStrategy } from './strategy';
41
+
42
+ // Engine
43
+ export { LoyaltyEngine } from './engine';