@classytic/revenue 2.1.0 → 2.1.3

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.
@@ -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-Dn1TKahe.mjs";
3
+ import { g as SETTLEMENT_STATUS, r as SUBSCRIPTION_STATUS, w as HOLD_STATUS } from "./subscription.enums-95othr0i.mjs";
4
+ import { f as SettlementNotFoundError, g as WrongTransactionKindError, h as ValidationError, m as TransactionNotFoundError, n as BankFeedImportError, p as SubscriptionNotFoundError } from "./errors-LYYg9wcs.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;
@@ -267,10 +392,10 @@ var TransactionRepository = class extends Repository {
267
392
  processedAt: /* @__PURE__ */ new Date(),
268
393
  data: webhookEvent.data
269
394
  };
270
- const updated = await this.Model.findOneAndUpdate({
395
+ const updated = await this.findOneAndUpdate({
271
396
  _id: transaction._id,
272
397
  "webhook.eventId": { $ne: webhookEvent.id }
273
- }, { $set: { webhook: nextWebhook } }, { returnDocument: "after" }).lean();
398
+ }, { $set: { webhook: nextWebhook } }, { returnDocument: "after" });
274
399
  if (!updated) return await this.getByQuery({ _id: transaction._id }, readOpts) ?? transaction;
275
400
  await this.dispatch(createEvent(REVENUE_EVENTS.WEBHOOK_PROCESSED, {
276
401
  webhookType: webhookEvent.type,
@@ -937,20 +1062,25 @@ var TransactionRepository = class extends Repository {
937
1062
  const minAmount = filter.amount * (1 - pct);
938
1063
  const maxAmount = filter.amount * (1 + pct);
939
1064
  const targetKind = filter.kind ?? TRANSACTION_KIND.PAYMENT_FLOW;
1065
+ const validStatuses = targetKind === TRANSACTION_KIND.PAYMENT_FLOW ? [TRANSACTION_STATUS.VERIFIED, TRANSACTION_STATUS.COMPLETED] : [TRANSACTION_STATUS.IMPORTED, TRANSACTION_STATUS.MATCHED];
1066
+ const dateClauses = targetKind === TRANSACTION_KIND.BANK_FEED ? [{ postedDate: {
1067
+ $gte: start,
1068
+ $lte: end
1069
+ } }] : [{ verifiedAt: {
1070
+ $gte: start,
1071
+ $lte: end
1072
+ } }, { createdAt: {
1073
+ $gte: start,
1074
+ $lte: end
1075
+ } }];
940
1076
  const query = {
941
1077
  kind: targetKind,
942
- status: { $in: targetKind === TRANSACTION_KIND.PAYMENT_FLOW ? [TRANSACTION_STATUS.VERIFIED, TRANSACTION_STATUS.COMPLETED] : [TRANSACTION_STATUS.IMPORTED, TRANSACTION_STATUS.MATCHED] },
1078
+ status: { $in: validStatuses },
943
1079
  amount: {
944
1080
  $gte: minAmount,
945
1081
  $lte: maxAmount
946
1082
  },
947
- $or: [{ postedDate: {
948
- $gte: start,
949
- $lte: end
950
- } }, { createdAt: {
951
- $gte: start,
952
- $lte: end
953
- } }]
1083
+ $or: dateClauses
954
1084
  };
955
1085
  if (filter.currency !== void 0) query.currency = filter.currency;
956
1086
  if (filter.counterpartyName !== void 0) query["counterparty.name"] = {
@@ -1004,41 +1134,48 @@ function escapeRegex(input) {
1004
1134
  //#endregion
1005
1135
  //#region src/repositories/subscription.repository.ts
1006
1136
  /**
1007
- * SubscriptionRepository — data layer + domain verbs.
1137
+ * SubscriptionRepository — data layer + domain verbs for the recurring-
1138
+ * billing lifecycle.
1008
1139
  *
1009
- * CRUD inherited from mongokit. Domain verbs: activate, cancel, pause, resume.
1140
+ * **CRUD inherited** from mongokit (via {@link RevenueRepositoryBase}):
1141
+ * `getById`, `getByQuery`, `getAll`, `create`, `update`, `delete`,
1142
+ * `findOneAndUpdate`, `count`, `exists`, `claim`, `cursor`, `updateMany`,
1143
+ * `deleteMany`. All participate in `multiTenantPlugin` scope filtering
1144
+ * when wired.
1010
1145
  *
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).
1146
+ * **Domain verbs (state transitions):** `activate`, `cancel`, `pause`,
1147
+ * `resume`. Each runs the state-machine guard (`SUBSCRIPTION_STATE_MACHINE`
1148
+ * invalid transitions throw, never silently no-op), persists the
1149
+ * resulting writes through {@link RevenueRepositoryBase.optsFromCtx} so
1150
+ * tenant scope is preserved end-to-end, then dispatches its
1151
+ * `revenue:subscription.*` event via {@link RevenueRepositoryBase.dispatch}.
1152
+ *
1153
+ * **Multi-tenant correctness.** Every internal `getById`/`update` call
1154
+ * threads `ctx.organizationId` through `optsFromCtx(ctx)`. Without this
1155
+ * threading the inner read would either throw
1156
+ * `Missing 'organizationId' in context` (when `multiTenantPlugin` is
1157
+ * required) or — worse — return another tenant's subscription matching
1158
+ * the same `_id` shape (when `required: false`). 2.1.0 had this bug; 2.1.1+
1159
+ * is correct.
1160
+ *
1161
+ * @example Activate a pending sub
1162
+ * ```ts
1163
+ * const ctx = { organizationId: 'org_42', actorId: 'user_99' };
1164
+ * const sub = await subRepo.create(
1165
+ * { customerId: 'cust_1', planKey: 'monthly', amount: 999, currency: 'USD',
1166
+ * status: SUBSCRIPTION_STATUS.PENDING, isActive: false },
1167
+ * ctx,
1168
+ * );
1169
+ * await subRepo.activate(String(sub._id), {}, ctx);
1170
+ * ```
1015
1171
  */
1016
- var SubscriptionRepository = class extends Repository {
1017
- deps;
1172
+ var SubscriptionRepository = class extends RevenueRepositoryBase {
1018
1173
  constructor(model, plugins = []) {
1019
1174
  super(model, plugins);
1020
1175
  }
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
1176
  async activate(subscriptionId, options = {}, ctx = {}) {
1041
- const sub = await this.getById(subscriptionId);
1177
+ const opts = this.optsFromCtx(ctx);
1178
+ const sub = await this.getById(subscriptionId, opts);
1042
1179
  if (!sub) throw new SubscriptionNotFoundError(subscriptionId);
1043
1180
  SUBSCRIPTION_STATE_MACHINE.validate(sub.status, SUBSCRIPTION_STATUS.ACTIVE, subscriptionId);
1044
1181
  const now = options.timestamp ?? /* @__PURE__ */ new Date();
@@ -1052,7 +1189,7 @@ var SubscriptionRepository = class extends Repository {
1052
1189
  isActive: true,
1053
1190
  activatedAt: now,
1054
1191
  endDate
1055
- }, { throwOnNotFound: true });
1192
+ }, this.optsFromCtx(ctx, { throwOnNotFound: true }));
1056
1193
  if (!updated) throw new SubscriptionNotFoundError(subscriptionId);
1057
1194
  await this.dispatch(createEvent(REVENUE_EVENTS.SUBSCRIPTION_ACTIVATED, {
1058
1195
  subscription: updated,
@@ -1064,7 +1201,8 @@ var SubscriptionRepository = class extends Repository {
1064
1201
  return updated;
1065
1202
  }
1066
1203
  async cancel(subscriptionId, options = {}, ctx = {}) {
1067
- const sub = await this.getById(subscriptionId);
1204
+ const opts = this.optsFromCtx(ctx);
1205
+ const sub = await this.getById(subscriptionId, opts);
1068
1206
  if (!sub) throw new SubscriptionNotFoundError(subscriptionId);
1069
1207
  SUBSCRIPTION_STATE_MACHINE.validate(sub.status, SUBSCRIPTION_STATUS.CANCELLED, subscriptionId);
1070
1208
  const updates = {
@@ -1079,7 +1217,7 @@ var SubscriptionRepository = class extends Repository {
1079
1217
  updates.isActive = sub.isActive;
1080
1218
  delete updates.canceledAt;
1081
1219
  }
1082
- const updated = await this.update(subscriptionId, updates, { throwOnNotFound: true });
1220
+ const updated = await this.update(subscriptionId, updates, this.optsFromCtx(ctx, { throwOnNotFound: true }));
1083
1221
  if (!updated) throw new SubscriptionNotFoundError(subscriptionId);
1084
1222
  await this.dispatch(createEvent(REVENUE_EVENTS.SUBSCRIPTION_CANCELLED, {
1085
1223
  subscription: updated,
@@ -1092,7 +1230,8 @@ var SubscriptionRepository = class extends Repository {
1092
1230
  return updated;
1093
1231
  }
1094
1232
  async pause(subscriptionId, options = {}, ctx = {}) {
1095
- const sub = await this.getById(subscriptionId);
1233
+ const opts = this.optsFromCtx(ctx);
1234
+ const sub = await this.getById(subscriptionId, opts);
1096
1235
  if (!sub) throw new SubscriptionNotFoundError(subscriptionId);
1097
1236
  SUBSCRIPTION_STATE_MACHINE.validate(sub.status, SUBSCRIPTION_STATUS.PAUSED, subscriptionId);
1098
1237
  const updated = await this.update(subscriptionId, {
@@ -1100,7 +1239,7 @@ var SubscriptionRepository = class extends Repository {
1100
1239
  isActive: false,
1101
1240
  pausedAt: /* @__PURE__ */ new Date(),
1102
1241
  pauseReason: options.reason
1103
- }, { throwOnNotFound: true });
1242
+ }, this.optsFromCtx(ctx, { throwOnNotFound: true }));
1104
1243
  if (!updated) throw new SubscriptionNotFoundError(subscriptionId);
1105
1244
  await this.dispatch(createEvent(REVENUE_EVENTS.SUBSCRIPTION_PAUSED, {
1106
1245
  subscription: updated,
@@ -1112,7 +1251,8 @@ var SubscriptionRepository = class extends Repository {
1112
1251
  return updated;
1113
1252
  }
1114
1253
  async resume(subscriptionId, options = {}, ctx = {}) {
1115
- const sub = await this.getById(subscriptionId);
1254
+ const opts = this.optsFromCtx(ctx);
1255
+ const sub = await this.getById(subscriptionId, opts);
1116
1256
  if (!sub) throw new SubscriptionNotFoundError(subscriptionId);
1117
1257
  SUBSCRIPTION_STATE_MACHINE.validate(sub.status, SUBSCRIPTION_STATUS.ACTIVE, subscriptionId);
1118
1258
  const updates = {
@@ -1123,7 +1263,7 @@ var SubscriptionRepository = class extends Repository {
1123
1263
  const pauseDuration = Date.now() - sub.pausedAt.getTime();
1124
1264
  updates.endDate = new Date(sub.endDate.getTime() + pauseDuration);
1125
1265
  }
1126
- const updated = await this.update(subscriptionId, updates, { throwOnNotFound: true });
1266
+ const updated = await this.update(subscriptionId, updates, this.optsFromCtx(ctx, { throwOnNotFound: true }));
1127
1267
  if (!updated) throw new SubscriptionNotFoundError(subscriptionId);
1128
1268
  await this.dispatch(createEvent(REVENUE_EVENTS.SUBSCRIPTION_RESUMED, {
1129
1269
  subscription: updated,
@@ -1139,45 +1279,34 @@ var SubscriptionRepository = class extends Repository {
1139
1279
  //#endregion
1140
1280
  //#region src/repositories/settlement.repository.ts
1141
1281
  /**
1142
- * SettlementRepository — data layer + domain verbs.
1282
+ * SettlementRepository — payouts to recipients (organizations, vendors,
1283
+ * affiliates).
1284
+ *
1285
+ * **CRUD inherited** via {@link RevenueRepositoryBase}. **Domain verbs:**
1286
+ * `schedule`, `processPending`, `complete`, `fail`. State machine:
1287
+ * `pending → processing → completed | failed`; `failed` with
1288
+ * `retry: true` resets to `pending` with a delayed `scheduledAt`.
1143
1289
  *
1144
- * CRUD inherited from mongokit. Domain verbs: schedule, processPending, complete, fail.
1290
+ * **Multi-tenant correctness.** Every read/write threads `ctx` through
1291
+ * {@link RevenueRepositoryBase.optsFromCtx} so `multiTenantPlugin`
1292
+ * scope filters apply. 2.1.0 had several `getById`/`update` calls that
1293
+ * dropped ctx — fixed in 2.1.1+.
1145
1294
  *
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.
1295
+ * Bridges (`ledger`, `notification`) fire on `complete()` so a host
1296
+ * can pin double-entry book-keeping or push a "you got paid" email
1297
+ * without the repo knowing about either subsystem.
1149
1298
  */
1150
- var SettlementRepository = class extends Repository {
1151
- deps;
1299
+ var SettlementRepository = class extends RevenueRepositoryBase {
1152
1300
  constructor(model, plugins = []) {
1153
1301
  super(model, plugins);
1154
1302
  }
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
1303
  async schedule(params, ctx = {}) {
1175
1304
  const settlement = await this.create({
1176
1305
  ...params,
1177
1306
  status: SETTLEMENT_STATUS.PENDING,
1178
1307
  scheduledAt: params.scheduledAt ?? /* @__PURE__ */ new Date(),
1179
1308
  retryCount: 0
1180
- });
1309
+ }, this.optsFromCtx(ctx));
1181
1310
  await this.dispatch(createEvent(REVENUE_EVENTS.SETTLEMENT_SCHEDULED, {
1182
1311
  settlement,
1183
1312
  scheduledAt: settlement.scheduledAt
@@ -1198,7 +1327,7 @@ var SettlementRepository = class extends Repository {
1198
1327
  filters: query,
1199
1328
  limit: options.limit ?? 50,
1200
1329
  sort: { scheduledAt: 1 }
1201
- })).data ?? [];
1330
+ }, this.optsFromCtx(ctx))).data ?? [];
1202
1331
  if (options.dryRun) return {
1203
1332
  processed: pending.length,
1204
1333
  succeeded: 0,
@@ -1206,7 +1335,7 @@ var SettlementRepository = class extends Repository {
1206
1335
  settlements: pending,
1207
1336
  errors: []
1208
1337
  };
1209
- const results = {
1338
+ const out = {
1210
1339
  processed: 0,
1211
1340
  succeeded: 0,
1212
1341
  failed: 0,
@@ -1219,9 +1348,9 @@ var SettlementRepository = class extends Repository {
1219
1348
  await this.update(settlement._id, {
1220
1349
  status: SETTLEMENT_STATUS.PROCESSING,
1221
1350
  processedAt: /* @__PURE__ */ new Date()
1222
- });
1223
- results.succeeded++;
1224
- results.settlements.push(settlement);
1351
+ }, this.optsFromCtx(ctx));
1352
+ out.succeeded++;
1353
+ out.settlements.push(settlement);
1225
1354
  await this.dispatch(createEvent(REVENUE_EVENTS.SETTLEMENT_PROCESSING, {
1226
1355
  settlement,
1227
1356
  processedAt: /* @__PURE__ */ new Date()
@@ -1230,18 +1359,19 @@ var SettlementRepository = class extends Repository {
1230
1359
  resourceId: settlement.publicId
1231
1360
  }), ctx);
1232
1361
  } catch (err) {
1233
- results.failed++;
1234
- results.errors.push({
1362
+ out.failed++;
1363
+ out.errors.push({
1235
1364
  settlementId: settlement._id,
1236
1365
  error: err
1237
1366
  });
1238
1367
  }
1239
- results.processed++;
1368
+ out.processed++;
1240
1369
  }
1241
- return results;
1370
+ return out;
1242
1371
  }
1243
1372
  async complete(settlementId, details = {}, ctx = {}) {
1244
- const settlement = await this.getById(settlementId);
1373
+ const opts = this.optsFromCtx(ctx);
1374
+ const settlement = await this.getById(settlementId, opts);
1245
1375
  if (!settlement) throw new SettlementNotFoundError(settlementId);
1246
1376
  SETTLEMENT_STATE_MACHINE.validate(settlement.status, SETTLEMENT_STATUS.COMPLETED, settlementId);
1247
1377
  const updates = {
@@ -1263,7 +1393,7 @@ var SettlementRepository = class extends Repository {
1263
1393
  transactionHash: details.transactionHash,
1264
1394
  transferredAt: details.transferredAt ?? /* @__PURE__ */ new Date()
1265
1395
  };
1266
- const updated = await this.update(settlementId, updates, { throwOnNotFound: true });
1396
+ const updated = await this.update(settlementId, updates, this.optsFromCtx(ctx, { throwOnNotFound: true }));
1267
1397
  if (!updated) throw new SettlementNotFoundError(settlementId);
1268
1398
  await this.deps.bridges.ledger?.onSettlementCompleted?.(updated, ctx);
1269
1399
  await this.deps.bridges.notification?.onSettlementCompleted?.(updated, ctx);
@@ -1277,7 +1407,8 @@ var SettlementRepository = class extends Repository {
1277
1407
  return updated;
1278
1408
  }
1279
1409
  async fail(settlementId, reason, options = {}, ctx = {}) {
1280
- const settlement = await this.getById(settlementId);
1410
+ const opts = this.optsFromCtx(ctx);
1411
+ const settlement = await this.getById(settlementId, opts);
1281
1412
  if (!settlement) throw new SettlementNotFoundError(settlementId);
1282
1413
  if (options.retry) await this.update(settlementId, {
1283
1414
  status: SETTLEMENT_STATUS.PENDING,
@@ -1285,7 +1416,7 @@ var SettlementRepository = class extends Repository {
1285
1416
  failureReason: reason,
1286
1417
  failureCode: options.code,
1287
1418
  scheduledAt: new Date(Date.now() + 36e5)
1288
- });
1419
+ }, opts);
1289
1420
  else {
1290
1421
  SETTLEMENT_STATE_MACHINE.validate(settlement.status, SETTLEMENT_STATUS.FAILED, settlementId);
1291
1422
  await this.update(settlementId, {
@@ -1293,9 +1424,9 @@ var SettlementRepository = class extends Repository {
1293
1424
  failedAt: /* @__PURE__ */ new Date(),
1294
1425
  failureReason: reason,
1295
1426
  failureCode: options.code
1296
- });
1427
+ }, opts);
1297
1428
  }
1298
- const updated = await this.getById(settlementId);
1429
+ const updated = await this.getById(settlementId, opts);
1299
1430
  await this.dispatch(createEvent(REVENUE_EVENTS.SETTLEMENT_FAILED, {
1300
1431
  settlement: updated,
1301
1432
  reason,
@@ -1,4 +1,4 @@
1
- import { a as reverseTax, c as reverseCommission, i as getTaxType, n as calculateSplits, o as validateTaxCalculation, r as calculateTax, s as calculateCommission, t as calculateOrganizationPayout } from "../splits-D8XkNWgX.mjs";
1
+ import { a as reverseTax, c as reverseCommission, i as getTaxType, n as calculateSplits, o as validateTaxCalculation, r as calculateTax, s as calculateCommission, t as calculateOrganizationPayout } from "../splits-CNfQj92L.mjs";
2
2
  import { C as sumMoney, E as toSmallestUnit, S as subtractMoney, T as toMajor, _ as isZeroMoney, a as CurrencyMismatchError, b as multiplyMoney, c as addMoney, d as fromMajor, f as fromSmallestUnit, g as isPositiveMoney, h as isNegativeMoney, i as CURRENCIES, l as compareMoney, m as isMoney, n as getAuditTrail, o as MINOR_UNIT_FACTOR, p as isCurrencyCode, r as getLastStateChange, s as absMoney, t as appendAuditEvent, u as equalsMoney, v as minorUnitFactor, w as toCurrencyCode, x as negateMoney, y as money } from "../audit-Ba2XB2C4.mjs";
3
3
 
4
4
  export { CURRENCIES, CurrencyMismatchError, MINOR_UNIT_FACTOR, absMoney, addMoney, appendAuditEvent, calculateCommission, calculateOrganizationPayout, calculateSplits, calculateTax, compareMoney, equalsMoney, fromMajor, fromSmallestUnit, getAuditTrail, getLastStateChange, getTaxType, isCurrencyCode, isMoney, isNegativeMoney, isPositiveMoney, isZeroMoney, minorUnitFactor, money, multiplyMoney, negateMoney, reverseCommission, reverseTax, subtractMoney, sumMoney, toCurrencyCode, toMajor, toSmallestUnit, validateTaxCalculation };
@@ -1,4 +1,4 @@
1
- import { A as transactionBaseSchema, C as subscriptionBaseSchema, D as TransactionCreateInput, E as subscriptionUpdateSchema, M as transactionListFilterSchema, N as transactionUpdateSchema, O as TransactionListFilter, S as SubscriptionUpdateInput, T as subscriptionListFilterSchema, _ as settlementCreateSchema, a as escrowReleaseSchema, b as SubscriptionCreateInput, c as PaymentVerifyInput, d as paymentVerifySchema, f as refundSchema, g as settlementBaseSchema, h as SettlementUpdateInput, i as escrowHoldSchema, j as transactionCreateSchema, k as TransactionUpdateInput, l as RefundInput, m as SettlementListFilter, n as EscrowReleaseInput, o as splitRuleSchema, p as SettlementCreateInput, r as SplitRuleInput, s as PaymentIntentInput, t as EscrowHoldInput, u as paymentIntentSchema, v as settlementListFilterSchema, w as subscriptionCreateSchema, x as SubscriptionListFilter, y as settlementUpdateSchema } from "../escrow.schema-9yh4Q-aQ.mjs";
1
+ import { A as transactionBaseSchema, C as subscriptionBaseSchema, D as TransactionCreateInput, E as subscriptionUpdateSchema, M as transactionListFilterSchema, N as transactionUpdateSchema, O as TransactionListFilter, S as SubscriptionUpdateInput, T as subscriptionListFilterSchema, _ as settlementCreateSchema, a as escrowReleaseSchema, b as SubscriptionCreateInput, c as PaymentVerifyInput, d as paymentVerifySchema, f as refundSchema, g as settlementBaseSchema, h as SettlementUpdateInput, i as escrowHoldSchema, j as transactionCreateSchema, k as TransactionUpdateInput, l as RefundInput, m as SettlementListFilter, n as EscrowReleaseInput, o as splitRuleSchema, p as SettlementCreateInput, r as SplitRuleInput, s as PaymentIntentInput, t as EscrowHoldInput, u as paymentIntentSchema, v as settlementListFilterSchema, w as subscriptionCreateSchema, x as SubscriptionListFilter, y as settlementUpdateSchema } from "../escrow.schema-YuBgjL-I.mjs";
2
2
  import { z } from "zod";
3
3
 
4
4
  //#region src/validators/bank-feed.schema.d.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/revenue",
3
- "version": "2.1.0",
3
+ "version": "2.1.3",
4
4
  "description": "Payment lifecycle engine — transactions, subscriptions, escrow, settlements, commissions. MongoKit-powered, Arc-compatible, framework-agnostic.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -100,9 +100,9 @@
100
100
  "LICENSE"
101
101
  ],
102
102
  "peerDependencies": {
103
- "@classytic/mongokit": ">=3.13.0",
104
- "@classytic/repo-core": ">=0.4.0",
103
+ "@classytic/mongokit": ">=3.13.3",
105
104
  "@classytic/primitives": ">=0.5.0",
105
+ "@classytic/repo-core": ">=0.4.2",
106
106
  "mongoose": ">=9.4.1",
107
107
  "zod": ">=4.0.0"
108
108
  },
@@ -110,9 +110,9 @@
110
110
  "node": ">=18.0.0"
111
111
  },
112
112
  "devDependencies": {
113
- "@classytic/mongokit": ">=3.13.0",
114
- "@classytic/repo-core": ">=0.4.0",
113
+ "@classytic/mongokit": "^3.13.3",
115
114
  "@classytic/primitives": ">=0.5.0",
115
+ "@classytic/repo-core": "^0.4.2",
116
116
  "@types/node": "^22.8.7",
117
117
  "mongoose": "^9.4.1",
118
118
  "tsdown": "^0.20.3",