@classytic/revenue 1.1.4 → 2.0.1
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 +90 -0
- package/README.md +638 -632
- package/dist/audit-B39B0Sdq.mjs +53 -0
- package/dist/audit-DZ0eTr9g.d.mts +89 -0
- package/dist/bridges/index.d.mts +2 -0
- package/dist/bridges/index.mjs +1 -0
- package/dist/context-DRqSeTPM.d.mts +35 -0
- package/dist/core/state-machines.d.mts +53 -0
- package/dist/core/state-machines.mjs +153 -0
- package/dist/engine-types-CcjIb4Fy.d.mts +611 -0
- package/dist/enums/index.d.mts +3 -157
- package/dist/enums/index.mjs +3 -55
- package/dist/errors-DHa8JVQ-.mjs +92 -0
- package/dist/escrow.schema-BBv9oVEW.mjs +322 -0
- package/dist/escrow.schema-D5X32LwX.d.mts +629 -0
- package/dist/event-constants-CEMitnIV.mjs +53 -0
- package/dist/events/index.d.mts +3 -0
- package/dist/events/index.mjs +4 -0
- package/dist/index.d.mts +77 -9
- package/dist/index.mjs +465 -29
- package/dist/monetization.enums-BtiU3t8o.mjs +39 -0
- package/dist/monetization.enums-D2xbxXJM.d.mts +34 -0
- package/dist/plugins/plugin.interface.d.mts +28 -0
- package/dist/plugins/plugin.interface.mjs +26 -0
- package/dist/providers/index.d.mts +2 -3
- package/dist/providers/index.mjs +2 -2
- package/dist/{base-DCoyIUj6.mjs → registry-DhFMsSn5.mjs} +34 -36
- package/dist/{base-CsTlVQJe.d.mts → registry-SvIGPAx_.d.mts} +73 -66
- package/dist/repositories/create-repositories.d.mts +21 -0
- package/dist/repositories/create-repositories.mjs +12 -0
- package/dist/revenue-bridges-sdlrR85c.d.mts +145 -0
- package/dist/revenue-event-catalog-BX3g7RUi.d.mts +823 -0
- package/dist/revenue-event-catalog-LqxPnsU_.mjs +388 -0
- package/dist/settlement.repository-DHIPx5S4.mjs +771 -0
- package/dist/shared/index.d.mts +2 -0
- package/dist/shared/index.mjs +4 -0
- package/dist/splits-BAfY-a9P.mjs +123 -0
- package/dist/subscription.enums-tfoAgsTv.mjs +172 -0
- package/dist/transaction.enums-u4MshXcL.d.mts +154 -0
- package/dist/validators/index.d.mts +2 -0
- package/dist/validators/index.mjs +3 -0
- package/package.json +37 -38
- package/dist/application/services/index.d.mts +0 -4
- package/dist/application/services/index.mjs +0 -3
- package/dist/category-resolver-DV83N8ok.mjs +0 -284
- package/dist/commission-split-BzB8cd39.mjs +0 -485
- package/dist/core/events.d.mts +0 -294
- package/dist/core/events.mjs +0 -100
- package/dist/core/index.d.mts +0 -9
- package/dist/core/index.mjs +0 -8
- package/dist/errors-rRdOqnWx.d.mts +0 -787
- package/dist/escrow.enums-CZGrrdg7.mjs +0 -101
- package/dist/escrow.enums-DwdLuuve.d.mts +0 -78
- package/dist/idempotency-DaYcUGY1.mjs +0 -172
- package/dist/index-Dsp7H5Wb.d.mts +0 -471
- package/dist/infrastructure/plugins/index.d.mts +0 -239
- package/dist/infrastructure/plugins/index.mjs +0 -345
- package/dist/money-CvrDOijQ.mjs +0 -271
- package/dist/money-DPG8AtJ8.d.mts +0 -112
- package/dist/payment.enums-HAuAS9Pp.d.mts +0 -70
- package/dist/payment.enums-tEFVa-Xp.mjs +0 -69
- package/dist/plugin-BbK0OVHy.d.mts +0 -327
- package/dist/plugin-Cd_V04Em.mjs +0 -210
- package/dist/reconciliation/index.d.mts +0 -193
- package/dist/reconciliation/index.mjs +0 -192
- package/dist/retry-HHCOXYdn.d.mts +0 -186
- package/dist/revenue-BhdS7nXh.mjs +0 -553
- package/dist/schemas/index.d.mts +0 -2665
- package/dist/schemas/index.mjs +0 -717
- package/dist/schemas/validation.d.mts +0 -375
- package/dist/schemas/validation.mjs +0 -325
- package/dist/settlement.enums-DFhkqZEY.d.mts +0 -132
- package/dist/settlement.schema-DnNSFpGd.d.mts +0 -344
- package/dist/settlement.service-DjzAjezU.d.mts +0 -594
- package/dist/settlement.service-DmdKv0Zu.mjs +0 -2511
- package/dist/split.enums-BrjabxIX.mjs +0 -86
- package/dist/split.enums-DmskfLOM.d.mts +0 -43
- package/dist/tax-BoCt5cEd.d.mts +0 -61
- package/dist/tax-EQ15DO81.mjs +0 -162
- package/dist/transaction.enums-pCyMFT4Z.mjs +0 -96
- package/dist/utils/index.d.mts +0 -428
- package/dist/utils/index.mjs +0 -346
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
import { n as createEvent, t as REVENUE_EVENTS } from "./event-constants-CEMitnIV.mjs";
|
|
2
|
+
import { F as TRANSACTION_STATUS, g as SETTLEMENT_STATUS, r as SUBSCRIPTION_STATUS, w as HOLD_STATUS } from "./subscription.enums-tfoAgsTv.mjs";
|
|
3
|
+
import { d as SubscriptionNotFoundError, f as TransactionNotFoundError, p as ValidationError, u as SettlementNotFoundError } from "./errors-DHa8JVQ-.mjs";
|
|
4
|
+
import { SETTLEMENT_STATE_MACHINE, SUBSCRIPTION_STATE_MACHINE, TRANSACTION_STATE_MACHINE } from "./core/state-machines.mjs";
|
|
5
|
+
import { a as reverseTax, c as reverseCommission, n as calculateSplits, s as calculateCommission, t as calculateOrganizationPayout } from "./splits-BAfY-a9P.mjs";
|
|
6
|
+
import { Repository, withTransaction } from "@classytic/mongokit";
|
|
7
|
+
|
|
8
|
+
//#region src/repositories/transaction.repository.ts
|
|
9
|
+
/**
|
|
10
|
+
* TransactionRepository — extends mongokit Repository.
|
|
11
|
+
*
|
|
12
|
+
* CRUD inherited: getAll, getById, getByQuery, create, update, delete, count, exists.
|
|
13
|
+
* Domain verbs: createPaymentIntent, verify, refund, handleWebhook, hold, release, split.
|
|
14
|
+
*
|
|
15
|
+
* All domain verbs return raw mongokit docs — no custom envelopes.
|
|
16
|
+
* Composite results (refund creates a new doc) are stored in metadata on the primary doc.
|
|
17
|
+
*/
|
|
18
|
+
var TransactionRepository = class extends Repository {
|
|
19
|
+
deps;
|
|
20
|
+
constructor(model, plugins = []) {
|
|
21
|
+
super(model, plugins);
|
|
22
|
+
}
|
|
23
|
+
inject(deps) {
|
|
24
|
+
this.deps = deps;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Thread `ctx.organizationId` (and future ctx fields) into mongokit
|
|
28
|
+
* options so the `multiTenantPlugin` can auto-scope filters, queries,
|
|
29
|
+
* and inserts. Merges any caller-supplied extras. Centralizing this
|
|
30
|
+
* here means every domain verb participates in scope isolation without
|
|
31
|
+
* per-call boilerplate.
|
|
32
|
+
*/
|
|
33
|
+
optsFromCtx(ctx, extra = {}) {
|
|
34
|
+
const out = { ...extra };
|
|
35
|
+
if (ctx.organizationId !== void 0) out.organizationId = ctx.organizationId;
|
|
36
|
+
if (ctx.session !== void 0) out.session = ctx.session;
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Save an event to the host-owned outbox, session-bound when available.
|
|
41
|
+
*
|
|
42
|
+
* When `session` is passed, the outbox row commits atomically with the
|
|
43
|
+
* business write (P8 true session-bound write). When absent, the save
|
|
44
|
+
* happens after commit — still durable via the host's relay, but with a
|
|
45
|
+
* small at-most-once window on process crash.
|
|
46
|
+
*
|
|
47
|
+
* Isolated try/catch: an outbox failure never throws out of this helper;
|
|
48
|
+
* the caller still issues a transport.publish.
|
|
49
|
+
*/
|
|
50
|
+
async saveToOutbox(event, session) {
|
|
51
|
+
if (!this.deps.outbox) return;
|
|
52
|
+
try {
|
|
53
|
+
await this.deps.outbox.save(event, session !== void 0 ? { session } : {});
|
|
54
|
+
} catch (err) {
|
|
55
|
+
this.deps.logger?.error("[revenue] outbox.save failed for", event.type, err);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Publish an event to the in-process `EventTransport` after commit.
|
|
60
|
+
* Transport failure is logged — the host relay will still redeliver from
|
|
61
|
+
* the outbox, so in-process subscribers missing an event is recoverable.
|
|
62
|
+
*/
|
|
63
|
+
async publishToTransport(event) {
|
|
64
|
+
try {
|
|
65
|
+
await this.deps.events.publish(event);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
this.deps.logger?.error("[revenue] events.publish failed for", event.type, err);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Non-transactional dispatch (used by verbs that don't open their own
|
|
72
|
+
* `withTransaction` block): outbox.save (session-bound when ctx provides
|
|
73
|
+
* one) → transport.publish. Matches arc's EventOutbox + MemoryEventTransport
|
|
74
|
+
* wiring bit-for-bit.
|
|
75
|
+
*/
|
|
76
|
+
async dispatch(event, ctx = {}) {
|
|
77
|
+
await this.saveToOutbox(event, ctx.session);
|
|
78
|
+
await this.publishToTransport(event);
|
|
79
|
+
}
|
|
80
|
+
/** Creates transaction + calls provider. Returns the created transaction doc. */
|
|
81
|
+
async createPaymentIntent(params, ctx = {}) {
|
|
82
|
+
const currency = params.currency ?? this.deps.defaultCurrency;
|
|
83
|
+
const provider = this.deps.providers.get(params.gateway);
|
|
84
|
+
if (params.idempotencyKey) {
|
|
85
|
+
const existing = await this.getByQuery({ idempotencyKey: params.idempotencyKey }, this.optsFromCtx(ctx, { throwOnNotFound: false }));
|
|
86
|
+
if (existing) return existing;
|
|
87
|
+
}
|
|
88
|
+
const commissionRate = this.deps.commission?.defaultRate ?? 0;
|
|
89
|
+
const gatewayFeeRate = this.deps.commission?.gatewayFeeRate ?? 0;
|
|
90
|
+
const commission = calculateCommission(params.amount, commissionRate, gatewayFeeRate);
|
|
91
|
+
let gatewayData = { type: params.gateway };
|
|
92
|
+
if (params.amount > 0) {
|
|
93
|
+
const intent = await provider.createIntent({
|
|
94
|
+
amount: params.amount,
|
|
95
|
+
currency,
|
|
96
|
+
metadata: params.metadata,
|
|
97
|
+
...params.paymentData
|
|
98
|
+
});
|
|
99
|
+
gatewayData = {
|
|
100
|
+
type: params.gateway,
|
|
101
|
+
sessionId: intent.sessionId,
|
|
102
|
+
paymentIntentId: intent.paymentIntentId ?? intent.id,
|
|
103
|
+
metadata: {
|
|
104
|
+
clientSecret: intent.clientSecret,
|
|
105
|
+
paymentUrl: intent.paymentUrl,
|
|
106
|
+
instructions: intent.instructions
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
const transaction = await this.create({
|
|
111
|
+
organizationId: ctx.organizationId,
|
|
112
|
+
customerId: params.data?.customerId ?? null,
|
|
113
|
+
type: params.monetizationType === "subscription" ? "subscription" : "purchase",
|
|
114
|
+
flow: "inflow",
|
|
115
|
+
tags: params.monetizationType ? [params.monetizationType] : [],
|
|
116
|
+
amount: params.amount,
|
|
117
|
+
currency,
|
|
118
|
+
fee: commission?.gatewayFeeAmount ?? 0,
|
|
119
|
+
tax: 0,
|
|
120
|
+
net: params.amount - (commission?.gatewayFeeAmount ?? 0),
|
|
121
|
+
method: params.gateway,
|
|
122
|
+
status: params.amount === 0 ? TRANSACTION_STATUS.VERIFIED : TRANSACTION_STATUS.PENDING,
|
|
123
|
+
gateway: gatewayData,
|
|
124
|
+
commission: commission ?? void 0,
|
|
125
|
+
sourceId: params.data?.sourceId,
|
|
126
|
+
sourceModel: params.data?.sourceModel,
|
|
127
|
+
idempotencyKey: params.idempotencyKey,
|
|
128
|
+
metadata: params.metadata
|
|
129
|
+
}, this.optsFromCtx(ctx));
|
|
130
|
+
await this.dispatch(createEvent(REVENUE_EVENTS.MONETIZATION_CREATED, {
|
|
131
|
+
monetizationType: params.monetizationType,
|
|
132
|
+
transaction
|
|
133
|
+
}, ctx, {
|
|
134
|
+
resource: "transaction",
|
|
135
|
+
resourceId: transaction.publicId
|
|
136
|
+
}), ctx);
|
|
137
|
+
return transaction;
|
|
138
|
+
}
|
|
139
|
+
/** Verifies payment via provider, updates status. Returns the updated doc. */
|
|
140
|
+
async verify(paymentIntentId, options = {}, ctx = {}) {
|
|
141
|
+
const readOpts = this.optsFromCtx(ctx, { throwOnNotFound: false });
|
|
142
|
+
let transaction = await this.getByQuery({ "gateway.sessionId": paymentIntentId }, readOpts);
|
|
143
|
+
if (!transaction) transaction = await this.getByQuery({ "gateway.paymentIntentId": paymentIntentId }, readOpts);
|
|
144
|
+
if (!transaction) transaction = await this.getById(paymentIntentId, readOpts);
|
|
145
|
+
if (!transaction) throw new TransactionNotFoundError(paymentIntentId);
|
|
146
|
+
const provider = this.deps.providers.get(transaction.method);
|
|
147
|
+
const intentId = transaction.gateway?.paymentIntentId ?? transaction.gateway?.sessionId ?? paymentIntentId;
|
|
148
|
+
const paymentResult = await provider.verifyPayment(intentId);
|
|
149
|
+
let newStatus;
|
|
150
|
+
if (paymentResult.status === "succeeded") newStatus = TRANSACTION_STATUS.VERIFIED;
|
|
151
|
+
else if (paymentResult.status === "failed") newStatus = TRANSACTION_STATUS.FAILED;
|
|
152
|
+
else if (paymentResult.status === "requires_action") newStatus = TRANSACTION_STATUS.REQUIRES_ACTION;
|
|
153
|
+
else newStatus = TRANSACTION_STATUS.PROCESSING;
|
|
154
|
+
TRANSACTION_STATE_MACHINE.validate(transaction.status, newStatus, String(transaction._id));
|
|
155
|
+
const updates = { status: newStatus };
|
|
156
|
+
if (newStatus === TRANSACTION_STATUS.VERIFIED) {
|
|
157
|
+
updates.verifiedAt = /* @__PURE__ */ new Date();
|
|
158
|
+
updates.verifiedBy = options.verifiedBy;
|
|
159
|
+
} else if (newStatus === TRANSACTION_STATUS.FAILED) {
|
|
160
|
+
updates.failedAt = /* @__PURE__ */ new Date();
|
|
161
|
+
updates.failureReason = "Payment verification failed";
|
|
162
|
+
}
|
|
163
|
+
const updated = await this.update(transaction._id, updates, this.optsFromCtx(ctx));
|
|
164
|
+
if (newStatus === TRANSACTION_STATUS.VERIFIED) {
|
|
165
|
+
await this.deps.bridges.ledger?.onPaymentVerified?.(updated, ctx);
|
|
166
|
+
await this.deps.bridges.notification?.onPaymentVerified?.(updated, ctx);
|
|
167
|
+
}
|
|
168
|
+
const eventName = newStatus === TRANSACTION_STATUS.VERIFIED ? REVENUE_EVENTS.PAYMENT_VERIFIED : newStatus === TRANSACTION_STATUS.FAILED ? REVENUE_EVENTS.PAYMENT_FAILED : newStatus === TRANSACTION_STATUS.REQUIRES_ACTION ? REVENUE_EVENTS.PAYMENT_REQUIRES_ACTION : REVENUE_EVENTS.PAYMENT_PROCESSING;
|
|
169
|
+
await this.dispatch(createEvent(eventName, {
|
|
170
|
+
transaction: updated,
|
|
171
|
+
paymentResult,
|
|
172
|
+
verifiedBy: options.verifiedBy
|
|
173
|
+
}, ctx, {
|
|
174
|
+
resource: "transaction",
|
|
175
|
+
resourceId: updated?.publicId
|
|
176
|
+
}), ctx);
|
|
177
|
+
return updated;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Creates refund transaction, updates original. Returns the refund transaction doc.
|
|
181
|
+
*
|
|
182
|
+
* The provider call happens OUTSIDE the transaction — it's a non-idempotent external
|
|
183
|
+
* side effect we can't roll back. The two Mongo writes (create refund + update original)
|
|
184
|
+
* run inside `withTransaction` so they commit atomically or both abort. Bridges and
|
|
185
|
+
* event emission run AFTER commit because they're independent side effects; rolling
|
|
186
|
+
* them back would not undo external state anyway.
|
|
187
|
+
*
|
|
188
|
+
* Powered by mongokit 3.6's module-level `withTransaction` helper. Automatically
|
|
189
|
+
* retries on `TransientTransactionError` / `UnknownTransactionCommitResult`.
|
|
190
|
+
*/
|
|
191
|
+
async refund(transactionId, amount, options = {}, ctx = {}) {
|
|
192
|
+
const transaction = await this.getById(transactionId, this.optsFromCtx(ctx));
|
|
193
|
+
if (!transaction) throw new TransactionNotFoundError(transactionId);
|
|
194
|
+
const refundAmount = amount ?? transaction.amount;
|
|
195
|
+
const existingRefunded = transaction.refundedAmount ?? 0;
|
|
196
|
+
const isPartialRefund = existingRefunded + refundAmount < transaction.amount;
|
|
197
|
+
const newStatus = isPartialRefund ? TRANSACTION_STATUS.PARTIALLY_REFUNDED : TRANSACTION_STATUS.REFUNDED;
|
|
198
|
+
TRANSACTION_STATE_MACHINE.validate(transaction.status, newStatus, transactionId);
|
|
199
|
+
const provider = this.deps.providers.get(transaction.method);
|
|
200
|
+
const paymentId = transaction.gateway?.paymentIntentId ?? transaction.gateway?.sessionId ?? transactionId;
|
|
201
|
+
await provider.refund(paymentId, refundAmount, { reason: options.reason });
|
|
202
|
+
const reversedCommission = reverseCommission(transaction.commission, transaction.amount, refundAmount);
|
|
203
|
+
const reversedTax = transaction.tax ? reverseTax({
|
|
204
|
+
isApplicable: true,
|
|
205
|
+
rate: 0,
|
|
206
|
+
baseAmount: transaction.amount,
|
|
207
|
+
taxAmount: transaction.tax,
|
|
208
|
+
totalAmount: transaction.amount + transaction.tax,
|
|
209
|
+
pricesIncludeTax: false
|
|
210
|
+
}, transaction.amount, refundAmount) : void 0;
|
|
211
|
+
const pendingEvents = [];
|
|
212
|
+
const refundTransaction = await withTransaction(this.Model.db, async (session) => {
|
|
213
|
+
const writeOpts = this.optsFromCtx(ctx, { session });
|
|
214
|
+
const refundTxn = await this.create({
|
|
215
|
+
organizationId: transaction.organizationId,
|
|
216
|
+
customerId: transaction.customerId,
|
|
217
|
+
type: "refund",
|
|
218
|
+
flow: "outflow",
|
|
219
|
+
tags: ["refund"],
|
|
220
|
+
amount: refundAmount,
|
|
221
|
+
currency: transaction.currency,
|
|
222
|
+
fee: reversedCommission?.gatewayFeeAmount ?? 0,
|
|
223
|
+
tax: reversedTax?.taxAmount ?? 0,
|
|
224
|
+
net: refundAmount - (reversedCommission?.gatewayFeeAmount ?? 0) - (reversedTax?.taxAmount ?? 0),
|
|
225
|
+
method: transaction.method,
|
|
226
|
+
status: TRANSACTION_STATUS.VERIFIED,
|
|
227
|
+
gateway: transaction.gateway,
|
|
228
|
+
commission: reversedCommission ?? void 0,
|
|
229
|
+
relatedTransactionId: transaction._id,
|
|
230
|
+
sourceId: transaction.sourceId,
|
|
231
|
+
sourceModel: transaction.sourceModel,
|
|
232
|
+
verifiedAt: /* @__PURE__ */ new Date(),
|
|
233
|
+
metadata: { reason: options.reason }
|
|
234
|
+
}, writeOpts);
|
|
235
|
+
await this.update(transactionId, {
|
|
236
|
+
status: newStatus,
|
|
237
|
+
refundedAmount: existingRefunded + refundAmount,
|
|
238
|
+
refundedAt: /* @__PURE__ */ new Date()
|
|
239
|
+
}, writeOpts);
|
|
240
|
+
const event = createEvent(REVENUE_EVENTS.PAYMENT_REFUNDED, {
|
|
241
|
+
transaction,
|
|
242
|
+
refundTransaction: refundTxn,
|
|
243
|
+
refundAmount,
|
|
244
|
+
reason: options.reason,
|
|
245
|
+
isPartialRefund
|
|
246
|
+
}, ctx, {
|
|
247
|
+
resource: "transaction",
|
|
248
|
+
resourceId: transaction.publicId
|
|
249
|
+
});
|
|
250
|
+
await this.saveToOutbox(event, session);
|
|
251
|
+
pendingEvents.push(event);
|
|
252
|
+
return refundTxn;
|
|
253
|
+
});
|
|
254
|
+
await this.deps.bridges.ledger?.onRefundProcessed?.(transaction, refundTransaction, ctx);
|
|
255
|
+
await this.deps.bridges.notification?.onRefundProcessed?.(refundTransaction, ctx);
|
|
256
|
+
for (const ev of pendingEvents) await this.publishToTransport(ev);
|
|
257
|
+
return refundTransaction;
|
|
258
|
+
}
|
|
259
|
+
/** Handles provider webhook. Returns the updated transaction doc (or null if not found). */
|
|
260
|
+
async handleWebhook(providerName, payload, headers, ctx = {}) {
|
|
261
|
+
const webhookEvent = await this.deps.providers.get(providerName).handleWebhook(payload, headers);
|
|
262
|
+
const readOpts = this.optsFromCtx(ctx, { throwOnNotFound: false });
|
|
263
|
+
const sessionId = webhookEvent.data?.sessionId;
|
|
264
|
+
const intentId = webhookEvent.data?.paymentIntentId;
|
|
265
|
+
let transaction = sessionId ? await this.getByQuery({ "gateway.sessionId": sessionId }, readOpts) : null;
|
|
266
|
+
if (!transaction && intentId) transaction = await this.getByQuery({ "gateway.paymentIntentId": intentId }, readOpts);
|
|
267
|
+
if (!transaction) return null;
|
|
268
|
+
if (transaction.webhook?.eventId === webhookEvent.id) return transaction;
|
|
269
|
+
const nextWebhook = {
|
|
270
|
+
eventId: webhookEvent.id,
|
|
271
|
+
eventType: webhookEvent.type,
|
|
272
|
+
receivedAt: /* @__PURE__ */ new Date(),
|
|
273
|
+
processedAt: /* @__PURE__ */ new Date(),
|
|
274
|
+
data: webhookEvent.data
|
|
275
|
+
};
|
|
276
|
+
const updated = await this.Model.findOneAndUpdate({
|
|
277
|
+
_id: transaction._id,
|
|
278
|
+
"webhook.eventId": { $ne: webhookEvent.id }
|
|
279
|
+
}, { $set: { webhook: nextWebhook } }, { returnDocument: "after" }).lean();
|
|
280
|
+
if (!updated) return await this.getByQuery({ _id: transaction._id }, readOpts) ?? transaction;
|
|
281
|
+
await this.dispatch(createEvent(REVENUE_EVENTS.WEBHOOK_PROCESSED, {
|
|
282
|
+
webhookType: webhookEvent.type,
|
|
283
|
+
provider: providerName,
|
|
284
|
+
event: webhookEvent,
|
|
285
|
+
transaction: updated
|
|
286
|
+
}, ctx, {
|
|
287
|
+
resource: "transaction",
|
|
288
|
+
resourceId: updated?.publicId
|
|
289
|
+
}), ctx);
|
|
290
|
+
return updated;
|
|
291
|
+
}
|
|
292
|
+
/** Places hold on verified transaction. Returns the updated doc. */
|
|
293
|
+
async hold(transactionId, options = {}, ctx = {}) {
|
|
294
|
+
const transaction = await this.getById(transactionId, this.optsFromCtx(ctx));
|
|
295
|
+
if (!transaction) throw new TransactionNotFoundError(transactionId);
|
|
296
|
+
if (transaction.status !== TRANSACTION_STATUS.VERIFIED) throw new ValidationError("Can only hold verified transactions", { status: transaction.status });
|
|
297
|
+
const holdAmount = options.amount ?? transaction.amount;
|
|
298
|
+
const updated = await this.update(transactionId, { hold: {
|
|
299
|
+
status: HOLD_STATUS.HELD,
|
|
300
|
+
heldAmount: holdAmount,
|
|
301
|
+
releasedAmount: 0,
|
|
302
|
+
reason: options.reason ?? "manual_hold",
|
|
303
|
+
heldAt: /* @__PURE__ */ new Date(),
|
|
304
|
+
holdUntil: options.holdUntil,
|
|
305
|
+
releases: [],
|
|
306
|
+
metadata: options.metadata
|
|
307
|
+
} }, this.optsFromCtx(ctx));
|
|
308
|
+
await this.dispatch(createEvent(REVENUE_EVENTS.ESCROW_HELD, {
|
|
309
|
+
transaction: updated,
|
|
310
|
+
heldAmount: holdAmount,
|
|
311
|
+
reason: options.reason
|
|
312
|
+
}, ctx, {
|
|
313
|
+
resource: "transaction",
|
|
314
|
+
resourceId: updated?.publicId
|
|
315
|
+
}), ctx);
|
|
316
|
+
return updated;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Releases held funds. Returns the updated transaction doc.
|
|
320
|
+
*
|
|
321
|
+
* The hold update and the escrow_release transaction create happen inside
|
|
322
|
+
* `withTransaction` — a mid-flow crash can't leave the hold marked released
|
|
323
|
+
* without the corresponding outflow record (or vice versa).
|
|
324
|
+
*/
|
|
325
|
+
async release(transactionId, options, ctx = {}) {
|
|
326
|
+
const transaction = await this.getById(transactionId, this.optsFromCtx(ctx));
|
|
327
|
+
if (!transaction) throw new TransactionNotFoundError(transactionId);
|
|
328
|
+
if (!transaction.hold || transaction.hold.status !== HOLD_STATUS.HELD && transaction.hold.status !== HOLD_STATUS.PARTIALLY_RELEASED) throw new ValidationError("Transaction does not have an active hold");
|
|
329
|
+
const releaseAmount = options.amount ?? transaction.hold.heldAmount - transaction.hold.releasedAmount;
|
|
330
|
+
const newReleasedAmount = transaction.hold.releasedAmount + releaseAmount;
|
|
331
|
+
const isFullRelease = newReleasedAmount >= transaction.hold.heldAmount;
|
|
332
|
+
const release = {
|
|
333
|
+
amount: releaseAmount,
|
|
334
|
+
recipientId: options.recipientId,
|
|
335
|
+
recipientType: options.recipientType,
|
|
336
|
+
releasedAt: /* @__PURE__ */ new Date(),
|
|
337
|
+
releasedBy: options.releasedBy,
|
|
338
|
+
reason: options.reason,
|
|
339
|
+
metadata: options.metadata
|
|
340
|
+
};
|
|
341
|
+
const pendingEvents = [];
|
|
342
|
+
const updated = await withTransaction(this.Model.db, async (session) => {
|
|
343
|
+
const writeOpts = this.optsFromCtx(ctx, { session });
|
|
344
|
+
const result = await this.update(transactionId, { hold: {
|
|
345
|
+
...transaction.hold,
|
|
346
|
+
status: isFullRelease ? HOLD_STATUS.RELEASED : HOLD_STATUS.PARTIALLY_RELEASED,
|
|
347
|
+
releasedAmount: newReleasedAmount,
|
|
348
|
+
releasedAt: isFullRelease ? /* @__PURE__ */ new Date() : transaction.hold.releasedAt,
|
|
349
|
+
releases: [...transaction.hold.releases ?? [], release]
|
|
350
|
+
} }, writeOpts);
|
|
351
|
+
if (options.createTransaction !== false) await this.create({
|
|
352
|
+
organizationId: transaction.organizationId,
|
|
353
|
+
customerId: options.recipientId,
|
|
354
|
+
type: "escrow_release",
|
|
355
|
+
flow: "outflow",
|
|
356
|
+
tags: ["escrow", "release"],
|
|
357
|
+
amount: releaseAmount,
|
|
358
|
+
currency: transaction.currency,
|
|
359
|
+
fee: 0,
|
|
360
|
+
tax: 0,
|
|
361
|
+
net: releaseAmount,
|
|
362
|
+
method: transaction.method,
|
|
363
|
+
status: TRANSACTION_STATUS.VERIFIED,
|
|
364
|
+
relatedTransactionId: transaction._id,
|
|
365
|
+
sourceId: transaction.sourceId,
|
|
366
|
+
sourceModel: transaction.sourceModel,
|
|
367
|
+
verifiedAt: /* @__PURE__ */ new Date(),
|
|
368
|
+
metadata: options.metadata
|
|
369
|
+
}, writeOpts);
|
|
370
|
+
const event = createEvent(REVENUE_EVENTS.ESCROW_RELEASED, {
|
|
371
|
+
transaction: result,
|
|
372
|
+
releaseAmount,
|
|
373
|
+
recipientId: options.recipientId,
|
|
374
|
+
recipientType: options.recipientType,
|
|
375
|
+
isFullRelease,
|
|
376
|
+
isPartialRelease: !isFullRelease
|
|
377
|
+
}, ctx, {
|
|
378
|
+
resource: "transaction",
|
|
379
|
+
resourceId: result?.publicId
|
|
380
|
+
});
|
|
381
|
+
await this.saveToOutbox(event, session);
|
|
382
|
+
pendingEvents.push(event);
|
|
383
|
+
return result;
|
|
384
|
+
});
|
|
385
|
+
for (const ev of pendingEvents) await this.publishToTransport(ev);
|
|
386
|
+
return updated;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Splits payment among recipients. Returns the updated transaction doc.
|
|
390
|
+
*
|
|
391
|
+
* N + 2 writes (one create per recipient, one update on the parent, one
|
|
392
|
+
* platform_revenue create) all commit atomically. Partial splits are the
|
|
393
|
+
* worst class of bug in a payments system — this is exactly what
|
|
394
|
+
* `withTransaction` is for.
|
|
395
|
+
*/
|
|
396
|
+
async split(transactionId, rules, ctx = {}) {
|
|
397
|
+
const transaction = await this.getById(transactionId, this.optsFromCtx(ctx));
|
|
398
|
+
if (!transaction) throw new TransactionNotFoundError(transactionId);
|
|
399
|
+
const gatewayFeeRate = this.deps.commission?.gatewayFeeRate ?? 0;
|
|
400
|
+
const splits = calculateSplits(transaction.amount, rules, gatewayFeeRate);
|
|
401
|
+
const orgPayout = calculateOrganizationPayout(transaction.amount, splits);
|
|
402
|
+
const pendingEvents = [];
|
|
403
|
+
const updated = await withTransaction(this.Model.db, async (session) => {
|
|
404
|
+
const writeOpts = this.optsFromCtx(ctx, { session });
|
|
405
|
+
for (const s of splits) await this.create({
|
|
406
|
+
organizationId: transaction.organizationId,
|
|
407
|
+
customerId: s.recipientId,
|
|
408
|
+
type: "commission",
|
|
409
|
+
flow: "outflow",
|
|
410
|
+
tags: ["split", s.type],
|
|
411
|
+
amount: s.grossAmount,
|
|
412
|
+
currency: transaction.currency,
|
|
413
|
+
fee: s.gatewayFeeAmount,
|
|
414
|
+
tax: 0,
|
|
415
|
+
net: s.netAmount,
|
|
416
|
+
method: transaction.method,
|
|
417
|
+
status: TRANSACTION_STATUS.VERIFIED,
|
|
418
|
+
relatedTransactionId: transaction._id,
|
|
419
|
+
sourceId: transaction.sourceId,
|
|
420
|
+
sourceModel: transaction.sourceModel,
|
|
421
|
+
verifiedAt: /* @__PURE__ */ new Date()
|
|
422
|
+
}, writeOpts);
|
|
423
|
+
const result = await this.update(transactionId, {
|
|
424
|
+
splits,
|
|
425
|
+
metadata: {
|
|
426
|
+
...transaction.metadata,
|
|
427
|
+
organizationPayout: orgPayout
|
|
428
|
+
}
|
|
429
|
+
}, writeOpts);
|
|
430
|
+
await this.create({
|
|
431
|
+
organizationId: transaction.organizationId,
|
|
432
|
+
type: "platform_revenue",
|
|
433
|
+
flow: "inflow",
|
|
434
|
+
tags: ["split", "platform"],
|
|
435
|
+
amount: orgPayout,
|
|
436
|
+
currency: transaction.currency,
|
|
437
|
+
fee: 0,
|
|
438
|
+
tax: 0,
|
|
439
|
+
net: orgPayout,
|
|
440
|
+
method: transaction.method,
|
|
441
|
+
status: TRANSACTION_STATUS.VERIFIED,
|
|
442
|
+
relatedTransactionId: transaction._id,
|
|
443
|
+
verifiedAt: /* @__PURE__ */ new Date()
|
|
444
|
+
}, writeOpts);
|
|
445
|
+
const event = createEvent(REVENUE_EVENTS.ESCROW_SPLIT, {
|
|
446
|
+
transaction: result,
|
|
447
|
+
splits,
|
|
448
|
+
organizationPayout: orgPayout
|
|
449
|
+
}, ctx, {
|
|
450
|
+
resource: "transaction",
|
|
451
|
+
resourceId: transaction.publicId
|
|
452
|
+
});
|
|
453
|
+
await this.saveToOutbox(event, session);
|
|
454
|
+
pendingEvents.push(event);
|
|
455
|
+
return result;
|
|
456
|
+
});
|
|
457
|
+
for (const ev of pendingEvents) await this.publishToTransport(ev);
|
|
458
|
+
return updated;
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
//#endregion
|
|
463
|
+
//#region src/repositories/subscription.repository.ts
|
|
464
|
+
/**
|
|
465
|
+
* SubscriptionRepository — data layer + domain verbs.
|
|
466
|
+
*
|
|
467
|
+
* CRUD inherited from mongokit. Domain verbs: activate, cancel, pause, resume.
|
|
468
|
+
*
|
|
469
|
+
* Events: each domain verb calls `this.deps.events.publish(createEvent(...))`
|
|
470
|
+
* with a fully-qualified `REVENUE_EVENTS.*` name. Hosts can subscribe glob-style
|
|
471
|
+
* via `revenue.events.subscribe('revenue:subscription.*', handler)` — the
|
|
472
|
+
* injected transport is arc-compatible (PACKAGE_RULES §13–§14).
|
|
473
|
+
*/
|
|
474
|
+
var SubscriptionRepository = class extends Repository {
|
|
475
|
+
deps;
|
|
476
|
+
constructor(model, plugins = []) {
|
|
477
|
+
super(model, plugins);
|
|
478
|
+
}
|
|
479
|
+
inject(deps) {
|
|
480
|
+
this.deps = deps;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Host-owned outbox save → in-process transport publish (PACKAGE_RULES P8).
|
|
484
|
+
* Session-bound when `ctx.session` is present (atomic outbox row write).
|
|
485
|
+
*/
|
|
486
|
+
async dispatch(event, ctx = {}) {
|
|
487
|
+
if (this.deps.outbox) try {
|
|
488
|
+
await this.deps.outbox.save(event, ctx.session !== void 0 ? { session: ctx.session } : {});
|
|
489
|
+
} catch (err) {
|
|
490
|
+
this.deps.logger?.error("[revenue] outbox.save failed for", event.type, err);
|
|
491
|
+
}
|
|
492
|
+
try {
|
|
493
|
+
await this.deps.events.publish(event);
|
|
494
|
+
} catch (err) {
|
|
495
|
+
this.deps.logger?.error("[revenue] events.publish failed for", event.type, err);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
async activate(subscriptionId, options = {}, ctx = {}) {
|
|
499
|
+
const sub = await this.getById(subscriptionId);
|
|
500
|
+
if (!sub) throw new SubscriptionNotFoundError(subscriptionId);
|
|
501
|
+
SUBSCRIPTION_STATE_MACHINE.validate(sub.status, SUBSCRIPTION_STATUS.ACTIVE, subscriptionId);
|
|
502
|
+
const now = options.timestamp ?? /* @__PURE__ */ new Date();
|
|
503
|
+
const endDate = new Date(now);
|
|
504
|
+
if (sub.planKey === "monthly") endDate.setMonth(endDate.getMonth() + 1);
|
|
505
|
+
else if (sub.planKey === "quarterly") endDate.setMonth(endDate.getMonth() + 3);
|
|
506
|
+
else if (sub.planKey === "yearly") endDate.setFullYear(endDate.getFullYear() + 1);
|
|
507
|
+
else endDate.setDate(endDate.getDate() + 30);
|
|
508
|
+
const updated = await this.update(subscriptionId, {
|
|
509
|
+
status: SUBSCRIPTION_STATUS.ACTIVE,
|
|
510
|
+
isActive: true,
|
|
511
|
+
activatedAt: now,
|
|
512
|
+
endDate
|
|
513
|
+
}, { throwOnNotFound: true });
|
|
514
|
+
if (!updated) throw new SubscriptionNotFoundError(subscriptionId);
|
|
515
|
+
await this.dispatch(createEvent(REVENUE_EVENTS.SUBSCRIPTION_ACTIVATED, {
|
|
516
|
+
subscription: updated,
|
|
517
|
+
activatedAt: now
|
|
518
|
+
}, ctx, {
|
|
519
|
+
resource: "subscription",
|
|
520
|
+
resourceId: updated?.publicId
|
|
521
|
+
}), ctx);
|
|
522
|
+
return updated;
|
|
523
|
+
}
|
|
524
|
+
async cancel(subscriptionId, options = {}, ctx = {}) {
|
|
525
|
+
const sub = await this.getById(subscriptionId);
|
|
526
|
+
if (!sub) throw new SubscriptionNotFoundError(subscriptionId);
|
|
527
|
+
SUBSCRIPTION_STATE_MACHINE.validate(sub.status, SUBSCRIPTION_STATUS.CANCELLED, subscriptionId);
|
|
528
|
+
const updates = {
|
|
529
|
+
status: SUBSCRIPTION_STATUS.CANCELLED,
|
|
530
|
+
isActive: false,
|
|
531
|
+
canceledAt: /* @__PURE__ */ new Date(),
|
|
532
|
+
cancellationReason: options.reason
|
|
533
|
+
};
|
|
534
|
+
if (!options.immediate && sub.endDate) {
|
|
535
|
+
updates.cancelAt = sub.endDate;
|
|
536
|
+
updates.status = sub.status;
|
|
537
|
+
updates.isActive = sub.isActive;
|
|
538
|
+
delete updates.canceledAt;
|
|
539
|
+
}
|
|
540
|
+
const updated = await this.update(subscriptionId, updates, { throwOnNotFound: true });
|
|
541
|
+
if (!updated) throw new SubscriptionNotFoundError(subscriptionId);
|
|
542
|
+
await this.dispatch(createEvent(REVENUE_EVENTS.SUBSCRIPTION_CANCELLED, {
|
|
543
|
+
subscription: updated,
|
|
544
|
+
immediate: options.immediate,
|
|
545
|
+
reason: options.reason
|
|
546
|
+
}, ctx, {
|
|
547
|
+
resource: "subscription",
|
|
548
|
+
resourceId: updated?.publicId
|
|
549
|
+
}), ctx);
|
|
550
|
+
return updated;
|
|
551
|
+
}
|
|
552
|
+
async pause(subscriptionId, options = {}, ctx = {}) {
|
|
553
|
+
const sub = await this.getById(subscriptionId);
|
|
554
|
+
if (!sub) throw new SubscriptionNotFoundError(subscriptionId);
|
|
555
|
+
SUBSCRIPTION_STATE_MACHINE.validate(sub.status, SUBSCRIPTION_STATUS.PAUSED, subscriptionId);
|
|
556
|
+
const updated = await this.update(subscriptionId, {
|
|
557
|
+
status: SUBSCRIPTION_STATUS.PAUSED,
|
|
558
|
+
isActive: false,
|
|
559
|
+
pausedAt: /* @__PURE__ */ new Date(),
|
|
560
|
+
pauseReason: options.reason
|
|
561
|
+
}, { throwOnNotFound: true });
|
|
562
|
+
if (!updated) throw new SubscriptionNotFoundError(subscriptionId);
|
|
563
|
+
await this.dispatch(createEvent(REVENUE_EVENTS.SUBSCRIPTION_PAUSED, {
|
|
564
|
+
subscription: updated,
|
|
565
|
+
reason: options.reason
|
|
566
|
+
}, ctx, {
|
|
567
|
+
resource: "subscription",
|
|
568
|
+
resourceId: updated?.publicId
|
|
569
|
+
}), ctx);
|
|
570
|
+
return updated;
|
|
571
|
+
}
|
|
572
|
+
async resume(subscriptionId, options = {}, ctx = {}) {
|
|
573
|
+
const sub = await this.getById(subscriptionId);
|
|
574
|
+
if (!sub) throw new SubscriptionNotFoundError(subscriptionId);
|
|
575
|
+
SUBSCRIPTION_STATE_MACHINE.validate(sub.status, SUBSCRIPTION_STATUS.ACTIVE, subscriptionId);
|
|
576
|
+
const updates = {
|
|
577
|
+
status: SUBSCRIPTION_STATUS.ACTIVE,
|
|
578
|
+
isActive: true
|
|
579
|
+
};
|
|
580
|
+
if (options.extendPeriod && sub.pausedAt && sub.endDate) {
|
|
581
|
+
const pauseDuration = Date.now() - sub.pausedAt.getTime();
|
|
582
|
+
updates.endDate = new Date(sub.endDate.getTime() + pauseDuration);
|
|
583
|
+
}
|
|
584
|
+
const updated = await this.update(subscriptionId, updates, { throwOnNotFound: true });
|
|
585
|
+
if (!updated) throw new SubscriptionNotFoundError(subscriptionId);
|
|
586
|
+
await this.dispatch(createEvent(REVENUE_EVENTS.SUBSCRIPTION_RESUMED, {
|
|
587
|
+
subscription: updated,
|
|
588
|
+
extendPeriod: options.extendPeriod
|
|
589
|
+
}, ctx, {
|
|
590
|
+
resource: "subscription",
|
|
591
|
+
resourceId: updated?.publicId
|
|
592
|
+
}), ctx);
|
|
593
|
+
return updated;
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
//#endregion
|
|
598
|
+
//#region src/repositories/settlement.repository.ts
|
|
599
|
+
/**
|
|
600
|
+
* SettlementRepository — data layer + domain verbs.
|
|
601
|
+
*
|
|
602
|
+
* CRUD inherited from mongokit. Domain verbs: schedule, processPending, complete, fail.
|
|
603
|
+
*
|
|
604
|
+
* Events are published via the injected `events` transport (arc-compatible).
|
|
605
|
+
* Hosts subscribe glob-style via `revenue.events.subscribe('revenue:settlement.*', h)`.
|
|
606
|
+
* See PACKAGE_RULES §13–§14.
|
|
607
|
+
*/
|
|
608
|
+
var SettlementRepository = class extends Repository {
|
|
609
|
+
deps;
|
|
610
|
+
constructor(model, plugins = []) {
|
|
611
|
+
super(model, plugins);
|
|
612
|
+
}
|
|
613
|
+
inject(deps) {
|
|
614
|
+
this.deps = deps;
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Host-owned outbox save → in-process transport publish (PACKAGE_RULES P8).
|
|
618
|
+
* Session-bound when `ctx.session` is present (atomic outbox row write).
|
|
619
|
+
*/
|
|
620
|
+
async dispatch(event, ctx = {}) {
|
|
621
|
+
if (this.deps.outbox) try {
|
|
622
|
+
await this.deps.outbox.save(event, ctx.session !== void 0 ? { session: ctx.session } : {});
|
|
623
|
+
} catch (err) {
|
|
624
|
+
this.deps.logger?.error("[revenue] outbox.save failed for", event.type, err);
|
|
625
|
+
}
|
|
626
|
+
try {
|
|
627
|
+
await this.deps.events.publish(event);
|
|
628
|
+
} catch (err) {
|
|
629
|
+
this.deps.logger?.error("[revenue] events.publish failed for", event.type, err);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
async schedule(params, ctx = {}) {
|
|
633
|
+
const settlement = await this.create({
|
|
634
|
+
...params,
|
|
635
|
+
status: SETTLEMENT_STATUS.PENDING,
|
|
636
|
+
scheduledAt: params.scheduledAt ?? /* @__PURE__ */ new Date(),
|
|
637
|
+
retryCount: 0
|
|
638
|
+
});
|
|
639
|
+
await this.dispatch(createEvent(REVENUE_EVENTS.SETTLEMENT_SCHEDULED, {
|
|
640
|
+
settlement,
|
|
641
|
+
scheduledAt: settlement.scheduledAt
|
|
642
|
+
}, ctx, {
|
|
643
|
+
resource: "settlement",
|
|
644
|
+
resourceId: settlement.publicId
|
|
645
|
+
}), ctx);
|
|
646
|
+
return settlement;
|
|
647
|
+
}
|
|
648
|
+
async processPending(options = {}, ctx = {}) {
|
|
649
|
+
const query = {
|
|
650
|
+
status: "pending",
|
|
651
|
+
scheduledAt: { $lte: /* @__PURE__ */ new Date() }
|
|
652
|
+
};
|
|
653
|
+
if (options.organizationId) query.organizationId = options.organizationId;
|
|
654
|
+
if (options.payoutMethod) query.payoutMethod = options.payoutMethod;
|
|
655
|
+
const pending = (await this.getAll({
|
|
656
|
+
filters: query,
|
|
657
|
+
limit: options.limit ?? 50,
|
|
658
|
+
sort: { scheduledAt: 1 }
|
|
659
|
+
})).docs ?? [];
|
|
660
|
+
if (options.dryRun) return {
|
|
661
|
+
processed: pending.length,
|
|
662
|
+
succeeded: 0,
|
|
663
|
+
failed: 0,
|
|
664
|
+
settlements: pending,
|
|
665
|
+
errors: []
|
|
666
|
+
};
|
|
667
|
+
const results = {
|
|
668
|
+
processed: 0,
|
|
669
|
+
succeeded: 0,
|
|
670
|
+
failed: 0,
|
|
671
|
+
settlements: [],
|
|
672
|
+
errors: []
|
|
673
|
+
};
|
|
674
|
+
for (const settlement of pending) {
|
|
675
|
+
try {
|
|
676
|
+
SETTLEMENT_STATE_MACHINE.validate(settlement.status, SETTLEMENT_STATUS.PROCESSING, String(settlement._id));
|
|
677
|
+
await this.update(settlement._id, {
|
|
678
|
+
status: SETTLEMENT_STATUS.PROCESSING,
|
|
679
|
+
processedAt: /* @__PURE__ */ new Date()
|
|
680
|
+
});
|
|
681
|
+
results.succeeded++;
|
|
682
|
+
results.settlements.push(settlement);
|
|
683
|
+
await this.dispatch(createEvent(REVENUE_EVENTS.SETTLEMENT_PROCESSING, {
|
|
684
|
+
settlement,
|
|
685
|
+
processedAt: /* @__PURE__ */ new Date()
|
|
686
|
+
}, ctx, {
|
|
687
|
+
resource: "settlement",
|
|
688
|
+
resourceId: settlement.publicId
|
|
689
|
+
}), ctx);
|
|
690
|
+
} catch (err) {
|
|
691
|
+
results.failed++;
|
|
692
|
+
results.errors.push({
|
|
693
|
+
settlementId: settlement._id,
|
|
694
|
+
error: err
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
results.processed++;
|
|
698
|
+
}
|
|
699
|
+
return results;
|
|
700
|
+
}
|
|
701
|
+
async complete(settlementId, details = {}, ctx = {}) {
|
|
702
|
+
const settlement = await this.getById(settlementId);
|
|
703
|
+
if (!settlement) throw new SettlementNotFoundError(settlementId);
|
|
704
|
+
SETTLEMENT_STATE_MACHINE.validate(settlement.status, SETTLEMENT_STATUS.COMPLETED, settlementId);
|
|
705
|
+
const updates = {
|
|
706
|
+
status: SETTLEMENT_STATUS.COMPLETED,
|
|
707
|
+
completedAt: /* @__PURE__ */ new Date(),
|
|
708
|
+
notes: details.notes,
|
|
709
|
+
metadata: {
|
|
710
|
+
...settlement.metadata,
|
|
711
|
+
...details.metadata
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
if (details.transferReference) updates.bankTransferDetails = {
|
|
715
|
+
...settlement.bankTransferDetails,
|
|
716
|
+
transferReference: details.transferReference,
|
|
717
|
+
transferredAt: details.transferredAt ?? /* @__PURE__ */ new Date()
|
|
718
|
+
};
|
|
719
|
+
if (details.transactionHash) updates.cryptoDetails = {
|
|
720
|
+
...settlement.cryptoDetails,
|
|
721
|
+
transactionHash: details.transactionHash,
|
|
722
|
+
transferredAt: details.transferredAt ?? /* @__PURE__ */ new Date()
|
|
723
|
+
};
|
|
724
|
+
const updated = await this.update(settlementId, updates, { throwOnNotFound: true });
|
|
725
|
+
if (!updated) throw new SettlementNotFoundError(settlementId);
|
|
726
|
+
await this.deps.bridges.ledger?.onSettlementCompleted?.(updated, ctx);
|
|
727
|
+
await this.deps.bridges.notification?.onSettlementCompleted?.(updated, ctx);
|
|
728
|
+
await this.dispatch(createEvent(REVENUE_EVENTS.SETTLEMENT_COMPLETED, {
|
|
729
|
+
settlement: updated,
|
|
730
|
+
completedAt: /* @__PURE__ */ new Date()
|
|
731
|
+
}, ctx, {
|
|
732
|
+
resource: "settlement",
|
|
733
|
+
resourceId: updated?.publicId
|
|
734
|
+
}), ctx);
|
|
735
|
+
return updated;
|
|
736
|
+
}
|
|
737
|
+
async fail(settlementId, reason, options = {}, ctx = {}) {
|
|
738
|
+
const settlement = await this.getById(settlementId);
|
|
739
|
+
if (!settlement) throw new SettlementNotFoundError(settlementId);
|
|
740
|
+
if (options.retry) await this.update(settlementId, {
|
|
741
|
+
status: SETTLEMENT_STATUS.PENDING,
|
|
742
|
+
retryCount: (settlement.retryCount ?? 0) + 1,
|
|
743
|
+
failureReason: reason,
|
|
744
|
+
failureCode: options.code,
|
|
745
|
+
scheduledAt: new Date(Date.now() + 36e5)
|
|
746
|
+
});
|
|
747
|
+
else {
|
|
748
|
+
SETTLEMENT_STATE_MACHINE.validate(settlement.status, SETTLEMENT_STATUS.FAILED, settlementId);
|
|
749
|
+
await this.update(settlementId, {
|
|
750
|
+
status: SETTLEMENT_STATUS.FAILED,
|
|
751
|
+
failedAt: /* @__PURE__ */ new Date(),
|
|
752
|
+
failureReason: reason,
|
|
753
|
+
failureCode: options.code
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
const updated = await this.getById(settlementId);
|
|
757
|
+
await this.dispatch(createEvent(REVENUE_EVENTS.SETTLEMENT_FAILED, {
|
|
758
|
+
settlement: updated,
|
|
759
|
+
reason,
|
|
760
|
+
code: options.code,
|
|
761
|
+
retry: options.retry
|
|
762
|
+
}, ctx, {
|
|
763
|
+
resource: "settlement",
|
|
764
|
+
resourceId: updated?.publicId
|
|
765
|
+
}), ctx);
|
|
766
|
+
return updated;
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
//#endregion
|
|
771
|
+
export { SubscriptionRepository as n, TransactionRepository as r, SettlementRepository as t };
|