@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.
- package/CHANGELOG.md +66 -0
- package/README.md +33 -10
- package/dist/{bank-feed-DJtLvz_7.mjs → bank-feed-BlQeq2rK.mjs} +1 -1
- package/dist/core/state-machines.mjs +2 -2
- package/dist/{engine-types-txFXOiQS.d.mts → engine-types-Jctrbasz.d.mts} +192 -80
- package/dist/enums/index.mjs +1 -1
- package/dist/{escrow.schema-9yh4Q-aQ.d.mts → escrow.schema-YuBgjL-I.d.mts} +11 -11
- package/dist/events/index.mjs +2 -2
- package/dist/index.d.mts +14 -5
- package/dist/index.mjs +30 -8
- package/dist/providers/index.mjs +1 -1
- package/dist/repositories/create-repositories.d.mts +1 -1
- package/dist/repositories/create-repositories.mjs +1 -1
- package/dist/{revenue-event-catalog-CgZ57M-f.mjs → revenue-event-catalog-BvjNVnPd.mjs} +1 -1
- package/dist/{settlement.repository-Ba2U17zY.mjs → settlement.repository-BAdc9qGl.mjs} +252 -121
- package/dist/shared/index.mjs +1 -1
- package/dist/validators/index.d.mts +1 -1
- package/package.json +5 -5
- /package/dist/{errors-Dt46UZL_.mjs → errors-LYYg9wcs.mjs} +0 -0
- /package/dist/{event-constants-CTiDNWzc.mjs → event-constants-Dn1TKahe.mjs} +0 -0
- /package/dist/{splits-D8XkNWgX.mjs → splits-CNfQj92L.mjs} +0 -0
- /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-
|
|
3
|
-
import { g as SETTLEMENT_STATUS, r as SUBSCRIPTION_STATUS, w as HOLD_STATUS } from "./subscription.enums-
|
|
4
|
-
import { f as SettlementNotFoundError, g as WrongTransactionKindError, h as ValidationError, m as TransactionNotFoundError, n as BankFeedImportError, p as SubscriptionNotFoundError } from "./errors-
|
|
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-
|
|
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/
|
|
10
|
-
|
|
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
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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 = {
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
*
|
|
35
|
-
* business write (P8 true session-bound write)
|
|
36
|
-
*
|
|
37
|
-
*
|
|
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
|
-
*
|
|
40
|
-
*
|
|
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
|
|
53
|
-
* the outbox, so in-process
|
|
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.
|
|
395
|
+
const updated = await this.findOneAndUpdate({
|
|
271
396
|
_id: transaction._id,
|
|
272
397
|
"webhook.eventId": { $ne: webhookEvent.id }
|
|
273
|
-
}, { $set: { webhook: nextWebhook } }, { returnDocument: "after" })
|
|
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:
|
|
1078
|
+
status: { $in: validStatuses },
|
|
943
1079
|
amount: {
|
|
944
1080
|
$gte: minAmount,
|
|
945
1081
|
$lte: maxAmount
|
|
946
1082
|
},
|
|
947
|
-
$or:
|
|
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
|
|
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
|
-
*
|
|
1012
|
-
*
|
|
1013
|
-
*
|
|
1014
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 —
|
|
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
|
-
*
|
|
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
|
-
*
|
|
1147
|
-
*
|
|
1148
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
1224
|
-
|
|
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
|
-
|
|
1234
|
-
|
|
1362
|
+
out.failed++;
|
|
1363
|
+
out.errors.push({
|
|
1235
1364
|
settlementId: settlement._id,
|
|
1236
1365
|
error: err
|
|
1237
1366
|
});
|
|
1238
1367
|
}
|
|
1239
|
-
|
|
1368
|
+
out.processed++;
|
|
1240
1369
|
}
|
|
1241
|
-
return
|
|
1370
|
+
return out;
|
|
1242
1371
|
}
|
|
1243
1372
|
async complete(settlementId, details = {}, ctx = {}) {
|
|
1244
|
-
const
|
|
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
|
|
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,
|
package/dist/shared/index.mjs
CHANGED
|
@@ -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-
|
|
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-
|
|
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.
|
|
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.
|
|
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": "
|
|
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",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|