@classytic/loyalty 0.7.0 → 0.10.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/CHANGELOG.md +134 -0
- package/dist/constants-DfrENxFG.mjs +75 -0
- package/dist/domain/earn-base.d.mts +35 -0
- package/dist/domain/earn-base.mjs +37 -0
- package/dist/domain/enums/index.d.mts +2 -2
- package/dist/domain/enums/index.mjs +1 -2
- package/dist/domain/rule-schedule.d.mts +24 -0
- package/dist/domain/rule-schedule.mjs +12 -0
- package/dist/events/index.d.mts +2 -2
- package/dist/events/index.mjs +3 -3
- package/dist/{events-BLjg3uLA.mjs → events-DhZolW61.mjs} +104 -4
- package/dist/{index-BIPHUsfR.d.mts → index-BvyzKU7R.d.mts} +283 -34
- package/dist/{index-BIhVuwIZ.d.mts → index-CorB0JL6.d.mts} +255 -109
- package/dist/index-DRrEhE0r.d.mts +604 -0
- package/dist/{index-YUFamhDk.d.mts → index-Db5Gb7x_.d.mts} +187 -39
- package/dist/index-tjMMmPzU.d.mts +36 -0
- package/dist/index.d.mts +121 -30
- package/dist/index.mjs +59 -10
- package/dist/{loyalty-events-DXirolM_.mjs → loyalty-events-j29B66YW.mjs} +20 -13
- package/dist/models/index.d.mts +2 -2
- package/dist/models/index.mjs +1 -2
- package/dist/{models-weWqS5Bc.mjs → models-BEGLduz1.mjs} +276 -7
- package/dist/notifications/index.d.mts +3 -4
- package/dist/repositories/index.d.mts +2 -2
- package/dist/repositories/index.mjs +1 -2
- package/dist/{repositories-DErtHQou.mjs → repositories-ER68JmUo.mjs} +1161 -99
- package/dist/types/index.d.mts +2 -3
- package/package.json +39 -22
- package/dist/config-CtkGj3xZ.d.mts +0 -178
- package/dist/constants-DOPBh6kt.mjs +0 -48
- package/dist/index-Cp_sXNpG.d.mts +0 -349
- package/dist/index-D_Oe3aDk.d.mts +0 -19
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,140 @@
|
|
|
3
3
|
Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
4
4
|
adhering to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
5
5
|
|
|
6
|
+
## 0.10.0 — 2026-10-18
|
|
7
|
+
|
|
8
|
+
> 0.8.0 and 0.9.0 were never published: their changes (below) ship in this release.
|
|
9
|
+
|
|
10
|
+
### Changed — peer floors
|
|
11
|
+
|
|
12
|
+
`@classytic/mongokit` `>=3.41.0`, `@classytic/primitives` `>=0.32.0`, `@classytic/repo-core` `>=0.31.0`, `@classytic/validation` `>=0.6.0`, `mongoose` `>=9.11.0`.
|
|
13
|
+
|
|
14
|
+
### Added — stamp cards ("buy 9, the 10th is free")
|
|
15
|
+
- Three models (`LoyaltyStampProgram`, `LoyaltyStampEntry`, `LoyaltyStampAward`) and three
|
|
16
|
+
repositories: `stampProgram` (CRUD), `stamps` (`recordOrder`, `settle`, `reverseOrder`,
|
|
17
|
+
`progress`), `stampAward` (reads).
|
|
18
|
+
- A program counts `order`, `visit` (one per business day in `timezone`), `item` (quantity) or
|
|
19
|
+
`spend` (`perMinor`), optionally narrowed by category slugs, products, branches and a window;
|
|
20
|
+
stamps may expire (`stampValidityDays`); awards repeat until `maxRewardsPerMember`.
|
|
21
|
+
- Every guard is an index or a CAS: one entry per (program, member, order), one visit per day,
|
|
22
|
+
one award per sequence, each entry's `consumed` count claimed in the award's unit of work.
|
|
23
|
+
- Rewards: points (earned with source `campaign` — a grant with no single sale behind it, so
|
|
24
|
+
`loyalty.points.earned` keeps its enum) or a voucher through the new REQUIRED-when-used
|
|
25
|
+
`LoyaltyRuntime.rewardIssuer` port, idempotent on `awardKey`. A voucher award is claimed
|
|
26
|
+
`pending` and issued after commit; `settle` resumes a pending one.
|
|
27
|
+
- `reverseOrder` removes an undone order's unconsumed stamps; stamps already in an award are kept
|
|
28
|
+
and reported (`retainedInAwards`).
|
|
29
|
+
- Refused at write: an empty allow-list, `spend` without `perMinor`, a window that ends before it
|
|
30
|
+
starts, and a voucher reward on an engine bound without `rewardIssuer`.
|
|
31
|
+
`stampProgramInputSchema` / `stampProgramPatchSchema` are the strict wire bodies.
|
|
32
|
+
- `StampProgram`, `StampEntry` and `StampAward` type their stored ids as `Types.ObjectId` (what a
|
|
33
|
+
read returns); the progress, record and reversal results and event payloads stay strings.
|
|
34
|
+
- Events: `loyalty.stamp.earned`, `loyalty.stamp.reversed`, `loyalty.stamp.award_issued`.
|
|
35
|
+
|
|
36
|
+
### Added — `item` stamps on lines sold by measure
|
|
37
|
+
`StampOrderLine.quantityStep` (optional; absent = 1). A line whose step is not a whole number (2.35 kg
|
|
38
|
+
of rice on a 0.001 kg step) earns ONE `item` stamp, never `floor(kg)` or the gram count; whole-unit
|
|
39
|
+
lines still count units. `recordOrder` refuses a fractional quantity on a line that declares no
|
|
40
|
+
step (it used to be floored) and a non-positive step. Pinned by
|
|
41
|
+
`tests/unit/stamp-fractional-quantity.test.ts`.
|
|
42
|
+
|
|
43
|
+
## 0.8.0
|
|
44
|
+
|
|
45
|
+
### Fixed — one expiry sweep expires every due lot
|
|
46
|
+
|
|
47
|
+
`processExpirations` read ONE page of 100 due lots per call, and the hourly schedule calls it once,
|
|
48
|
+
so a burst of due lots (a year-end lifetime) expired at 2,400 a day — members kept spending points
|
|
49
|
+
already past their date. It now pages until no lot due by the call's `now` remains, in
|
|
50
|
+
`(expiresAt, _id)` order, over a new partial index `expiry_sweep` that skips expired history and
|
|
51
|
+
non-expiring rows (203 → 3 documents examined). Pinned by `tests/scenarios/point-expiry.scenario.test.ts`.
|
|
52
|
+
|
|
53
|
+
### ⚠ Changed — string-key partial indexes are `{ $gte: '' }`, so lookups by key use them
|
|
54
|
+
|
|
55
|
+
`{ $type: 'string' }` is never implied by an equality query, so lookups by point-transaction
|
|
56
|
+
`idempotencyKey`, member `cardId` and `referralCode` were collection scans. `$gte: ''` admits the
|
|
57
|
+
same rows. Renamed `idempotencyKey_str`, `cardId_str`, `referralCode_str`.
|
|
58
|
+
**Deploy:** build, then `db:retire-type-string-indexes -- --apply`.
|
|
59
|
+
|
|
60
|
+
### Changed (breaking) — every points fact carries what a ledger needs to book it
|
|
61
|
+
- `earnPoints` REQUIRES `source: 'order' | 'action' | 'referral' | 'campaign'` (`EARN_SOURCES`),
|
|
62
|
+
refused at runtime when unknown. Only `order` has a sale behind it; a consumer may defer part of
|
|
63
|
+
that sale's price, and treat every other source as a grant. Stamped on the row and on
|
|
64
|
+
`loyalty.points.earned`. The kernel's own callers set it (rules → `order` / `action`,
|
|
65
|
+
referrals → `referral`, `awardBatch` → `campaign`).
|
|
66
|
+
- `pointValueMinor` (what ONE point was worth at that moment) is stamped on earn, adjust,
|
|
67
|
+
reverse and expire rows and on their events; `loyalty.redemption.reversed` carries the
|
|
68
|
+
redemption's own rate. The RATE, not a product — a reversal has claimed/collected/shortfall
|
|
69
|
+
quantities and a consumer multiplies whichever it books.
|
|
70
|
+
- New `loyalty.points.lot_expired`, one per expired lot, dispatched INSIDE the lot's unit of
|
|
71
|
+
work — a crash can no longer leave a lot stamped expired with no event. The aggregate
|
|
72
|
+
`loyalty.points.expired` summary is unchanged.
|
|
73
|
+
- `loyalty.redemption.reversed.transactionId` is omitted when nothing was credited (was `''`,
|
|
74
|
+
which collides as a key).
|
|
75
|
+
|
|
76
|
+
### Added — `redemption.outstandingLiability(ctx)`
|
|
77
|
+
What the program owes: spendable balances plus points held by open reservations, at the current
|
|
78
|
+
point value (`LoyaltyLiabilitySnapshot`). The figure a ledger's liability reconciles against.
|
|
79
|
+
|
|
80
|
+
### Fixed — events carry string ids, as their catalog declares
|
|
81
|
+
`createEvent` converts ObjectIds in the payload to strings. An in-process subscriber received
|
|
82
|
+
ObjectIds while the outbox delivered strings, so a consumer validating against the published
|
|
83
|
+
schema refused every live delivery. Pinned by a test that parses every emitted event with its
|
|
84
|
+
own definition.
|
|
85
|
+
|
|
86
|
+
### Changed — members are placed in a tier from the start
|
|
87
|
+
`enroll` assigns the entry tier (the highest one 0 lifetime points qualifies for) unless one is
|
|
88
|
+
given — was `null` until a sweep. `evaluateMember(id, ctx, { upgradeOnly })` promotes but never
|
|
89
|
+
demotes (the earn path); `qualifiedTierName(programId, lifetime)` answers placement.
|
|
90
|
+
|
|
91
|
+
### Added — `earnBase`, the one definition of what an order earns on
|
|
92
|
+
`@classytic/loyalty/domain/earn-base` (zero imports): earning lines at the price charged,
|
|
93
|
+
minus their pro-rata share of every order discount (promo, manual, points redeemed).
|
|
94
|
+
Shipping, fee lines and tax added on top never earn; tax inside a shelf price is part of that
|
|
95
|
+
price. Returns `{ orderTotal, items }` that sum exactly (largest remainder), so an `order` rule
|
|
96
|
+
and a `category` rule read one base. Matches Smile.io / LoyaltyLion / Square / Odoo.
|
|
97
|
+
|
|
98
|
+
### Added — undo loyalty for a refunded or canceled purchase
|
|
99
|
+
|
|
100
|
+
- `pointTransaction.reverseEarned({ referenceType, referenceId, fraction, reason }, ctx)`
|
|
101
|
+
claws back `floor(earned × fraction)` of what a reference earned, minus what
|
|
102
|
+
earlier reversals claimed. Converges by state: replays and concurrent callers
|
|
103
|
+
reverse nothing extra (the idempotency key is derived from the target).
|
|
104
|
+
Points already spent are returned as `shortfall`, never driven below zero;
|
|
105
|
+
`lifetime` drops by the full claim.
|
|
106
|
+
- `redemption.undoForOwner(ownerIds, ctx, { reason })` reverses confirmed and
|
|
107
|
+
releases reserved redemptions held against any of the owner's identities.
|
|
108
|
+
- `redemption.confirmForOwner(ownerIds, ctx)` confirms holds still `reserved`
|
|
109
|
+
against an order that exists — the net for a checkout whose inline confirm
|
|
110
|
+
failed. `confirm(id, ctx, { ownerExists: true })` skips the TTL refusal for it.
|
|
111
|
+
- **Behaviour change — tier `benefits.pointsMultiplier` is applied.** It was stored
|
|
112
|
+
and never read, so a Gold ×2 member earned like ×1. `evaluateOrder` now adds
|
|
113
|
+
`floor(rulePoints × (m − 1))` for m > 1 as its own `earn` row
|
|
114
|
+
(`Tier bonus: <tier> ×m`, idempotency `<order>:tier-multiplier`), using the
|
|
115
|
+
override tier when set. Programs with multipliers configured will award more
|
|
116
|
+
from this release on. `reverseEarned` reverses it with the rule points.
|
|
117
|
+
- **Behaviour change — points expire when the program says so.** `expiry` was
|
|
118
|
+
resolved and read by nothing, so every earn had no `expiresAt` and the sweep
|
|
119
|
+
never expired anything. `earnPoints` now stamps `now + expiry.defaultDays`
|
|
120
|
+
when the caller sets none. `expiry.enabled` without a whole `defaultDays >= 1`
|
|
121
|
+
is refused at build.
|
|
122
|
+
- **Live program settings.** `LoyaltyRuntime.programSettings()` supplies the
|
|
123
|
+
operator-owned settings (point value, redemption limits, expiry) PER OPERATION
|
|
124
|
+
over the shape's values; one rule set (`assertProgram`) validates boot and live
|
|
125
|
+
values alike, and an invalid live value fails the operation. A hold snapshots
|
|
126
|
+
the rate it was priced at.
|
|
127
|
+
- **Behaviour change — redemption refuses what it used to wave through:** a 0%
|
|
128
|
+
cap ("switched off") and a cap that leaves fewer points than the minimum now
|
|
129
|
+
return `valid: false` with a reason; before they were a zero- or sub-minimum
|
|
130
|
+
"success" that `reserve` recorded.
|
|
131
|
+
- Earning rules honour their schedule: `startsAt`/`endsAt` (what the admin form
|
|
132
|
+
writes) AND `conditions.dateRange`. Before, only `dateRange` was read, so an
|
|
133
|
+
ended rule kept earning. `isRuleInSchedule` — also at the light subpath
|
|
134
|
+
`@classytic/loyalty/domain/rule-schedule` — is the one predicate for earn and preview.
|
|
135
|
+
- `LoyaltyRedemptionPort` / `LoyaltyRedemption` / `LoyaltyReserveInput` — the
|
|
136
|
+
checkout contract, owned here so till and web pipelines `import type` one shape.
|
|
137
|
+
- Ledger type `reverse` (`TRANSACTION_TYPES` now has 7 values) and event
|
|
138
|
+
`loyalty.points.reversed` (`PointsReversed` in the catalog).
|
|
139
|
+
|
|
6
140
|
## 0.7.0
|
|
7
141
|
|
|
8
142
|
### Fixed — a `UnitOfWork` yielding a non-transactional session ran the body anyway
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
//#region src/constants.ts
|
|
2
|
+
const DEFAULT_PROGRAM_ID = "default";
|
|
3
|
+
const TRANSACTION_TYPES = Object.freeze([
|
|
4
|
+
"earn",
|
|
5
|
+
"redeem",
|
|
6
|
+
"adjust",
|
|
7
|
+
"expire",
|
|
8
|
+
"transfer",
|
|
9
|
+
"referral_bonus",
|
|
10
|
+
"reverse"
|
|
11
|
+
]);
|
|
12
|
+
/**
|
|
13
|
+
* Why points were EARNED — a closed set, required on every earn. `order` is points
|
|
14
|
+
* granted with a sale (a consumer defers part of that sale's price); every other source
|
|
15
|
+
* is a grant with no sale behind it.
|
|
16
|
+
*/
|
|
17
|
+
const EARN_SOURCES = Object.freeze([
|
|
18
|
+
"order",
|
|
19
|
+
"action",
|
|
20
|
+
"referral",
|
|
21
|
+
"campaign"
|
|
22
|
+
]);
|
|
23
|
+
const MEMBER_STATUSES = Object.freeze([
|
|
24
|
+
"active",
|
|
25
|
+
"inactive",
|
|
26
|
+
"suspended"
|
|
27
|
+
]);
|
|
28
|
+
const RULE_TYPES = Object.freeze([
|
|
29
|
+
"order",
|
|
30
|
+
"action",
|
|
31
|
+
"category",
|
|
32
|
+
"tier_bonus"
|
|
33
|
+
]);
|
|
34
|
+
const RULE_STATUSES = Object.freeze([
|
|
35
|
+
"active",
|
|
36
|
+
"paused",
|
|
37
|
+
"expired"
|
|
38
|
+
]);
|
|
39
|
+
const REFERRAL_STATUSES = Object.freeze([
|
|
40
|
+
"pending",
|
|
41
|
+
"approved",
|
|
42
|
+
"rejected",
|
|
43
|
+
"expired"
|
|
44
|
+
]);
|
|
45
|
+
const REDEMPTION_STATUSES = Object.freeze([
|
|
46
|
+
"reserved",
|
|
47
|
+
"confirmed",
|
|
48
|
+
"released",
|
|
49
|
+
"expired",
|
|
50
|
+
"reversed"
|
|
51
|
+
]);
|
|
52
|
+
const EARNING_ACTIONS = Object.freeze([
|
|
53
|
+
"signup",
|
|
54
|
+
"referral",
|
|
55
|
+
"birthday",
|
|
56
|
+
"review",
|
|
57
|
+
"profile_complete"
|
|
58
|
+
]);
|
|
59
|
+
const STAMP_PROGRAM_STATUSES = Object.freeze([
|
|
60
|
+
"active",
|
|
61
|
+
"paused",
|
|
62
|
+
"ended"
|
|
63
|
+
]);
|
|
64
|
+
/** What one stamp counts: a qualifying order, a business day with one, an item, or `perMinor` of spend. */
|
|
65
|
+
const STAMP_EARN_UNITS = Object.freeze([
|
|
66
|
+
"order",
|
|
67
|
+
"visit",
|
|
68
|
+
"item",
|
|
69
|
+
"spend"
|
|
70
|
+
]);
|
|
71
|
+
const STAMP_REWARD_KINDS = Object.freeze(["points", "voucher"]);
|
|
72
|
+
/** `pending` = claimed (stamps consumed) but the external reward not yet issued; settle resumes it. */
|
|
73
|
+
const STAMP_AWARD_STATUSES = Object.freeze(["pending", "issued"]);
|
|
74
|
+
//#endregion
|
|
75
|
+
export { REDEMPTION_STATUSES as a, RULE_TYPES as c, STAMP_PROGRAM_STATUSES as d, STAMP_REWARD_KINDS as f, MEMBER_STATUSES as i, STAMP_AWARD_STATUSES as l, EARNING_ACTIONS as n, REFERRAL_STATUSES as o, TRANSACTION_TYPES as p, EARN_SOURCES as r, RULE_STATUSES as s, DEFAULT_PROGRAM_ID as t, STAMP_EARN_UNITS as u };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//#region src/domain/earn-base.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The amount an order earns on — ONE definition for the award, the preview and
|
|
4
|
+
* every host: earning lines at the price charged, minus their pro-rata share of
|
|
5
|
+
* every order discount (promo, manual, points redeemed). Shipping, fee lines and
|
|
6
|
+
* tax added on top never earn; tax inside a shelf price is part of that price.
|
|
7
|
+
*
|
|
8
|
+
* Integer minor units in and out. Zero imports, so a client can share it.
|
|
9
|
+
*/
|
|
10
|
+
export interface EarnBaseLine {
|
|
11
|
+
/** What the line was charged before order-level discounts (`lineTotal`), minor units. */
|
|
12
|
+
amount: number;
|
|
13
|
+
/** False for a line that is not merchandise — a service fee, a surcharge. */
|
|
14
|
+
earns: boolean;
|
|
15
|
+
quantity?: number;
|
|
16
|
+
categoryId?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface EarnBaseInput {
|
|
19
|
+
lines: readonly EarnBaseLine[];
|
|
20
|
+
/** Every order-level discount, minor units — spread over ALL lines, fees included. */
|
|
21
|
+
discount: number;
|
|
22
|
+
}
|
|
23
|
+
export interface EarnBaseItem {
|
|
24
|
+
amount: number;
|
|
25
|
+
quantity: number;
|
|
26
|
+
categoryId?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface EarnBase {
|
|
29
|
+
/** Σ `items[].amount` — what an `order` rule reads. */
|
|
30
|
+
orderTotal: number;
|
|
31
|
+
/** Per earning line, net of its discount share — what a `category` rule reads. */
|
|
32
|
+
items: EarnBaseItem[];
|
|
33
|
+
}
|
|
34
|
+
export declare function earnBase({ lines, discount }: EarnBaseInput): EarnBase;
|
|
35
|
+
//#endregion
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
//#region src/domain/earn-base.ts
|
|
2
|
+
const whole = (n) => Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
|
|
3
|
+
function earnBase({ lines, discount }) {
|
|
4
|
+
const priced = lines.map((l) => ({
|
|
5
|
+
...l,
|
|
6
|
+
amount: whole(l.amount)
|
|
7
|
+
}));
|
|
8
|
+
const gross = priced.reduce((s, l) => s + l.amount, 0);
|
|
9
|
+
const off = Math.min(whole(discount), gross);
|
|
10
|
+
const earning = priced.filter((l) => l.earns && l.amount > 0);
|
|
11
|
+
if (gross === 0 || earning.length === 0) return {
|
|
12
|
+
orderTotal: 0,
|
|
13
|
+
items: []
|
|
14
|
+
};
|
|
15
|
+
const earningGross = earning.reduce((s, l) => s + l.amount, 0);
|
|
16
|
+
const target = earningGross - Math.round(off * earningGross / gross);
|
|
17
|
+
const shares = earning.map((l) => l.amount * target / earningGross);
|
|
18
|
+
const nets = shares.map(Math.floor);
|
|
19
|
+
let left = target - nets.reduce((s, n) => s + n, 0);
|
|
20
|
+
const byRemainder = shares.map((s, i) => [s - Math.floor(s), i]).sort((a, b) => b[0] - a[0]);
|
|
21
|
+
for (const [, i] of byRemainder) {
|
|
22
|
+
if (left <= 0) break;
|
|
23
|
+
nets[i] = (nets[i] ?? 0) + 1;
|
|
24
|
+
left -= 1;
|
|
25
|
+
}
|
|
26
|
+
const items = earning.map((l, i) => ({
|
|
27
|
+
amount: nets[i] ?? 0,
|
|
28
|
+
quantity: l.quantity ?? 1,
|
|
29
|
+
...l.categoryId ? { categoryId: l.categoryId } : {}
|
|
30
|
+
})).filter((it) => it.amount > 0);
|
|
31
|
+
return {
|
|
32
|
+
orderTotal: items.reduce((s, it) => s + it.amount, 0),
|
|
33
|
+
items
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
export { earnBase };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
export { EARNING_ACTIONS, EarningAction, MEMBER_STATUSES, MemberStatus, REDEMPTION_STATUSES, REFERRAL_STATUSES, RULE_STATUSES, RULE_TYPES, RedemptionStatus, ReferralStatus, RuleStatus, RuleType, TRANSACTION_TYPES, TransactionType };
|
|
1
|
+
import { C as STAMP_REWARD_KINDS, S as STAMP_PROGRAM_STATUSES, _ as REFERRAL_STATUSES, a as ReferralStatus, b as STAMP_AWARD_STATUSES, c as StampAwardStatus, d as StampRewardKind, f as TransactionType, g as REDEMPTION_STATUSES, h as MEMBER_STATUSES, i as RedemptionStatus, l as StampEarnUnit, m as EARN_SOURCES, n as EarningAction, o as RuleStatus, p as EARNING_ACTIONS, r as MemberStatus, s as RuleType, t as EarnSource, u as StampProgramStatus, v as RULE_STATUSES, w as TRANSACTION_TYPES, x as STAMP_EARN_UNITS, y as RULE_TYPES } from "../../index-tjMMmPzU.mjs";
|
|
2
|
+
export { EARNING_ACTIONS, EARN_SOURCES, EarnSource, EarningAction, MEMBER_STATUSES, MemberStatus, REDEMPTION_STATUSES, REFERRAL_STATUSES, RULE_STATUSES, RULE_TYPES, RedemptionStatus, ReferralStatus, RuleStatus, RuleType, STAMP_AWARD_STATUSES, STAMP_EARN_UNITS, STAMP_PROGRAM_STATUSES, STAMP_REWARD_KINDS, StampAwardStatus, StampEarnUnit, StampProgramStatus, StampRewardKind, TRANSACTION_TYPES, TransactionType };
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { EARNING_ACTIONS, MEMBER_STATUSES, REDEMPTION_STATUSES, REFERRAL_STATUSES, RULE_STATUSES, RULE_TYPES, TRANSACTION_TYPES };
|
|
1
|
+
export { n as EARNING_ACTIONS, r as EARN_SOURCES, i as MEMBER_STATUSES, a as REDEMPTION_STATUSES, o as REFERRAL_STATUSES, s as RULE_STATUSES, c as RULE_TYPES, l as STAMP_AWARD_STATUSES, u as STAMP_EARN_UNITS, d as STAMP_PROGRAM_STATUSES, f as STAMP_REWARD_KINDS, p as TRANSACTION_TYPES } from "../../constants-DfrENxFG.mjs";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region src/domain/rule-schedule.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Is an earning rule live at `now`? The ONE schedule predicate — the earn path
|
|
4
|
+
* and any preview call this, so the two cannot disagree.
|
|
5
|
+
*
|
|
6
|
+
* A rule carries its window twice: top-level `startsAt`/`endsAt` (what the admin
|
|
7
|
+
* form writes) and `conditions.dateRange` (older rules, the API). It is live only
|
|
8
|
+
* inside BOTH. Ignoring the top-level pair is how an "Ends At" date never stopped
|
|
9
|
+
* a rule.
|
|
10
|
+
*/
|
|
11
|
+
type Instant = Date | string | null | undefined;
|
|
12
|
+
export interface ScheduledRuleLike {
|
|
13
|
+
startsAt?: Instant;
|
|
14
|
+
endsAt?: Instant;
|
|
15
|
+
conditions?: {
|
|
16
|
+
dateRange?: {
|
|
17
|
+
start?: Instant;
|
|
18
|
+
end?: Instant;
|
|
19
|
+
} | undefined;
|
|
20
|
+
} | undefined;
|
|
21
|
+
}
|
|
22
|
+
export declare function isRuleInSchedule(rule: ScheduledRuleLike, now: Date): boolean;
|
|
23
|
+
//#endregion
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/domain/rule-schedule.ts
|
|
2
|
+
const at = (v) => v ? new Date(v).getTime() : null;
|
|
3
|
+
function isRuleInSchedule(rule, now) {
|
|
4
|
+
const t = now.getTime();
|
|
5
|
+
const starts = [at(rule.startsAt), at(rule.conditions?.dateRange?.start)];
|
|
6
|
+
const ends = [at(rule.endsAt), at(rule.conditions?.dateRange?.end)];
|
|
7
|
+
if (starts.some((s) => s !== null && t < s)) return false;
|
|
8
|
+
if (ends.some((e) => e !== null && t > e)) return false;
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
export { isRuleInSchedule };
|
package/dist/events/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { A as
|
|
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
|
+
import { $ as TierOverrideCleared, A as MemberReactivated, B as RedemptionConfirmed, C as TierOverrideLifecyclePayload, D as LoyaltyEventSchema, E as LoyaltyEventPayloadOf, F as PointsExpired, G as ReferralRecorded, H as RedemptionReserved, I as PointsExpiringSoon, J as StampAwardIssued, K as ReferralRejected, L as PointsLotExpired, M as MultiTierReferralAwarded, N as PointsAdjusted, O as MemberDeactivated, P as PointsEarned, Q as TierGraceStarted, R as PointsRedeemed, S as TierChangedPayload, T as LoyaltyEventDefinition, U as RedemptionReversed, V as RedemptionReleased, W as ReferralApproved, X as StampReversed, Y as StampEarned, Z as TierDowngraded, _ as RedemptionReversedPayload, a as LoyaltyEventMap, at as createEvent, b as StampEarnedPayload, c as LoyaltyEvents, d as PointsAdjustedPayload, et as TierOverrideExpired, f as PointsEarnedPayload, g as RedemptionPayload, h as PointsReversedPayload, i as CampaignAwardedPayload, it as InProcessLoyaltyBusOptions, j as MemberSuspended, k as MemberEnrolled, l as MemberLifecyclePayload, m as PointsLotExpiredPayload, n as EventHandler, nt as loyaltyEventDefinitions, o as LoyaltyEventName, p as PointsExpiredPayload, q as ReferralRewarded, r as EventTransport, rt as InProcessLoyaltyBus, s as LoyaltyEventPayload, t as DomainEvent, tt as TierUpgraded, u as MultiTierReferralAwardedPayload, v as ReferralPayload, w as CampaignAwarded, x as StampReversedPayload, y as StampAwardIssuedPayload, z as PointsReversed } from "../index-Db5Gb7x_.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, PointsLotExpired, type PointsLotExpiredPayload, PointsRedeemed, PointsReversed, type PointsReversedPayload, RedemptionConfirmed, type RedemptionPayload, RedemptionReleased, RedemptionReserved, RedemptionReversed, type RedemptionReversedPayload, ReferralApproved, type ReferralPayload, ReferralRecorded, ReferralRejected, ReferralRewarded, StampAwardIssued, type StampAwardIssuedPayload, StampEarned, type StampEarnedPayload, StampReversed, type StampReversedPayload, type TierChangedPayload, TierDowngraded, TierGraceStarted, TierOverrideCleared, TierOverrideExpired, type TierOverrideLifecyclePayload, TierUpgraded, createEvent, loyaltyEventDefinitions };
|
package/dist/events/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { C as
|
|
2
|
-
import { n as createEvent, t as LoyaltyEvents } from "../loyalty-events-
|
|
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
|
+
import { A as loyaltyEventDefinitions, C as StampEarned, D as TierOverrideCleared, E as TierGraceStarted, O as TierOverrideExpired, S as StampAwardIssued, T as TierDowngraded, _ as RedemptionReversed, a as MemberSuspended, b as ReferralRejected, c as PointsEarned, d as PointsLotExpired, f as PointsRedeemed, g as RedemptionReserved, h as RedemptionReleased, i as MemberReactivated, j as InProcessLoyaltyBus, k as TierUpgraded, l as PointsExpired, m as RedemptionConfirmed, n as MemberDeactivated, o as MultiTierReferralAwarded, p as PointsReversed, r as MemberEnrolled, s as PointsAdjusted, t as CampaignAwarded, u as PointsExpiringSoon, v as ReferralApproved, w as StampReversed, x as ReferralRewarded, y as ReferralRecorded } from "../events-DhZolW61.mjs";
|
|
2
|
+
import { n as createEvent, t as LoyaltyEvents } from "../loyalty-events-j29B66YW.mjs";
|
|
3
|
+
export { CampaignAwarded, InProcessLoyaltyBus, LoyaltyEvents, MemberDeactivated, MemberEnrolled, MemberReactivated, MemberSuspended, MultiTierReferralAwarded, PointsAdjusted, PointsEarned, PointsExpired, PointsExpiringSoon, PointsLotExpired, PointsRedeemed, PointsReversed, RedemptionConfirmed, RedemptionReleased, RedemptionReserved, RedemptionReversed, ReferralApproved, ReferralRecorded, ReferralRejected, ReferralRewarded, StampAwardIssued, StampEarned, StampReversed, TierDowngraded, TierGraceStarted, TierOverrideCleared, TierOverrideExpired, TierUpgraded, createEvent, loyaltyEventDefinitions };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { r as EARN_SOURCES } from "./constants-DfrENxFG.mjs";
|
|
2
|
+
import { t as LoyaltyEvents } from "./loyalty-events-j29B66YW.mjs";
|
|
2
3
|
import { InProcessEventBus } from "@classytic/primitives/event-infra";
|
|
3
4
|
import { createEvent } from "@classytic/primitives/events";
|
|
4
5
|
import { z } from "zod";
|
|
@@ -50,11 +51,15 @@ const memberLifecycleSchema = z.object({
|
|
|
50
51
|
status: z.string()
|
|
51
52
|
});
|
|
52
53
|
/** Mirrors `PointsEarnedPayload`. */
|
|
54
|
+
/** Minor units one point was worth at the fact — integer, at least 1. */
|
|
55
|
+
const pointValue = z.number().int().min(1);
|
|
53
56
|
const pointsEarnedSchema = z.object({
|
|
54
57
|
memberId: z.string(),
|
|
55
58
|
externalId: z.string(),
|
|
56
59
|
externalType: z.string(),
|
|
57
60
|
points: z.number(),
|
|
61
|
+
source: z.enum(EARN_SOURCES),
|
|
62
|
+
pointValueMinor: pointValue,
|
|
58
63
|
balanceAfter: z.number(),
|
|
59
64
|
transactionId: z.string(),
|
|
60
65
|
earningRuleName: z.string().optional(),
|
|
@@ -66,16 +71,41 @@ const pointsAdjustedSchema = z.object({
|
|
|
66
71
|
memberId: z.string(),
|
|
67
72
|
externalId: z.string(),
|
|
68
73
|
points: z.number(),
|
|
74
|
+
pointValueMinor: pointValue,
|
|
69
75
|
balanceAfter: z.number(),
|
|
70
76
|
transactionId: z.string(),
|
|
71
77
|
description: z.string()
|
|
72
78
|
});
|
|
79
|
+
/** Mirrors `PointsReversedPayload`. */
|
|
80
|
+
const pointsReversedSchema = z.object({
|
|
81
|
+
memberId: z.string(),
|
|
82
|
+
externalId: z.string(),
|
|
83
|
+
referenceType: z.string(),
|
|
84
|
+
referenceId: z.string(),
|
|
85
|
+
claimed: z.number().int().nonnegative(),
|
|
86
|
+
collected: z.number().int().nonnegative(),
|
|
87
|
+
shortfall: z.number().int().nonnegative(),
|
|
88
|
+
pointValueMinor: pointValue,
|
|
89
|
+
balanceAfter: z.number(),
|
|
90
|
+
transactionId: z.string(),
|
|
91
|
+
reason: z.string()
|
|
92
|
+
});
|
|
73
93
|
/** Mirrors `PointsExpiredPayload` — reused by EXPIRED + EXPIRING_SOON. */
|
|
74
94
|
const pointsExpiredSchema = z.object({
|
|
75
95
|
membersAffected: z.number().int().nonnegative(),
|
|
76
96
|
pointsExpired: z.number().int().nonnegative(),
|
|
77
97
|
transactionCount: z.number().int().nonnegative()
|
|
78
98
|
});
|
|
99
|
+
/** Mirrors `PointsLotExpiredPayload`. */
|
|
100
|
+
const pointsLotExpiredSchema = z.object({
|
|
101
|
+
memberId: z.string(),
|
|
102
|
+
externalId: z.string(),
|
|
103
|
+
transactionId: z.string(),
|
|
104
|
+
lotTransactionId: z.string(),
|
|
105
|
+
points: z.number().int().positive(),
|
|
106
|
+
pointValueMinor: pointValue,
|
|
107
|
+
balanceAfter: z.number()
|
|
108
|
+
});
|
|
79
109
|
/** Mirrors `TierChangedPayload` — reused by UPGRADED + DOWNGRADED + GRACE_STARTED. */
|
|
80
110
|
const tierChangedSchema = z.object({
|
|
81
111
|
memberId: z.string(),
|
|
@@ -125,7 +155,8 @@ const redemptionReversedSchema = z.object({
|
|
|
125
155
|
originalPoints: z.number(),
|
|
126
156
|
proportional: z.boolean(),
|
|
127
157
|
reason: z.string().optional(),
|
|
128
|
-
|
|
158
|
+
pointValueMinor: pointValue,
|
|
159
|
+
transactionId: z.string().optional()
|
|
129
160
|
});
|
|
130
161
|
/** Mirrors `CampaignAwardedPayload`. */
|
|
131
162
|
const campaignAwardedSchema = z.object({
|
|
@@ -146,6 +177,45 @@ const multiTierReferralAwardedSchema = z.object({
|
|
|
146
177
|
transactionId: z.string()
|
|
147
178
|
}))
|
|
148
179
|
});
|
|
180
|
+
/** Mirrors `StampEarnedPayload`. */
|
|
181
|
+
const stampEarnedSchema = z.object({
|
|
182
|
+
stampProgramId: z.string(),
|
|
183
|
+
memberId: z.string(),
|
|
184
|
+
externalId: z.string(),
|
|
185
|
+
entryId: z.string(),
|
|
186
|
+
orderRef: z.string(),
|
|
187
|
+
stamps: z.number().int().positive(),
|
|
188
|
+
businessDay: z.string(),
|
|
189
|
+
expiresAt: z.string().optional()
|
|
190
|
+
});
|
|
191
|
+
/** Mirrors `StampReversedPayload`. */
|
|
192
|
+
const stampReversedSchema = z.object({
|
|
193
|
+
stampProgramId: z.string(),
|
|
194
|
+
memberId: z.string(),
|
|
195
|
+
entryId: z.string(),
|
|
196
|
+
orderRef: z.string(),
|
|
197
|
+
reversed: z.number().int().nonnegative(),
|
|
198
|
+
retainedInAwards: z.number().int().nonnegative()
|
|
199
|
+
});
|
|
200
|
+
/** Mirrors `StampAwardIssuedPayload`. */
|
|
201
|
+
const stampAwardIssuedSchema = z.object({
|
|
202
|
+
stampProgramId: z.string(),
|
|
203
|
+
memberId: z.string(),
|
|
204
|
+
externalId: z.string(),
|
|
205
|
+
awardId: z.string(),
|
|
206
|
+
sequence: z.number().int().positive(),
|
|
207
|
+
stampsConsumed: z.number().int().positive(),
|
|
208
|
+
reward: z.discriminatedUnion("kind", [z.object({
|
|
209
|
+
kind: z.literal("points"),
|
|
210
|
+
points: z.number().int().positive(),
|
|
211
|
+
pointTransactionId: z.string()
|
|
212
|
+
}), z.object({
|
|
213
|
+
kind: z.literal("voucher"),
|
|
214
|
+
voucherProgramId: z.string(),
|
|
215
|
+
voucherId: z.string(),
|
|
216
|
+
code: z.string()
|
|
217
|
+
})])
|
|
218
|
+
});
|
|
149
219
|
const MemberEnrolled = defineLoyaltyEvent({
|
|
150
220
|
name: LoyaltyEvents.MEMBER_ENROLLED,
|
|
151
221
|
description: "A new loyalty member was enrolled in a program.",
|
|
@@ -181,11 +251,21 @@ const PointsAdjusted = defineLoyaltyEvent({
|
|
|
181
251
|
description: "Points were manually adjusted (admin correction / override).",
|
|
182
252
|
zodSchema: pointsAdjustedSchema
|
|
183
253
|
});
|
|
254
|
+
const PointsReversed = defineLoyaltyEvent({
|
|
255
|
+
name: LoyaltyEvents.POINTS_REVERSED,
|
|
256
|
+
description: "Earned points were clawed back because the purchase that earned them was refunded or canceled.",
|
|
257
|
+
zodSchema: pointsReversedSchema
|
|
258
|
+
});
|
|
184
259
|
const PointsExpired = defineLoyaltyEvent({
|
|
185
260
|
name: LoyaltyEvents.POINTS_EXPIRED,
|
|
186
261
|
description: "A batch of points expired across one or more members.",
|
|
187
262
|
zodSchema: pointsExpiredSchema
|
|
188
263
|
});
|
|
264
|
+
const PointsLotExpired = defineLoyaltyEvent({
|
|
265
|
+
name: LoyaltyEvents.POINTS_LOT_EXPIRED,
|
|
266
|
+
description: "One lot of earned points expired — dispatched atomically with the expiry.",
|
|
267
|
+
zodSchema: pointsLotExpiredSchema
|
|
268
|
+
});
|
|
189
269
|
const PointsExpiringSoon = defineLoyaltyEvent({
|
|
190
270
|
name: LoyaltyEvents.POINTS_EXPIRING_SOON,
|
|
191
271
|
description: "Points are approaching their expiry window — host can notify members.",
|
|
@@ -266,6 +346,21 @@ const MultiTierReferralAwarded = defineLoyaltyEvent({
|
|
|
266
346
|
description: "Ancestor referrers were awarded points via a multi-tier referral chain.",
|
|
267
347
|
zodSchema: multiTierReferralAwardedSchema
|
|
268
348
|
});
|
|
349
|
+
const StampEarned = defineLoyaltyEvent({
|
|
350
|
+
name: LoyaltyEvents.STAMP_EARNED,
|
|
351
|
+
description: "An order earned stamps on a stamp-card program.",
|
|
352
|
+
zodSchema: stampEarnedSchema
|
|
353
|
+
});
|
|
354
|
+
const StampReversed = defineLoyaltyEvent({
|
|
355
|
+
name: LoyaltyEvents.STAMP_REVERSED,
|
|
356
|
+
description: "An undone order's unconsumed stamps were reversed; stamps already awarded are kept.",
|
|
357
|
+
zodSchema: stampReversedSchema
|
|
358
|
+
});
|
|
359
|
+
const StampAwardIssued = defineLoyaltyEvent({
|
|
360
|
+
name: LoyaltyEvents.STAMP_AWARD_ISSUED,
|
|
361
|
+
description: "A full stamp card was exchanged for its reward (points or a voucher).",
|
|
362
|
+
zodSchema: stampAwardIssuedSchema
|
|
363
|
+
});
|
|
269
364
|
/**
|
|
270
365
|
* Every loyalty event defined in the package — pass to Arc's
|
|
271
366
|
* `EventRegistry`. Hosts wire ONE array; the whole `loyalty.*` namespace
|
|
@@ -280,7 +375,9 @@ const loyaltyEventDefinitions = [
|
|
|
280
375
|
PointsEarned,
|
|
281
376
|
PointsRedeemed,
|
|
282
377
|
PointsAdjusted,
|
|
378
|
+
PointsReversed,
|
|
283
379
|
PointsExpired,
|
|
380
|
+
PointsLotExpired,
|
|
284
381
|
PointsExpiringSoon,
|
|
285
382
|
TierUpgraded,
|
|
286
383
|
TierDowngraded,
|
|
@@ -296,7 +393,10 @@ const loyaltyEventDefinitions = [
|
|
|
296
393
|
ReferralApproved,
|
|
297
394
|
ReferralRejected,
|
|
298
395
|
ReferralRewarded,
|
|
299
|
-
MultiTierReferralAwarded
|
|
396
|
+
MultiTierReferralAwarded,
|
|
397
|
+
StampEarned,
|
|
398
|
+
StampReversed,
|
|
399
|
+
StampAwardIssued
|
|
300
400
|
];
|
|
301
401
|
//#endregion
|
|
302
|
-
export {
|
|
402
|
+
export { loyaltyEventDefinitions as A, StampEarned as C, TierOverrideCleared as D, TierGraceStarted as E, TierOverrideExpired as O, StampAwardIssued as S, TierDowngraded as T, RedemptionReversed as _, MemberSuspended as a, ReferralRejected as b, PointsEarned as c, PointsLotExpired as d, PointsRedeemed as f, RedemptionReserved as g, RedemptionReleased as h, MemberReactivated as i, InProcessLoyaltyBus as j, TierUpgraded as k, PointsExpired as l, RedemptionConfirmed as m, MemberDeactivated as n, MultiTierReferralAwarded as o, PointsReversed as p, MemberEnrolled as r, PointsAdjusted as s, CampaignAwarded as t, PointsExpiringSoon as u, ReferralApproved as v, StampReversed as w, ReferralRewarded as x, ReferralRecorded as y };
|