@classytic/revenue 2.1.0 → 2.2.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.
Files changed (26) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/README.md +41 -10
  3. package/dist/{bank-feed-DJtLvz_7.mjs → bank-feed-ClxNob_I.mjs} +1 -1
  4. package/dist/core/state-machines.mjs +2 -2
  5. package/dist/{engine-types-txFXOiQS.d.mts → engine-types-ChFPg3kw.d.mts} +230 -80
  6. package/dist/enums/index.mjs +1 -1
  7. package/dist/{errors-Dt46UZL_.mjs → errors-Bt5NRVMq.mjs} +19 -2
  8. package/dist/{escrow.schema-C-b41z_G.mjs → escrow.schema-BcKdzrJ7.mjs} +5 -0
  9. package/dist/{escrow.schema-9yh4Q-aQ.d.mts → escrow.schema-BdDHuQ8C.d.mts} +80 -20
  10. package/dist/{event-constants-CTiDNWzc.mjs → event-constants-DM_-A57b.mjs} +7 -0
  11. package/dist/events/index.d.mts +1 -1
  12. package/dist/events/index.mjs +2 -2
  13. package/dist/index.d.mts +38 -7
  14. package/dist/index.mjs +38 -9
  15. package/dist/providers/index.mjs +1 -1
  16. package/dist/repositories/create-repositories.d.mts +1 -1
  17. package/dist/repositories/create-repositories.mjs +1 -1
  18. package/dist/{revenue-event-catalog-CgZ57M-f.mjs → revenue-event-catalog-B9aZmNpL.mjs} +90 -2
  19. package/dist/{revenue-event-catalog-JpJcyK1E.d.mts → revenue-event-catalog-BU_KYN2-.d.mts} +645 -0
  20. package/dist/{settlement.repository-Ba2U17zY.mjs → settlement.repository-CfvgX3et.mjs} +315 -123
  21. package/dist/shared/index.mjs +1 -1
  22. package/dist/validators/index.d.mts +1 -1
  23. package/dist/validators/index.mjs +1 -1
  24. package/package.json +7 -7
  25. /package/dist/{splits-D8XkNWgX.mjs → splits-CNfQj92L.mjs} +0 -0
  26. /package/dist/{subscription.enums-DoIr56O6.mjs → subscription.enums-95othr0i.mjs} +0 -0
@@ -1,43 +1,176 @@
1
1
  import { _ as TRANSACTION_STATUS, a as TRANSACTION_KIND, s as initialStatusFor } from "./bank-feed.enums-kYTLTTbe.mjs";
2
- import { n as createEvent, t as REVENUE_EVENTS } from "./event-constants-CTiDNWzc.mjs";
3
- import { g as SETTLEMENT_STATUS, r as SUBSCRIPTION_STATUS, w as HOLD_STATUS } from "./subscription.enums-DoIr56O6.mjs";
4
- import { f as SettlementNotFoundError, g as WrongTransactionKindError, h as ValidationError, m as TransactionNotFoundError, n as BankFeedImportError, p as SubscriptionNotFoundError } from "./errors-Dt46UZL_.mjs";
2
+ import { n as createEvent, t as REVENUE_EVENTS } from "./event-constants-DM_-A57b.mjs";
3
+ import { g as SETTLEMENT_STATUS, r as SUBSCRIPTION_STATUS, w as HOLD_STATUS } from "./subscription.enums-95othr0i.mjs";
4
+ import { _ as WrongTransactionKindError, g as ValidationError, h as TransactionNotFoundError, m as SubscriptionNotFoundError, n as BankFeedImportError, o as MethodKindLockedError, p as SettlementNotFoundError } from "./errors-Bt5NRVMq.mjs";
5
5
  import { SETTLEMENT_STATE_MACHINE, SUBSCRIPTION_STATE_MACHINE, TRANSACTION_STATE_MACHINE, smFor } from "./core/state-machines.mjs";
6
- import { a as reverseTax, c as reverseCommission, n as calculateSplits, s as calculateCommission, t as calculateOrganizationPayout } from "./splits-D8XkNWgX.mjs";
7
- import { Repository, withTransaction } from "@classytic/mongokit";
6
+ import { a as reverseTax, c as reverseCommission, n as calculateSplits, s as calculateCommission, t as calculateOrganizationPayout } from "./splits-CNfQj92L.mjs";
7
+ import { Repository, repoOptionsFromCtx, withTransaction } from "@classytic/mongokit";
8
8
 
9
- //#region src/repositories/transaction.repository.ts
10
- var TransactionRepository = class extends Repository {
9
+ //#region src/repositories/base.repository.ts
10
+ /**
11
+ * RevenueRepositoryBase — shared scaffolding for every revenue repo.
12
+ *
13
+ * Eliminates the three places ctx-threading and event-dispatch were
14
+ * hand-rolled (transaction / subscription / settlement) and previously
15
+ * drifted: subscription + settlement repos forgot to forward
16
+ * `ctx.organizationId` into their inner `getById` / `update` calls,
17
+ * which surfaced as `Missing 'organizationId' in context for 'getById'`
18
+ * the moment a host enabled `multiTenantPlugin` (the canonical 2026-Q2
19
+ * bug — fixed in 2.1.1).
20
+ *
21
+ * Two responsibilities:
22
+ *
23
+ * 1. **Thread request context into mongokit options.** Every domain
24
+ * verb that touches the DB (read or write) ends with a mongokit
25
+ * method whose options bag is what `multiTenantPlugin`,
26
+ * `softDeletePlugin`, the audit plugins, and `withTransaction`
27
+ * read from. {@link optsFromCtx} centralises that translation
28
+ * using mongokit's typed extractor — adding a new canonical field
29
+ * to `RevenueContext` is now a single line, not three.
30
+ *
31
+ * 2. **Dispatch domain events.** Outbox-save (session-bound when the
32
+ * caller threads a Mongoose `ClientSession`) followed by
33
+ * transport-publish, with isolated try/catch so a transport
34
+ * failure never aborts the business write. PACKAGE_RULES P8 / §5.5.
35
+ *
36
+ * Subclasses inject their domain-specific deps via {@link inject}; this
37
+ * base only requires the cross-cutting trio (events / outbox / logger)
38
+ * which every revenue repo has.
39
+ *
40
+ * @typeParam TDoc - The Mongoose document type the subclass operates on.
41
+ * @typeParam TDeps - The full deps shape (must extend `BaseRevenueRepoDeps`).
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * export class SubscriptionRepository extends RevenueRepositoryBase<
46
+ * SubscriptionDocument,
47
+ * SubscriptionRepoDeps
48
+ * > {
49
+ * async activate(id: string, ctx: RevenueContext = {}) {
50
+ * const sub = await this.getById(id, this.optsFromCtx(ctx));
51
+ * // ...
52
+ * await this.dispatch(createEvent(...), ctx);
53
+ * return updated;
54
+ * }
55
+ * }
56
+ * ```
57
+ */
58
+ /**
59
+ * Abstract base for `TransactionRepository`, `SubscriptionRepository`,
60
+ * `SettlementRepository`.
61
+ *
62
+ * Concrete subclasses MUST:
63
+ * - declare a `Deps` interface that extends {@link BaseRevenueRepoDeps}
64
+ * - call `super(model, plugins)` from the constructor
65
+ * - call `inject(deps)` once during engine boot
66
+ *
67
+ * Subclasses SHOULD use {@link optsFromCtx} for every mongokit call
68
+ * that takes an options bag, and {@link dispatch} for every event
69
+ * publish.
70
+ */
71
+ var RevenueRepositoryBase = class extends Repository {
72
+ /**
73
+ * Subclass-specific deps. `!` because the engine wires this once via
74
+ * `inject(deps)` immediately after construction; calling any domain
75
+ * verb before injection is a programming error and the runtime
76
+ * will fail-loud with `Cannot read properties of undefined`.
77
+ */
11
78
  deps;
12
79
  constructor(model, plugins = []) {
13
80
  super(model, plugins);
14
81
  }
82
+ /**
83
+ * Wire engine-managed deps. Called exactly once per repository
84
+ * instance during {@link createRevenue} bootstrap. Subclasses with
85
+ * extra steps (caching, prebuilding state machine maps) override and
86
+ * call `super.inject(deps)`.
87
+ */
15
88
  inject(deps) {
16
89
  this.deps = deps;
17
90
  }
18
91
  /**
19
- * Thread `ctx.organizationId` (and future ctx fields) into mongokit
20
- * options so the `multiTenantPlugin` can auto-scope filters, queries,
21
- * and inserts. Merges any caller-supplied extras. Centralizing this
22
- * here means every domain verb participates in scope isolation without
23
- * per-call boilerplate.
92
+ * Translate {@link RevenueContext} into a mongokit options bag.
93
+ *
94
+ * Forwards every canonical field mongokit's bundled plugins read —
95
+ * `organizationId` (multiTenant), `userId` / `user` (audit),
96
+ * `session` (transactions), `requestId` (observability) — plus
97
+ * the revenue-specific `_bypassTenant` flag for platform-admin
98
+ * cross-org reads.
99
+ *
100
+ * Pass `extra` for caller-specific options like `throwOnNotFound`,
101
+ * `lean`, `populate`, `select` — the spread is `extra`-first so
102
+ * ctx wins on a key collision (intentional: callers shouldn't
103
+ * be smuggling tenant fields through `extra`).
104
+ *
105
+ * @param ctx - The request-scoped revenue context.
106
+ * @param extra - Additional mongokit options merged in.
24
107
  */
25
- optsFromCtx(ctx, extra = {}) {
26
- const out = { ...extra };
27
- if (ctx.organizationId !== void 0) out.organizationId = ctx.organizationId;
28
- if (ctx.session !== void 0) out.session = ctx.session;
108
+ optsFromCtx(ctx = {}, extra = {}) {
109
+ const out = {
110
+ ...extra,
111
+ ...repoOptionsFromCtx(ctx)
112
+ };
113
+ if (ctx._bypassTenant === true) out._bypassTenant = true;
29
114
  return out;
30
115
  }
31
116
  /**
117
+ * Persist an event to the host outbox, then publish to the in-process
118
+ * transport. The two sides have asymmetric failure handling — see
119
+ * PACKAGE_RULES §P8.
120
+ *
121
+ * When `ctx.session` is set, the outbox `save` runs inside the same
122
+ * Mongoose transaction (true P8 session-bound write); when absent, the
123
+ * outbox row lands after commit — still durable via the host's relay,
124
+ * with only a small at-most-once window on process crash.
125
+ *
126
+ * 1. **`outbox.save` failures PROPAGATE.** If we can't durably record
127
+ * the event, the caller's transaction MUST roll back so the
128
+ * business doc and the event row land atomically (or neither
129
+ * lands). Swallowing a save failure breaks the transactional-
130
+ * outbox correctness argument — the parent doc would land while
131
+ * the event vanishes.
132
+ *
133
+ * 2. **`events.publish` failures are SWALLOWED.** The host's outbox
134
+ * relay re-publishes from the durable row on its next poll. Even
135
+ * without an outbox, in-process subscribers shouldn't be able to
136
+ * break the business operation — they're best-effort consumers.
137
+ *
138
+ * @param event - Pre-built domain event (use `createEvent(REVENUE_EVENTS.X, payload, ctx, meta)`).
139
+ * @param ctx - The same context that produced the business write.
140
+ */
141
+ async dispatch(event, ctx = {}) {
142
+ if (this.deps.outbox) try {
143
+ await this.deps.outbox.save(event, ctx.session !== void 0 ? { session: ctx.session } : {});
144
+ } catch (err) {
145
+ this.deps.logger?.error("[revenue] outbox.save failed for", event.type, err);
146
+ throw err;
147
+ }
148
+ try {
149
+ await this.deps.events.publish(event);
150
+ } catch (err) {
151
+ this.deps.logger?.error("[revenue] events.publish failed for", event.type, err);
152
+ }
153
+ }
154
+ };
155
+
156
+ //#endregion
157
+ //#region src/repositories/transaction.repository.ts
158
+ var TransactionRepository = class extends RevenueRepositoryBase {
159
+ constructor(model, plugins = []) {
160
+ super(model, plugins);
161
+ }
162
+ /**
32
163
  * Save an event to the host-owned outbox, session-bound when available.
33
164
  *
34
- * When `session` is passed, the outbox row commits atomically with the
35
- * business write (P8 true session-bound write). When absent, the save
36
- * happens after commit still durable via the host's relay, but with a
37
- * small at-most-once window on process crash.
165
+ * Called INSIDE a `withTransaction` body. The outbox row commits
166
+ * atomically with the business write (P8 true session-bound write)
167
+ * if outbox.save fails, this method re-throws so `withTransaction`
168
+ * rolls the parent write back. Without propagation, the parent doc
169
+ * would commit while the event row vanishes, defeating the whole
170
+ * transactional-outbox correctness argument.
38
171
  *
39
- * Isolated try/catch: an outbox failure never throws out of this helper;
40
- * the caller still issues a transport.publish.
172
+ * Logging happens before the re-throw so the failure surfaces in
173
+ * observability without losing the original stack trace.
41
174
  */
42
175
  async saveToOutbox(event, session) {
43
176
  if (!this.deps.outbox) return;
@@ -45,12 +178,14 @@ var TransactionRepository = class extends Repository {
45
178
  await this.deps.outbox.save(event, session !== void 0 ? { session } : {});
46
179
  } catch (err) {
47
180
  this.deps.logger?.error("[revenue] outbox.save failed for", event.type, err);
181
+ throw err;
48
182
  }
49
183
  }
50
184
  /**
51
185
  * Publish an event to the in-process `EventTransport` after commit.
52
- * Transport failure is logged — the host relay will still redeliver from
53
- * the outbox, so in-process subscribers missing an event is recoverable.
186
+ * Transport failure is logged and swallowed — the host relay re-delivers
187
+ * from the durable outbox row on the next poll, so in-process
188
+ * subscribers missing an event is recoverable. Best-effort by design.
54
189
  */
55
190
  async publishToTransport(event) {
56
191
  try {
@@ -59,16 +194,6 @@ var TransactionRepository = class extends Repository {
59
194
  this.deps.logger?.error("[revenue] events.publish failed for", event.type, err);
60
195
  }
61
196
  }
62
- /**
63
- * Non-transactional dispatch (used by verbs that don't open their own
64
- * `withTransaction` block): outbox.save (session-bound when ctx provides
65
- * one) → transport.publish. Matches arc's EventOutbox + MemoryEventTransport
66
- * wiring bit-for-bit.
67
- */
68
- async dispatch(event, ctx = {}) {
69
- await this.saveToOutbox(event, ctx.session);
70
- await this.publishToTransport(event);
71
- }
72
197
  /** Creates transaction + calls provider. Returns the created transaction doc. */
73
198
  async createPaymentIntent(params, ctx = {}) {
74
199
  const currency = params.currency ?? this.deps.defaultCurrency;
@@ -87,6 +212,7 @@ var TransactionRepository = class extends Repository {
87
212
  amount: params.amount,
88
213
  currency
89
214
  },
215
+ methodKind: params.methodKind,
90
216
  metadata: params.metadata,
91
217
  ...params.paymentData
92
218
  });
@@ -113,6 +239,7 @@ var TransactionRepository = class extends Repository {
113
239
  tax: 0,
114
240
  net: params.amount - (commission?.gatewayFeeAmount ?? 0),
115
241
  method: params.gateway,
242
+ methodKind: params.methodKind,
116
243
  status: params.amount === 0 ? TRANSACTION_STATUS.VERIFIED : TRANSACTION_STATUS.PENDING,
117
244
  gateway: gatewayData,
118
245
  commission: commission ?? void 0,
@@ -217,6 +344,7 @@ var TransactionRepository = class extends Repository {
217
344
  tax: reversedTax?.taxAmount ?? 0,
218
345
  net: refundAmount - (reversedCommission?.gatewayFeeAmount ?? 0) - (reversedTax?.taxAmount ?? 0),
219
346
  method: transaction.method,
347
+ methodKind: transaction.methodKind,
220
348
  status: TRANSACTION_STATUS.VERIFIED,
221
349
  gateway: transaction.gateway,
222
350
  commission: reversedCommission ?? void 0,
@@ -267,10 +395,10 @@ var TransactionRepository = class extends Repository {
267
395
  processedAt: /* @__PURE__ */ new Date(),
268
396
  data: webhookEvent.data
269
397
  };
270
- const updated = await this.Model.findOneAndUpdate({
398
+ const updated = await this.findOneAndUpdate({
271
399
  _id: transaction._id,
272
400
  "webhook.eventId": { $ne: webhookEvent.id }
273
- }, { $set: { webhook: nextWebhook } }, { returnDocument: "after" }).lean();
401
+ }, { $set: { webhook: nextWebhook } }, { returnDocument: "after" });
274
402
  if (!updated) return await this.getByQuery({ _id: transaction._id }, readOpts) ?? transaction;
275
403
  await this.dispatch(createEvent(REVENUE_EVENTS.WEBHOOK_PROCESSED, {
276
404
  webhookType: webhookEvent.type,
@@ -354,6 +482,7 @@ var TransactionRepository = class extends Repository {
354
482
  tax: 0,
355
483
  net: releaseAmount,
356
484
  method: transaction.method,
485
+ methodKind: transaction.methodKind,
357
486
  status: TRANSACTION_STATUS.VERIFIED,
358
487
  relatedTransactionId: transaction._id,
359
488
  sourceId: transaction.sourceId,
@@ -408,6 +537,7 @@ var TransactionRepository = class extends Repository {
408
537
  tax: 0,
409
538
  net: s.netAmount,
410
539
  method: transaction.method,
540
+ methodKind: transaction.methodKind,
411
541
  status: TRANSACTION_STATUS.VERIFIED,
412
542
  relatedTransactionId: transaction._id,
413
543
  sourceId: transaction.sourceId,
@@ -432,6 +562,7 @@ var TransactionRepository = class extends Repository {
432
562
  tax: 0,
433
563
  net: orgPayout,
434
564
  method: transaction.method,
565
+ methodKind: transaction.methodKind,
435
566
  status: TRANSACTION_STATUS.VERIFIED,
436
567
  relatedTransactionId: transaction._id,
437
568
  verifiedAt: /* @__PURE__ */ new Date()
@@ -479,6 +610,7 @@ var TransactionRepository = class extends Repository {
479
610
  * `source` (provenance — `'plaid'`, `'ofx'`, …).
480
611
  */
481
612
  async import(rows, opts, ctx = {}) {
613
+ if (!opts.methodKind) throw new BankFeedImportError("`opts.methodKind` is required on TransactionRepository.import() — pick the canonical PaymentMethodKind for the source (e.g. `'bank_transfer'` for Plaid/OFX, `'card'` for a Stripe balance, `'wallet'` for PayPal, `'cryptocurrency'` for an exchange).");
482
614
  const startedAt = Date.now();
483
615
  if (!Array.isArray(rows) || rows.length === 0) return {
484
616
  inserted: 0,
@@ -538,6 +670,7 @@ var TransactionRepository = class extends Repository {
538
670
  source: opts.source,
539
671
  type: "bank_feed",
540
672
  tags: ["bank_feed", opts.source],
673
+ methodKind: opts.methodKind,
541
674
  fee: 0,
542
675
  tax: 0,
543
676
  net: absoluteAmount,
@@ -622,7 +755,8 @@ var TransactionRepository = class extends Repository {
622
755
  if (page.transactions && page.transactions.length > 0) {
623
756
  const report = await this.import(page.transactions, {
624
757
  bankAccountId: params.bankAccountId,
625
- source: providerName
758
+ source: providerName,
759
+ methodKind: params.methodKind
626
760
  }, ctx);
627
761
  totalImported += report.inserted;
628
762
  totalUpdated += report.updated;
@@ -664,7 +798,8 @@ var TransactionRepository = class extends Repository {
664
798
  });
665
799
  return this.import(parsed.transactions, {
666
800
  bankAccountId: upload.bankAccountId,
667
- source: providerName
801
+ source: providerName,
802
+ methodKind: upload.methodKind
668
803
  }, ctx);
669
804
  }
670
805
  /**
@@ -687,6 +822,7 @@ var TransactionRepository = class extends Repository {
687
822
  tax: 0,
688
823
  net: data.amount,
689
824
  method: "manual",
825
+ methodKind: data.methodKind,
690
826
  status: initialStatusFor(TRANSACTION_KIND.MANUAL),
691
827
  source: "manual",
692
828
  ...data.description !== void 0 ? { description: data.description } : {},
@@ -701,6 +837,56 @@ var TransactionRepository = class extends Repository {
701
837
  }, this.optsFromCtx(ctx));
702
838
  }
703
839
  /**
840
+ * Backfill the `methodKind` on a Transaction created with kind
841
+ * unknown — the canonical use case is hosted-checkout (Stripe
842
+ * Checkout, PayPal redirect, Razorpay Checkout) where the customer
843
+ * picks their payment method on the gateway's UI, AFTER the host has
844
+ * already created the PaymentIntent + Transaction with
845
+ * `methodKind: 'other'`.
846
+ *
847
+ * Call this from your verification / webhook handler once you know
848
+ * the customer's actual choice — e.g. inside
849
+ * `payment_intent.succeeded`:
850
+ *
851
+ * ```ts
852
+ * await transactionRepository.backfillMethodKind(
853
+ * tx._id,
854
+ * stripePaymentIntentToKind(event.data.object),
855
+ * ctx,
856
+ * );
857
+ * ```
858
+ *
859
+ * **Guard rule.** Atomic CAS — succeeds only when the doc has
860
+ * `methodKind === 'other'` AND `status === 'pending'`. Any other
861
+ * combination throws `MethodKindLockedError` (HTTP 409): once a
862
+ * transaction has a specific kind (or has settled past pending),
863
+ * silently overwriting it would corrupt downstream analytics and
864
+ * accounting reports.
865
+ *
866
+ * Emits `revenue:transaction.updated` with `changedFields:
867
+ * ['methodKind']` so subscribers can re-bucket the row.
868
+ */
869
+ async backfillMethodKind(transactionId, methodKind, ctx = {}) {
870
+ const updated = await this.findOneAndUpdate({
871
+ _id: transactionId,
872
+ methodKind: "other",
873
+ status: TRANSACTION_STATUS.PENDING
874
+ }, { $set: { methodKind } }, { returnDocument: "after" });
875
+ if (!updated) {
876
+ const existing = await this.getById(transactionId, this.optsFromCtx(ctx, { throwOnNotFound: false }));
877
+ if (!existing) throw new TransactionNotFoundError(transactionId);
878
+ throw new MethodKindLockedError(transactionId, existing.methodKind ?? "unknown", existing.status ?? "unknown");
879
+ }
880
+ await this.dispatch(createEvent(REVENUE_EVENTS.TRANSACTION_UPDATED, {
881
+ transaction: updated,
882
+ changedFields: ["methodKind"]
883
+ }, ctx, {
884
+ resource: "transaction",
885
+ resourceId: updated.publicId
886
+ }), ctx);
887
+ return updated;
888
+ }
889
+ /**
704
890
  * Match a bank-feed / manual transaction to GL accounts, optionally
705
891
  * cross-linking to an upstream payment-flow transaction.
706
892
  *
@@ -937,20 +1123,25 @@ var TransactionRepository = class extends Repository {
937
1123
  const minAmount = filter.amount * (1 - pct);
938
1124
  const maxAmount = filter.amount * (1 + pct);
939
1125
  const targetKind = filter.kind ?? TRANSACTION_KIND.PAYMENT_FLOW;
1126
+ const validStatuses = targetKind === TRANSACTION_KIND.PAYMENT_FLOW ? [TRANSACTION_STATUS.VERIFIED, TRANSACTION_STATUS.COMPLETED] : [TRANSACTION_STATUS.IMPORTED, TRANSACTION_STATUS.MATCHED];
1127
+ const dateClauses = targetKind === TRANSACTION_KIND.BANK_FEED ? [{ postedDate: {
1128
+ $gte: start,
1129
+ $lte: end
1130
+ } }] : [{ verifiedAt: {
1131
+ $gte: start,
1132
+ $lte: end
1133
+ } }, { createdAt: {
1134
+ $gte: start,
1135
+ $lte: end
1136
+ } }];
940
1137
  const query = {
941
1138
  kind: targetKind,
942
- status: { $in: targetKind === TRANSACTION_KIND.PAYMENT_FLOW ? [TRANSACTION_STATUS.VERIFIED, TRANSACTION_STATUS.COMPLETED] : [TRANSACTION_STATUS.IMPORTED, TRANSACTION_STATUS.MATCHED] },
1139
+ status: { $in: validStatuses },
943
1140
  amount: {
944
1141
  $gte: minAmount,
945
1142
  $lte: maxAmount
946
1143
  },
947
- $or: [{ postedDate: {
948
- $gte: start,
949
- $lte: end
950
- } }, { createdAt: {
951
- $gte: start,
952
- $lte: end
953
- } }]
1144
+ $or: dateClauses
954
1145
  };
955
1146
  if (filter.currency !== void 0) query.currency = filter.currency;
956
1147
  if (filter.counterpartyName !== void 0) query["counterparty.name"] = {
@@ -1004,41 +1195,48 @@ function escapeRegex(input) {
1004
1195
  //#endregion
1005
1196
  //#region src/repositories/subscription.repository.ts
1006
1197
  /**
1007
- * SubscriptionRepository — data layer + domain verbs.
1198
+ * SubscriptionRepository — data layer + domain verbs for the recurring-
1199
+ * billing lifecycle.
1200
+ *
1201
+ * **CRUD inherited** from mongokit (via {@link RevenueRepositoryBase}):
1202
+ * `getById`, `getByQuery`, `getAll`, `create`, `update`, `delete`,
1203
+ * `findOneAndUpdate`, `count`, `exists`, `claim`, `cursor`, `updateMany`,
1204
+ * `deleteMany`. All participate in `multiTenantPlugin` scope filtering
1205
+ * when wired.
1008
1206
  *
1009
- * CRUD inherited from mongokit. Domain verbs: activate, cancel, pause, resume.
1207
+ * **Domain verbs (state transitions):** `activate`, `cancel`, `pause`,
1208
+ * `resume`. Each runs the state-machine guard (`SUBSCRIPTION_STATE_MACHINE`
1209
+ * — invalid transitions throw, never silently no-op), persists the
1210
+ * resulting writes through {@link RevenueRepositoryBase.optsFromCtx} so
1211
+ * tenant scope is preserved end-to-end, then dispatches its
1212
+ * `revenue:subscription.*` event via {@link RevenueRepositoryBase.dispatch}.
1010
1213
  *
1011
- * Events: each domain verb calls `this.deps.events.publish(createEvent(...))`
1012
- * with a fully-qualified `REVENUE_EVENTS.*` name. Hosts can subscribe glob-style
1013
- * via `revenue.events.subscribe('revenue:subscription.*', handler)` the
1014
- * injected transport is arc-compatible (PACKAGE_RULES §13–§14).
1214
+ * **Multi-tenant correctness.** Every internal `getById`/`update` call
1215
+ * threads `ctx.organizationId` through `optsFromCtx(ctx)`. Without this
1216
+ * threading the inner read would either throw
1217
+ * `Missing 'organizationId' in context` (when `multiTenantPlugin` is
1218
+ * required) or — worse — return another tenant's subscription matching
1219
+ * the same `_id` shape (when `required: false`). 2.1.0 had this bug; 2.1.1+
1220
+ * is correct.
1221
+ *
1222
+ * @example Activate a pending sub
1223
+ * ```ts
1224
+ * const ctx = { organizationId: 'org_42', actorId: 'user_99' };
1225
+ * const sub = await subRepo.create(
1226
+ * { customerId: 'cust_1', planKey: 'monthly', amount: 999, currency: 'USD',
1227
+ * status: SUBSCRIPTION_STATUS.PENDING, isActive: false },
1228
+ * ctx,
1229
+ * );
1230
+ * await subRepo.activate(String(sub._id), {}, ctx);
1231
+ * ```
1015
1232
  */
1016
- var SubscriptionRepository = class extends Repository {
1017
- deps;
1233
+ var SubscriptionRepository = class extends RevenueRepositoryBase {
1018
1234
  constructor(model, plugins = []) {
1019
1235
  super(model, plugins);
1020
1236
  }
1021
- inject(deps) {
1022
- this.deps = deps;
1023
- }
1024
- /**
1025
- * Host-owned outbox save → in-process transport publish (PACKAGE_RULES P8).
1026
- * Session-bound when `ctx.session` is present (atomic outbox row write).
1027
- */
1028
- async dispatch(event, ctx = {}) {
1029
- if (this.deps.outbox) try {
1030
- await this.deps.outbox.save(event, ctx.session !== void 0 ? { session: ctx.session } : {});
1031
- } catch (err) {
1032
- this.deps.logger?.error("[revenue] outbox.save failed for", event.type, err);
1033
- }
1034
- try {
1035
- await this.deps.events.publish(event);
1036
- } catch (err) {
1037
- this.deps.logger?.error("[revenue] events.publish failed for", event.type, err);
1038
- }
1039
- }
1040
1237
  async activate(subscriptionId, options = {}, ctx = {}) {
1041
- const sub = await this.getById(subscriptionId);
1238
+ const opts = this.optsFromCtx(ctx);
1239
+ const sub = await this.getById(subscriptionId, opts);
1042
1240
  if (!sub) throw new SubscriptionNotFoundError(subscriptionId);
1043
1241
  SUBSCRIPTION_STATE_MACHINE.validate(sub.status, SUBSCRIPTION_STATUS.ACTIVE, subscriptionId);
1044
1242
  const now = options.timestamp ?? /* @__PURE__ */ new Date();
@@ -1052,7 +1250,7 @@ var SubscriptionRepository = class extends Repository {
1052
1250
  isActive: true,
1053
1251
  activatedAt: now,
1054
1252
  endDate
1055
- }, { throwOnNotFound: true });
1253
+ }, this.optsFromCtx(ctx, { throwOnNotFound: true }));
1056
1254
  if (!updated) throw new SubscriptionNotFoundError(subscriptionId);
1057
1255
  await this.dispatch(createEvent(REVENUE_EVENTS.SUBSCRIPTION_ACTIVATED, {
1058
1256
  subscription: updated,
@@ -1064,7 +1262,8 @@ var SubscriptionRepository = class extends Repository {
1064
1262
  return updated;
1065
1263
  }
1066
1264
  async cancel(subscriptionId, options = {}, ctx = {}) {
1067
- const sub = await this.getById(subscriptionId);
1265
+ const opts = this.optsFromCtx(ctx);
1266
+ const sub = await this.getById(subscriptionId, opts);
1068
1267
  if (!sub) throw new SubscriptionNotFoundError(subscriptionId);
1069
1268
  SUBSCRIPTION_STATE_MACHINE.validate(sub.status, SUBSCRIPTION_STATUS.CANCELLED, subscriptionId);
1070
1269
  const updates = {
@@ -1079,7 +1278,7 @@ var SubscriptionRepository = class extends Repository {
1079
1278
  updates.isActive = sub.isActive;
1080
1279
  delete updates.canceledAt;
1081
1280
  }
1082
- const updated = await this.update(subscriptionId, updates, { throwOnNotFound: true });
1281
+ const updated = await this.update(subscriptionId, updates, this.optsFromCtx(ctx, { throwOnNotFound: true }));
1083
1282
  if (!updated) throw new SubscriptionNotFoundError(subscriptionId);
1084
1283
  await this.dispatch(createEvent(REVENUE_EVENTS.SUBSCRIPTION_CANCELLED, {
1085
1284
  subscription: updated,
@@ -1092,7 +1291,8 @@ var SubscriptionRepository = class extends Repository {
1092
1291
  return updated;
1093
1292
  }
1094
1293
  async pause(subscriptionId, options = {}, ctx = {}) {
1095
- const sub = await this.getById(subscriptionId);
1294
+ const opts = this.optsFromCtx(ctx);
1295
+ const sub = await this.getById(subscriptionId, opts);
1096
1296
  if (!sub) throw new SubscriptionNotFoundError(subscriptionId);
1097
1297
  SUBSCRIPTION_STATE_MACHINE.validate(sub.status, SUBSCRIPTION_STATUS.PAUSED, subscriptionId);
1098
1298
  const updated = await this.update(subscriptionId, {
@@ -1100,7 +1300,7 @@ var SubscriptionRepository = class extends Repository {
1100
1300
  isActive: false,
1101
1301
  pausedAt: /* @__PURE__ */ new Date(),
1102
1302
  pauseReason: options.reason
1103
- }, { throwOnNotFound: true });
1303
+ }, this.optsFromCtx(ctx, { throwOnNotFound: true }));
1104
1304
  if (!updated) throw new SubscriptionNotFoundError(subscriptionId);
1105
1305
  await this.dispatch(createEvent(REVENUE_EVENTS.SUBSCRIPTION_PAUSED, {
1106
1306
  subscription: updated,
@@ -1112,7 +1312,8 @@ var SubscriptionRepository = class extends Repository {
1112
1312
  return updated;
1113
1313
  }
1114
1314
  async resume(subscriptionId, options = {}, ctx = {}) {
1115
- const sub = await this.getById(subscriptionId);
1315
+ const opts = this.optsFromCtx(ctx);
1316
+ const sub = await this.getById(subscriptionId, opts);
1116
1317
  if (!sub) throw new SubscriptionNotFoundError(subscriptionId);
1117
1318
  SUBSCRIPTION_STATE_MACHINE.validate(sub.status, SUBSCRIPTION_STATUS.ACTIVE, subscriptionId);
1118
1319
  const updates = {
@@ -1123,7 +1324,7 @@ var SubscriptionRepository = class extends Repository {
1123
1324
  const pauseDuration = Date.now() - sub.pausedAt.getTime();
1124
1325
  updates.endDate = new Date(sub.endDate.getTime() + pauseDuration);
1125
1326
  }
1126
- const updated = await this.update(subscriptionId, updates, { throwOnNotFound: true });
1327
+ const updated = await this.update(subscriptionId, updates, this.optsFromCtx(ctx, { throwOnNotFound: true }));
1127
1328
  if (!updated) throw new SubscriptionNotFoundError(subscriptionId);
1128
1329
  await this.dispatch(createEvent(REVENUE_EVENTS.SUBSCRIPTION_RESUMED, {
1129
1330
  subscription: updated,
@@ -1139,45 +1340,34 @@ var SubscriptionRepository = class extends Repository {
1139
1340
  //#endregion
1140
1341
  //#region src/repositories/settlement.repository.ts
1141
1342
  /**
1142
- * SettlementRepository — data layer + domain verbs.
1343
+ * SettlementRepository — payouts to recipients (organizations, vendors,
1344
+ * affiliates).
1143
1345
  *
1144
- * CRUD inherited from mongokit. Domain verbs: schedule, processPending, complete, fail.
1346
+ * **CRUD inherited** via {@link RevenueRepositoryBase}. **Domain verbs:**
1347
+ * `schedule`, `processPending`, `complete`, `fail`. State machine:
1348
+ * `pending → processing → completed | failed`; `failed` with
1349
+ * `retry: true` resets to `pending` with a delayed `scheduledAt`.
1145
1350
  *
1146
- * Events are published via the injected `events` transport (arc-compatible).
1147
- * Hosts subscribe glob-style via `revenue.events.subscribe('revenue:settlement.*', h)`.
1148
- * See PACKAGE_RULES §13–§14.
1351
+ * **Multi-tenant correctness.** Every read/write threads `ctx` through
1352
+ * {@link RevenueRepositoryBase.optsFromCtx} so `multiTenantPlugin`
1353
+ * scope filters apply. 2.1.0 had several `getById`/`update` calls that
1354
+ * dropped ctx — fixed in 2.1.1+.
1355
+ *
1356
+ * Bridges (`ledger`, `notification`) fire on `complete()` so a host
1357
+ * can pin double-entry book-keeping or push a "you got paid" email
1358
+ * without the repo knowing about either subsystem.
1149
1359
  */
1150
- var SettlementRepository = class extends Repository {
1151
- deps;
1360
+ var SettlementRepository = class extends RevenueRepositoryBase {
1152
1361
  constructor(model, plugins = []) {
1153
1362
  super(model, plugins);
1154
1363
  }
1155
- inject(deps) {
1156
- this.deps = deps;
1157
- }
1158
- /**
1159
- * Host-owned outbox save → in-process transport publish (PACKAGE_RULES P8).
1160
- * Session-bound when `ctx.session` is present (atomic outbox row write).
1161
- */
1162
- async dispatch(event, ctx = {}) {
1163
- if (this.deps.outbox) try {
1164
- await this.deps.outbox.save(event, ctx.session !== void 0 ? { session: ctx.session } : {});
1165
- } catch (err) {
1166
- this.deps.logger?.error("[revenue] outbox.save failed for", event.type, err);
1167
- }
1168
- try {
1169
- await this.deps.events.publish(event);
1170
- } catch (err) {
1171
- this.deps.logger?.error("[revenue] events.publish failed for", event.type, err);
1172
- }
1173
- }
1174
1364
  async schedule(params, ctx = {}) {
1175
1365
  const settlement = await this.create({
1176
1366
  ...params,
1177
1367
  status: SETTLEMENT_STATUS.PENDING,
1178
1368
  scheduledAt: params.scheduledAt ?? /* @__PURE__ */ new Date(),
1179
1369
  retryCount: 0
1180
- });
1370
+ }, this.optsFromCtx(ctx));
1181
1371
  await this.dispatch(createEvent(REVENUE_EVENTS.SETTLEMENT_SCHEDULED, {
1182
1372
  settlement,
1183
1373
  scheduledAt: settlement.scheduledAt
@@ -1198,7 +1388,7 @@ var SettlementRepository = class extends Repository {
1198
1388
  filters: query,
1199
1389
  limit: options.limit ?? 50,
1200
1390
  sort: { scheduledAt: 1 }
1201
- })).data ?? [];
1391
+ }, this.optsFromCtx(ctx))).data ?? [];
1202
1392
  if (options.dryRun) return {
1203
1393
  processed: pending.length,
1204
1394
  succeeded: 0,
@@ -1206,7 +1396,7 @@ var SettlementRepository = class extends Repository {
1206
1396
  settlements: pending,
1207
1397
  errors: []
1208
1398
  };
1209
- const results = {
1399
+ const out = {
1210
1400
  processed: 0,
1211
1401
  succeeded: 0,
1212
1402
  failed: 0,
@@ -1219,9 +1409,9 @@ var SettlementRepository = class extends Repository {
1219
1409
  await this.update(settlement._id, {
1220
1410
  status: SETTLEMENT_STATUS.PROCESSING,
1221
1411
  processedAt: /* @__PURE__ */ new Date()
1222
- });
1223
- results.succeeded++;
1224
- results.settlements.push(settlement);
1412
+ }, this.optsFromCtx(ctx));
1413
+ out.succeeded++;
1414
+ out.settlements.push(settlement);
1225
1415
  await this.dispatch(createEvent(REVENUE_EVENTS.SETTLEMENT_PROCESSING, {
1226
1416
  settlement,
1227
1417
  processedAt: /* @__PURE__ */ new Date()
@@ -1230,18 +1420,19 @@ var SettlementRepository = class extends Repository {
1230
1420
  resourceId: settlement.publicId
1231
1421
  }), ctx);
1232
1422
  } catch (err) {
1233
- results.failed++;
1234
- results.errors.push({
1423
+ out.failed++;
1424
+ out.errors.push({
1235
1425
  settlementId: settlement._id,
1236
1426
  error: err
1237
1427
  });
1238
1428
  }
1239
- results.processed++;
1429
+ out.processed++;
1240
1430
  }
1241
- return results;
1431
+ return out;
1242
1432
  }
1243
1433
  async complete(settlementId, details = {}, ctx = {}) {
1244
- const settlement = await this.getById(settlementId);
1434
+ const opts = this.optsFromCtx(ctx);
1435
+ const settlement = await this.getById(settlementId, opts);
1245
1436
  if (!settlement) throw new SettlementNotFoundError(settlementId);
1246
1437
  SETTLEMENT_STATE_MACHINE.validate(settlement.status, SETTLEMENT_STATUS.COMPLETED, settlementId);
1247
1438
  const updates = {
@@ -1263,7 +1454,7 @@ var SettlementRepository = class extends Repository {
1263
1454
  transactionHash: details.transactionHash,
1264
1455
  transferredAt: details.transferredAt ?? /* @__PURE__ */ new Date()
1265
1456
  };
1266
- const updated = await this.update(settlementId, updates, { throwOnNotFound: true });
1457
+ const updated = await this.update(settlementId, updates, this.optsFromCtx(ctx, { throwOnNotFound: true }));
1267
1458
  if (!updated) throw new SettlementNotFoundError(settlementId);
1268
1459
  await this.deps.bridges.ledger?.onSettlementCompleted?.(updated, ctx);
1269
1460
  await this.deps.bridges.notification?.onSettlementCompleted?.(updated, ctx);
@@ -1277,7 +1468,8 @@ var SettlementRepository = class extends Repository {
1277
1468
  return updated;
1278
1469
  }
1279
1470
  async fail(settlementId, reason, options = {}, ctx = {}) {
1280
- const settlement = await this.getById(settlementId);
1471
+ const opts = this.optsFromCtx(ctx);
1472
+ const settlement = await this.getById(settlementId, opts);
1281
1473
  if (!settlement) throw new SettlementNotFoundError(settlementId);
1282
1474
  if (options.retry) await this.update(settlementId, {
1283
1475
  status: SETTLEMENT_STATUS.PENDING,
@@ -1285,7 +1477,7 @@ var SettlementRepository = class extends Repository {
1285
1477
  failureReason: reason,
1286
1478
  failureCode: options.code,
1287
1479
  scheduledAt: new Date(Date.now() + 36e5)
1288
- });
1480
+ }, opts);
1289
1481
  else {
1290
1482
  SETTLEMENT_STATE_MACHINE.validate(settlement.status, SETTLEMENT_STATUS.FAILED, settlementId);
1291
1483
  await this.update(settlementId, {
@@ -1293,9 +1485,9 @@ var SettlementRepository = class extends Repository {
1293
1485
  failedAt: /* @__PURE__ */ new Date(),
1294
1486
  failureReason: reason,
1295
1487
  failureCode: options.code
1296
- });
1488
+ }, opts);
1297
1489
  }
1298
- const updated = await this.getById(settlementId);
1490
+ const updated = await this.getById(settlementId, opts);
1299
1491
  await this.dispatch(createEvent(REVENUE_EVENTS.SETTLEMENT_FAILED, {
1300
1492
  settlement: updated,
1301
1493
  reason,