@classytic/loyalty 0.5.0 → 0.6.0

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.
package/LICENSE CHANGED
@@ -1,21 +1,75 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Classytic
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ Classytic Source-Available License (Community & Commercial), Version 1.0
2
+
3
+ Copyright (c) 2026 Classytic LLC. All rights reserved.
4
+
5
+ This software is source-available, not open source. Please read this License
6
+ before using it.
7
+
8
+ 1. DEFINITIONS
9
+ "Software" means the contents of this package and any accompanying source
10
+ code, object code, or documentation, in any form.
11
+ "Licensor" means Classytic LLC, a limited liability company organized in the
12
+ United States.
13
+ "You" means the individual or legal entity exercising rights under this
14
+ License.
15
+ "Evaluation Use" means use for evaluation, development, testing,
16
+ experimentation, or internal non-commercial purposes only.
17
+ "Production Use" means any use of the Software that is live, commercial,
18
+ revenue-generating, or customer-facing, or any use other than Evaluation Use.
19
+ "Commercial License" means a separate written agreement executed with
20
+ Licensor that grants Production Use rights.
21
+
22
+ 2. OWNERSHIP
23
+ The Software is licensed, not sold. Licensor retains all right, title, and
24
+ interest in and to the Software, including all intellectual property rights.
25
+ All rights not expressly granted in this License are reserved by Licensor.
26
+
27
+ 3. EVALUATION GRANT
28
+ Subject to Your compliance with this License, Licensor grants You a
29
+ worldwide, royalty-free, non-exclusive, non-transferable, revocable license
30
+ to download, install, run, and modify the Software solely for Evaluation Use.
31
+
32
+ 4. PRODUCTION USE REQUIRES A COMMERCIAL LICENSE
33
+ Production Use of the Software is NOT permitted under this License and
34
+ requires a Commercial License from Licensor. A Commercial License may be
35
+ granted at Licensor's discretion, with or without a fee, and may confer
36
+ perpetual, irrevocable, and transferable Production Use rights as set out in
37
+ that agreement. Contact licensing@classytic.com to obtain one.
38
+
39
+ 5. RESTRICTIONS
40
+ You may not:
41
+ (a) make any Production Use of the Software without a Commercial License;
42
+ (b) redistribute, resell, rent, sublicense, or otherwise make the Software
43
+ (in whole or in part, original or modified) available to third parties as
44
+ a stand-alone product, framework, library, or hosted/managed service that
45
+ competes with the Software or with Licensor;
46
+ (c) remove, obscure, or alter any copyright, license, or attribution notice.
47
+
48
+ 6. FEEDBACK AND CONTRIBUTIONS
49
+ Any feedback, suggestions, or contributions You provide relating to the
50
+ Software may be used by Licensor for any purpose without obligation to You.
51
+
52
+ 7. TERMINATION
53
+ This License and the Evaluation Grant terminate automatically if You breach
54
+ any term. Upon termination You must cease all use of the Software and destroy
55
+ all copies in Your possession. Rights under a Commercial License are governed
56
+ by that agreement's own terms and survive termination of this License to the
57
+ extent stated there.
58
+
59
+ 8. DISCLAIMER OF WARRANTY
60
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
61
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
62
+ FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
63
+
64
+ 9. LIMITATION OF LIABILITY
65
+ TO THE MAXIMUM EXTENT PERMITTED BY LAW, IN NO EVENT SHALL LICENSOR BE LIABLE
66
+ FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
67
+ TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE
68
+ OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
69
+
70
+ 10. GOVERNING LAW
71
+ This License is governed by the laws of the State of Delaware, United States,
72
+ without regard to its conflict-of-laws principles. (Adjust to Your LLC's state
73
+ of formation.)
74
+
75
+ For commercial licensing, contact: licensing@classytic.com
package/README.md CHANGED
@@ -22,8 +22,8 @@ import mongoose from 'mongoose';
22
22
  const engine = createLoyaltyEngine({
23
23
  mongoose: mongoose.connection,
24
24
  tenant: false, // single-tenant (or { field: 'orgId' } for multi-tenant)
25
- program: { conversionRate: 10 },
26
- redemption: { minPoints: 100, maxRedeemPercent: 50 },
25
+ program: { pointValueMinor: 10 }, // 1 point redeems for 10 minor units (e.g. 10 paisa)
26
+ redemption: { minPoints: 100, maxRedeemPercent: 50 }, // money amounts are integer MINOR units
27
27
  referral: { referrerRewardPoints: 200, refereeRewardPoints: 100 },
28
28
  });
29
29
 
@@ -1,12 +1,12 @@
1
- import { t as NotificationAdapter } from "./notification.port-CJ9nXj0k.mjs";
1
+ import { t as NotificationAdapter } from "./notification.port-wY4IKimz.mjs";
2
+ import { ResolvedTenantConfig, TenantConfig } from "@classytic/repo-core/tenant";
2
3
  import { EventTransport } from "@classytic/primitives/events";
3
- import { Connection } from "mongoose";
4
4
  import { OutboxStore } from "@classytic/primitives/outbox";
5
- import { TenantConfig, TenantFieldType } from "@classytic/repo-core/tenant";
6
-
7
5
  //#region src/types/config.d.ts
8
6
  type LoyaltyModelName = 'Member' | 'PointTransaction' | 'EarningRule' | 'TierDefinition' | 'Redemption' | 'Referral';
9
- type TenantConfig$1 = false | TenantConfig;
7
+ type TenantConfig$1 = TenantConfig | boolean;
8
+ /** @deprecated Use `ResolvedTenantConfig` from `@classytic/repo-core/tenant`. */
9
+ type ResolvedTenant = ResolvedTenantConfig;
10
10
  interface IndexDefinition {
11
11
  fields: Record<string, 1 | -1>;
12
12
  options?: Record<string, unknown> | undefined;
@@ -21,8 +21,7 @@ interface RepositoryPlugins {
21
21
  redemption?: PluginType[] | undefined;
22
22
  referral?: PluginType[] | undefined;
23
23
  }
24
- interface LoyaltyConfig {
25
- mongoose: Connection;
24
+ interface LoyaltyShape {
26
25
  tenant?: TenantConfig$1 | undefined;
27
26
  /**
28
27
  * IANA timezone name (e.g. `'Asia/Dhaka'`, `'America/New_York'`) used to
@@ -37,10 +36,22 @@ interface LoyaltyConfig {
37
36
  timezone?: string | undefined;
38
37
  program?: {
39
38
  id?: string | undefined;
40
- conversionRate?: number | undefined;
39
+ /**
40
+ * RATE: integer minor units ONE point redeems for (≥ 1) in the host's
41
+ * ambient currency. `pointValueMinor: 10` = 1 point → 10 paisa
42
+ * (৳0.10, i.e. the old `conversionRate: 10` "10 points per ৳1").
43
+ * Replaces `conversionRate` (0.5.x), whose implicit per-1-MAJOR-unit
44
+ * basis was ambiguous.
45
+ */
46
+ pointValueMinor?: number | undefined;
41
47
  } | undefined;
42
48
  redemption?: {
43
49
  minPoints?: number | undefined;
50
+ /**
51
+ * Minimum order total eligible for redemption — **integer minor
52
+ * units** (paisa for BDT). Compared against `validate()`'s
53
+ * `orderTotal` (also minor units).
54
+ */
44
55
  minOrderAmount?: number | undefined;
45
56
  maxRedeemPercent?: number | undefined;
46
57
  reservationTtlMinutes?: number | undefined;
@@ -85,15 +96,18 @@ interface LoyaltyConfig {
85
96
  /**
86
97
  * When true, existing Mongoose models with loyalty's names are deleted
87
98
  * from the connection before re-registering. Hot-reload / test fixtures
88
- * only. Default `false` — collision throws `LoyaltyModelCollisionError`.
99
+ * only. Default `false` — collision throws mongokit's `ModelCollisionError`.
89
100
  * Hosts that need two loyalty engines should use two Mongoose connections
90
101
  * (`mongoose.createConnection(...)`). See PACKAGE_RULES.md §21.
91
102
  */
92
103
  forceRecreate?: boolean | undefined;
104
+ }
105
+ interface LoyaltyRuntime {
93
106
  /**
94
- * Arc-compatible event transport. When omitted, the engine uses
95
- * `InProcessLoyaltyBus` — a ~60-line structural match of arc's
96
- * `MemoryEventTransport`. See PACKAGE_RULES §13.
107
+ * Arc-compatible event transport. When omitted, the engine creates (and
108
+ * OWNS, so `close()` closes) an `InProcessLoyaltyBus` — a ~60-line structural
109
+ * match of arc's `MemoryEventTransport`. A host-supplied transport is the
110
+ * host's to close. See PACKAGE_RULES §13.
97
111
  */
98
112
  eventTransport?: EventTransport | undefined;
99
113
  /**
@@ -120,19 +134,24 @@ interface LoyaltyConfig {
120
134
  * "Runtime capabilities".
121
135
  */
122
136
  allowNonTransactional?: boolean | undefined;
123
- }
124
- interface ResolvedTenant {
125
- enabled: boolean;
126
- tenantField: string;
127
- ref?: string;
128
- fieldType: TenantFieldType;
129
- contextKey: string;
137
+ /**
138
+ * Where the engine's boot warnings go — today, the `allowNonTransactional`
139
+ * downgrade notice. Defaults to `console`. A downgrade that carries
140
+ * partial-write risk on stored value must be VISIBLE, and a host that routes
141
+ * logs elsewhere would otherwise never see it.
142
+ */
143
+ logger?: {
144
+ warn?(message: string, ...args: unknown[]): void;
145
+ error?(message: string, ...args: unknown[]): void;
146
+ } | undefined;
130
147
  }
131
148
  interface ResolvedConfig {
132
149
  programId: string;
133
- conversionRate: number;
150
+ /** Integer minor units one point redeems for (≥ 1). See `LoyaltyShape.program.pointValueMinor`. */
151
+ pointValueMinor: number;
134
152
  redemption: {
135
153
  minPoints: number;
154
+ /** Integer minor units. */
136
155
  minOrderAmount: number;
137
156
  maxRedeemPercent: number;
138
157
  reservationTtlMinutes: number;
@@ -153,7 +172,7 @@ interface ResolvedConfig {
153
172
  };
154
173
  /** Resolved IANA zone for wall-clock rule evaluation; `undefined` = UTC. */
155
174
  timezone?: string;
156
- tenant: ResolvedTenant;
175
+ tenant: ResolvedTenantConfig;
157
176
  }
158
177
  //#endregion
159
- export { PluginType as a, ResolvedTenant as c, ModelName as i, TenantConfig$1 as l, LoyaltyConfig as n, RepositoryPlugins as o, LoyaltyModelName as r, ResolvedConfig as s, IndexDefinition as t };
178
+ export { ModelName as a, ResolvedConfig as c, LoyaltyShape as i, ResolvedTenant as l, LoyaltyModelName as n, PluginType as o, LoyaltyRuntime as r, RepositoryPlugins as s, IndexDefinition as t, TenantConfig$1 as u };
@@ -1,6 +1,4 @@
1
1
  //#region src/constants.ts
2
- const DEFAULT_TENANT_FIELD = "organizationId";
3
- const DEFAULT_TENANT_TYPE = "objectId";
4
2
  const DEFAULT_PROGRAM_ID = "default";
5
3
  const TRANSACTION_TYPES = Object.freeze([
6
4
  "earn",
@@ -47,4 +45,4 @@ const EARNING_ACTIONS = Object.freeze([
47
45
  "profile_complete"
48
46
  ]);
49
47
  //#endregion
50
- export { MEMBER_STATUSES as a, RULE_STATUSES as c, EARNING_ACTIONS as i, RULE_TYPES as l, DEFAULT_TENANT_FIELD as n, REDEMPTION_STATUSES as o, DEFAULT_TENANT_TYPE as r, REFERRAL_STATUSES as s, DEFAULT_PROGRAM_ID as t, TRANSACTION_TYPES as u };
48
+ export { REFERRAL_STATUSES as a, TRANSACTION_TYPES as c, REDEMPTION_STATUSES as i, EARNING_ACTIONS as n, RULE_STATUSES as o, MEMBER_STATUSES as r, RULE_TYPES as s, DEFAULT_PROGRAM_ID as t };
@@ -1,2 +1,2 @@
1
- import { a as RuleStatus, c as EARNING_ACTIONS, d as REFERRAL_STATUSES, f as RULE_STATUSES, i as ReferralStatus, l as MEMBER_STATUSES, m as TRANSACTION_TYPES, n as MemberStatus, o as RuleType, p as RULE_TYPES, r as RedemptionStatus, s as TransactionType, t as EarningAction, u as REDEMPTION_STATUSES } from "../../index-DMK8a98J.mjs";
1
+ import { a as RuleStatus, c as EARNING_ACTIONS, d as REFERRAL_STATUSES, f as RULE_STATUSES, i as ReferralStatus, l as MEMBER_STATUSES, m as TRANSACTION_TYPES, n as MemberStatus, o as RuleType, p as RULE_TYPES, r as RedemptionStatus, s as TransactionType, t as EarningAction, u as REDEMPTION_STATUSES } from "../../index-D_Oe3aDk.mjs";
2
2
  export { EARNING_ACTIONS, EarningAction, MEMBER_STATUSES, MemberStatus, REDEMPTION_STATUSES, REFERRAL_STATUSES, RULE_STATUSES, RULE_TYPES, RedemptionStatus, ReferralStatus, RuleStatus, RuleType, TRANSACTION_TYPES, TransactionType };
@@ -1,2 +1,2 @@
1
- import { a as MEMBER_STATUSES, c as RULE_STATUSES, i as EARNING_ACTIONS, l as RULE_TYPES, o as REDEMPTION_STATUSES, s as REFERRAL_STATUSES, u as TRANSACTION_TYPES } from "../../constants-DkQYkOK1.mjs";
1
+ import { a as REFERRAL_STATUSES, c as TRANSACTION_TYPES, i as REDEMPTION_STATUSES, n as EARNING_ACTIONS, o as RULE_STATUSES, r as MEMBER_STATUSES, s as RULE_TYPES } from "../../constants-DOPBh6kt.mjs";
2
2
  export { EARNING_ACTIONS, MEMBER_STATUSES, REDEMPTION_STATUSES, REFERRAL_STATUSES, RULE_STATUSES, RULE_TYPES, TRANSACTION_TYPES };
@@ -1,2 +1,2 @@
1
- import { A as PointsExpired, B as ReferralRewarded, C as MemberDeactivated, D as MultiTierReferralAwarded, E as MemberSuspended, F as RedemptionReserved, G as TierUpgraded, H as TierGraceStarted, I as RedemptionReversed, J as InProcessLoyaltyBusOptions, K as loyaltyEventDefinitions, L as ReferralApproved, M as PointsRedeemed, N as RedemptionConfirmed, O as PointsAdjusted, P as RedemptionReleased, R as ReferralRecorded, S as LoyaltyEventSchema, T as MemberReactivated, U as TierOverrideCleared, V as TierDowngraded, W as TierOverrideExpired, Y as createEvent, _ as TierChangedPayload, a as LoyaltyEventMap, b as LoyaltyEventDefinition, c as LoyaltyEvents, d as PointsAdjustedPayload, f as PointsEarnedPayload, g as ReferralPayload, h as RedemptionReversedPayload, i as CampaignAwardedPayload, j as PointsExpiringSoon, k as PointsEarned, l as MemberLifecyclePayload, m as RedemptionPayload, n as EventHandler, o as LoyaltyEventName, p as PointsExpiredPayload, q as InProcessLoyaltyBus, r as EventTransport, s as LoyaltyEventPayload, t as DomainEvent, u as MultiTierReferralAwardedPayload, v as TierOverrideLifecyclePayload, w as MemberEnrolled, x as LoyaltyEventPayloadOf, y as CampaignAwarded, z as ReferralRejected } from "../index-D_5Ojc2c.mjs";
2
- export { CampaignAwarded, CampaignAwardedPayload, DomainEvent, EventHandler, EventTransport, InProcessLoyaltyBus, InProcessLoyaltyBusOptions, LoyaltyEventDefinition, LoyaltyEventMap, LoyaltyEventName, LoyaltyEventPayload, LoyaltyEventPayloadOf, LoyaltyEventSchema, LoyaltyEvents, MemberDeactivated, MemberEnrolled, MemberLifecyclePayload, MemberReactivated, MemberSuspended, MultiTierReferralAwarded, MultiTierReferralAwardedPayload, PointsAdjusted, PointsAdjustedPayload, PointsEarned, PointsEarnedPayload, PointsExpired, PointsExpiredPayload, PointsExpiringSoon, PointsRedeemed, RedemptionConfirmed, RedemptionPayload, RedemptionReleased, RedemptionReserved, RedemptionReversed, RedemptionReversedPayload, ReferralApproved, ReferralPayload, ReferralRecorded, ReferralRejected, ReferralRewarded, TierChangedPayload, TierDowngraded, TierGraceStarted, TierOverrideCleared, TierOverrideExpired, TierOverrideLifecyclePayload, TierUpgraded, createEvent, loyaltyEventDefinitions };
1
+ import { A as PointsExpired, B as ReferralRewarded, C as MemberDeactivated, D as MultiTierReferralAwarded, E as MemberSuspended, F as RedemptionReserved, G as TierUpgraded, H as TierGraceStarted, I as RedemptionReversed, J as InProcessLoyaltyBusOptions, K as loyaltyEventDefinitions, L as ReferralApproved, M as PointsRedeemed, N as RedemptionConfirmed, O as PointsAdjusted, P as RedemptionReleased, R as ReferralRecorded, S as LoyaltyEventSchema, T as MemberReactivated, U as TierOverrideCleared, V as TierDowngraded, W as TierOverrideExpired, Y as createEvent, _ as TierChangedPayload, a as LoyaltyEventMap, b as LoyaltyEventDefinition, c as LoyaltyEvents, d as PointsAdjustedPayload, f as PointsEarnedPayload, g as ReferralPayload, h as RedemptionReversedPayload, i as CampaignAwardedPayload, j as PointsExpiringSoon, k as PointsEarned, l as MemberLifecyclePayload, m as RedemptionPayload, n as EventHandler, o as LoyaltyEventName, p as PointsExpiredPayload, q as InProcessLoyaltyBus, r as EventTransport, s as LoyaltyEventPayload, t as DomainEvent, u as MultiTierReferralAwardedPayload, v as TierOverrideLifecyclePayload, w as MemberEnrolled, x as LoyaltyEventPayloadOf, y as CampaignAwarded, z as ReferralRejected } from "../index-YUFamhDk.mjs";
2
+ export { CampaignAwarded, type CampaignAwardedPayload, type DomainEvent, type EventHandler, type EventTransport, InProcessLoyaltyBus, type InProcessLoyaltyBusOptions, type LoyaltyEventDefinition, type LoyaltyEventMap, type LoyaltyEventName, type LoyaltyEventPayload, type LoyaltyEventPayloadOf, type LoyaltyEventSchema, LoyaltyEvents, MemberDeactivated, MemberEnrolled, type MemberLifecyclePayload, MemberReactivated, MemberSuspended, MultiTierReferralAwarded, type MultiTierReferralAwardedPayload, PointsAdjusted, type PointsAdjustedPayload, PointsEarned, type PointsEarnedPayload, PointsExpired, type PointsExpiredPayload, PointsExpiringSoon, PointsRedeemed, RedemptionConfirmed, type RedemptionPayload, RedemptionReleased, RedemptionReserved, RedemptionReversed, type RedemptionReversedPayload, ReferralApproved, type ReferralPayload, ReferralRecorded, ReferralRejected, ReferralRewarded, type TierChangedPayload, TierDowngraded, TierGraceStarted, TierOverrideCleared, TierOverrideExpired, type TierOverrideLifecyclePayload, TierUpgraded, createEvent, loyaltyEventDefinitions };
@@ -1,3 +1,3 @@
1
- import { C as TierOverrideExpired, E as InProcessLoyaltyBus, S as TierOverrideCleared, T as loyaltyEventDefinitions, _ as ReferralRecorded, a as MemberSuspended, b as TierDowngraded, c as PointsEarned, d as PointsRedeemed, f as RedemptionConfirmed, g as ReferralApproved, h as RedemptionReversed, i as MemberReactivated, l as PointsExpired, m as RedemptionReserved, n as MemberDeactivated, o as MultiTierReferralAwarded, p as RedemptionReleased, r as MemberEnrolled, s as PointsAdjusted, t as CampaignAwarded, u as PointsExpiringSoon, v as ReferralRejected, w as TierUpgraded, x as TierGraceStarted, y as ReferralRewarded } from "../events-DN5_p6T0.mjs";
2
- import { n as createEvent, t as LoyaltyEvents } from "../loyalty-events-C38JA3cU.mjs";
1
+ import { C as TierOverrideExpired, E as InProcessLoyaltyBus, S as TierOverrideCleared, T as loyaltyEventDefinitions, _ as ReferralRecorded, a as MemberSuspended, b as TierDowngraded, c as PointsEarned, d as PointsRedeemed, f as RedemptionConfirmed, g as ReferralApproved, h as RedemptionReversed, i as MemberReactivated, l as PointsExpired, m as RedemptionReserved, n as MemberDeactivated, o as MultiTierReferralAwarded, p as RedemptionReleased, r as MemberEnrolled, s as PointsAdjusted, t as CampaignAwarded, u as PointsExpiringSoon, v as ReferralRejected, w as TierUpgraded, x as TierGraceStarted, y as ReferralRewarded } from "../events-BLjg3uLA.mjs";
2
+ import { n as createEvent, t as LoyaltyEvents } from "../loyalty-events-DXirolM_.mjs";
3
3
  export { CampaignAwarded, InProcessLoyaltyBus, LoyaltyEvents, MemberDeactivated, MemberEnrolled, MemberReactivated, MemberSuspended, MultiTierReferralAwarded, PointsAdjusted, PointsEarned, PointsExpired, PointsExpiringSoon, PointsRedeemed, RedemptionConfirmed, RedemptionReleased, RedemptionReserved, RedemptionReversed, ReferralApproved, ReferralRecorded, ReferralRejected, ReferralRewarded, TierDowngraded, TierGraceStarted, TierOverrideCleared, TierOverrideExpired, TierUpgraded, createEvent, loyaltyEventDefinitions };
@@ -1,35 +1,26 @@
1
- import { t as LoyaltyEvents } from "./loyalty-events-C38JA3cU.mjs";
2
- import { createEvent, matchEventPattern } from "@classytic/primitives/events";
1
+ import { t as LoyaltyEvents } from "./loyalty-events-DXirolM_.mjs";
2
+ import { InProcessEventBus } from "@classytic/primitives/event-infra";
3
+ import { createEvent } from "@classytic/primitives/events";
3
4
  import { z } from "zod";
4
5
  //#region src/events/in-process-bus.ts
5
- var InProcessLoyaltyBus = class {
6
- name = "in-process-loyalty";
7
- handlers = /* @__PURE__ */ new Map();
8
- logger;
9
- constructor(options) {
10
- this.logger = options?.logger ?? console;
11
- }
12
- async publish(event) {
13
- const matched = /* @__PURE__ */ new Set();
14
- for (const [pattern, set] of this.handlers.entries()) if (matchEventPattern(pattern, event.type)) for (const h of set) matched.add(h);
15
- for (const handler of matched) try {
16
- await handler(event);
17
- } catch (err) {
18
- this.logger.error(`[InProcessLoyaltyBus] Handler error for ${event.type}:`, err);
19
- }
20
- }
21
- async subscribe(pattern, handler) {
22
- if (!this.handlers.has(pattern)) this.handlers.set(pattern, /* @__PURE__ */ new Set());
23
- this.handlers.get(pattern)?.add(handler);
24
- return () => {
25
- const set = this.handlers.get(pattern);
26
- if (!set) return;
27
- set.delete(handler);
28
- if (set.size === 0) this.handlers.delete(pattern);
29
- };
30
- }
31
- async close() {
32
- this.handlers.clear();
6
+ /**
7
+ * In-process fan-out transport — the default when the host injects none.
8
+ *
9
+ * A thin subclass of `@classytic/primitives/event-infra`'s shared
10
+ * `InProcessEventBus` (the single implementation every kernel now defaults
11
+ * to). Prior semantics preserved exactly: glob matching via
12
+ * `matchEventPattern` (exact / `*` / `prefix.*` / `prefix:*`), Set-dedup so a
13
+ * handler matched by multiple patterns fires once, per-handler error
14
+ * isolation (errors logged; default `console`), idempotent `close()`.
15
+ * Structurally identical to arc's `MemoryEventTransport`.
16
+ */
17
+ var InProcessLoyaltyBus = class extends InProcessEventBus {
18
+ constructor(options = {}) {
19
+ super({
20
+ name: "in-process-loyalty",
21
+ logLabel: "loyalty",
22
+ ...options
23
+ });
33
24
  }
34
25
  };
35
26
  //#endregion
@@ -99,7 +90,7 @@ const redemptionSchema = z.object({
99
90
  externalId: z.string(),
100
91
  redemptionId: z.string(),
101
92
  points: z.number(),
102
- discountAmount: z.number(),
93
+ discountAmount: z.number().int().nonnegative(),
103
94
  ownerType: z.string(),
104
95
  ownerId: z.string()
105
96
  });
@@ -1,14 +1,14 @@
1
- import { i as ReferralStatus, r as RedemptionStatus } from "./index-DMK8a98J.mjs";
2
- import { S as EarningRule, _ as TierEvaluationBatch, d as EarningResult, f as ExpirationResult, g as TierEvaluation, h as RedemptionValidation, o as LoyaltyContext, v as PointTransaction, x as TierDefinition, y as Member } from "./index-YSsi4e_G.mjs";
3
- import { t as NotificationAdapter } from "./notification.port-CJ9nXj0k.mjs";
4
- import { c as ResolvedTenant, o as RepositoryPlugins, s as ResolvedConfig } from "./config-Dtun1duw.mjs";
5
- import { LoyaltyModels } from "./models/index.mjs";
1
+ import { i as ReferralStatus, r as RedemptionStatus } from "./index-D_Oe3aDk.mjs";
2
+ import { S as EarningRule, _ as TierEvaluationBatch, d as EarningResult, f as ExpirationResult, g as TierEvaluation, h as RedemptionValidation, o as LoyaltyContext, v as PointTransaction, x as TierDefinition, y as Member } from "./index-Cp_sXNpG.mjs";
3
+ import { t as NotificationAdapter } from "./notification.port-wY4IKimz.mjs";
4
+ import { c as ResolvedConfig, s as RepositoryPlugins } from "./config-CtkGj3xZ.mjs";
5
+ import { n as LoyaltyModels } from "./index-D6GvuvmF.mjs";
6
6
  import { PluginType, Repository } from "@classytic/mongokit";
7
- import { EventTransport } from "@classytic/primitives/events";
7
+ import { ResolvedTenantConfig } from "@classytic/repo-core/tenant";
8
8
  import { ClientSession, Model } from "mongoose";
9
+ import { EventTransport } from "@classytic/primitives/events";
9
10
  import { OutboxStore } from "@classytic/primitives/outbox";
10
11
  import { ApprovalChain } from "@classytic/primitives/approval";
11
-
12
12
  //#region src/domain/ports/unit-of-work.port.d.ts
13
13
  interface UnitOfWork {
14
14
  withTransaction<T>(callback: (session: ClientSession) => Promise<T>): Promise<T>;
@@ -138,12 +138,20 @@ declare class EarningRuleRepository extends Repository<EarningRule> {
138
138
  constructor(model: Model<EarningRule>, plugins?: PluginType[]);
139
139
  private applyCreateDefaults;
140
140
  /**
141
- * Auto-injects `programId` from `LoyaltyConfig.program.id` and the tenant id
141
+ * Auto-injects `programId` from `LoyaltyShape.program.id` and the tenant id
142
142
  * from repo options when omitted. This keeps custom-program engines aligned
143
143
  * with `MemberRepository.enroll(...)`, which uses the same config default.
144
144
  */
145
145
  create(data: Parameters<Repository<EarningRule>['create']>[0], options?: Parameters<Repository<EarningRule>['create']>[1]): Promise<EarningRule>;
146
146
  createMany(docs: Parameters<Repository<EarningRule>['createMany']>[0], options?: Parameters<Repository<EarningRule>['createMany']>[1]): Promise<EarningRule[]>;
147
+ /**
148
+ * Evaluates every active order/category/tier_bonus rule against an order.
149
+ *
150
+ * `orderTotal` and `items[].amount` are **integers in the host's minor
151
+ * currency unit** (paisa for BDT) — `50000` = ৳500. Rates on the rule
152
+ * (`amountPerPoint`, `pointsPerAmount` + `rateBasis`) are quoted in the
153
+ * same minor units.
154
+ */
147
155
  evaluateOrder(input: {
148
156
  memberId: string;
149
157
  orderId: string;
@@ -172,7 +180,19 @@ declare class EarningRuleRepository extends Repository<EarningRule> {
172
180
  * machine's `TZ` env (PACKAGE_RULES P12).
173
181
  */
174
182
  private weekday;
183
+ /**
184
+ * `orderTotal` is integer minor units. Two rate shapes:
185
+ * - `pointsPerAmount` + `rateBasis`: `points = pointsPerAmount × (orderTotal / rateBasis)`
186
+ * - `amountPerPoint` (minor units per point): `points = orderTotal / amountPerPoint`
187
+ * Rounding is applied ONCE at the end, per the rule's `roundingMode`.
188
+ */
175
189
  private calculateOrderPoints;
190
+ /**
191
+ * `pointsPerAmount` has NO implicit basis — a rule that carries it without
192
+ * an explicit integer minor-unit `rateBasis` is malformed (the schema
193
+ * enforces the pairing on write; this guards legacy/hand-written docs).
194
+ */
195
+ private requireRateBasis;
176
196
  private calculateCategoryPoints;
177
197
  private applyRounding;
178
198
  }
@@ -184,8 +204,18 @@ interface Redemption {
184
204
  programId: string;
185
205
  pointsReserved: number;
186
206
  pointsConfirmed: number;
207
+ /**
208
+ * Discount granted for the redeemed points — **integer minor units** (paisa
209
+ * for BDT). Invariant: always exactly `pointsReserved × pointValueMinor`
210
+ * (no orphaned remainder — see `RedemptionRepository.validate`).
211
+ */
187
212
  discountAmount: number;
188
- conversionRate: number;
213
+ /**
214
+ * RATE snapshot at reserve time: integer minor units ONE point redeems for
215
+ * (≥ 1). `pointValueMinor: 10` = 1 point → 10 paisa (৳0.10).
216
+ * Replaces the old `conversionRate` (points per 1 MAJOR unit).
217
+ */
218
+ pointValueMinor: number;
189
219
  status: RedemptionStatus;
190
220
  ownerType: string;
191
221
  ownerId: string;
@@ -200,11 +230,13 @@ interface Redemption {
200
230
  transactionId?: string;
201
231
  /**
202
232
  * Optional voucher-like partial-use tracking populated by hosts when
203
- * the redemption is backed by a gift voucher / reward card. When
204
- * present, `reverseConfirmed({ proportional: true })` credits back
233
+ * the redemption is backed by a gift voucher / reward card. Both are
234
+ * **integer minor units**. When present,
235
+ * `reverseConfirmed({ proportional: true })` credits back
205
236
  * `(remainingBalance / initialBalance) × pointsConfirmed`.
206
237
  */
207
238
  initialBalance?: number;
239
+ /** Integer minor units. See `initialBalance`. */
208
240
  remainingBalance?: number;
209
241
  /**
210
242
  * Optional multi-step approval chain (P7 canonical — same shape as
@@ -240,14 +272,40 @@ declare class RedemptionRepository extends Repository<Redemption> {
240
272
  private get _dispatchDeps();
241
273
  /**
242
274
  * Atomic compare-and-set status transition.
243
- * Returns null if the current status doesn't match `expectedStatus` (CAS mismatch).
275
+ *
276
+ * `null` means ONE thing and one thing only: the guarded update matched
277
+ * no document, i.e. the status was not `expectedStatus`. `throwOnNotFound:
278
+ * false` already produces that `null`, so there is nothing legitimate left
279
+ * for a `catch` to convert.
280
+ *
281
+ * There used to be a bare `} catch { return null; }` here. It laundered
282
+ * every real failure into a CAS miss, and callers map a CAS miss to
283
+ * `ValidationError("Redemption status must be 'reserved', got '<status>'")`
284
+ * — a NON-retryable 4xx built from the status read BEFORE the write. So a
285
+ * routine Mongo `WriteConflict` (a retryable transient, the normal outcome
286
+ * of two concurrent confirms) surfaced as a client error whose message
287
+ * could literally read "must be 'reserved', got 'reserved'": an operator
288
+ * chasing a contradiction while the actual fix was to retry.
289
+ *
290
+ * Anything that throws in here is a real error and must propagate.
244
291
  */
245
292
  atomicStatusTransition(id: string, expectedStatus: string, data: Record<string, unknown>, ctx?: Record<string, unknown>): Promise<Redemption | null>;
293
+ /**
294
+ * Validates a redemption request. `orderTotal` is an **integer in the
295
+ * host's minor currency unit** (paisa for BDT); the returned
296
+ * `discountAmount` is in the same minor units.
297
+ */
246
298
  validate(input: {
247
299
  memberId: string;
248
300
  pointsToRedeem: number;
249
301
  orderTotal: number;
250
302
  }, ctx: LoyaltyContext): Promise<RedemptionValidation>;
303
+ /**
304
+ * Reserves points against an order. `orderTotal` is an **integer in the
305
+ * host's minor currency unit** (paisa for BDT). The persisted
306
+ * `discountAmount` is minor units and the reserve-time `pointValueMinor`
307
+ * rate is snapshotted on the doc.
308
+ */
251
309
  reserve(input: {
252
310
  memberId: string;
253
311
  pointsToRedeem: number;
@@ -317,7 +375,16 @@ declare class ReferralRepository extends Repository<Referral> {
317
375
  createMany(docs: Parameters<Repository<Referral>['createMany']>[0], options?: Parameters<Repository<Referral>['createMany']>[1]): Promise<Referral[]>;
318
376
  /**
319
377
  * Atomic compare-and-set status transition.
320
- * Returns null if the current status doesn't match `expectedStatus`.
378
+ *
379
+ * `null` means ONE thing: the guarded update matched no document, i.e.
380
+ * the status was not `expectedStatus`. `throwOnNotFound: false` already
381
+ * returns that `null`, so the bare `} catch { return null; }` that used
382
+ * to sit here could only ever hide REAL errors — most damagingly a
383
+ * retryable Mongo `WriteConflict`, which callers then re-threw as a
384
+ * non-retryable `ValidationError` describing a status that had not
385
+ * actually gone stale.
386
+ *
387
+ * Anything that throws in here is a real error and must propagate.
321
388
  */
322
389
  atomicStatusTransition(id: string, expectedStatus: string, data: Record<string, unknown>, ctx?: Record<string, unknown>): Promise<Referral | null>;
323
390
  generateCode(memberId: string, ctx: LoyaltyContext): Promise<string>;
@@ -381,7 +448,7 @@ interface LoyaltyRepositories {
381
448
  interface CreateRepositoriesInput {
382
449
  models: LoyaltyModels;
383
450
  plugins?: RepositoryPlugins | undefined;
384
- tenant?: ResolvedTenant | undefined;
451
+ tenant?: ResolvedTenantConfig | undefined;
385
452
  config: ResolvedConfig;
386
453
  events: EventTransport;
387
454
  outbox?: OutboxStore | undefined;
@@ -1,9 +1,14 @@
1
- import { a as RuleStatus, n as MemberStatus, o as RuleType, s as TransactionType } from "./index-DMK8a98J.mjs";
1
+ import { a as RuleStatus, n as MemberStatus, o as RuleType, s as TransactionType } from "./index-D_Oe3aDk.mjs";
2
+ import "./config-CtkGj3xZ.mjs";
2
3
  import { OperationContext } from "@classytic/primitives/context";
3
4
  import { ApprovalChain } from "@classytic/primitives/approval";
4
-
5
5
  //#region src/domain/entities/earning-rule.d.ts
6
6
  interface EarningConditions {
7
+ /**
8
+ * Minimum order total for the rule to apply — **integer minor units** of the
9
+ * host's ambient currency (paisa for BDT). `10000` = ৳100. Compared against
10
+ * `evaluateOrder`'s `orderTotal` (also minor units).
11
+ */
7
12
  minOrderAmount?: number;
8
13
  categories?: string[];
9
14
  tiers?: string[];
@@ -12,7 +17,7 @@ interface EarningConditions {
12
17
  * Days the rule applies, JS `getDay()` convention: 0=Sun … 6=Sat. Empty /
13
18
  * omitted = every day. Evaluated against the wall-clock weekday in the
14
19
  * engine's configured `timezone` (UTC when unset), never the server's local
15
- * clock. See `LoyaltyConfig.timezone`.
20
+ * clock. See `LoyaltyShape.timezone`.
16
21
  */
17
22
  dayOfWeek?: number[];
18
23
  dateRange?: {
@@ -21,7 +26,24 @@ interface EarningConditions {
21
26
  };
22
27
  }
23
28
  interface EarningReward {
29
+ /**
30
+ * Points awarded per `rateBasis` minor units of spend:
31
+ * `points = pointsPerAmount × (orderTotalMinor / rateBasis)` (rounded once
32
+ * per `roundingMode`). REQUIRES `rateBasis` — there is no implicit
33
+ * "per 1 major unit" basis anymore (that ambiguity was the bug).
34
+ * "5 points per ৳100" = `{ pointsPerAmount: 5, rateBasis: 10000 }`.
35
+ */
24
36
  pointsPerAmount?: number;
37
+ /**
38
+ * RATE: the explicit integer minor-unit basis `pointsPerAmount` is quoted
39
+ * against (≥ 1). Required whenever `pointsPerAmount` is set.
40
+ */
41
+ rateBasis?: number;
42
+ /**
43
+ * RATE: integer minor units of spend per single point (≥ 1).
44
+ * `points = orderTotalMinor / amountPerPoint` (rounded per `roundingMode`).
45
+ * "1 point per ৳100" = `amountPerPoint: 10000`.
46
+ */
25
47
  amountPerPoint?: number;
26
48
  fixedPoints?: number;
27
49
  multiplier?: number;
@@ -48,6 +70,11 @@ interface EarningRule {
48
70
  //#region src/domain/entities/tier-definition.d.ts
49
71
  interface TierQualification {
50
72
  minLifetimePoints?: number;
73
+ /**
74
+ * Minimum lifetime spend — **integer minor units** (paisa for BDT).
75
+ * Declared for host-side qualification logic only: the in-package tier
76
+ * evaluation checks lifetime POINTS exclusively and never reads this field.
77
+ */
51
78
  minLifetimeSpend?: number;
52
79
  evaluationPeriodDays?: number;
53
80
  }
@@ -178,6 +205,10 @@ interface RedemptionValidation {
178
205
  valid: boolean;
179
206
  error?: string;
180
207
  pointsToRedeem: number;
208
+ /**
209
+ * Discount in **integer minor units** (paisa for BDT) — always exactly
210
+ * `pointsToRedeem × pointValueMinor`.
211
+ */
181
212
  discountAmount: number;
182
213
  maxAllowedPoints: number;
183
214
  }
@@ -278,7 +309,7 @@ interface CreateEarningRuleInput {
278
309
  * Loyalty program this rule belongs to.
279
310
  *
280
311
  * Optional for repository-created rules: `EarningRuleRepository.create(...)`
281
- * and `createMany(...)` fill this from `LoyaltyConfig.program.id`, matching
312
+ * and `createMany(...)` fill this from `LoyaltyShape.program.id`, matching
282
313
  * the default used by `MemberRepository.enroll(...)`. Pass explicitly only
283
314
  * when creating a rule for a different program.
284
315
  */