@classytic/revenue 1.0.6 → 1.1.2
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/README.md +581 -633
- package/dist/application/services/index.d.ts +6 -0
- package/dist/application/services/index.js +3288 -0
- package/dist/application/services/index.js.map +1 -0
- package/dist/core/events.d.ts +455 -0
- package/dist/core/events.js +122 -0
- package/dist/core/events.js.map +1 -0
- package/dist/core/index.d.ts +12 -889
- package/dist/core/index.js +2372 -786
- package/dist/core/index.js.map +1 -1
- package/dist/enums/index.d.ts +29 -8
- package/dist/enums/index.js +41 -8
- package/dist/enums/index.js.map +1 -1
- package/dist/escrow.enums-CE0VQsfe.d.ts +76 -0
- package/dist/{index-BnEXsnLJ.d.ts → index-DxIK0UmZ.d.ts} +281 -26
- package/dist/index-EnfKzDbs.d.ts +806 -0
- package/dist/{index-C5SsOrV0.d.ts → index-cLJBLUvx.d.ts} +55 -111
- package/dist/index.d.ts +16 -16
- package/dist/index.js +2558 -2192
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/plugins/index.d.ts +267 -0
- package/dist/infrastructure/plugins/index.js +292 -0
- package/dist/infrastructure/plugins/index.js.map +1 -0
- package/dist/money-widWVD7r.d.ts +111 -0
- package/dist/payment.enums-C1BiGlRa.d.ts +69 -0
- package/dist/plugin-Bb9HOE10.d.ts +336 -0
- package/dist/providers/index.d.ts +19 -6
- package/dist/providers/index.js +22 -3
- package/dist/providers/index.js.map +1 -1
- package/dist/reconciliation/index.d.ts +215 -0
- package/dist/reconciliation/index.js +140 -0
- package/dist/reconciliation/index.js.map +1 -0
- package/dist/{retry-80lBCmSe.d.ts → retry-D4hFUwVk.d.ts} +1 -41
- package/dist/schemas/index.d.ts +1653 -49
- package/dist/schemas/index.js +233 -19
- package/dist/schemas/index.js.map +1 -1
- package/dist/schemas/validation.d.ts +4 -4
- package/dist/schemas/validation.js +16 -15
- package/dist/schemas/validation.js.map +1 -1
- package/dist/settlement.enums-ByC1x0ye.d.ts +130 -0
- package/dist/settlement.schema-CpamV7ZY.d.ts +343 -0
- package/dist/split.enums-DG3TxQf9.d.ts +42 -0
- package/dist/tax-CV8A0sxl.d.ts +60 -0
- package/dist/utils/index.d.ts +487 -13
- package/dist/utils/index.js +351 -289
- package/dist/utils/index.js.map +1 -1
- package/package.json +22 -9
- package/dist/actions-Ctf2XUL-.d.ts +0 -519
- package/dist/payment.enums-B_RwB8iR.d.ts +0 -184
- package/dist/services/index.d.ts +0 -3
- package/dist/services/index.js +0 -1702
- package/dist/services/index.js.map +0 -1
- package/dist/split.schema-DLVF3XBI.d.ts +0 -1122
- package/dist/transaction.enums-7uBnuswI.d.ts +0 -87
package/dist/core/index.js
CHANGED
|
@@ -132,12 +132,10 @@ var EventBus = class {
|
|
|
132
132
|
this.handlers.get(event)?.delete(handler);
|
|
133
133
|
this.onceHandlers.get(event)?.delete(handler);
|
|
134
134
|
}
|
|
135
|
-
|
|
136
|
-
* Emit an event (fire and forget, non-blocking)
|
|
137
|
-
*/
|
|
138
|
-
emit(event, payload) {
|
|
135
|
+
emit(event, data) {
|
|
139
136
|
const fullPayload = {
|
|
140
|
-
...
|
|
137
|
+
...data,
|
|
138
|
+
type: event,
|
|
141
139
|
timestamp: /* @__PURE__ */ new Date()
|
|
142
140
|
};
|
|
143
141
|
const handlers = this.handlers.get(event);
|
|
@@ -174,6 +172,7 @@ var EventBus = class {
|
|
|
174
172
|
async emitAsync(event, payload) {
|
|
175
173
|
const fullPayload = {
|
|
176
174
|
...payload,
|
|
175
|
+
type: event,
|
|
177
176
|
timestamp: /* @__PURE__ */ new Date()
|
|
178
177
|
};
|
|
179
178
|
const promises = [];
|
|
@@ -324,19 +323,19 @@ function loggingPlugin(options = {}) {
|
|
|
324
323
|
version: "1.0.0",
|
|
325
324
|
description: "Logs all revenue operations",
|
|
326
325
|
hooks: {
|
|
327
|
-
"payment.create.
|
|
326
|
+
"payment.create.after": async (ctx, input, next) => {
|
|
328
327
|
ctx.logger[level]("Creating payment", { amount: input.amount, currency: input.currency });
|
|
329
328
|
const result = await next();
|
|
330
|
-
ctx.logger[level]("Payment created", {
|
|
329
|
+
ctx.logger[level]("Payment created", { paymentIntentId: result?.paymentIntentId });
|
|
331
330
|
return result;
|
|
332
331
|
},
|
|
333
|
-
"payment.verify.
|
|
332
|
+
"payment.verify.after": async (ctx, input, next) => {
|
|
334
333
|
ctx.logger[level]("Verifying payment", { id: input.id });
|
|
335
334
|
const result = await next();
|
|
336
335
|
ctx.logger[level]("Payment verified", { verified: result?.verified });
|
|
337
336
|
return result;
|
|
338
337
|
},
|
|
339
|
-
"payment.refund.
|
|
338
|
+
"payment.refund.after": async (ctx, input, next) => {
|
|
340
339
|
ctx.logger[level]("Processing refund", { transactionId: input.transactionId, amount: input.amount });
|
|
341
340
|
const result = await next();
|
|
342
341
|
ctx.logger[level]("Refund processed", { refundId: result?.refundId });
|
|
@@ -569,16 +568,29 @@ var IdempotencyManager = class {
|
|
|
569
568
|
}
|
|
570
569
|
/**
|
|
571
570
|
* Hash request parameters for validation
|
|
571
|
+
* Uses deterministic JSON serialization and simple hash function
|
|
572
572
|
*/
|
|
573
573
|
hashRequest(params) {
|
|
574
|
-
|
|
574
|
+
let normalized;
|
|
575
|
+
if (params === null || params === void 0) {
|
|
576
|
+
normalized = null;
|
|
577
|
+
} else if (typeof params === "object" && !Array.isArray(params)) {
|
|
578
|
+
const sortedKeys = Object.keys(params).sort();
|
|
579
|
+
normalized = sortedKeys.reduce((acc, key) => {
|
|
580
|
+
acc[key] = params[key];
|
|
581
|
+
return acc;
|
|
582
|
+
}, {});
|
|
583
|
+
} else {
|
|
584
|
+
normalized = params;
|
|
585
|
+
}
|
|
586
|
+
const json = JSON.stringify(normalized);
|
|
575
587
|
let hash = 0;
|
|
576
588
|
for (let i = 0; i < json.length; i++) {
|
|
577
589
|
const char = json.charCodeAt(i);
|
|
578
590
|
hash = (hash << 5) - hash + char;
|
|
579
591
|
hash = hash & hash;
|
|
580
592
|
}
|
|
581
|
-
return hash.toString(36);
|
|
593
|
+
return Math.abs(hash).toString(36);
|
|
582
594
|
}
|
|
583
595
|
/**
|
|
584
596
|
* Execute operation with idempotency protection
|
|
@@ -651,12 +663,21 @@ var IdempotencyManager = class {
|
|
|
651
663
|
const fullKey = key.startsWith(this.prefix) ? key : `${this.prefix}${key}`;
|
|
652
664
|
await this.store.delete(fullKey);
|
|
653
665
|
}
|
|
666
|
+
/**
|
|
667
|
+
* Destroy the idempotency manager and cleanup resources
|
|
668
|
+
* Call this when shutting down to prevent memory leaks
|
|
669
|
+
*/
|
|
670
|
+
destroy() {
|
|
671
|
+
if (this.store instanceof MemoryIdempotencyStore) {
|
|
672
|
+
this.store.destroy();
|
|
673
|
+
}
|
|
674
|
+
}
|
|
654
675
|
};
|
|
655
676
|
function createIdempotencyManager(config) {
|
|
656
677
|
return new IdempotencyManager(config);
|
|
657
678
|
}
|
|
658
679
|
|
|
659
|
-
// src/utils/retry.ts
|
|
680
|
+
// src/shared/utils/resilience/retry.ts
|
|
660
681
|
var DEFAULT_CONFIG = {
|
|
661
682
|
maxAttempts: 3,
|
|
662
683
|
baseDelay: 1e3,
|
|
@@ -1059,35 +1080,13 @@ function isRevenueError(error) {
|
|
|
1059
1080
|
return error instanceof RevenueError;
|
|
1060
1081
|
}
|
|
1061
1082
|
|
|
1062
|
-
// src/utils/hooks.ts
|
|
1063
|
-
function triggerHook(hooks, event, data, logger) {
|
|
1064
|
-
const handlers = hooks[event] ?? [];
|
|
1065
|
-
if (handlers.length === 0) {
|
|
1066
|
-
return;
|
|
1067
|
-
}
|
|
1068
|
-
Promise.all(
|
|
1069
|
-
handlers.map(
|
|
1070
|
-
(handler) => Promise.resolve(handler(data)).catch((error) => {
|
|
1071
|
-
logger.error(`Hook "${event}" failed:`, {
|
|
1072
|
-
error: error.message,
|
|
1073
|
-
stack: error.stack,
|
|
1074
|
-
event,
|
|
1075
|
-
// Don't log full data (could be huge)
|
|
1076
|
-
dataKeys: Object.keys(data)
|
|
1077
|
-
});
|
|
1078
|
-
})
|
|
1079
|
-
)
|
|
1080
|
-
).catch(() => {
|
|
1081
|
-
});
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
1083
|
// src/enums/transaction.enums.ts
|
|
1085
|
-
var
|
|
1086
|
-
|
|
1087
|
-
|
|
1084
|
+
var TRANSACTION_FLOW = {
|
|
1085
|
+
INFLOW: "inflow",
|
|
1086
|
+
OUTFLOW: "outflow"
|
|
1088
1087
|
};
|
|
1089
|
-
var
|
|
1090
|
-
|
|
1088
|
+
var TRANSACTION_FLOW_VALUES = Object.values(
|
|
1089
|
+
TRANSACTION_FLOW
|
|
1091
1090
|
);
|
|
1092
1091
|
var TRANSACTION_STATUS = {
|
|
1093
1092
|
PENDING: "pending",
|
|
@@ -1112,13 +1111,13 @@ var LIBRARY_CATEGORIES = {
|
|
|
1112
1111
|
var LIBRARY_CATEGORY_VALUES = Object.values(
|
|
1113
1112
|
LIBRARY_CATEGORIES
|
|
1114
1113
|
);
|
|
1115
|
-
new Set(
|
|
1114
|
+
new Set(TRANSACTION_FLOW_VALUES);
|
|
1116
1115
|
new Set(
|
|
1117
1116
|
TRANSACTION_STATUS_VALUES
|
|
1118
1117
|
);
|
|
1119
1118
|
new Set(LIBRARY_CATEGORY_VALUES);
|
|
1120
1119
|
|
|
1121
|
-
// src/utils/category-resolver.ts
|
|
1120
|
+
// src/shared/utils/validators/category-resolver.ts
|
|
1122
1121
|
function resolveCategory(entity, monetizationType, categoryMappings = {}) {
|
|
1123
1122
|
if (entity && categoryMappings[entity]) {
|
|
1124
1123
|
return categoryMappings[entity];
|
|
@@ -1135,7 +1134,7 @@ function resolveCategory(entity, monetizationType, categoryMappings = {}) {
|
|
|
1135
1134
|
}
|
|
1136
1135
|
}
|
|
1137
1136
|
|
|
1138
|
-
// src/utils/commission.ts
|
|
1137
|
+
// src/shared/utils/calculators/commission.ts
|
|
1139
1138
|
function calculateCommission(amount, commissionRate, gatewayFeeRate = 0) {
|
|
1140
1139
|
if (!commissionRate || commissionRate <= 0) {
|
|
1141
1140
|
return null;
|
|
@@ -1149,9 +1148,9 @@ function calculateCommission(amount, commissionRate, gatewayFeeRate = 0) {
|
|
|
1149
1148
|
if (gatewayFeeRate < 0 || gatewayFeeRate > 1) {
|
|
1150
1149
|
throw new Error("Gateway fee rate must be between 0 and 1");
|
|
1151
1150
|
}
|
|
1152
|
-
const grossAmount = Math.round(amount * commissionRate
|
|
1153
|
-
const gatewayFeeAmount = Math.round(amount * gatewayFeeRate
|
|
1154
|
-
const netAmount = Math.max(0,
|
|
1151
|
+
const grossAmount = Math.round(amount * commissionRate);
|
|
1152
|
+
const gatewayFeeAmount = Math.round(amount * gatewayFeeRate);
|
|
1153
|
+
const netAmount = Math.max(0, grossAmount - gatewayFeeAmount);
|
|
1155
1154
|
return {
|
|
1156
1155
|
rate: commissionRate,
|
|
1157
1156
|
grossAmount,
|
|
@@ -1165,10 +1164,22 @@ function reverseCommission(originalCommission, originalAmount, refundAmount) {
|
|
|
1165
1164
|
if (!originalCommission?.netAmount) {
|
|
1166
1165
|
return null;
|
|
1167
1166
|
}
|
|
1167
|
+
if (!originalAmount || originalAmount <= 0) {
|
|
1168
|
+
throw new ValidationError("Original amount must be greater than 0", { originalAmount });
|
|
1169
|
+
}
|
|
1170
|
+
if (refundAmount < 0) {
|
|
1171
|
+
throw new ValidationError("Refund amount cannot be negative", { refundAmount });
|
|
1172
|
+
}
|
|
1173
|
+
if (refundAmount > originalAmount) {
|
|
1174
|
+
throw new ValidationError(
|
|
1175
|
+
`Refund amount (${refundAmount}) exceeds original amount (${originalAmount})`,
|
|
1176
|
+
{ refundAmount, originalAmount }
|
|
1177
|
+
);
|
|
1178
|
+
}
|
|
1168
1179
|
const refundRatio = refundAmount / originalAmount;
|
|
1169
|
-
const reversedNetAmount = Math.round(originalCommission.netAmount * refundRatio
|
|
1170
|
-
const reversedGrossAmount = Math.round(originalCommission.grossAmount * refundRatio
|
|
1171
|
-
const reversedGatewayFee = Math.round(originalCommission.gatewayFeeAmount * refundRatio
|
|
1180
|
+
const reversedNetAmount = Math.round(originalCommission.netAmount * refundRatio);
|
|
1181
|
+
const reversedGrossAmount = Math.round(originalCommission.grossAmount * refundRatio);
|
|
1182
|
+
const reversedGatewayFee = Math.round(originalCommission.gatewayFeeAmount * refundRatio);
|
|
1172
1183
|
return {
|
|
1173
1184
|
rate: originalCommission.rate,
|
|
1174
1185
|
grossAmount: reversedGrossAmount,
|
|
@@ -1180,6 +1191,41 @@ function reverseCommission(originalCommission, originalAmount, refundAmount) {
|
|
|
1180
1191
|
};
|
|
1181
1192
|
}
|
|
1182
1193
|
|
|
1194
|
+
// src/infrastructure/config/resolver.ts
|
|
1195
|
+
function resolveConfig(options) {
|
|
1196
|
+
const config = {
|
|
1197
|
+
targetModels: [],
|
|
1198
|
+
categoryMappings: {}
|
|
1199
|
+
};
|
|
1200
|
+
if (options.commissionRate !== void 0) {
|
|
1201
|
+
config.commissionRates = {
|
|
1202
|
+
"*": options.commissionRate
|
|
1203
|
+
// Global default for all categories
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
if (options.gatewayFeeRate !== void 0) {
|
|
1207
|
+
config.gatewayFeeRates = {
|
|
1208
|
+
"*": options.gatewayFeeRate
|
|
1209
|
+
// Global default for all gateways
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
return config;
|
|
1213
|
+
}
|
|
1214
|
+
function getCommissionRate(config, category) {
|
|
1215
|
+
if (!config?.commissionRates) return 0;
|
|
1216
|
+
if (category in config.commissionRates) {
|
|
1217
|
+
return config.commissionRates[category];
|
|
1218
|
+
}
|
|
1219
|
+
return config.commissionRates["*"] ?? 0;
|
|
1220
|
+
}
|
|
1221
|
+
function getGatewayFeeRate(config, gateway) {
|
|
1222
|
+
if (!config?.gatewayFeeRates) return 0;
|
|
1223
|
+
if (gateway in config.gatewayFeeRates) {
|
|
1224
|
+
return config.gatewayFeeRates[gateway];
|
|
1225
|
+
}
|
|
1226
|
+
return config.gatewayFeeRates["*"] ?? 0;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1183
1229
|
// src/enums/monetization.enums.ts
|
|
1184
1230
|
var MONETIZATION_TYPES = {
|
|
1185
1231
|
FREE: "free",
|
|
@@ -1191,19 +1237,520 @@ var MONETIZATION_TYPE_VALUES = Object.values(
|
|
|
1191
1237
|
);
|
|
1192
1238
|
new Set(MONETIZATION_TYPE_VALUES);
|
|
1193
1239
|
|
|
1194
|
-
// src/
|
|
1240
|
+
// src/core/state-machine/StateMachine.ts
|
|
1241
|
+
var StateMachine = class {
|
|
1242
|
+
/**
|
|
1243
|
+
* @param transitions - Map of state → allowed next states
|
|
1244
|
+
* @param resourceType - Type of resource (for error messages)
|
|
1245
|
+
*/
|
|
1246
|
+
constructor(transitions, resourceType) {
|
|
1247
|
+
this.transitions = transitions;
|
|
1248
|
+
this.resourceType = resourceType;
|
|
1249
|
+
}
|
|
1250
|
+
/**
|
|
1251
|
+
* Validate state transition is allowed
|
|
1252
|
+
*
|
|
1253
|
+
* @param from - Current state
|
|
1254
|
+
* @param to - Target state
|
|
1255
|
+
* @param resourceId - ID of the resource being transitioned
|
|
1256
|
+
* @throws InvalidStateTransitionError if transition is invalid
|
|
1257
|
+
*
|
|
1258
|
+
* @example
|
|
1259
|
+
* ```typescript
|
|
1260
|
+
* try {
|
|
1261
|
+
* stateMachine.validate('pending', 'completed', 'tx_123');
|
|
1262
|
+
* } catch (error) {
|
|
1263
|
+
* if (error instanceof InvalidStateTransitionError) {
|
|
1264
|
+
* console.error('Invalid transition:', error.message);
|
|
1265
|
+
* }
|
|
1266
|
+
* }
|
|
1267
|
+
* ```
|
|
1268
|
+
*/
|
|
1269
|
+
validate(from, to, resourceId) {
|
|
1270
|
+
const allowedTransitions = this.transitions.get(from);
|
|
1271
|
+
if (!allowedTransitions?.has(to)) {
|
|
1272
|
+
throw new InvalidStateTransitionError(
|
|
1273
|
+
this.resourceType,
|
|
1274
|
+
resourceId,
|
|
1275
|
+
from,
|
|
1276
|
+
to
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
/**
|
|
1281
|
+
* Check if transition is valid (non-throwing)
|
|
1282
|
+
*
|
|
1283
|
+
* @param from - Current state
|
|
1284
|
+
* @param to - Target state
|
|
1285
|
+
* @returns true if transition is allowed
|
|
1286
|
+
*
|
|
1287
|
+
* @example
|
|
1288
|
+
* ```typescript
|
|
1289
|
+
* if (stateMachine.canTransition('pending', 'processing')) {
|
|
1290
|
+
* // Safe to proceed with transition
|
|
1291
|
+
* transaction.status = 'processing';
|
|
1292
|
+
* }
|
|
1293
|
+
* ```
|
|
1294
|
+
*/
|
|
1295
|
+
canTransition(from, to) {
|
|
1296
|
+
return this.transitions.get(from)?.has(to) ?? false;
|
|
1297
|
+
}
|
|
1298
|
+
/**
|
|
1299
|
+
* Get all allowed next states from current state
|
|
1300
|
+
*
|
|
1301
|
+
* @param from - Current state
|
|
1302
|
+
* @returns Array of allowed next states
|
|
1303
|
+
*
|
|
1304
|
+
* @example
|
|
1305
|
+
* ```typescript
|
|
1306
|
+
* const nextStates = stateMachine.getAllowedTransitions('pending');
|
|
1307
|
+
* console.log(nextStates); // ['processing', 'failed']
|
|
1308
|
+
* ```
|
|
1309
|
+
*/
|
|
1310
|
+
getAllowedTransitions(from) {
|
|
1311
|
+
return Array.from(this.transitions.get(from) ?? []);
|
|
1312
|
+
}
|
|
1313
|
+
/**
|
|
1314
|
+
* Check if state is terminal (no outgoing transitions)
|
|
1315
|
+
*
|
|
1316
|
+
* @param state - State to check
|
|
1317
|
+
* @returns true if state has no outgoing transitions
|
|
1318
|
+
*
|
|
1319
|
+
* @example
|
|
1320
|
+
* ```typescript
|
|
1321
|
+
* stateMachine.isTerminalState('completed'); // true
|
|
1322
|
+
* stateMachine.isTerminalState('pending'); // false
|
|
1323
|
+
* ```
|
|
1324
|
+
*/
|
|
1325
|
+
isTerminalState(state) {
|
|
1326
|
+
const transitions = this.transitions.get(state);
|
|
1327
|
+
return !transitions || transitions.size === 0;
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Get the resource type this state machine manages
|
|
1331
|
+
*
|
|
1332
|
+
* @returns Resource type string
|
|
1333
|
+
*/
|
|
1334
|
+
getResourceType() {
|
|
1335
|
+
return this.resourceType;
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Validate state transition and create audit event
|
|
1339
|
+
*
|
|
1340
|
+
* This is a convenience method that combines validation with audit event creation.
|
|
1341
|
+
* Use this when you want to both validate a transition and record it in the audit trail.
|
|
1342
|
+
*
|
|
1343
|
+
* @param from - Current state
|
|
1344
|
+
* @param to - Target state
|
|
1345
|
+
* @param resourceId - ID of the resource being transitioned
|
|
1346
|
+
* @param context - Optional audit context (who, why, metadata)
|
|
1347
|
+
* @returns StateChangeEvent ready to be appended to document metadata
|
|
1348
|
+
* @throws InvalidStateTransitionError if transition is invalid
|
|
1349
|
+
*
|
|
1350
|
+
* @example
|
|
1351
|
+
* ```typescript
|
|
1352
|
+
* import { appendAuditEvent } from '@classytic/revenue';
|
|
1353
|
+
*
|
|
1354
|
+
* // Validate and create audit event
|
|
1355
|
+
* const auditEvent = TRANSACTION_STATE_MACHINE.validateAndCreateAuditEvent(
|
|
1356
|
+
* transaction.status,
|
|
1357
|
+
* 'verified',
|
|
1358
|
+
* transaction._id.toString(),
|
|
1359
|
+
* {
|
|
1360
|
+
* changedBy: 'admin_123',
|
|
1361
|
+
* reason: 'Payment verified by payment gateway',
|
|
1362
|
+
* metadata: { verificationId: 'ver_abc' }
|
|
1363
|
+
* }
|
|
1364
|
+
* );
|
|
1365
|
+
*
|
|
1366
|
+
* // Apply state change
|
|
1367
|
+
* transaction.status = 'verified';
|
|
1368
|
+
*
|
|
1369
|
+
* // Append audit event to metadata
|
|
1370
|
+
* Object.assign(transaction, appendAuditEvent(transaction, auditEvent));
|
|
1371
|
+
*
|
|
1372
|
+
* // Save
|
|
1373
|
+
* await transaction.save();
|
|
1374
|
+
* ```
|
|
1375
|
+
*/
|
|
1376
|
+
validateAndCreateAuditEvent(from, to, resourceId, context) {
|
|
1377
|
+
this.validate(from, to, resourceId);
|
|
1378
|
+
return {
|
|
1379
|
+
resourceType: this.resourceType,
|
|
1380
|
+
resourceId,
|
|
1381
|
+
fromState: from,
|
|
1382
|
+
toState: to,
|
|
1383
|
+
changedAt: /* @__PURE__ */ new Date(),
|
|
1384
|
+
changedBy: context?.changedBy,
|
|
1385
|
+
reason: context?.reason,
|
|
1386
|
+
metadata: context?.metadata
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
};
|
|
1390
|
+
|
|
1391
|
+
// src/enums/subscription.enums.ts
|
|
1392
|
+
var SUBSCRIPTION_STATUS = {
|
|
1393
|
+
ACTIVE: "active",
|
|
1394
|
+
PAUSED: "paused",
|
|
1395
|
+
CANCELLED: "cancelled",
|
|
1396
|
+
EXPIRED: "expired",
|
|
1397
|
+
PENDING: "pending",
|
|
1398
|
+
PENDING_RENEWAL: "pending_renewal",
|
|
1399
|
+
INACTIVE: "inactive"
|
|
1400
|
+
};
|
|
1401
|
+
var SUBSCRIPTION_STATUS_VALUES = Object.values(
|
|
1402
|
+
SUBSCRIPTION_STATUS
|
|
1403
|
+
);
|
|
1404
|
+
var PLAN_KEYS = {
|
|
1405
|
+
MONTHLY: "monthly",
|
|
1406
|
+
QUARTERLY: "quarterly",
|
|
1407
|
+
YEARLY: "yearly"
|
|
1408
|
+
};
|
|
1409
|
+
var PLAN_KEY_VALUES = Object.values(PLAN_KEYS);
|
|
1410
|
+
new Set(
|
|
1411
|
+
SUBSCRIPTION_STATUS_VALUES
|
|
1412
|
+
);
|
|
1413
|
+
new Set(PLAN_KEY_VALUES);
|
|
1414
|
+
|
|
1415
|
+
// src/enums/settlement.enums.ts
|
|
1416
|
+
var SETTLEMENT_STATUS = {
|
|
1417
|
+
PENDING: "pending",
|
|
1418
|
+
PROCESSING: "processing",
|
|
1419
|
+
COMPLETED: "completed",
|
|
1420
|
+
FAILED: "failed",
|
|
1421
|
+
CANCELLED: "cancelled"
|
|
1422
|
+
};
|
|
1423
|
+
var SETTLEMENT_TYPE = {
|
|
1424
|
+
SPLIT_PAYOUT: "split_payout"};
|
|
1425
|
+
|
|
1426
|
+
// src/enums/escrow.enums.ts
|
|
1427
|
+
var HOLD_STATUS = {
|
|
1428
|
+
PENDING: "pending",
|
|
1429
|
+
HELD: "held",
|
|
1430
|
+
RELEASED: "released",
|
|
1431
|
+
CANCELLED: "cancelled",
|
|
1432
|
+
EXPIRED: "expired",
|
|
1433
|
+
PARTIALLY_RELEASED: "partially_released"
|
|
1434
|
+
};
|
|
1435
|
+
var HOLD_STATUS_VALUES = Object.values(HOLD_STATUS);
|
|
1436
|
+
var RELEASE_REASON = {
|
|
1437
|
+
PAYMENT_VERIFIED: "payment_verified",
|
|
1438
|
+
MANUAL_RELEASE: "manual_release",
|
|
1439
|
+
AUTO_RELEASE: "auto_release",
|
|
1440
|
+
DISPUTE_RESOLVED: "dispute_resolved"
|
|
1441
|
+
};
|
|
1442
|
+
var RELEASE_REASON_VALUES = Object.values(
|
|
1443
|
+
RELEASE_REASON
|
|
1444
|
+
);
|
|
1445
|
+
var HOLD_REASON = {
|
|
1446
|
+
PAYMENT_VERIFICATION: "payment_verification",
|
|
1447
|
+
FRAUD_CHECK: "fraud_check",
|
|
1448
|
+
MANUAL_REVIEW: "manual_review",
|
|
1449
|
+
DISPUTE: "dispute",
|
|
1450
|
+
COMPLIANCE: "compliance"
|
|
1451
|
+
};
|
|
1452
|
+
var HOLD_REASON_VALUES = Object.values(HOLD_REASON);
|
|
1453
|
+
new Set(HOLD_STATUS_VALUES);
|
|
1454
|
+
new Set(RELEASE_REASON_VALUES);
|
|
1455
|
+
new Set(HOLD_REASON_VALUES);
|
|
1456
|
+
|
|
1457
|
+
// src/enums/split.enums.ts
|
|
1458
|
+
var SPLIT_TYPE = {
|
|
1459
|
+
PLATFORM_COMMISSION: "platform_commission",
|
|
1460
|
+
AFFILIATE_COMMISSION: "affiliate_commission",
|
|
1461
|
+
REFERRAL_COMMISSION: "referral_commission",
|
|
1462
|
+
PARTNER_COMMISSION: "partner_commission",
|
|
1463
|
+
CUSTOM: "custom"
|
|
1464
|
+
};
|
|
1465
|
+
var SPLIT_TYPE_VALUES = Object.values(SPLIT_TYPE);
|
|
1466
|
+
var SPLIT_STATUS = {
|
|
1467
|
+
PENDING: "pending",
|
|
1468
|
+
DUE: "due",
|
|
1469
|
+
PAID: "paid",
|
|
1470
|
+
WAIVED: "waived",
|
|
1471
|
+
CANCELLED: "cancelled"
|
|
1472
|
+
};
|
|
1473
|
+
var SPLIT_STATUS_VALUES = Object.values(
|
|
1474
|
+
SPLIT_STATUS
|
|
1475
|
+
);
|
|
1476
|
+
var PAYOUT_METHOD = {
|
|
1477
|
+
BANK_TRANSFER: "bank_transfer",
|
|
1478
|
+
MOBILE_WALLET: "mobile_wallet",
|
|
1479
|
+
PLATFORM_BALANCE: "platform_balance",
|
|
1480
|
+
CRYPTO: "crypto",
|
|
1481
|
+
CHECK: "check",
|
|
1482
|
+
MANUAL: "manual"
|
|
1483
|
+
};
|
|
1484
|
+
var PAYOUT_METHOD_VALUES = Object.values(PAYOUT_METHOD);
|
|
1485
|
+
new Set(SPLIT_TYPE_VALUES);
|
|
1486
|
+
new Set(SPLIT_STATUS_VALUES);
|
|
1487
|
+
new Set(PAYOUT_METHOD_VALUES);
|
|
1488
|
+
|
|
1489
|
+
// src/core/state-machine/definitions.ts
|
|
1490
|
+
var TRANSACTION_STATE_MACHINE = new StateMachine(
|
|
1491
|
+
/* @__PURE__ */ new Map([
|
|
1492
|
+
[
|
|
1493
|
+
TRANSACTION_STATUS.PENDING,
|
|
1494
|
+
/* @__PURE__ */ new Set([
|
|
1495
|
+
TRANSACTION_STATUS.PAYMENT_INITIATED,
|
|
1496
|
+
TRANSACTION_STATUS.PROCESSING,
|
|
1497
|
+
TRANSACTION_STATUS.VERIFIED,
|
|
1498
|
+
// Allow direct verification (manual payments)
|
|
1499
|
+
TRANSACTION_STATUS.FAILED,
|
|
1500
|
+
TRANSACTION_STATUS.CANCELLED
|
|
1501
|
+
])
|
|
1502
|
+
],
|
|
1503
|
+
[
|
|
1504
|
+
TRANSACTION_STATUS.PAYMENT_INITIATED,
|
|
1505
|
+
/* @__PURE__ */ new Set([
|
|
1506
|
+
TRANSACTION_STATUS.PROCESSING,
|
|
1507
|
+
TRANSACTION_STATUS.VERIFIED,
|
|
1508
|
+
// Allow direct verification (webhook/instant payments)
|
|
1509
|
+
TRANSACTION_STATUS.REQUIRES_ACTION,
|
|
1510
|
+
TRANSACTION_STATUS.FAILED,
|
|
1511
|
+
TRANSACTION_STATUS.CANCELLED
|
|
1512
|
+
])
|
|
1513
|
+
],
|
|
1514
|
+
[
|
|
1515
|
+
TRANSACTION_STATUS.PROCESSING,
|
|
1516
|
+
/* @__PURE__ */ new Set([
|
|
1517
|
+
TRANSACTION_STATUS.VERIFIED,
|
|
1518
|
+
TRANSACTION_STATUS.REQUIRES_ACTION,
|
|
1519
|
+
TRANSACTION_STATUS.FAILED
|
|
1520
|
+
])
|
|
1521
|
+
],
|
|
1522
|
+
[
|
|
1523
|
+
TRANSACTION_STATUS.REQUIRES_ACTION,
|
|
1524
|
+
/* @__PURE__ */ new Set([
|
|
1525
|
+
TRANSACTION_STATUS.VERIFIED,
|
|
1526
|
+
TRANSACTION_STATUS.FAILED,
|
|
1527
|
+
TRANSACTION_STATUS.CANCELLED,
|
|
1528
|
+
TRANSACTION_STATUS.EXPIRED
|
|
1529
|
+
])
|
|
1530
|
+
],
|
|
1531
|
+
[
|
|
1532
|
+
TRANSACTION_STATUS.VERIFIED,
|
|
1533
|
+
/* @__PURE__ */ new Set([
|
|
1534
|
+
TRANSACTION_STATUS.COMPLETED,
|
|
1535
|
+
TRANSACTION_STATUS.REFUNDED,
|
|
1536
|
+
TRANSACTION_STATUS.PARTIALLY_REFUNDED,
|
|
1537
|
+
TRANSACTION_STATUS.CANCELLED
|
|
1538
|
+
])
|
|
1539
|
+
],
|
|
1540
|
+
[
|
|
1541
|
+
TRANSACTION_STATUS.COMPLETED,
|
|
1542
|
+
/* @__PURE__ */ new Set([
|
|
1543
|
+
TRANSACTION_STATUS.REFUNDED,
|
|
1544
|
+
TRANSACTION_STATUS.PARTIALLY_REFUNDED
|
|
1545
|
+
])
|
|
1546
|
+
],
|
|
1547
|
+
[
|
|
1548
|
+
TRANSACTION_STATUS.PARTIALLY_REFUNDED,
|
|
1549
|
+
/* @__PURE__ */ new Set([TRANSACTION_STATUS.REFUNDED])
|
|
1550
|
+
],
|
|
1551
|
+
// Terminal states
|
|
1552
|
+
[TRANSACTION_STATUS.FAILED, /* @__PURE__ */ new Set([])],
|
|
1553
|
+
[TRANSACTION_STATUS.REFUNDED, /* @__PURE__ */ new Set([])],
|
|
1554
|
+
[TRANSACTION_STATUS.CANCELLED, /* @__PURE__ */ new Set([])],
|
|
1555
|
+
[TRANSACTION_STATUS.EXPIRED, /* @__PURE__ */ new Set([])]
|
|
1556
|
+
]),
|
|
1557
|
+
"transaction"
|
|
1558
|
+
);
|
|
1559
|
+
var SUBSCRIPTION_STATE_MACHINE = new StateMachine(
|
|
1560
|
+
/* @__PURE__ */ new Map([
|
|
1561
|
+
[
|
|
1562
|
+
SUBSCRIPTION_STATUS.PENDING,
|
|
1563
|
+
/* @__PURE__ */ new Set([
|
|
1564
|
+
SUBSCRIPTION_STATUS.ACTIVE,
|
|
1565
|
+
SUBSCRIPTION_STATUS.CANCELLED
|
|
1566
|
+
])
|
|
1567
|
+
],
|
|
1568
|
+
[
|
|
1569
|
+
SUBSCRIPTION_STATUS.ACTIVE,
|
|
1570
|
+
/* @__PURE__ */ new Set([
|
|
1571
|
+
SUBSCRIPTION_STATUS.PAUSED,
|
|
1572
|
+
SUBSCRIPTION_STATUS.PENDING_RENEWAL,
|
|
1573
|
+
SUBSCRIPTION_STATUS.CANCELLED,
|
|
1574
|
+
SUBSCRIPTION_STATUS.EXPIRED
|
|
1575
|
+
])
|
|
1576
|
+
],
|
|
1577
|
+
[
|
|
1578
|
+
SUBSCRIPTION_STATUS.PENDING_RENEWAL,
|
|
1579
|
+
/* @__PURE__ */ new Set([
|
|
1580
|
+
SUBSCRIPTION_STATUS.ACTIVE,
|
|
1581
|
+
SUBSCRIPTION_STATUS.CANCELLED,
|
|
1582
|
+
SUBSCRIPTION_STATUS.EXPIRED
|
|
1583
|
+
])
|
|
1584
|
+
],
|
|
1585
|
+
[
|
|
1586
|
+
SUBSCRIPTION_STATUS.PAUSED,
|
|
1587
|
+
/* @__PURE__ */ new Set([
|
|
1588
|
+
SUBSCRIPTION_STATUS.ACTIVE,
|
|
1589
|
+
SUBSCRIPTION_STATUS.CANCELLED
|
|
1590
|
+
])
|
|
1591
|
+
],
|
|
1592
|
+
[
|
|
1593
|
+
SUBSCRIPTION_STATUS.INACTIVE,
|
|
1594
|
+
/* @__PURE__ */ new Set([
|
|
1595
|
+
SUBSCRIPTION_STATUS.ACTIVE,
|
|
1596
|
+
SUBSCRIPTION_STATUS.CANCELLED
|
|
1597
|
+
])
|
|
1598
|
+
],
|
|
1599
|
+
// Terminal states
|
|
1600
|
+
[SUBSCRIPTION_STATUS.CANCELLED, /* @__PURE__ */ new Set([])],
|
|
1601
|
+
[SUBSCRIPTION_STATUS.EXPIRED, /* @__PURE__ */ new Set([])]
|
|
1602
|
+
]),
|
|
1603
|
+
"subscription"
|
|
1604
|
+
);
|
|
1605
|
+
var SETTLEMENT_STATE_MACHINE = new StateMachine(
|
|
1606
|
+
/* @__PURE__ */ new Map([
|
|
1607
|
+
[
|
|
1608
|
+
SETTLEMENT_STATUS.PENDING,
|
|
1609
|
+
/* @__PURE__ */ new Set([
|
|
1610
|
+
SETTLEMENT_STATUS.PROCESSING,
|
|
1611
|
+
SETTLEMENT_STATUS.CANCELLED
|
|
1612
|
+
])
|
|
1613
|
+
],
|
|
1614
|
+
[
|
|
1615
|
+
SETTLEMENT_STATUS.PROCESSING,
|
|
1616
|
+
/* @__PURE__ */ new Set([
|
|
1617
|
+
SETTLEMENT_STATUS.COMPLETED,
|
|
1618
|
+
SETTLEMENT_STATUS.FAILED
|
|
1619
|
+
])
|
|
1620
|
+
],
|
|
1621
|
+
[
|
|
1622
|
+
SETTLEMENT_STATUS.FAILED,
|
|
1623
|
+
/* @__PURE__ */ new Set([
|
|
1624
|
+
SETTLEMENT_STATUS.PENDING,
|
|
1625
|
+
// Allow retry
|
|
1626
|
+
SETTLEMENT_STATUS.CANCELLED
|
|
1627
|
+
])
|
|
1628
|
+
],
|
|
1629
|
+
// Terminal states
|
|
1630
|
+
[SETTLEMENT_STATUS.COMPLETED, /* @__PURE__ */ new Set([])],
|
|
1631
|
+
[SETTLEMENT_STATUS.CANCELLED, /* @__PURE__ */ new Set([])]
|
|
1632
|
+
]),
|
|
1633
|
+
"settlement"
|
|
1634
|
+
);
|
|
1635
|
+
var HOLD_STATE_MACHINE = new StateMachine(
|
|
1636
|
+
/* @__PURE__ */ new Map([
|
|
1637
|
+
[
|
|
1638
|
+
HOLD_STATUS.HELD,
|
|
1639
|
+
/* @__PURE__ */ new Set([
|
|
1640
|
+
HOLD_STATUS.RELEASED,
|
|
1641
|
+
HOLD_STATUS.PARTIALLY_RELEASED,
|
|
1642
|
+
HOLD_STATUS.CANCELLED,
|
|
1643
|
+
HOLD_STATUS.EXPIRED
|
|
1644
|
+
])
|
|
1645
|
+
],
|
|
1646
|
+
[
|
|
1647
|
+
HOLD_STATUS.PARTIALLY_RELEASED,
|
|
1648
|
+
/* @__PURE__ */ new Set([
|
|
1649
|
+
HOLD_STATUS.RELEASED,
|
|
1650
|
+
HOLD_STATUS.CANCELLED
|
|
1651
|
+
])
|
|
1652
|
+
],
|
|
1653
|
+
// Terminal states
|
|
1654
|
+
[HOLD_STATUS.RELEASED, /* @__PURE__ */ new Set([])],
|
|
1655
|
+
[HOLD_STATUS.CANCELLED, /* @__PURE__ */ new Set([])],
|
|
1656
|
+
[HOLD_STATUS.EXPIRED, /* @__PURE__ */ new Set([])]
|
|
1657
|
+
]),
|
|
1658
|
+
"escrow_hold"
|
|
1659
|
+
);
|
|
1660
|
+
var SPLIT_STATE_MACHINE = new StateMachine(
|
|
1661
|
+
/* @__PURE__ */ new Map([
|
|
1662
|
+
[
|
|
1663
|
+
SPLIT_STATUS.PENDING,
|
|
1664
|
+
/* @__PURE__ */ new Set([
|
|
1665
|
+
SPLIT_STATUS.DUE,
|
|
1666
|
+
SPLIT_STATUS.PAID,
|
|
1667
|
+
SPLIT_STATUS.WAIVED,
|
|
1668
|
+
SPLIT_STATUS.CANCELLED
|
|
1669
|
+
])
|
|
1670
|
+
],
|
|
1671
|
+
[
|
|
1672
|
+
SPLIT_STATUS.DUE,
|
|
1673
|
+
/* @__PURE__ */ new Set([
|
|
1674
|
+
SPLIT_STATUS.PAID,
|
|
1675
|
+
SPLIT_STATUS.WAIVED,
|
|
1676
|
+
SPLIT_STATUS.CANCELLED
|
|
1677
|
+
])
|
|
1678
|
+
],
|
|
1679
|
+
// Terminal states
|
|
1680
|
+
[SPLIT_STATUS.PAID, /* @__PURE__ */ new Set([])],
|
|
1681
|
+
[SPLIT_STATUS.WAIVED, /* @__PURE__ */ new Set([])],
|
|
1682
|
+
[SPLIT_STATUS.CANCELLED, /* @__PURE__ */ new Set([])]
|
|
1683
|
+
]),
|
|
1684
|
+
"split"
|
|
1685
|
+
);
|
|
1686
|
+
|
|
1687
|
+
// src/infrastructure/audit/types.ts
|
|
1688
|
+
function appendAuditEvent(document, event) {
|
|
1689
|
+
const stateHistory = document.metadata?.stateHistory ?? [];
|
|
1690
|
+
return {
|
|
1691
|
+
...document,
|
|
1692
|
+
metadata: {
|
|
1693
|
+
...document.metadata,
|
|
1694
|
+
stateHistory: [...stateHistory, event]
|
|
1695
|
+
}
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
// src/application/services/monetization.service.ts
|
|
1195
1700
|
var MonetizationService = class {
|
|
1196
1701
|
models;
|
|
1197
1702
|
providers;
|
|
1198
1703
|
config;
|
|
1199
|
-
|
|
1704
|
+
plugins;
|
|
1200
1705
|
logger;
|
|
1706
|
+
events;
|
|
1707
|
+
retryConfig;
|
|
1708
|
+
circuitBreaker;
|
|
1201
1709
|
constructor(container) {
|
|
1202
1710
|
this.models = container.get("models");
|
|
1203
1711
|
this.providers = container.get("providers");
|
|
1204
1712
|
this.config = container.get("config");
|
|
1205
|
-
this.
|
|
1713
|
+
this.plugins = container.get("plugins");
|
|
1206
1714
|
this.logger = container.get("logger");
|
|
1715
|
+
this.events = container.get("events");
|
|
1716
|
+
this.retryConfig = container.get("retryConfig");
|
|
1717
|
+
this.circuitBreaker = container.get("circuitBreaker");
|
|
1718
|
+
}
|
|
1719
|
+
/**
|
|
1720
|
+
* Create plugin context for hook execution
|
|
1721
|
+
* @private
|
|
1722
|
+
*/
|
|
1723
|
+
getPluginContext(idempotencyKey) {
|
|
1724
|
+
return {
|
|
1725
|
+
events: this.events,
|
|
1726
|
+
logger: this.logger,
|
|
1727
|
+
storage: /* @__PURE__ */ new Map(),
|
|
1728
|
+
meta: {
|
|
1729
|
+
idempotencyKey: idempotencyKey ?? void 0,
|
|
1730
|
+
requestId: nanoid(),
|
|
1731
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
1732
|
+
}
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
/**
|
|
1736
|
+
* Execute provider call with retry and circuit breaker protection
|
|
1737
|
+
* @private
|
|
1738
|
+
*/
|
|
1739
|
+
async executeProviderCall(operation, operationName) {
|
|
1740
|
+
const withCircuitBreaker = this.circuitBreaker ? () => this.circuitBreaker.execute(operation) : operation;
|
|
1741
|
+
if (this.retryConfig && Object.keys(this.retryConfig).length > 0) {
|
|
1742
|
+
return retry(withCircuitBreaker, {
|
|
1743
|
+
...this.retryConfig,
|
|
1744
|
+
onRetry: (error, attempt, delay) => {
|
|
1745
|
+
this.logger.warn(
|
|
1746
|
+
`[${operationName}] Retry attempt ${attempt} after ${delay}ms:`,
|
|
1747
|
+
error
|
|
1748
|
+
);
|
|
1749
|
+
this.retryConfig.onRetry?.(error, attempt, delay);
|
|
1750
|
+
}
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1753
|
+
return withCircuitBreaker();
|
|
1207
1754
|
}
|
|
1208
1755
|
/**
|
|
1209
1756
|
* Create a new monetization (purchase, subscription, or free item)
|
|
@@ -1216,8 +1763,8 @@ var MonetizationService = class {
|
|
|
1216
1763
|
* data: {
|
|
1217
1764
|
* organizationId: '...',
|
|
1218
1765
|
* customerId: '...',
|
|
1219
|
-
*
|
|
1220
|
-
*
|
|
1766
|
+
* sourceId: order._id,
|
|
1767
|
+
* sourceModel: 'Order',
|
|
1221
1768
|
* },
|
|
1222
1769
|
* planKey: 'one_time',
|
|
1223
1770
|
* monetizationType: 'purchase',
|
|
@@ -1230,8 +1777,8 @@ var MonetizationService = class {
|
|
|
1230
1777
|
* data: {
|
|
1231
1778
|
* organizationId: '...',
|
|
1232
1779
|
* customerId: '...',
|
|
1233
|
-
*
|
|
1234
|
-
*
|
|
1780
|
+
* sourceId: subscription._id,
|
|
1781
|
+
* sourceModel: 'Subscription',
|
|
1235
1782
|
* },
|
|
1236
1783
|
* planKey: 'monthly',
|
|
1237
1784
|
* monetizationType: 'subscription',
|
|
@@ -1242,192 +1789,300 @@ var MonetizationService = class {
|
|
|
1242
1789
|
* @returns Result with subscription, transaction, and paymentIntent
|
|
1243
1790
|
*/
|
|
1244
1791
|
async create(params) {
|
|
1792
|
+
return this.plugins.executeHook(
|
|
1793
|
+
"monetization.create.before",
|
|
1794
|
+
this.getPluginContext(params.idempotencyKey),
|
|
1795
|
+
params,
|
|
1796
|
+
async () => {
|
|
1797
|
+
const {
|
|
1798
|
+
data,
|
|
1799
|
+
planKey,
|
|
1800
|
+
amount,
|
|
1801
|
+
currency = "BDT",
|
|
1802
|
+
gateway = "manual",
|
|
1803
|
+
entity = null,
|
|
1804
|
+
monetizationType = MONETIZATION_TYPES.SUBSCRIPTION,
|
|
1805
|
+
paymentData,
|
|
1806
|
+
metadata = {},
|
|
1807
|
+
idempotencyKey = null
|
|
1808
|
+
} = params;
|
|
1809
|
+
if (!planKey) {
|
|
1810
|
+
throw new MissingRequiredFieldError("planKey");
|
|
1811
|
+
}
|
|
1812
|
+
if (amount < 0) {
|
|
1813
|
+
throw new InvalidAmountError(amount);
|
|
1814
|
+
}
|
|
1815
|
+
const isFree = amount === 0;
|
|
1816
|
+
const provider = this.providers[gateway];
|
|
1817
|
+
if (!provider) {
|
|
1818
|
+
throw new ProviderNotFoundError(gateway, Object.keys(this.providers));
|
|
1819
|
+
}
|
|
1820
|
+
let paymentIntent = null;
|
|
1821
|
+
let transaction = null;
|
|
1822
|
+
if (!isFree) {
|
|
1823
|
+
try {
|
|
1824
|
+
paymentIntent = await this.executeProviderCall(
|
|
1825
|
+
() => provider.createIntent({
|
|
1826
|
+
amount,
|
|
1827
|
+
currency,
|
|
1828
|
+
metadata: {
|
|
1829
|
+
...metadata,
|
|
1830
|
+
type: "subscription",
|
|
1831
|
+
planKey
|
|
1832
|
+
}
|
|
1833
|
+
}),
|
|
1834
|
+
`${gateway}.createIntent`
|
|
1835
|
+
);
|
|
1836
|
+
} catch (error) {
|
|
1837
|
+
throw new PaymentIntentCreationError(gateway, error);
|
|
1838
|
+
}
|
|
1839
|
+
const category = resolveCategory(entity, monetizationType, this.config.categoryMappings);
|
|
1840
|
+
const transactionFlow = this.config.transactionTypeMapping?.[category] ?? this.config.transactionTypeMapping?.[monetizationType] ?? TRANSACTION_FLOW.INFLOW;
|
|
1841
|
+
const commissionRate = getCommissionRate(this.config, category);
|
|
1842
|
+
const gatewayFeeRate = getGatewayFeeRate(this.config, gateway);
|
|
1843
|
+
const commission = calculateCommission(amount, commissionRate, gatewayFeeRate);
|
|
1844
|
+
const tax = params.tax;
|
|
1845
|
+
const TransactionModel = this.models.Transaction;
|
|
1846
|
+
const baseAmount = tax?.pricesIncludeTax ? tax.baseAmount : amount;
|
|
1847
|
+
const feeAmount = commission?.gatewayFeeAmount || 0;
|
|
1848
|
+
const taxAmount = tax?.taxAmount || 0;
|
|
1849
|
+
const netAmount = baseAmount - feeAmount - taxAmount;
|
|
1850
|
+
transaction = await TransactionModel.create({
|
|
1851
|
+
organizationId: data.organizationId,
|
|
1852
|
+
customerId: data.customerId ?? null,
|
|
1853
|
+
// ✅ UNIFIED: Use category as type directly
|
|
1854
|
+
type: category,
|
|
1855
|
+
// 'subscription', 'purchase', etc.
|
|
1856
|
+
flow: transactionFlow,
|
|
1857
|
+
// ✅ Use config-driven transaction type
|
|
1858
|
+
// Auto-tagging (middleware will handle, but we can set explicitly)
|
|
1859
|
+
tags: category === "subscription" ? ["recurring", "subscription"] : [],
|
|
1860
|
+
// ✅ UNIFIED: Amount structure
|
|
1861
|
+
// When prices include tax, use baseAmount (tax already extracted)
|
|
1862
|
+
amount: baseAmount,
|
|
1863
|
+
currency,
|
|
1864
|
+
fee: feeAmount,
|
|
1865
|
+
tax: taxAmount,
|
|
1866
|
+
net: netAmount,
|
|
1867
|
+
// ✅ UNIFIED: Tax details (if tax plugin used)
|
|
1868
|
+
...tax && {
|
|
1869
|
+
taxDetails: {
|
|
1870
|
+
type: tax.type === "collected" ? "sales_tax" : tax.type === "paid" ? "vat" : "none",
|
|
1871
|
+
rate: tax.rate || 0,
|
|
1872
|
+
isInclusive: tax.pricesIncludeTax || false
|
|
1873
|
+
}
|
|
1874
|
+
},
|
|
1875
|
+
method: paymentData?.method ?? "manual",
|
|
1876
|
+
status: paymentIntent.status === "succeeded" ? "verified" : "pending",
|
|
1877
|
+
// ✅ Map 'succeeded' to valid TransactionStatusValue
|
|
1878
|
+
gateway: {
|
|
1879
|
+
type: gateway,
|
|
1880
|
+
// Gateway/provider type (e.g., 'stripe', 'manual')
|
|
1881
|
+
provider: gateway,
|
|
1882
|
+
sessionId: paymentIntent.sessionId,
|
|
1883
|
+
paymentIntentId: paymentIntent.paymentIntentId,
|
|
1884
|
+
chargeId: paymentIntent.id,
|
|
1885
|
+
metadata: paymentIntent.metadata
|
|
1886
|
+
},
|
|
1887
|
+
paymentDetails: {
|
|
1888
|
+
...paymentData
|
|
1889
|
+
},
|
|
1890
|
+
// Commission (for marketplace/splits)
|
|
1891
|
+
...commission && { commission },
|
|
1892
|
+
// ✅ UNIFIED: Source reference (renamed from reference)
|
|
1893
|
+
...data.sourceId && { sourceId: data.sourceId },
|
|
1894
|
+
...data.sourceModel && { sourceModel: data.sourceModel },
|
|
1895
|
+
metadata: {
|
|
1896
|
+
...metadata,
|
|
1897
|
+
planKey,
|
|
1898
|
+
entity,
|
|
1899
|
+
monetizationType,
|
|
1900
|
+
paymentIntentId: paymentIntent.id
|
|
1901
|
+
},
|
|
1902
|
+
idempotencyKey: idempotencyKey ?? `sub_${nanoid(16)}`
|
|
1903
|
+
});
|
|
1904
|
+
}
|
|
1905
|
+
let subscription = null;
|
|
1906
|
+
if (this.models.Subscription) {
|
|
1907
|
+
const SubscriptionModel = this.models.Subscription;
|
|
1908
|
+
const result2 = await this.plugins.executeHook(
|
|
1909
|
+
"subscription.create.before",
|
|
1910
|
+
this.getPluginContext(idempotencyKey),
|
|
1911
|
+
{
|
|
1912
|
+
subscriptionId: void 0,
|
|
1913
|
+
// Not yet created - populated in after hook
|
|
1914
|
+
planKey,
|
|
1915
|
+
customerId: data.customerId,
|
|
1916
|
+
organizationId: data.organizationId,
|
|
1917
|
+
entity
|
|
1918
|
+
},
|
|
1919
|
+
async () => {
|
|
1920
|
+
const subscriptionData = {
|
|
1921
|
+
organizationId: data.organizationId,
|
|
1922
|
+
customerId: data.customerId ?? null,
|
|
1923
|
+
planKey,
|
|
1924
|
+
amount,
|
|
1925
|
+
currency,
|
|
1926
|
+
status: isFree ? "active" : "pending",
|
|
1927
|
+
isActive: isFree,
|
|
1928
|
+
gateway,
|
|
1929
|
+
transactionId: transaction?._id ?? null,
|
|
1930
|
+
paymentIntentId: paymentIntent?.id ?? null,
|
|
1931
|
+
metadata: {
|
|
1932
|
+
...metadata,
|
|
1933
|
+
isFree,
|
|
1934
|
+
entity,
|
|
1935
|
+
monetizationType
|
|
1936
|
+
},
|
|
1937
|
+
...data
|
|
1938
|
+
};
|
|
1939
|
+
delete subscriptionData.sourceId;
|
|
1940
|
+
delete subscriptionData.sourceModel;
|
|
1941
|
+
const sub = await SubscriptionModel.create(subscriptionData);
|
|
1942
|
+
await this.plugins.executeHook(
|
|
1943
|
+
"subscription.create.after",
|
|
1944
|
+
this.getPluginContext(idempotencyKey),
|
|
1945
|
+
{
|
|
1946
|
+
subscriptionId: sub._id.toString(),
|
|
1947
|
+
planKey,
|
|
1948
|
+
customerId: data.customerId,
|
|
1949
|
+
organizationId: data.organizationId,
|
|
1950
|
+
entity
|
|
1951
|
+
},
|
|
1952
|
+
async () => ({ subscription: sub, transaction })
|
|
1953
|
+
);
|
|
1954
|
+
return { subscription: sub, transaction };
|
|
1955
|
+
}
|
|
1956
|
+
);
|
|
1957
|
+
subscription = result2.subscription;
|
|
1958
|
+
}
|
|
1959
|
+
this.events.emit("monetization.created", {
|
|
1960
|
+
monetizationType,
|
|
1961
|
+
subscription: subscription ?? void 0,
|
|
1962
|
+
transaction: transaction ?? void 0,
|
|
1963
|
+
paymentIntent: paymentIntent ?? void 0
|
|
1964
|
+
});
|
|
1965
|
+
if (monetizationType === MONETIZATION_TYPES.PURCHASE) {
|
|
1966
|
+
if (transaction) {
|
|
1967
|
+
this.events.emit("purchase.created", {
|
|
1968
|
+
monetizationType,
|
|
1969
|
+
subscription: subscription ?? void 0,
|
|
1970
|
+
transaction,
|
|
1971
|
+
paymentIntent: paymentIntent ?? void 0
|
|
1972
|
+
});
|
|
1973
|
+
}
|
|
1974
|
+
} else if (monetizationType === MONETIZATION_TYPES.SUBSCRIPTION) {
|
|
1975
|
+
if (subscription) {
|
|
1976
|
+
this.events.emit("subscription.created", {
|
|
1977
|
+
subscriptionId: subscription._id.toString(),
|
|
1978
|
+
subscription,
|
|
1979
|
+
transactionId: transaction?._id?.toString()
|
|
1980
|
+
});
|
|
1981
|
+
}
|
|
1982
|
+
} else if (monetizationType === MONETIZATION_TYPES.FREE) {
|
|
1983
|
+
this.events.emit("free.created", {
|
|
1984
|
+
monetizationType,
|
|
1985
|
+
subscription: subscription ?? void 0,
|
|
1986
|
+
transaction: transaction ?? void 0,
|
|
1987
|
+
paymentIntent: paymentIntent ?? void 0
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1990
|
+
const result = {
|
|
1991
|
+
subscription,
|
|
1992
|
+
transaction,
|
|
1993
|
+
paymentIntent
|
|
1994
|
+
};
|
|
1995
|
+
return this.plugins.executeHook(
|
|
1996
|
+
"monetization.create.after",
|
|
1997
|
+
this.getPluginContext(params.idempotencyKey),
|
|
1998
|
+
params,
|
|
1999
|
+
async () => result
|
|
2000
|
+
);
|
|
2001
|
+
}
|
|
2002
|
+
);
|
|
2003
|
+
}
|
|
2004
|
+
/**
|
|
2005
|
+
* Activate subscription after payment verification
|
|
2006
|
+
*
|
|
2007
|
+
* @param subscriptionId - Subscription ID or transaction ID
|
|
2008
|
+
* @param options - Activation options
|
|
2009
|
+
* @returns Updated subscription
|
|
2010
|
+
*/
|
|
2011
|
+
async activate(subscriptionId, options = {}) {
|
|
2012
|
+
return this.plugins.executeHook(
|
|
2013
|
+
"subscription.activate.before",
|
|
2014
|
+
this.getPluginContext(),
|
|
2015
|
+
{ subscriptionId, ...options },
|
|
2016
|
+
async () => {
|
|
2017
|
+
const { timestamp = /* @__PURE__ */ new Date() } = options;
|
|
2018
|
+
if (!this.models.Subscription) {
|
|
2019
|
+
throw new ModelNotRegisteredError("Subscription");
|
|
2020
|
+
}
|
|
2021
|
+
const SubscriptionModel = this.models.Subscription;
|
|
2022
|
+
const subscription = await SubscriptionModel.findById(subscriptionId);
|
|
2023
|
+
if (!subscription) {
|
|
2024
|
+
throw new SubscriptionNotFoundError(subscriptionId);
|
|
2025
|
+
}
|
|
2026
|
+
if (subscription.isActive) {
|
|
2027
|
+
this.logger.warn("Subscription already active", { subscriptionId });
|
|
2028
|
+
return subscription;
|
|
2029
|
+
}
|
|
2030
|
+
const periodEnd = this._calculatePeriodEnd(subscription.planKey, timestamp);
|
|
2031
|
+
const auditEvent = SUBSCRIPTION_STATE_MACHINE.validateAndCreateAuditEvent(
|
|
2032
|
+
subscription.status,
|
|
2033
|
+
"active",
|
|
2034
|
+
subscription._id.toString(),
|
|
2035
|
+
{
|
|
2036
|
+
changedBy: "system",
|
|
2037
|
+
reason: `Subscription activated for plan: ${subscription.planKey}`,
|
|
2038
|
+
metadata: { planKey: subscription.planKey, startDate: timestamp, endDate: periodEnd }
|
|
2039
|
+
}
|
|
2040
|
+
);
|
|
2041
|
+
subscription.isActive = true;
|
|
2042
|
+
subscription.status = "active";
|
|
2043
|
+
subscription.startDate = timestamp;
|
|
2044
|
+
subscription.endDate = periodEnd;
|
|
2045
|
+
subscription.activatedAt = timestamp;
|
|
2046
|
+
Object.assign(subscription, appendAuditEvent(subscription, auditEvent));
|
|
2047
|
+
await subscription.save();
|
|
2048
|
+
this.events.emit("subscription.activated", {
|
|
2049
|
+
subscription,
|
|
2050
|
+
activatedAt: timestamp
|
|
2051
|
+
});
|
|
2052
|
+
return this.plugins.executeHook(
|
|
2053
|
+
"subscription.activate.after",
|
|
2054
|
+
this.getPluginContext(),
|
|
2055
|
+
{ subscriptionId, ...options },
|
|
2056
|
+
async () => subscription
|
|
2057
|
+
);
|
|
2058
|
+
}
|
|
2059
|
+
);
|
|
2060
|
+
}
|
|
2061
|
+
/**
|
|
2062
|
+
* Renew subscription
|
|
2063
|
+
*
|
|
2064
|
+
* @param subscriptionId - Subscription ID
|
|
2065
|
+
* @param params - Renewal parameters
|
|
2066
|
+
* @returns { subscription, transaction, paymentIntent }
|
|
2067
|
+
*/
|
|
2068
|
+
async renew(subscriptionId, params = {}) {
|
|
1245
2069
|
const {
|
|
1246
|
-
data,
|
|
1247
|
-
planKey,
|
|
1248
|
-
amount,
|
|
1249
|
-
currency = "BDT",
|
|
1250
2070
|
gateway = "manual",
|
|
1251
2071
|
entity = null,
|
|
1252
|
-
monetizationType = MONETIZATION_TYPES.SUBSCRIPTION,
|
|
1253
2072
|
paymentData,
|
|
1254
2073
|
metadata = {},
|
|
1255
2074
|
idempotencyKey = null
|
|
1256
2075
|
} = params;
|
|
1257
|
-
if (!
|
|
1258
|
-
throw new
|
|
2076
|
+
if (!this.models.Subscription) {
|
|
2077
|
+
throw new ModelNotRegisteredError("Subscription");
|
|
2078
|
+
}
|
|
2079
|
+
const SubscriptionModel = this.models.Subscription;
|
|
2080
|
+
const subscription = await SubscriptionModel.findById(subscriptionId);
|
|
2081
|
+
if (!subscription) {
|
|
2082
|
+
throw new SubscriptionNotFoundError(subscriptionId);
|
|
1259
2083
|
}
|
|
1260
|
-
if (amount
|
|
1261
|
-
throw new InvalidAmountError(
|
|
1262
|
-
}
|
|
1263
|
-
const isFree = amount === 0;
|
|
1264
|
-
const provider = this.providers[gateway];
|
|
1265
|
-
if (!provider) {
|
|
1266
|
-
throw new ProviderNotFoundError(gateway, Object.keys(this.providers));
|
|
1267
|
-
}
|
|
1268
|
-
let paymentIntent = null;
|
|
1269
|
-
let transaction = null;
|
|
1270
|
-
if (!isFree) {
|
|
1271
|
-
try {
|
|
1272
|
-
paymentIntent = await provider.createIntent({
|
|
1273
|
-
amount,
|
|
1274
|
-
currency,
|
|
1275
|
-
metadata: {
|
|
1276
|
-
...metadata,
|
|
1277
|
-
type: "subscription",
|
|
1278
|
-
planKey
|
|
1279
|
-
}
|
|
1280
|
-
});
|
|
1281
|
-
} catch (error) {
|
|
1282
|
-
throw new PaymentIntentCreationError(gateway, error);
|
|
1283
|
-
}
|
|
1284
|
-
const category = resolveCategory(entity, monetizationType, this.config.categoryMappings);
|
|
1285
|
-
const transactionType = this.config.transactionTypeMapping?.subscription ?? this.config.transactionTypeMapping?.[monetizationType] ?? TRANSACTION_TYPE.INCOME;
|
|
1286
|
-
const commissionRate = this.config.commissionRates?.[category] ?? 0;
|
|
1287
|
-
const gatewayFeeRate = this.config.gatewayFeeRates?.[gateway] ?? 0;
|
|
1288
|
-
const commission = calculateCommission(amount, commissionRate, gatewayFeeRate);
|
|
1289
|
-
const TransactionModel = this.models.Transaction;
|
|
1290
|
-
transaction = await TransactionModel.create({
|
|
1291
|
-
organizationId: data.organizationId,
|
|
1292
|
-
customerId: data.customerId ?? null,
|
|
1293
|
-
amount,
|
|
1294
|
-
currency,
|
|
1295
|
-
category,
|
|
1296
|
-
type: transactionType,
|
|
1297
|
-
method: paymentData?.method ?? "manual",
|
|
1298
|
-
status: paymentIntent.status === "succeeded" ? "verified" : "pending",
|
|
1299
|
-
gateway: {
|
|
1300
|
-
type: gateway,
|
|
1301
|
-
sessionId: paymentIntent.sessionId,
|
|
1302
|
-
paymentIntentId: paymentIntent.paymentIntentId,
|
|
1303
|
-
provider: paymentIntent.provider,
|
|
1304
|
-
metadata: paymentIntent.metadata
|
|
1305
|
-
},
|
|
1306
|
-
paymentDetails: {
|
|
1307
|
-
provider: gateway,
|
|
1308
|
-
...paymentData
|
|
1309
|
-
},
|
|
1310
|
-
...commission && { commission },
|
|
1311
|
-
// Only include if commission exists
|
|
1312
|
-
// Polymorphic reference (top-level, not metadata)
|
|
1313
|
-
...data.referenceId && { referenceId: data.referenceId },
|
|
1314
|
-
...data.referenceModel && { referenceModel: data.referenceModel },
|
|
1315
|
-
metadata: {
|
|
1316
|
-
...metadata,
|
|
1317
|
-
planKey,
|
|
1318
|
-
entity,
|
|
1319
|
-
monetizationType,
|
|
1320
|
-
paymentIntentId: paymentIntent.id
|
|
1321
|
-
},
|
|
1322
|
-
idempotencyKey: idempotencyKey ?? `sub_${nanoid(16)}`
|
|
1323
|
-
});
|
|
1324
|
-
}
|
|
1325
|
-
let subscription = null;
|
|
1326
|
-
if (this.models.Subscription) {
|
|
1327
|
-
const SubscriptionModel = this.models.Subscription;
|
|
1328
|
-
const subscriptionData = {
|
|
1329
|
-
organizationId: data.organizationId,
|
|
1330
|
-
customerId: data.customerId ?? null,
|
|
1331
|
-
planKey,
|
|
1332
|
-
amount,
|
|
1333
|
-
currency,
|
|
1334
|
-
status: isFree ? "active" : "pending",
|
|
1335
|
-
isActive: isFree,
|
|
1336
|
-
gateway,
|
|
1337
|
-
transactionId: transaction?._id ?? null,
|
|
1338
|
-
paymentIntentId: paymentIntent?.id ?? null,
|
|
1339
|
-
metadata: {
|
|
1340
|
-
...metadata,
|
|
1341
|
-
isFree,
|
|
1342
|
-
entity,
|
|
1343
|
-
monetizationType
|
|
1344
|
-
},
|
|
1345
|
-
...data
|
|
1346
|
-
};
|
|
1347
|
-
delete subscriptionData.referenceId;
|
|
1348
|
-
delete subscriptionData.referenceModel;
|
|
1349
|
-
subscription = await SubscriptionModel.create(subscriptionData);
|
|
1350
|
-
}
|
|
1351
|
-
const eventData = {
|
|
1352
|
-
subscription,
|
|
1353
|
-
transaction,
|
|
1354
|
-
paymentIntent,
|
|
1355
|
-
isFree,
|
|
1356
|
-
monetizationType
|
|
1357
|
-
};
|
|
1358
|
-
if (monetizationType === MONETIZATION_TYPES.PURCHASE) {
|
|
1359
|
-
this._triggerHook("purchase.created", eventData);
|
|
1360
|
-
} else if (monetizationType === MONETIZATION_TYPES.SUBSCRIPTION) {
|
|
1361
|
-
this._triggerHook("subscription.created", eventData);
|
|
1362
|
-
} else if (monetizationType === MONETIZATION_TYPES.FREE) {
|
|
1363
|
-
this._triggerHook("free.created", eventData);
|
|
1364
|
-
}
|
|
1365
|
-
this._triggerHook("monetization.created", eventData);
|
|
1366
|
-
return {
|
|
1367
|
-
subscription,
|
|
1368
|
-
transaction,
|
|
1369
|
-
paymentIntent
|
|
1370
|
-
};
|
|
1371
|
-
}
|
|
1372
|
-
/**
|
|
1373
|
-
* Activate subscription after payment verification
|
|
1374
|
-
*
|
|
1375
|
-
* @param subscriptionId - Subscription ID or transaction ID
|
|
1376
|
-
* @param options - Activation options
|
|
1377
|
-
* @returns Updated subscription
|
|
1378
|
-
*/
|
|
1379
|
-
async activate(subscriptionId, options = {}) {
|
|
1380
|
-
const { timestamp = /* @__PURE__ */ new Date() } = options;
|
|
1381
|
-
if (!this.models.Subscription) {
|
|
1382
|
-
throw new ModelNotRegisteredError("Subscription");
|
|
1383
|
-
}
|
|
1384
|
-
const SubscriptionModel = this.models.Subscription;
|
|
1385
|
-
const subscription = await SubscriptionModel.findById(subscriptionId);
|
|
1386
|
-
if (!subscription) {
|
|
1387
|
-
throw new SubscriptionNotFoundError(subscriptionId);
|
|
1388
|
-
}
|
|
1389
|
-
if (subscription.isActive) {
|
|
1390
|
-
this.logger.warn("Subscription already active", { subscriptionId });
|
|
1391
|
-
return subscription;
|
|
1392
|
-
}
|
|
1393
|
-
const periodEnd = this._calculatePeriodEnd(subscription.planKey, timestamp);
|
|
1394
|
-
subscription.isActive = true;
|
|
1395
|
-
subscription.status = "active";
|
|
1396
|
-
subscription.startDate = timestamp;
|
|
1397
|
-
subscription.endDate = periodEnd;
|
|
1398
|
-
subscription.activatedAt = timestamp;
|
|
1399
|
-
await subscription.save();
|
|
1400
|
-
this._triggerHook("subscription.activated", {
|
|
1401
|
-
subscription,
|
|
1402
|
-
activatedAt: timestamp
|
|
1403
|
-
});
|
|
1404
|
-
return subscription;
|
|
1405
|
-
}
|
|
1406
|
-
/**
|
|
1407
|
-
* Renew subscription
|
|
1408
|
-
*
|
|
1409
|
-
* @param subscriptionId - Subscription ID
|
|
1410
|
-
* @param params - Renewal parameters
|
|
1411
|
-
* @returns { subscription, transaction, paymentIntent }
|
|
1412
|
-
*/
|
|
1413
|
-
async renew(subscriptionId, params = {}) {
|
|
1414
|
-
const {
|
|
1415
|
-
gateway = "manual",
|
|
1416
|
-
entity = null,
|
|
1417
|
-
paymentData,
|
|
1418
|
-
metadata = {},
|
|
1419
|
-
idempotencyKey = null
|
|
1420
|
-
} = params;
|
|
1421
|
-
if (!this.models.Subscription) {
|
|
1422
|
-
throw new ModelNotRegisteredError("Subscription");
|
|
1423
|
-
}
|
|
1424
|
-
const SubscriptionModel = this.models.Subscription;
|
|
1425
|
-
const subscription = await SubscriptionModel.findById(subscriptionId);
|
|
1426
|
-
if (!subscription) {
|
|
1427
|
-
throw new SubscriptionNotFoundError(subscriptionId);
|
|
1428
|
-
}
|
|
1429
|
-
if (subscription.amount === 0) {
|
|
1430
|
-
throw new InvalidAmountError(0, "Free subscriptions do not require renewal");
|
|
2084
|
+
if (subscription.amount === 0) {
|
|
2085
|
+
throw new InvalidAmountError(0, "Free subscriptions do not require renewal");
|
|
1431
2086
|
}
|
|
1432
2087
|
const provider = this.providers[gateway];
|
|
1433
2088
|
if (!provider) {
|
|
@@ -1451,36 +2106,48 @@ var MonetizationService = class {
|
|
|
1451
2106
|
const effectiveEntity = entity ?? subscription.metadata?.entity;
|
|
1452
2107
|
const effectiveMonetizationType = subscription.metadata?.monetizationType ?? MONETIZATION_TYPES.SUBSCRIPTION;
|
|
1453
2108
|
const category = resolveCategory(effectiveEntity, effectiveMonetizationType, this.config.categoryMappings);
|
|
1454
|
-
const
|
|
1455
|
-
const commissionRate = this.config
|
|
1456
|
-
const gatewayFeeRate = this.config
|
|
2109
|
+
const transactionFlow = this.config.transactionTypeMapping?.[category] ?? this.config.transactionTypeMapping?.subscription_renewal ?? this.config.transactionTypeMapping?.[effectiveMonetizationType] ?? TRANSACTION_FLOW.INFLOW;
|
|
2110
|
+
const commissionRate = getCommissionRate(this.config, category);
|
|
2111
|
+
const gatewayFeeRate = getGatewayFeeRate(this.config, gateway);
|
|
1457
2112
|
const commission = calculateCommission(subscription.amount, commissionRate, gatewayFeeRate);
|
|
2113
|
+
const feeAmount = commission?.gatewayFeeAmount || 0;
|
|
2114
|
+
const netAmount = subscription.amount - feeAmount;
|
|
1458
2115
|
const TransactionModel = this.models.Transaction;
|
|
1459
2116
|
const transaction = await TransactionModel.create({
|
|
1460
2117
|
organizationId: subscription.organizationId,
|
|
1461
2118
|
customerId: subscription.customerId,
|
|
2119
|
+
// ✅ UNIFIED: Use category as type directly
|
|
2120
|
+
type: category,
|
|
2121
|
+
// 'subscription', etc.
|
|
2122
|
+
flow: transactionFlow,
|
|
2123
|
+
// ✅ Use config-driven transaction type
|
|
2124
|
+
tags: ["recurring", "subscription", "renewal"],
|
|
2125
|
+
// ✅ UNIFIED: Amount structure
|
|
1462
2126
|
amount: subscription.amount,
|
|
1463
2127
|
currency: subscription.currency ?? "BDT",
|
|
1464
|
-
|
|
1465
|
-
|
|
2128
|
+
fee: feeAmount,
|
|
2129
|
+
tax: 0,
|
|
2130
|
+
// Tax plugin would add this
|
|
2131
|
+
net: netAmount,
|
|
1466
2132
|
method: paymentData?.method ?? "manual",
|
|
1467
2133
|
status: paymentIntent.status === "succeeded" ? "verified" : "pending",
|
|
2134
|
+
// ✅ Map 'succeeded' to valid TransactionStatusValue
|
|
1468
2135
|
gateway: {
|
|
1469
|
-
|
|
2136
|
+
provider: gateway,
|
|
1470
2137
|
sessionId: paymentIntent.sessionId,
|
|
1471
2138
|
paymentIntentId: paymentIntent.paymentIntentId,
|
|
1472
|
-
|
|
2139
|
+
chargeId: paymentIntent.id,
|
|
1473
2140
|
metadata: paymentIntent.metadata
|
|
1474
2141
|
},
|
|
1475
2142
|
paymentDetails: {
|
|
1476
2143
|
provider: gateway,
|
|
1477
2144
|
...paymentData
|
|
1478
2145
|
},
|
|
2146
|
+
// Commission (for marketplace/splits)
|
|
1479
2147
|
...commission && { commission },
|
|
1480
|
-
//
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
referenceModel: "Subscription",
|
|
2148
|
+
// ✅ UNIFIED: Source reference (renamed from reference)
|
|
2149
|
+
sourceId: subscription._id,
|
|
2150
|
+
sourceModel: "Subscription",
|
|
1484
2151
|
metadata: {
|
|
1485
2152
|
...metadata,
|
|
1486
2153
|
subscriptionId: subscription._id.toString(),
|
|
@@ -1496,10 +2163,10 @@ var MonetizationService = class {
|
|
|
1496
2163
|
subscription.renewalTransactionId = transaction._id;
|
|
1497
2164
|
subscription.renewalCount = (subscription.renewalCount ?? 0) + 1;
|
|
1498
2165
|
await subscription.save();
|
|
1499
|
-
this.
|
|
2166
|
+
this.events.emit("subscription.renewed", {
|
|
1500
2167
|
subscription,
|
|
1501
2168
|
transaction,
|
|
1502
|
-
paymentIntent,
|
|
2169
|
+
paymentIntent: paymentIntent ?? void 0,
|
|
1503
2170
|
renewalCount: subscription.renewalCount
|
|
1504
2171
|
});
|
|
1505
2172
|
return {
|
|
@@ -1516,33 +2183,56 @@ var MonetizationService = class {
|
|
|
1516
2183
|
* @returns Updated subscription
|
|
1517
2184
|
*/
|
|
1518
2185
|
async cancel(subscriptionId, options = {}) {
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
2186
|
+
return this.plugins.executeHook(
|
|
2187
|
+
"subscription.cancel.before",
|
|
2188
|
+
this.getPluginContext(),
|
|
2189
|
+
{ subscriptionId, ...options },
|
|
2190
|
+
async () => {
|
|
2191
|
+
const { immediate = false, reason = null } = options;
|
|
2192
|
+
if (!this.models.Subscription) {
|
|
2193
|
+
throw new ModelNotRegisteredError("Subscription");
|
|
2194
|
+
}
|
|
2195
|
+
const SubscriptionModel = this.models.Subscription;
|
|
2196
|
+
const subscription = await SubscriptionModel.findById(subscriptionId);
|
|
2197
|
+
if (!subscription) {
|
|
2198
|
+
throw new SubscriptionNotFoundError(subscriptionId);
|
|
2199
|
+
}
|
|
2200
|
+
const now = /* @__PURE__ */ new Date();
|
|
2201
|
+
if (immediate) {
|
|
2202
|
+
const auditEvent = SUBSCRIPTION_STATE_MACHINE.validateAndCreateAuditEvent(
|
|
2203
|
+
subscription.status,
|
|
2204
|
+
"cancelled",
|
|
2205
|
+
subscription._id.toString(),
|
|
2206
|
+
{
|
|
2207
|
+
changedBy: "system",
|
|
2208
|
+
reason: `Subscription cancelled immediately${reason ? ": " + reason : ""}`,
|
|
2209
|
+
metadata: { cancellationReason: reason, immediate: true }
|
|
2210
|
+
}
|
|
2211
|
+
);
|
|
2212
|
+
subscription.isActive = false;
|
|
2213
|
+
subscription.status = "cancelled";
|
|
2214
|
+
subscription.canceledAt = now;
|
|
2215
|
+
subscription.cancellationReason = reason;
|
|
2216
|
+
Object.assign(subscription, appendAuditEvent(subscription, auditEvent));
|
|
2217
|
+
} else {
|
|
2218
|
+
subscription.cancelAt = subscription.endDate ?? now;
|
|
2219
|
+
subscription.cancellationReason = reason;
|
|
2220
|
+
}
|
|
2221
|
+
await subscription.save();
|
|
2222
|
+
this.events.emit("subscription.cancelled", {
|
|
2223
|
+
subscription,
|
|
2224
|
+
immediate,
|
|
2225
|
+
reason: reason ?? void 0,
|
|
2226
|
+
canceledAt: immediate ? now : subscription.cancelAt
|
|
2227
|
+
});
|
|
2228
|
+
return this.plugins.executeHook(
|
|
2229
|
+
"subscription.cancel.after",
|
|
2230
|
+
this.getPluginContext(),
|
|
2231
|
+
{ subscriptionId, ...options },
|
|
2232
|
+
async () => subscription
|
|
2233
|
+
);
|
|
2234
|
+
}
|
|
2235
|
+
);
|
|
1546
2236
|
}
|
|
1547
2237
|
/**
|
|
1548
2238
|
* Pause subscription
|
|
@@ -1552,30 +2242,53 @@ var MonetizationService = class {
|
|
|
1552
2242
|
* @returns Updated subscription
|
|
1553
2243
|
*/
|
|
1554
2244
|
async pause(subscriptionId, options = {}) {
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
2245
|
+
return this.plugins.executeHook(
|
|
2246
|
+
"subscription.pause.before",
|
|
2247
|
+
this.getPluginContext(),
|
|
2248
|
+
{ subscriptionId, ...options },
|
|
2249
|
+
async () => {
|
|
2250
|
+
const { reason = null } = options;
|
|
2251
|
+
if (!this.models.Subscription) {
|
|
2252
|
+
throw new ModelNotRegisteredError("Subscription");
|
|
2253
|
+
}
|
|
2254
|
+
const SubscriptionModel = this.models.Subscription;
|
|
2255
|
+
const subscription = await SubscriptionModel.findById(subscriptionId);
|
|
2256
|
+
if (!subscription) {
|
|
2257
|
+
throw new SubscriptionNotFoundError(subscriptionId);
|
|
2258
|
+
}
|
|
2259
|
+
if (!subscription.isActive) {
|
|
2260
|
+
throw new SubscriptionNotActiveError(subscriptionId, "Only active subscriptions can be paused");
|
|
2261
|
+
}
|
|
2262
|
+
const pausedAt = /* @__PURE__ */ new Date();
|
|
2263
|
+
const auditEvent = SUBSCRIPTION_STATE_MACHINE.validateAndCreateAuditEvent(
|
|
2264
|
+
subscription.status,
|
|
2265
|
+
"paused",
|
|
2266
|
+
subscription._id.toString(),
|
|
2267
|
+
{
|
|
2268
|
+
changedBy: "system",
|
|
2269
|
+
reason: `Subscription paused${reason ? ": " + reason : ""}`,
|
|
2270
|
+
metadata: { pauseReason: reason, pausedAt }
|
|
2271
|
+
}
|
|
2272
|
+
);
|
|
2273
|
+
subscription.isActive = false;
|
|
2274
|
+
subscription.status = "paused";
|
|
2275
|
+
subscription.pausedAt = pausedAt;
|
|
2276
|
+
subscription.pauseReason = reason;
|
|
2277
|
+
Object.assign(subscription, appendAuditEvent(subscription, auditEvent));
|
|
2278
|
+
await subscription.save();
|
|
2279
|
+
this.events.emit("subscription.paused", {
|
|
2280
|
+
subscription,
|
|
2281
|
+
reason: reason ?? void 0,
|
|
2282
|
+
pausedAt
|
|
2283
|
+
});
|
|
2284
|
+
return this.plugins.executeHook(
|
|
2285
|
+
"subscription.pause.after",
|
|
2286
|
+
this.getPluginContext(),
|
|
2287
|
+
{ subscriptionId, ...options },
|
|
2288
|
+
async () => subscription
|
|
2289
|
+
);
|
|
2290
|
+
}
|
|
2291
|
+
);
|
|
1579
2292
|
}
|
|
1580
2293
|
/**
|
|
1581
2294
|
* Resume subscription
|
|
@@ -1585,42 +2298,70 @@ var MonetizationService = class {
|
|
|
1585
2298
|
* @returns Updated subscription
|
|
1586
2299
|
*/
|
|
1587
2300
|
async resume(subscriptionId, options = {}) {
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
2301
|
+
return this.plugins.executeHook(
|
|
2302
|
+
"subscription.resume.before",
|
|
2303
|
+
this.getPluginContext(),
|
|
2304
|
+
{ subscriptionId, ...options },
|
|
2305
|
+
async () => {
|
|
2306
|
+
const { extendPeriod = false } = options;
|
|
2307
|
+
if (!this.models.Subscription) {
|
|
2308
|
+
throw new ModelNotRegisteredError("Subscription");
|
|
2309
|
+
}
|
|
2310
|
+
const SubscriptionModel = this.models.Subscription;
|
|
2311
|
+
const subscription = await SubscriptionModel.findById(subscriptionId);
|
|
2312
|
+
if (!subscription) {
|
|
2313
|
+
throw new SubscriptionNotFoundError(subscriptionId);
|
|
2314
|
+
}
|
|
2315
|
+
if (!subscription.pausedAt) {
|
|
2316
|
+
throw new InvalidStateTransitionError(
|
|
2317
|
+
"resume",
|
|
2318
|
+
"paused",
|
|
2319
|
+
subscription.status,
|
|
2320
|
+
"Only paused subscriptions can be resumed"
|
|
2321
|
+
);
|
|
2322
|
+
}
|
|
2323
|
+
const now = /* @__PURE__ */ new Date();
|
|
2324
|
+
const pausedAt = new Date(subscription.pausedAt);
|
|
2325
|
+
const pauseDuration = now.getTime() - pausedAt.getTime();
|
|
2326
|
+
const auditEvent = SUBSCRIPTION_STATE_MACHINE.validateAndCreateAuditEvent(
|
|
2327
|
+
subscription.status,
|
|
2328
|
+
"active",
|
|
2329
|
+
subscription._id.toString(),
|
|
2330
|
+
{
|
|
2331
|
+
changedBy: "system",
|
|
2332
|
+
reason: "Subscription resumed from paused state",
|
|
2333
|
+
metadata: {
|
|
2334
|
+
pausedAt,
|
|
2335
|
+
pauseDuration,
|
|
2336
|
+
extendPeriod,
|
|
2337
|
+
newEndDate: extendPeriod && subscription.endDate ? new Date(new Date(subscription.endDate).getTime() + pauseDuration) : void 0
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
);
|
|
2341
|
+
subscription.isActive = true;
|
|
2342
|
+
subscription.status = "active";
|
|
2343
|
+
subscription.pausedAt = null;
|
|
2344
|
+
subscription.pauseReason = null;
|
|
2345
|
+
if (extendPeriod && subscription.endDate) {
|
|
2346
|
+
const currentEnd = new Date(subscription.endDate);
|
|
2347
|
+
subscription.endDate = new Date(currentEnd.getTime() + pauseDuration);
|
|
2348
|
+
}
|
|
2349
|
+
Object.assign(subscription, appendAuditEvent(subscription, auditEvent));
|
|
2350
|
+
await subscription.save();
|
|
2351
|
+
this.events.emit("subscription.resumed", {
|
|
2352
|
+
subscription,
|
|
2353
|
+
extendPeriod,
|
|
2354
|
+
pauseDuration,
|
|
2355
|
+
resumedAt: now
|
|
2356
|
+
});
|
|
2357
|
+
return this.plugins.executeHook(
|
|
2358
|
+
"subscription.resume.after",
|
|
2359
|
+
this.getPluginContext(),
|
|
2360
|
+
{ subscriptionId, ...options },
|
|
2361
|
+
async () => subscription
|
|
2362
|
+
);
|
|
2363
|
+
}
|
|
2364
|
+
);
|
|
1624
2365
|
}
|
|
1625
2366
|
/**
|
|
1626
2367
|
* List subscriptions with filters
|
|
@@ -1677,28 +2418,61 @@ var MonetizationService = class {
|
|
|
1677
2418
|
}
|
|
1678
2419
|
return end;
|
|
1679
2420
|
}
|
|
1680
|
-
/**
|
|
1681
|
-
* Trigger event hook (fire-and-forget, non-blocking)
|
|
1682
|
-
* @private
|
|
1683
|
-
*/
|
|
1684
|
-
_triggerHook(event, data) {
|
|
1685
|
-
triggerHook(this.hooks, event, data, this.logger);
|
|
1686
|
-
}
|
|
1687
2421
|
};
|
|
1688
|
-
|
|
1689
|
-
// src/services/payment.service.ts
|
|
1690
2422
|
var PaymentService = class {
|
|
1691
2423
|
models;
|
|
1692
2424
|
providers;
|
|
1693
2425
|
config;
|
|
1694
|
-
|
|
2426
|
+
plugins;
|
|
1695
2427
|
logger;
|
|
2428
|
+
events;
|
|
2429
|
+
retryConfig;
|
|
2430
|
+
circuitBreaker;
|
|
1696
2431
|
constructor(container) {
|
|
1697
2432
|
this.models = container.get("models");
|
|
1698
2433
|
this.providers = container.get("providers");
|
|
1699
2434
|
this.config = container.get("config");
|
|
1700
|
-
this.
|
|
2435
|
+
this.plugins = container.get("plugins");
|
|
1701
2436
|
this.logger = container.get("logger");
|
|
2437
|
+
this.events = container.get("events");
|
|
2438
|
+
this.retryConfig = container.get("retryConfig");
|
|
2439
|
+
this.circuitBreaker = container.get("circuitBreaker");
|
|
2440
|
+
}
|
|
2441
|
+
/**
|
|
2442
|
+
* Create plugin context for hook execution
|
|
2443
|
+
* @private
|
|
2444
|
+
*/
|
|
2445
|
+
getPluginContext(idempotencyKey) {
|
|
2446
|
+
return {
|
|
2447
|
+
events: this.events,
|
|
2448
|
+
logger: this.logger,
|
|
2449
|
+
storage: /* @__PURE__ */ new Map(),
|
|
2450
|
+
meta: {
|
|
2451
|
+
idempotencyKey,
|
|
2452
|
+
requestId: nanoid(),
|
|
2453
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
2454
|
+
}
|
|
2455
|
+
};
|
|
2456
|
+
}
|
|
2457
|
+
/**
|
|
2458
|
+
* Execute provider call with retry and circuit breaker protection
|
|
2459
|
+
* @private
|
|
2460
|
+
*/
|
|
2461
|
+
async executeProviderCall(operation, operationName) {
|
|
2462
|
+
const withCircuitBreaker = this.circuitBreaker ? () => this.circuitBreaker.execute(operation) : operation;
|
|
2463
|
+
if (this.retryConfig && Object.keys(this.retryConfig).length > 0) {
|
|
2464
|
+
return retry(withCircuitBreaker, {
|
|
2465
|
+
...this.retryConfig,
|
|
2466
|
+
onRetry: (error, attempt, delay) => {
|
|
2467
|
+
this.logger.warn(
|
|
2468
|
+
`[${operationName}] Retry attempt ${attempt} after ${delay}ms:`,
|
|
2469
|
+
error
|
|
2470
|
+
);
|
|
2471
|
+
this.retryConfig.onRetry?.(error, attempt, delay);
|
|
2472
|
+
}
|
|
2473
|
+
});
|
|
2474
|
+
}
|
|
2475
|
+
return withCircuitBreaker();
|
|
1702
2476
|
}
|
|
1703
2477
|
/**
|
|
1704
2478
|
* Verify a payment
|
|
@@ -1708,72 +2482,132 @@ var PaymentService = class {
|
|
|
1708
2482
|
* @returns { transaction, status }
|
|
1709
2483
|
*/
|
|
1710
2484
|
async verify(paymentIntentId, options = {}) {
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
2485
|
+
return this.plugins.executeHook(
|
|
2486
|
+
"payment.verify.before",
|
|
2487
|
+
this.getPluginContext(),
|
|
2488
|
+
{ id: paymentIntentId, ...options },
|
|
2489
|
+
async () => {
|
|
2490
|
+
const { verifiedBy = null } = options;
|
|
2491
|
+
const TransactionModel = this.models.Transaction;
|
|
2492
|
+
const transaction = await this._findTransaction(TransactionModel, paymentIntentId);
|
|
2493
|
+
if (!transaction) {
|
|
2494
|
+
throw new TransactionNotFoundError(paymentIntentId);
|
|
2495
|
+
}
|
|
2496
|
+
if (transaction.status === "verified" || transaction.status === "completed") {
|
|
2497
|
+
throw new AlreadyVerifiedError(transaction._id.toString());
|
|
2498
|
+
}
|
|
2499
|
+
const gatewayType = transaction.gateway?.type ?? "manual";
|
|
2500
|
+
const provider = this.providers[gatewayType];
|
|
2501
|
+
if (!provider) {
|
|
2502
|
+
throw new ProviderNotFoundError(gatewayType, Object.keys(this.providers));
|
|
2503
|
+
}
|
|
2504
|
+
let paymentResult = null;
|
|
2505
|
+
try {
|
|
2506
|
+
const actualIntentId = transaction.gateway?.paymentIntentId || transaction.gateway?.sessionId || paymentIntentId;
|
|
2507
|
+
paymentResult = await this.executeProviderCall(
|
|
2508
|
+
() => provider.verifyPayment(actualIntentId),
|
|
2509
|
+
`${gatewayType}.verifyPayment`
|
|
2510
|
+
);
|
|
2511
|
+
} catch (error) {
|
|
2512
|
+
this.logger.error("Payment verification failed:", error);
|
|
2513
|
+
const auditEvent2 = TRANSACTION_STATE_MACHINE.validateAndCreateAuditEvent(
|
|
2514
|
+
transaction.status,
|
|
2515
|
+
"failed",
|
|
2516
|
+
transaction._id.toString(),
|
|
2517
|
+
{
|
|
2518
|
+
changedBy: "system",
|
|
2519
|
+
reason: `Payment verification failed: ${error.message}`,
|
|
2520
|
+
metadata: { error: error.message }
|
|
2521
|
+
}
|
|
2522
|
+
);
|
|
2523
|
+
transaction.status = "failed";
|
|
2524
|
+
transaction.failureReason = error.message;
|
|
2525
|
+
Object.assign(transaction, appendAuditEvent(transaction, auditEvent2));
|
|
2526
|
+
transaction.metadata = {
|
|
2527
|
+
...transaction.metadata,
|
|
2528
|
+
verificationError: error.message,
|
|
2529
|
+
failedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2530
|
+
};
|
|
2531
|
+
await transaction.save();
|
|
2532
|
+
this.events.emit("payment.failed", {
|
|
2533
|
+
transaction,
|
|
2534
|
+
error: error.message,
|
|
2535
|
+
provider: gatewayType,
|
|
2536
|
+
paymentIntentId
|
|
2537
|
+
});
|
|
2538
|
+
throw new PaymentVerificationError(paymentIntentId, error.message);
|
|
2539
|
+
}
|
|
2540
|
+
if (paymentResult.amount && paymentResult.amount !== transaction.amount) {
|
|
2541
|
+
throw new ValidationError(
|
|
2542
|
+
`Amount mismatch: expected ${transaction.amount}, got ${paymentResult.amount}`,
|
|
2543
|
+
{ expected: transaction.amount, actual: paymentResult.amount }
|
|
2544
|
+
);
|
|
2545
|
+
}
|
|
2546
|
+
if (paymentResult.currency && paymentResult.currency.toUpperCase() !== transaction.currency.toUpperCase()) {
|
|
2547
|
+
throw new ValidationError(
|
|
2548
|
+
`Currency mismatch: expected ${transaction.currency}, got ${paymentResult.currency}`,
|
|
2549
|
+
{ expected: transaction.currency, actual: paymentResult.currency }
|
|
2550
|
+
);
|
|
2551
|
+
}
|
|
2552
|
+
const newStatus = paymentResult.status === "succeeded" ? "verified" : paymentResult.status;
|
|
2553
|
+
const auditEvent = TRANSACTION_STATE_MACHINE.validateAndCreateAuditEvent(
|
|
2554
|
+
transaction.status,
|
|
2555
|
+
newStatus,
|
|
2556
|
+
transaction._id.toString(),
|
|
2557
|
+
{
|
|
2558
|
+
changedBy: verifiedBy ?? "system",
|
|
2559
|
+
reason: `Payment verification ${paymentResult.status === "succeeded" ? "succeeded" : "resulted in status: " + newStatus}`,
|
|
2560
|
+
metadata: { paymentResult: paymentResult.metadata }
|
|
2561
|
+
}
|
|
2562
|
+
);
|
|
2563
|
+
transaction.status = newStatus;
|
|
2564
|
+
transaction.verifiedAt = paymentResult.paidAt ?? /* @__PURE__ */ new Date();
|
|
2565
|
+
transaction.verifiedBy = verifiedBy;
|
|
2566
|
+
transaction.gateway = {
|
|
2567
|
+
...transaction.gateway,
|
|
2568
|
+
type: transaction.gateway?.type ?? "manual",
|
|
2569
|
+
verificationData: paymentResult.metadata
|
|
2570
|
+
};
|
|
2571
|
+
Object.assign(transaction, appendAuditEvent(transaction, auditEvent));
|
|
2572
|
+
await transaction.save();
|
|
2573
|
+
if (newStatus === "verified") {
|
|
2574
|
+
this.events.emit("payment.verified", {
|
|
2575
|
+
transaction,
|
|
2576
|
+
paymentResult,
|
|
2577
|
+
verifiedBy: verifiedBy || void 0
|
|
2578
|
+
});
|
|
2579
|
+
} else if (newStatus === "failed") {
|
|
2580
|
+
this.events.emit("payment.failed", {
|
|
2581
|
+
transaction,
|
|
2582
|
+
error: paymentResult.metadata?.errorMessage || "Payment verification failed",
|
|
2583
|
+
provider: gatewayType,
|
|
2584
|
+
paymentIntentId: transaction.gateway?.paymentIntentId || transaction.gateway?.sessionId || paymentIntentId
|
|
2585
|
+
});
|
|
2586
|
+
} else if (newStatus === "requires_action") {
|
|
2587
|
+
this.events.emit("payment.requires_action", {
|
|
2588
|
+
transaction,
|
|
2589
|
+
paymentResult,
|
|
2590
|
+
action: paymentResult.metadata?.requiredAction
|
|
2591
|
+
});
|
|
2592
|
+
} else if (newStatus === "processing") {
|
|
2593
|
+
this.events.emit("payment.processing", {
|
|
2594
|
+
transaction,
|
|
2595
|
+
paymentResult
|
|
2596
|
+
});
|
|
2597
|
+
}
|
|
2598
|
+
const result = {
|
|
2599
|
+
transaction,
|
|
2600
|
+
paymentResult,
|
|
2601
|
+
status: transaction.status
|
|
2602
|
+
};
|
|
2603
|
+
return this.plugins.executeHook(
|
|
2604
|
+
"payment.verify.after",
|
|
2605
|
+
this.getPluginContext(),
|
|
2606
|
+
{ id: paymentIntentId, ...options },
|
|
2607
|
+
async () => result
|
|
2608
|
+
);
|
|
2609
|
+
}
|
|
2610
|
+
);
|
|
1777
2611
|
}
|
|
1778
2612
|
/**
|
|
1779
2613
|
* Get payment status
|
|
@@ -1794,7 +2628,11 @@ var PaymentService = class {
|
|
|
1794
2628
|
}
|
|
1795
2629
|
let paymentResult = null;
|
|
1796
2630
|
try {
|
|
1797
|
-
|
|
2631
|
+
const actualIntentId = transaction.gateway?.paymentIntentId || transaction.gateway?.sessionId || paymentIntentId;
|
|
2632
|
+
paymentResult = await this.executeProviderCall(
|
|
2633
|
+
() => provider.getStatus(actualIntentId),
|
|
2634
|
+
`${gatewayType}.getStatus`
|
|
2635
|
+
);
|
|
1798
2636
|
} catch (error) {
|
|
1799
2637
|
this.logger.warn("Failed to get payment status from provider:", error);
|
|
1800
2638
|
return {
|
|
@@ -1819,99 +2657,163 @@ var PaymentService = class {
|
|
|
1819
2657
|
* @returns { transaction, refundResult }
|
|
1820
2658
|
*/
|
|
1821
2659
|
async refund(paymentId, amount = null, options = {}) {
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
2660
|
+
return this.plugins.executeHook(
|
|
2661
|
+
"payment.refund.before",
|
|
2662
|
+
this.getPluginContext(),
|
|
2663
|
+
{ transactionId: paymentId, amount, ...options },
|
|
2664
|
+
async () => {
|
|
2665
|
+
const { reason = null } = options;
|
|
2666
|
+
const TransactionModel = this.models.Transaction;
|
|
2667
|
+
const transaction = await this._findTransaction(TransactionModel, paymentId);
|
|
2668
|
+
if (!transaction) {
|
|
2669
|
+
throw new TransactionNotFoundError(paymentId);
|
|
2670
|
+
}
|
|
2671
|
+
if (transaction.status !== "verified" && transaction.status !== "completed" && transaction.status !== "partially_refunded") {
|
|
2672
|
+
throw new InvalidStateTransitionError(
|
|
2673
|
+
"transaction",
|
|
2674
|
+
transaction._id.toString(),
|
|
2675
|
+
transaction.status,
|
|
2676
|
+
"verified, completed, or partially_refunded"
|
|
2677
|
+
);
|
|
2678
|
+
}
|
|
2679
|
+
const gatewayType = transaction.gateway?.type ?? "manual";
|
|
2680
|
+
const provider = this.providers[gatewayType];
|
|
2681
|
+
if (!provider) {
|
|
2682
|
+
throw new ProviderNotFoundError(gatewayType, Object.keys(this.providers));
|
|
2683
|
+
}
|
|
2684
|
+
const capabilities = provider.getCapabilities();
|
|
2685
|
+
if (!capabilities.supportsRefunds) {
|
|
2686
|
+
throw new RefundNotSupportedError(gatewayType);
|
|
2687
|
+
}
|
|
2688
|
+
const refundedSoFar = transaction.refundedAmount ?? 0;
|
|
2689
|
+
const refundableAmount = transaction.amount - refundedSoFar;
|
|
2690
|
+
const refundAmount = amount ?? refundableAmount;
|
|
2691
|
+
if (refundAmount <= 0) {
|
|
2692
|
+
throw new ValidationError(`Refund amount must be positive, got ${refundAmount}`);
|
|
2693
|
+
}
|
|
2694
|
+
if (refundAmount > refundableAmount) {
|
|
2695
|
+
throw new ValidationError(
|
|
2696
|
+
`Refund amount (${refundAmount}) exceeds refundable balance (${refundableAmount})`,
|
|
2697
|
+
{ refundAmount, refundableAmount, alreadyRefunded: refundedSoFar }
|
|
2698
|
+
);
|
|
2699
|
+
}
|
|
2700
|
+
let refundResult;
|
|
2701
|
+
try {
|
|
2702
|
+
const actualIntentId = transaction.gateway?.paymentIntentId || transaction.gateway?.sessionId || paymentId;
|
|
2703
|
+
refundResult = await this.executeProviderCall(
|
|
2704
|
+
() => provider.refund(actualIntentId, refundAmount, { reason: reason ?? void 0 }),
|
|
2705
|
+
`${gatewayType}.refund`
|
|
2706
|
+
);
|
|
2707
|
+
} catch (error) {
|
|
2708
|
+
this.logger.error("Refund failed:", error);
|
|
2709
|
+
throw new RefundError(paymentId, error.message);
|
|
2710
|
+
}
|
|
2711
|
+
const refundFlow = this.config.transactionTypeMapping?.refund ?? TRANSACTION_FLOW.OUTFLOW;
|
|
2712
|
+
const refundCommission = transaction.commission ? reverseCommission(transaction.commission, transaction.amount, refundAmount) : null;
|
|
2713
|
+
let refundTaxAmount = 0;
|
|
2714
|
+
if (transaction.tax && transaction.tax > 0) {
|
|
2715
|
+
if (transaction.amount > 0) {
|
|
2716
|
+
const ratio = refundAmount / transaction.amount;
|
|
2717
|
+
refundTaxAmount = Math.round(transaction.tax * ratio);
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
const refundFeeAmount = refundCommission?.gatewayFeeAmount || 0;
|
|
2721
|
+
const refundNetAmount = refundAmount - refundFeeAmount - refundTaxAmount;
|
|
2722
|
+
const refundTransaction = await TransactionModel.create({
|
|
2723
|
+
organizationId: transaction.organizationId,
|
|
2724
|
+
customerId: transaction.customerId,
|
|
2725
|
+
// ✅ UNIFIED: Type = category (semantic), Flow = direction (from config)
|
|
2726
|
+
type: "refund",
|
|
2727
|
+
// Category: this is a refund transaction
|
|
2728
|
+
flow: refundFlow,
|
|
2729
|
+
// Direction: income or expense (from config)
|
|
2730
|
+
tags: ["refund"],
|
|
2731
|
+
// ✅ UNIFIED: Amount structure
|
|
2732
|
+
amount: refundAmount,
|
|
2733
|
+
currency: transaction.currency,
|
|
2734
|
+
fee: refundFeeAmount,
|
|
2735
|
+
tax: refundTaxAmount,
|
|
2736
|
+
net: refundNetAmount,
|
|
2737
|
+
// Tax details (if tax existed on original)
|
|
2738
|
+
...transaction.taxDetails && {
|
|
2739
|
+
taxDetails: transaction.taxDetails
|
|
2740
|
+
},
|
|
2741
|
+
method: transaction.method ?? "manual",
|
|
2742
|
+
status: "completed",
|
|
2743
|
+
gateway: {
|
|
2744
|
+
provider: transaction.gateway?.provider ?? "manual",
|
|
2745
|
+
paymentIntentId: refundResult.id,
|
|
2746
|
+
chargeId: refundResult.id
|
|
2747
|
+
},
|
|
2748
|
+
paymentDetails: transaction.paymentDetails,
|
|
2749
|
+
// Reversed commission
|
|
2750
|
+
...refundCommission && { commission: refundCommission },
|
|
2751
|
+
// ✅ UNIFIED: Source reference + related transaction
|
|
2752
|
+
...transaction.sourceId && { sourceId: transaction.sourceId },
|
|
2753
|
+
...transaction.sourceModel && { sourceModel: transaction.sourceModel },
|
|
2754
|
+
relatedTransactionId: transaction._id,
|
|
2755
|
+
// Link to original transaction
|
|
2756
|
+
metadata: {
|
|
2757
|
+
...transaction.metadata,
|
|
2758
|
+
isRefund: true,
|
|
2759
|
+
originalTransactionId: transaction._id.toString(),
|
|
2760
|
+
refundReason: reason,
|
|
2761
|
+
refundResult: refundResult.metadata
|
|
2762
|
+
},
|
|
2763
|
+
idempotencyKey: `refund_${transaction._id}_${Date.now()}`
|
|
2764
|
+
});
|
|
2765
|
+
const isPartialRefund = refundAmount < refundableAmount;
|
|
2766
|
+
const refundStatus = isPartialRefund ? "partially_refunded" : "refunded";
|
|
2767
|
+
const auditEvent = TRANSACTION_STATE_MACHINE.validateAndCreateAuditEvent(
|
|
2768
|
+
transaction.status,
|
|
2769
|
+
refundStatus,
|
|
2770
|
+
transaction._id.toString(),
|
|
2771
|
+
{
|
|
2772
|
+
changedBy: "system",
|
|
2773
|
+
reason: `Refund processed: ${isPartialRefund ? "partial" : "full"} refund of ${refundAmount}${reason ? " - " + reason : ""}`,
|
|
2774
|
+
metadata: {
|
|
2775
|
+
refundAmount,
|
|
2776
|
+
isPartialRefund,
|
|
2777
|
+
refundTransactionId: refundTransaction._id.toString()
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
);
|
|
2781
|
+
transaction.status = refundStatus;
|
|
2782
|
+
transaction.refundedAmount = (transaction.refundedAmount ?? 0) + refundAmount;
|
|
2783
|
+
transaction.refundedAt = refundResult.refundedAt ?? /* @__PURE__ */ new Date();
|
|
2784
|
+
Object.assign(transaction, appendAuditEvent(transaction, auditEvent));
|
|
2785
|
+
transaction.metadata = {
|
|
2786
|
+
...transaction.metadata,
|
|
2787
|
+
refundTransactionId: refundTransaction._id.toString(),
|
|
2788
|
+
refundReason: reason
|
|
2789
|
+
};
|
|
2790
|
+
await transaction.save();
|
|
2791
|
+
this.events.emit("payment.refunded", {
|
|
2792
|
+
transaction,
|
|
2793
|
+
refundTransaction,
|
|
2794
|
+
refundResult: {
|
|
2795
|
+
...refundResult,
|
|
2796
|
+
currency: refundResult.currency ?? "USD",
|
|
2797
|
+
metadata: refundResult.metadata ?? {}
|
|
2798
|
+
},
|
|
2799
|
+
refundAmount,
|
|
2800
|
+
reason: reason ?? void 0,
|
|
2801
|
+
isPartialRefund
|
|
2802
|
+
});
|
|
2803
|
+
const result = {
|
|
2804
|
+
transaction,
|
|
2805
|
+
refundTransaction,
|
|
2806
|
+
refundResult,
|
|
2807
|
+
status: transaction.status
|
|
2808
|
+
};
|
|
2809
|
+
return this.plugins.executeHook(
|
|
2810
|
+
"payment.refund.after",
|
|
2811
|
+
this.getPluginContext(),
|
|
2812
|
+
{ transactionId: paymentId, amount, ...options },
|
|
2813
|
+
async () => result
|
|
2814
|
+
);
|
|
2815
|
+
}
|
|
2816
|
+
);
|
|
1915
2817
|
}
|
|
1916
2818
|
/**
|
|
1917
2819
|
* Handle webhook from payment provider
|
|
@@ -1932,7 +2834,10 @@ var PaymentService = class {
|
|
|
1932
2834
|
}
|
|
1933
2835
|
let webhookEvent;
|
|
1934
2836
|
try {
|
|
1935
|
-
webhookEvent = await
|
|
2837
|
+
webhookEvent = await this.executeProviderCall(
|
|
2838
|
+
() => provider.handleWebhook(payload, headers),
|
|
2839
|
+
`${providerName}.handleWebhook`
|
|
2840
|
+
);
|
|
1936
2841
|
} catch (error) {
|
|
1937
2842
|
this.logger.error("Webhook processing failed:", error);
|
|
1938
2843
|
throw new ProviderError(
|
|
@@ -2002,19 +2907,46 @@ var PaymentService = class {
|
|
|
2002
2907
|
processedAt: /* @__PURE__ */ new Date(),
|
|
2003
2908
|
data: webhookEvent.data
|
|
2004
2909
|
};
|
|
2910
|
+
let newStatus = transaction.status;
|
|
2005
2911
|
if (webhookEvent.type === "payment.succeeded") {
|
|
2006
|
-
|
|
2007
|
-
transaction.verifiedAt = webhookEvent.createdAt;
|
|
2912
|
+
newStatus = "verified";
|
|
2008
2913
|
} else if (webhookEvent.type === "payment.failed") {
|
|
2009
|
-
|
|
2914
|
+
newStatus = "failed";
|
|
2010
2915
|
} else if (webhookEvent.type === "refund.succeeded") {
|
|
2011
|
-
|
|
2012
|
-
|
|
2916
|
+
newStatus = "refunded";
|
|
2917
|
+
}
|
|
2918
|
+
if (newStatus !== transaction.status) {
|
|
2919
|
+
const auditEvent = TRANSACTION_STATE_MACHINE.validateAndCreateAuditEvent(
|
|
2920
|
+
transaction.status,
|
|
2921
|
+
newStatus,
|
|
2922
|
+
transaction._id.toString(),
|
|
2923
|
+
{
|
|
2924
|
+
changedBy: "webhook",
|
|
2925
|
+
reason: `Webhook event: ${webhookEvent.type}`,
|
|
2926
|
+
metadata: {
|
|
2927
|
+
webhookId: webhookEvent.id,
|
|
2928
|
+
webhookType: webhookEvent.type,
|
|
2929
|
+
webhookData: webhookEvent.data
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
);
|
|
2933
|
+
transaction.status = newStatus;
|
|
2934
|
+
if (newStatus === "verified") {
|
|
2935
|
+
transaction.verifiedAt = webhookEvent.createdAt;
|
|
2936
|
+
} else if (newStatus === "refunded") {
|
|
2937
|
+
transaction.refundedAt = webhookEvent.createdAt;
|
|
2938
|
+
} else if (newStatus === "failed") {
|
|
2939
|
+
transaction.failedAt = webhookEvent.createdAt;
|
|
2940
|
+
}
|
|
2941
|
+
Object.assign(transaction, appendAuditEvent(transaction, auditEvent));
|
|
2013
2942
|
}
|
|
2014
2943
|
await transaction.save();
|
|
2015
|
-
this.
|
|
2944
|
+
this.events.emit("webhook.processed", {
|
|
2945
|
+
webhookType: webhookEvent.type,
|
|
2946
|
+
provider: webhookEvent.provider,
|
|
2016
2947
|
event: webhookEvent,
|
|
2017
|
-
transaction
|
|
2948
|
+
transaction,
|
|
2949
|
+
processedAt: /* @__PURE__ */ new Date()
|
|
2018
2950
|
});
|
|
2019
2951
|
return {
|
|
2020
2952
|
event: webhookEvent,
|
|
@@ -2062,13 +2994,6 @@ var PaymentService = class {
|
|
|
2062
2994
|
}
|
|
2063
2995
|
return provider;
|
|
2064
2996
|
}
|
|
2065
|
-
/**
|
|
2066
|
-
* Trigger event hook (fire-and-forget, non-blocking)
|
|
2067
|
-
* @private
|
|
2068
|
-
*/
|
|
2069
|
-
_triggerHook(event, data) {
|
|
2070
|
-
triggerHook(this.hooks, event, data, this.logger);
|
|
2071
|
-
}
|
|
2072
2997
|
/**
|
|
2073
2998
|
* Find transaction by sessionId, paymentIntentId, or transaction ID
|
|
2074
2999
|
* @private
|
|
@@ -2088,17 +3013,32 @@ var PaymentService = class {
|
|
|
2088
3013
|
return transaction;
|
|
2089
3014
|
}
|
|
2090
3015
|
};
|
|
2091
|
-
|
|
2092
|
-
// src/services/transaction.service.ts
|
|
2093
3016
|
var TransactionService = class {
|
|
2094
3017
|
models;
|
|
2095
|
-
|
|
3018
|
+
plugins;
|
|
3019
|
+
events;
|
|
2096
3020
|
logger;
|
|
2097
3021
|
constructor(container) {
|
|
2098
3022
|
this.models = container.get("models");
|
|
2099
|
-
this.
|
|
3023
|
+
this.plugins = container.get("plugins");
|
|
3024
|
+
this.events = container.get("events");
|
|
2100
3025
|
this.logger = container.get("logger");
|
|
2101
3026
|
}
|
|
3027
|
+
/**
|
|
3028
|
+
* Create plugin context for hook execution
|
|
3029
|
+
* @private
|
|
3030
|
+
*/
|
|
3031
|
+
getPluginContext() {
|
|
3032
|
+
return {
|
|
3033
|
+
events: this.events,
|
|
3034
|
+
logger: this.logger,
|
|
3035
|
+
storage: /* @__PURE__ */ new Map(),
|
|
3036
|
+
meta: {
|
|
3037
|
+
requestId: nanoid(),
|
|
3038
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
3039
|
+
}
|
|
3040
|
+
};
|
|
3041
|
+
}
|
|
2102
3042
|
/**
|
|
2103
3043
|
* Get transaction by ID
|
|
2104
3044
|
*
|
|
@@ -2155,102 +3095,46 @@ var TransactionService = class {
|
|
|
2155
3095
|
* @returns Updated transaction
|
|
2156
3096
|
*/
|
|
2157
3097
|
async update(transactionId, updates) {
|
|
2158
|
-
const
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
3098
|
+
const hookInput = { transactionId, updates };
|
|
3099
|
+
return this.plugins.executeHook(
|
|
3100
|
+
"transaction.update.before",
|
|
3101
|
+
this.getPluginContext(),
|
|
3102
|
+
hookInput,
|
|
3103
|
+
async () => {
|
|
3104
|
+
const TransactionModel = this.models.Transaction;
|
|
3105
|
+
const effectiveUpdates = hookInput.updates;
|
|
3106
|
+
const model = TransactionModel;
|
|
3107
|
+
let transaction;
|
|
3108
|
+
if (typeof model.update === "function") {
|
|
3109
|
+
transaction = await model.update(transactionId, effectiveUpdates);
|
|
3110
|
+
} else if (typeof model.findByIdAndUpdate === "function") {
|
|
3111
|
+
transaction = await model.findByIdAndUpdate(
|
|
3112
|
+
transactionId,
|
|
3113
|
+
{ $set: effectiveUpdates },
|
|
3114
|
+
{ new: true }
|
|
3115
|
+
);
|
|
3116
|
+
} else {
|
|
3117
|
+
throw new Error("Transaction model does not support update operations");
|
|
3118
|
+
}
|
|
3119
|
+
if (!transaction) {
|
|
3120
|
+
throw new TransactionNotFoundError(transactionId);
|
|
3121
|
+
}
|
|
3122
|
+
this.events.emit("transaction.updated", {
|
|
3123
|
+
transaction,
|
|
3124
|
+
updates: effectiveUpdates
|
|
3125
|
+
});
|
|
3126
|
+
return this.plugins.executeHook(
|
|
3127
|
+
"transaction.update.after",
|
|
3128
|
+
this.getPluginContext(),
|
|
3129
|
+
{ transactionId, updates: effectiveUpdates },
|
|
3130
|
+
async () => transaction
|
|
3131
|
+
);
|
|
3132
|
+
}
|
|
3133
|
+
);
|
|
2187
3134
|
}
|
|
2188
3135
|
};
|
|
2189
3136
|
|
|
2190
|
-
// src/
|
|
2191
|
-
var HOLD_STATUS = {
|
|
2192
|
-
PENDING: "pending",
|
|
2193
|
-
HELD: "held",
|
|
2194
|
-
RELEASED: "released",
|
|
2195
|
-
CANCELLED: "cancelled",
|
|
2196
|
-
EXPIRED: "expired",
|
|
2197
|
-
PARTIALLY_RELEASED: "partially_released"
|
|
2198
|
-
};
|
|
2199
|
-
var HOLD_STATUS_VALUES = Object.values(HOLD_STATUS);
|
|
2200
|
-
var RELEASE_REASON = {
|
|
2201
|
-
PAYMENT_VERIFIED: "payment_verified",
|
|
2202
|
-
MANUAL_RELEASE: "manual_release",
|
|
2203
|
-
AUTO_RELEASE: "auto_release",
|
|
2204
|
-
DISPUTE_RESOLVED: "dispute_resolved"
|
|
2205
|
-
};
|
|
2206
|
-
var RELEASE_REASON_VALUES = Object.values(
|
|
2207
|
-
RELEASE_REASON
|
|
2208
|
-
);
|
|
2209
|
-
var HOLD_REASON = {
|
|
2210
|
-
PAYMENT_VERIFICATION: "payment_verification",
|
|
2211
|
-
FRAUD_CHECK: "fraud_check",
|
|
2212
|
-
MANUAL_REVIEW: "manual_review",
|
|
2213
|
-
DISPUTE: "dispute",
|
|
2214
|
-
COMPLIANCE: "compliance"
|
|
2215
|
-
};
|
|
2216
|
-
var HOLD_REASON_VALUES = Object.values(HOLD_REASON);
|
|
2217
|
-
new Set(HOLD_STATUS_VALUES);
|
|
2218
|
-
new Set(RELEASE_REASON_VALUES);
|
|
2219
|
-
new Set(HOLD_REASON_VALUES);
|
|
2220
|
-
|
|
2221
|
-
// src/enums/split.enums.ts
|
|
2222
|
-
var SPLIT_TYPE = {
|
|
2223
|
-
PLATFORM_COMMISSION: "platform_commission",
|
|
2224
|
-
AFFILIATE_COMMISSION: "affiliate_commission",
|
|
2225
|
-
REFERRAL_COMMISSION: "referral_commission",
|
|
2226
|
-
PARTNER_COMMISSION: "partner_commission",
|
|
2227
|
-
CUSTOM: "custom"
|
|
2228
|
-
};
|
|
2229
|
-
var SPLIT_TYPE_VALUES = Object.values(SPLIT_TYPE);
|
|
2230
|
-
var SPLIT_STATUS = {
|
|
2231
|
-
PENDING: "pending",
|
|
2232
|
-
DUE: "due",
|
|
2233
|
-
PAID: "paid",
|
|
2234
|
-
WAIVED: "waived",
|
|
2235
|
-
CANCELLED: "cancelled"
|
|
2236
|
-
};
|
|
2237
|
-
var SPLIT_STATUS_VALUES = Object.values(
|
|
2238
|
-
SPLIT_STATUS
|
|
2239
|
-
);
|
|
2240
|
-
var PAYOUT_METHOD = {
|
|
2241
|
-
BANK_TRANSFER: "bank_transfer",
|
|
2242
|
-
MOBILE_WALLET: "mobile_wallet",
|
|
2243
|
-
PLATFORM_BALANCE: "platform_balance",
|
|
2244
|
-
CRYPTO: "crypto",
|
|
2245
|
-
CHECK: "check",
|
|
2246
|
-
MANUAL: "manual"
|
|
2247
|
-
};
|
|
2248
|
-
var PAYOUT_METHOD_VALUES = Object.values(PAYOUT_METHOD);
|
|
2249
|
-
new Set(SPLIT_TYPE_VALUES);
|
|
2250
|
-
new Set(SPLIT_STATUS_VALUES);
|
|
2251
|
-
new Set(PAYOUT_METHOD_VALUES);
|
|
2252
|
-
|
|
2253
|
-
// src/utils/commission-split.ts
|
|
3137
|
+
// src/shared/utils/calculators/commission-split.ts
|
|
2254
3138
|
function calculateSplits(amount, splitRules = [], gatewayFeeRate = 0) {
|
|
2255
3139
|
if (!splitRules || splitRules.length === 0) {
|
|
2256
3140
|
return [];
|
|
@@ -2269,9 +3153,9 @@ function calculateSplits(amount, splitRules = [], gatewayFeeRate = 0) {
|
|
|
2269
3153
|
if (rule.rate < 0 || rule.rate > 1) {
|
|
2270
3154
|
throw new Error(`Split rate must be between 0 and 1 for split ${index}`);
|
|
2271
3155
|
}
|
|
2272
|
-
const grossAmount = Math.round(amount * rule.rate
|
|
2273
|
-
const gatewayFeeAmount = index === 0 && gatewayFeeRate > 0 ? Math.round(amount * gatewayFeeRate
|
|
2274
|
-
const netAmount = Math.max(0,
|
|
3156
|
+
const grossAmount = Math.round(amount * rule.rate);
|
|
3157
|
+
const gatewayFeeAmount = index === 0 && gatewayFeeRate > 0 ? Math.round(amount * gatewayFeeRate) : 0;
|
|
3158
|
+
const netAmount = Math.max(0, grossAmount - gatewayFeeAmount);
|
|
2275
3159
|
return {
|
|
2276
3160
|
type: rule.type ?? SPLIT_TYPE.CUSTOM,
|
|
2277
3161
|
recipientId: rule.recipientId,
|
|
@@ -2289,57 +3173,92 @@ function calculateSplits(amount, splitRules = [], gatewayFeeRate = 0) {
|
|
|
2289
3173
|
}
|
|
2290
3174
|
function calculateOrganizationPayout(amount, splits = []) {
|
|
2291
3175
|
const totalSplitAmount = splits.reduce((sum, split) => sum + split.grossAmount, 0);
|
|
2292
|
-
return Math.max(0,
|
|
3176
|
+
return Math.max(0, amount - totalSplitAmount);
|
|
2293
3177
|
}
|
|
2294
3178
|
|
|
2295
|
-
// src/services/escrow.service.ts
|
|
3179
|
+
// src/application/services/escrow.service.ts
|
|
2296
3180
|
var EscrowService = class {
|
|
2297
3181
|
models;
|
|
2298
|
-
|
|
3182
|
+
plugins;
|
|
2299
3183
|
logger;
|
|
3184
|
+
events;
|
|
2300
3185
|
constructor(container) {
|
|
2301
3186
|
this.models = container.get("models");
|
|
2302
|
-
this.
|
|
3187
|
+
this.plugins = container.get("plugins");
|
|
2303
3188
|
this.logger = container.get("logger");
|
|
3189
|
+
this.events = container.get("events");
|
|
2304
3190
|
}
|
|
2305
3191
|
/**
|
|
2306
|
-
*
|
|
2307
|
-
*
|
|
2308
|
-
* @param transactionId - Transaction to hold
|
|
2309
|
-
* @param options - Hold options
|
|
2310
|
-
* @returns Updated transaction
|
|
3192
|
+
* Create plugin context for hook execution
|
|
3193
|
+
* @private
|
|
2311
3194
|
*/
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
throw new TransactionNotFoundError(transactionId);
|
|
2322
|
-
}
|
|
2323
|
-
if (transaction.status !== TRANSACTION_STATUS.VERIFIED) {
|
|
2324
|
-
throw new Error(`Cannot hold transaction with status: ${transaction.status}. Must be verified.`);
|
|
2325
|
-
}
|
|
2326
|
-
transaction.hold = {
|
|
2327
|
-
status: HOLD_STATUS.HELD,
|
|
2328
|
-
heldAmount: transaction.amount,
|
|
2329
|
-
releasedAmount: 0,
|
|
2330
|
-
reason,
|
|
2331
|
-
heldAt: /* @__PURE__ */ new Date(),
|
|
2332
|
-
...holdUntil && { holdUntil },
|
|
2333
|
-
releases: [],
|
|
2334
|
-
metadata
|
|
3195
|
+
getPluginContext() {
|
|
3196
|
+
return {
|
|
3197
|
+
events: this.events,
|
|
3198
|
+
logger: this.logger,
|
|
3199
|
+
storage: /* @__PURE__ */ new Map(),
|
|
3200
|
+
meta: {
|
|
3201
|
+
requestId: nanoid(),
|
|
3202
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
3203
|
+
}
|
|
2335
3204
|
};
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
3205
|
+
}
|
|
3206
|
+
/**
|
|
3207
|
+
* Hold funds in escrow
|
|
3208
|
+
*
|
|
3209
|
+
* @param transactionId - Transaction to hold
|
|
3210
|
+
* @param options - Hold options
|
|
3211
|
+
* @returns Updated transaction
|
|
3212
|
+
*/
|
|
3213
|
+
async hold(transactionId, options = {}) {
|
|
3214
|
+
return this.plugins.executeHook(
|
|
3215
|
+
"escrow.hold.before",
|
|
3216
|
+
this.getPluginContext(),
|
|
3217
|
+
{ transactionId, ...options },
|
|
3218
|
+
async () => {
|
|
3219
|
+
const {
|
|
3220
|
+
reason = HOLD_REASON.PAYMENT_VERIFICATION,
|
|
3221
|
+
holdUntil = null,
|
|
3222
|
+
metadata = {}
|
|
3223
|
+
} = options;
|
|
3224
|
+
const TransactionModel = this.models.Transaction;
|
|
3225
|
+
const transaction = await TransactionModel.findById(transactionId);
|
|
3226
|
+
if (!transaction) {
|
|
3227
|
+
throw new TransactionNotFoundError(transactionId);
|
|
3228
|
+
}
|
|
3229
|
+
if (transaction.status !== TRANSACTION_STATUS.VERIFIED) {
|
|
3230
|
+
throw new InvalidStateTransitionError(
|
|
3231
|
+
"transaction",
|
|
3232
|
+
transaction._id.toString(),
|
|
3233
|
+
transaction.status,
|
|
3234
|
+
TRANSACTION_STATUS.VERIFIED
|
|
3235
|
+
);
|
|
3236
|
+
}
|
|
3237
|
+
const heldAmount = transaction.amount;
|
|
3238
|
+
transaction.hold = {
|
|
3239
|
+
status: HOLD_STATUS.HELD,
|
|
3240
|
+
heldAmount,
|
|
3241
|
+
releasedAmount: 0,
|
|
3242
|
+
reason,
|
|
3243
|
+
heldAt: /* @__PURE__ */ new Date(),
|
|
3244
|
+
...holdUntil && { holdUntil },
|
|
3245
|
+
releases: [],
|
|
3246
|
+
metadata
|
|
3247
|
+
};
|
|
3248
|
+
await transaction.save();
|
|
3249
|
+
this.events.emit("escrow.held", {
|
|
3250
|
+
transaction,
|
|
3251
|
+
heldAmount,
|
|
3252
|
+
reason
|
|
3253
|
+
});
|
|
3254
|
+
return this.plugins.executeHook(
|
|
3255
|
+
"escrow.hold.after",
|
|
3256
|
+
this.getPluginContext(),
|
|
3257
|
+
{ transactionId, ...options },
|
|
3258
|
+
async () => transaction
|
|
3259
|
+
);
|
|
3260
|
+
}
|
|
3261
|
+
);
|
|
2343
3262
|
}
|
|
2344
3263
|
/**
|
|
2345
3264
|
* Release funds from escrow to recipient
|
|
@@ -2349,93 +3268,185 @@ var EscrowService = class {
|
|
|
2349
3268
|
* @returns { transaction, releaseTransaction }
|
|
2350
3269
|
*/
|
|
2351
3270
|
async release(transactionId, options) {
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
3271
|
+
return this.plugins.executeHook(
|
|
3272
|
+
"escrow.release.before",
|
|
3273
|
+
this.getPluginContext(),
|
|
3274
|
+
{ transactionId, ...options },
|
|
3275
|
+
async () => {
|
|
3276
|
+
const {
|
|
3277
|
+
amount = null,
|
|
3278
|
+
recipientId,
|
|
3279
|
+
recipientType = "organization",
|
|
3280
|
+
reason = RELEASE_REASON.PAYMENT_VERIFIED,
|
|
3281
|
+
releasedBy = null,
|
|
3282
|
+
createTransaction = true,
|
|
3283
|
+
metadata = {}
|
|
3284
|
+
} = options;
|
|
3285
|
+
const TransactionModel = this.models.Transaction;
|
|
3286
|
+
const transaction = await TransactionModel.findById(transactionId);
|
|
3287
|
+
if (!transaction) {
|
|
3288
|
+
throw new TransactionNotFoundError(transactionId);
|
|
3289
|
+
}
|
|
3290
|
+
if (!transaction.hold || transaction.hold.status !== HOLD_STATUS.HELD) {
|
|
3291
|
+
throw new InvalidStateTransitionError(
|
|
3292
|
+
"escrow_hold",
|
|
3293
|
+
transaction._id.toString(),
|
|
3294
|
+
transaction.hold?.status ?? "none",
|
|
3295
|
+
HOLD_STATUS.HELD
|
|
3296
|
+
);
|
|
3297
|
+
}
|
|
3298
|
+
if (!recipientId) {
|
|
3299
|
+
throw new ValidationError("recipientId is required for release", { transactionId });
|
|
3300
|
+
}
|
|
3301
|
+
const releaseAmount = amount ?? transaction.hold.heldAmount - transaction.hold.releasedAmount;
|
|
3302
|
+
const availableAmount = transaction.hold.heldAmount - transaction.hold.releasedAmount;
|
|
3303
|
+
if (releaseAmount > availableAmount) {
|
|
3304
|
+
throw new ValidationError(
|
|
3305
|
+
`Release amount (${releaseAmount}) exceeds available held amount (${availableAmount})`,
|
|
3306
|
+
{ releaseAmount, availableAmount, transactionId }
|
|
3307
|
+
);
|
|
3308
|
+
}
|
|
3309
|
+
const releaseRecord = {
|
|
3310
|
+
amount: releaseAmount,
|
|
3311
|
+
recipientId,
|
|
3312
|
+
recipientType,
|
|
3313
|
+
releasedAt: /* @__PURE__ */ new Date(),
|
|
3314
|
+
releasedBy,
|
|
3315
|
+
reason,
|
|
3316
|
+
metadata
|
|
3317
|
+
};
|
|
3318
|
+
transaction.hold.releases.push(releaseRecord);
|
|
3319
|
+
transaction.hold.releasedAmount += releaseAmount;
|
|
3320
|
+
const isFullRelease = transaction.hold.releasedAmount >= transaction.hold.heldAmount;
|
|
3321
|
+
const isPartialRelease = transaction.hold.releasedAmount > 0 && transaction.hold.releasedAmount < transaction.hold.heldAmount;
|
|
3322
|
+
if (isFullRelease) {
|
|
3323
|
+
const holdAuditEvent = HOLD_STATE_MACHINE.validateAndCreateAuditEvent(
|
|
3324
|
+
transaction.hold.status,
|
|
3325
|
+
HOLD_STATUS.RELEASED,
|
|
3326
|
+
transaction._id.toString(),
|
|
3327
|
+
{
|
|
3328
|
+
changedBy: releasedBy ?? "system",
|
|
3329
|
+
reason: `Escrow hold fully released: ${releaseAmount} to ${recipientId}${reason ? " - " + reason : ""}`,
|
|
3330
|
+
metadata: { releaseAmount, recipientId, releaseReason: reason }
|
|
3331
|
+
}
|
|
3332
|
+
);
|
|
3333
|
+
transaction.hold.status = HOLD_STATUS.RELEASED;
|
|
3334
|
+
transaction.hold.releasedAt = /* @__PURE__ */ new Date();
|
|
3335
|
+
const transactionAuditEvent = TRANSACTION_STATE_MACHINE.validateAndCreateAuditEvent(
|
|
3336
|
+
transaction.status,
|
|
3337
|
+
TRANSACTION_STATUS.COMPLETED,
|
|
3338
|
+
transaction._id.toString(),
|
|
3339
|
+
{
|
|
3340
|
+
changedBy: releasedBy ?? "system",
|
|
3341
|
+
reason: `Transaction completed after full escrow release`,
|
|
3342
|
+
metadata: { releaseAmount, recipientId }
|
|
3343
|
+
}
|
|
3344
|
+
);
|
|
3345
|
+
transaction.status = TRANSACTION_STATUS.COMPLETED;
|
|
3346
|
+
Object.assign(transaction, appendAuditEvent(transaction, holdAuditEvent));
|
|
3347
|
+
Object.assign(transaction, appendAuditEvent(transaction, transactionAuditEvent));
|
|
3348
|
+
} else if (isPartialRelease) {
|
|
3349
|
+
const auditEvent = HOLD_STATE_MACHINE.validateAndCreateAuditEvent(
|
|
3350
|
+
transaction.hold.status,
|
|
3351
|
+
HOLD_STATUS.PARTIALLY_RELEASED,
|
|
3352
|
+
transaction._id.toString(),
|
|
3353
|
+
{
|
|
3354
|
+
changedBy: releasedBy ?? "system",
|
|
3355
|
+
reason: `Partial escrow release: ${releaseAmount} of ${transaction.hold.heldAmount} to ${recipientId}${reason ? " - " + reason : ""}`,
|
|
3356
|
+
metadata: {
|
|
3357
|
+
releaseAmount,
|
|
3358
|
+
recipientId,
|
|
3359
|
+
releaseReason: reason,
|
|
3360
|
+
remainingHeld: transaction.hold.heldAmount - transaction.hold.releasedAmount
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
);
|
|
3364
|
+
transaction.hold.status = HOLD_STATUS.PARTIALLY_RELEASED;
|
|
3365
|
+
Object.assign(transaction, appendAuditEvent(transaction, auditEvent));
|
|
3366
|
+
}
|
|
3367
|
+
if ("markModified" in transaction) {
|
|
3368
|
+
transaction.markModified("hold");
|
|
3369
|
+
}
|
|
3370
|
+
await transaction.save();
|
|
3371
|
+
let releaseTaxAmount = 0;
|
|
3372
|
+
if (transaction.tax && transaction.tax > 0) {
|
|
3373
|
+
if (releaseAmount === availableAmount && !amount) {
|
|
3374
|
+
const releasedTaxSoFar = transaction.hold.releasedTaxAmount ?? 0;
|
|
3375
|
+
releaseTaxAmount = transaction.tax - releasedTaxSoFar;
|
|
3376
|
+
} else {
|
|
3377
|
+
const totalAmount = transaction.amount + transaction.tax;
|
|
3378
|
+
if (totalAmount > 0) {
|
|
3379
|
+
const taxRatio = transaction.tax / totalAmount;
|
|
3380
|
+
releaseTaxAmount = Math.round(releaseAmount * taxRatio);
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
const releaseNetAmount = releaseAmount - releaseTaxAmount;
|
|
3385
|
+
let releaseTransaction = null;
|
|
3386
|
+
if (createTransaction) {
|
|
3387
|
+
releaseTransaction = await TransactionModel.create({
|
|
3388
|
+
organizationId: transaction.organizationId,
|
|
3389
|
+
customerId: recipientId,
|
|
3390
|
+
// ✅ UNIFIED: Use 'escrow_release' as type, inflow
|
|
3391
|
+
type: "escrow_release",
|
|
3392
|
+
flow: "inflow",
|
|
3393
|
+
tags: ["escrow", "release"],
|
|
3394
|
+
// ✅ UNIFIED: Amount structure
|
|
3395
|
+
amount: releaseAmount,
|
|
3396
|
+
currency: transaction.currency,
|
|
3397
|
+
fee: 0,
|
|
3398
|
+
// No processing fees on releases
|
|
3399
|
+
tax: releaseTaxAmount,
|
|
3400
|
+
// ✅ Top-level number
|
|
3401
|
+
net: releaseNetAmount,
|
|
3402
|
+
// Copy tax details from original transaction
|
|
3403
|
+
...transaction.taxDetails && {
|
|
3404
|
+
taxDetails: transaction.taxDetails
|
|
3405
|
+
},
|
|
3406
|
+
method: transaction.method,
|
|
3407
|
+
status: "completed",
|
|
3408
|
+
gateway: transaction.gateway,
|
|
3409
|
+
// ✅ UNIFIED: Source reference (link to held transaction)
|
|
3410
|
+
sourceId: transaction._id,
|
|
3411
|
+
sourceModel: "Transaction",
|
|
3412
|
+
relatedTransactionId: transaction._id,
|
|
3413
|
+
metadata: {
|
|
3414
|
+
...metadata,
|
|
3415
|
+
isRelease: true,
|
|
3416
|
+
heldTransactionId: transaction._id.toString(),
|
|
3417
|
+
releaseReason: reason,
|
|
3418
|
+
recipientType,
|
|
3419
|
+
// Store original category for reference
|
|
3420
|
+
originalCategory: transaction.category
|
|
3421
|
+
},
|
|
3422
|
+
idempotencyKey: `release_${transaction._id}_${Date.now()}`
|
|
3423
|
+
});
|
|
3424
|
+
}
|
|
3425
|
+
this.events.emit("escrow.released", {
|
|
3426
|
+
transaction,
|
|
3427
|
+
releaseTransaction,
|
|
3428
|
+
releaseAmount,
|
|
3429
|
+
recipientId,
|
|
3430
|
+
recipientType,
|
|
3431
|
+
reason,
|
|
3432
|
+
isFullRelease,
|
|
3433
|
+
isPartialRelease
|
|
3434
|
+
});
|
|
3435
|
+
const result = {
|
|
3436
|
+
transaction,
|
|
3437
|
+
releaseTransaction,
|
|
3438
|
+
releaseAmount,
|
|
3439
|
+
isFullRelease,
|
|
3440
|
+
isPartialRelease
|
|
3441
|
+
};
|
|
3442
|
+
return this.plugins.executeHook(
|
|
3443
|
+
"escrow.release.after",
|
|
3444
|
+
this.getPluginContext(),
|
|
3445
|
+
{ transactionId, ...options },
|
|
3446
|
+
async () => result
|
|
3447
|
+
);
|
|
3448
|
+
}
|
|
3449
|
+
);
|
|
2439
3450
|
}
|
|
2440
3451
|
/**
|
|
2441
3452
|
* Cancel hold and release back to customer
|
|
@@ -2452,8 +3463,23 @@ var EscrowService = class {
|
|
|
2452
3463
|
throw new TransactionNotFoundError(transactionId);
|
|
2453
3464
|
}
|
|
2454
3465
|
if (!transaction.hold || transaction.hold.status !== HOLD_STATUS.HELD) {
|
|
2455
|
-
throw new
|
|
3466
|
+
throw new InvalidStateTransitionError(
|
|
3467
|
+
"escrow_hold",
|
|
3468
|
+
transaction._id.toString(),
|
|
3469
|
+
transaction.hold?.status ?? "none",
|
|
3470
|
+
HOLD_STATUS.HELD
|
|
3471
|
+
);
|
|
2456
3472
|
}
|
|
3473
|
+
const holdAuditEvent = HOLD_STATE_MACHINE.validateAndCreateAuditEvent(
|
|
3474
|
+
transaction.hold.status,
|
|
3475
|
+
HOLD_STATUS.CANCELLED,
|
|
3476
|
+
transaction._id.toString(),
|
|
3477
|
+
{
|
|
3478
|
+
changedBy: "system",
|
|
3479
|
+
reason: `Escrow hold cancelled${reason ? ": " + reason : ""}`,
|
|
3480
|
+
metadata: { cancelReason: reason, ...metadata }
|
|
3481
|
+
}
|
|
3482
|
+
);
|
|
2457
3483
|
transaction.hold.status = HOLD_STATUS.CANCELLED;
|
|
2458
3484
|
transaction.hold.cancelledAt = /* @__PURE__ */ new Date();
|
|
2459
3485
|
transaction.hold.metadata = {
|
|
@@ -2461,9 +3487,24 @@ var EscrowService = class {
|
|
|
2461
3487
|
...metadata,
|
|
2462
3488
|
cancelReason: reason
|
|
2463
3489
|
};
|
|
3490
|
+
const transactionAuditEvent = TRANSACTION_STATE_MACHINE.validateAndCreateAuditEvent(
|
|
3491
|
+
transaction.status,
|
|
3492
|
+
TRANSACTION_STATUS.CANCELLED,
|
|
3493
|
+
transaction._id.toString(),
|
|
3494
|
+
{
|
|
3495
|
+
changedBy: "system",
|
|
3496
|
+
reason: `Transaction cancelled due to escrow hold cancellation`,
|
|
3497
|
+
metadata: { cancelReason: reason }
|
|
3498
|
+
}
|
|
3499
|
+
);
|
|
2464
3500
|
transaction.status = TRANSACTION_STATUS.CANCELLED;
|
|
3501
|
+
Object.assign(transaction, appendAuditEvent(transaction, holdAuditEvent));
|
|
3502
|
+
Object.assign(transaction, appendAuditEvent(transaction, transactionAuditEvent));
|
|
3503
|
+
if ("markModified" in transaction) {
|
|
3504
|
+
transaction.markModified("hold");
|
|
3505
|
+
}
|
|
2465
3506
|
await transaction.save();
|
|
2466
|
-
this.
|
|
3507
|
+
this.events.emit("escrow.cancelled", {
|
|
2467
3508
|
transaction,
|
|
2468
3509
|
reason
|
|
2469
3510
|
});
|
|
@@ -2484,10 +3525,15 @@ var EscrowService = class {
|
|
|
2484
3525
|
throw new TransactionNotFoundError(transactionId);
|
|
2485
3526
|
}
|
|
2486
3527
|
if (!transaction.hold || transaction.hold.status !== HOLD_STATUS.HELD) {
|
|
2487
|
-
throw new
|
|
3528
|
+
throw new InvalidStateTransitionError(
|
|
3529
|
+
"escrow_hold",
|
|
3530
|
+
transaction._id.toString(),
|
|
3531
|
+
transaction.hold?.status ?? "none",
|
|
3532
|
+
HOLD_STATUS.HELD
|
|
3533
|
+
);
|
|
2488
3534
|
}
|
|
2489
3535
|
if (!splitRules || splitRules.length === 0) {
|
|
2490
|
-
throw new
|
|
3536
|
+
throw new ValidationError("splitRules cannot be empty", { transactionId });
|
|
2491
3537
|
}
|
|
2492
3538
|
const splits = calculateSplits(
|
|
2493
3539
|
transaction.amount,
|
|
@@ -2497,25 +3543,58 @@ var EscrowService = class {
|
|
|
2497
3543
|
transaction.splits = splits;
|
|
2498
3544
|
await transaction.save();
|
|
2499
3545
|
const splitTransactions = [];
|
|
2500
|
-
|
|
3546
|
+
const totalTax = transaction.tax ?? 0;
|
|
3547
|
+
const totalBaseAmount = transaction.amount;
|
|
3548
|
+
let allocatedTaxAmount = 0;
|
|
3549
|
+
const splitTaxAmounts = splits.map((split) => {
|
|
3550
|
+
if (!totalTax || totalBaseAmount <= 0) {
|
|
3551
|
+
return 0;
|
|
3552
|
+
}
|
|
3553
|
+
const ratio = split.grossAmount / totalBaseAmount;
|
|
3554
|
+
const taxAmount = Math.round(totalTax * ratio);
|
|
3555
|
+
allocatedTaxAmount += taxAmount;
|
|
3556
|
+
return taxAmount;
|
|
3557
|
+
});
|
|
3558
|
+
for (const [index, split] of splits.entries()) {
|
|
3559
|
+
const splitTaxAmount = totalTax > 0 ? splitTaxAmounts[index] ?? 0 : 0;
|
|
3560
|
+
const splitNetAmount = split.grossAmount - split.gatewayFeeAmount - splitTaxAmount;
|
|
2501
3561
|
const splitTransaction = await TransactionModel.create({
|
|
2502
3562
|
organizationId: transaction.organizationId,
|
|
2503
3563
|
customerId: split.recipientId,
|
|
2504
|
-
|
|
3564
|
+
// ✅ UNIFIED: Use split type directly (commission, platform_fee, etc.)
|
|
3565
|
+
type: split.type,
|
|
3566
|
+
flow: "outflow",
|
|
3567
|
+
// Splits are money going out
|
|
3568
|
+
tags: ["split", "commission"],
|
|
3569
|
+
// ✅ UNIFIED: Amount structure (gross, fee, tax, net)
|
|
3570
|
+
amount: split.grossAmount,
|
|
3571
|
+
// ✅ Gross amount (before deductions)
|
|
2505
3572
|
currency: transaction.currency,
|
|
2506
|
-
|
|
2507
|
-
|
|
3573
|
+
fee: split.gatewayFeeAmount,
|
|
3574
|
+
tax: splitTaxAmount,
|
|
3575
|
+
// ✅ Top-level number
|
|
3576
|
+
net: splitNetAmount,
|
|
3577
|
+
// ✅ Net = gross - fee - tax
|
|
3578
|
+
// Copy tax details from original transaction (if applicable)
|
|
3579
|
+
...transaction.taxDetails && splitTaxAmount > 0 && {
|
|
3580
|
+
taxDetails: transaction.taxDetails
|
|
3581
|
+
},
|
|
2508
3582
|
method: transaction.method,
|
|
2509
|
-
status:
|
|
3583
|
+
status: "completed",
|
|
2510
3584
|
gateway: transaction.gateway,
|
|
2511
|
-
|
|
2512
|
-
|
|
3585
|
+
// ✅ UNIFIED: Source reference (link to original transaction)
|
|
3586
|
+
sourceId: transaction._id,
|
|
3587
|
+
sourceModel: "Transaction",
|
|
3588
|
+
relatedTransactionId: transaction._id,
|
|
2513
3589
|
metadata: {
|
|
2514
3590
|
isSplit: true,
|
|
2515
3591
|
splitType: split.type,
|
|
2516
3592
|
recipientType: split.recipientType,
|
|
2517
3593
|
originalTransactionId: transaction._id.toString(),
|
|
2518
|
-
|
|
3594
|
+
// Store split details for reference
|
|
3595
|
+
splitGrossAmount: split.grossAmount,
|
|
3596
|
+
splitNetAmount: split.netAmount,
|
|
3597
|
+
// Original calculation
|
|
2519
3598
|
gatewayFeeAmount: split.gatewayFeeAmount
|
|
2520
3599
|
},
|
|
2521
3600
|
idempotencyKey: `split_${transaction._id}_${split.recipientId}_${Date.now()}`
|
|
@@ -2527,8 +3606,10 @@ var EscrowService = class {
|
|
|
2527
3606
|
}
|
|
2528
3607
|
await transaction.save();
|
|
2529
3608
|
const organizationPayout = calculateOrganizationPayout(transaction.amount, splits);
|
|
3609
|
+
const organizationTaxAmount = totalTax > 0 ? Math.max(0, totalTax - allocatedTaxAmount) : 0;
|
|
3610
|
+
const organizationPayoutTotal = totalTax > 0 ? organizationPayout + organizationTaxAmount : organizationPayout;
|
|
2530
3611
|
const organizationTransaction = await this.release(transactionId, {
|
|
2531
|
-
amount:
|
|
3612
|
+
amount: organizationPayoutTotal,
|
|
2532
3613
|
recipientId: transaction.organizationId?.toString() ?? "",
|
|
2533
3614
|
recipientType: "organization",
|
|
2534
3615
|
reason: RELEASE_REASON.PAYMENT_VERIFIED,
|
|
@@ -2539,7 +3620,7 @@ var EscrowService = class {
|
|
|
2539
3620
|
totalSplitAmount: transaction.amount - organizationPayout
|
|
2540
3621
|
}
|
|
2541
3622
|
});
|
|
2542
|
-
this.
|
|
3623
|
+
this.events.emit("escrow.split", {
|
|
2543
3624
|
transaction,
|
|
2544
3625
|
splits,
|
|
2545
3626
|
splitTransactions,
|
|
@@ -2574,8 +3655,490 @@ var EscrowService = class {
|
|
|
2574
3655
|
hasSplits: transaction.splits ? transaction.splits.length > 0 : false
|
|
2575
3656
|
};
|
|
2576
3657
|
}
|
|
2577
|
-
|
|
2578
|
-
|
|
3658
|
+
};
|
|
3659
|
+
|
|
3660
|
+
// src/application/services/settlement.service.ts
|
|
3661
|
+
var SettlementService = class {
|
|
3662
|
+
models;
|
|
3663
|
+
plugins;
|
|
3664
|
+
logger;
|
|
3665
|
+
events;
|
|
3666
|
+
constructor(container) {
|
|
3667
|
+
this.models = container.get("models");
|
|
3668
|
+
this.plugins = container.get("plugins");
|
|
3669
|
+
this.logger = container.get("logger");
|
|
3670
|
+
this.events = container.get("events");
|
|
3671
|
+
void this.plugins;
|
|
3672
|
+
}
|
|
3673
|
+
/**
|
|
3674
|
+
* Create settlements from transaction splits
|
|
3675
|
+
* Typically called after escrow is released
|
|
3676
|
+
*
|
|
3677
|
+
* @param transactionId - Transaction ID with splits
|
|
3678
|
+
* @param options - Creation options
|
|
3679
|
+
* @returns Array of created settlements
|
|
3680
|
+
*/
|
|
3681
|
+
async createFromSplits(transactionId, options = {}) {
|
|
3682
|
+
const {
|
|
3683
|
+
scheduledAt = /* @__PURE__ */ new Date(),
|
|
3684
|
+
payoutMethod = "bank_transfer",
|
|
3685
|
+
metadata = {}
|
|
3686
|
+
} = options;
|
|
3687
|
+
if (!this.models.Settlement) {
|
|
3688
|
+
throw new ModelNotRegisteredError("Settlement");
|
|
3689
|
+
}
|
|
3690
|
+
const TransactionModel = this.models.Transaction;
|
|
3691
|
+
const transaction = await TransactionModel.findById(transactionId);
|
|
3692
|
+
if (!transaction) {
|
|
3693
|
+
throw new TransactionNotFoundError(transactionId);
|
|
3694
|
+
}
|
|
3695
|
+
if (!transaction.splits || transaction.splits.length === 0) {
|
|
3696
|
+
throw new ValidationError("Transaction has no splits to settle", { transactionId });
|
|
3697
|
+
}
|
|
3698
|
+
const SettlementModel = this.models.Settlement;
|
|
3699
|
+
const settlements = [];
|
|
3700
|
+
for (const split of transaction.splits) {
|
|
3701
|
+
if (split.status === "paid") {
|
|
3702
|
+
this.logger.info("Split already paid, skipping", { splitId: split._id });
|
|
3703
|
+
continue;
|
|
3704
|
+
}
|
|
3705
|
+
const settlement = await SettlementModel.create({
|
|
3706
|
+
organizationId: transaction.organizationId,
|
|
3707
|
+
recipientId: split.recipientId,
|
|
3708
|
+
recipientType: split.recipientType,
|
|
3709
|
+
type: SETTLEMENT_TYPE.SPLIT_PAYOUT,
|
|
3710
|
+
status: SETTLEMENT_STATUS.PENDING,
|
|
3711
|
+
payoutMethod,
|
|
3712
|
+
amount: split.netAmount,
|
|
3713
|
+
currency: transaction.currency,
|
|
3714
|
+
sourceTransactionIds: [transaction._id],
|
|
3715
|
+
sourceSplitIds: [split._id?.toString() || ""],
|
|
3716
|
+
scheduledAt,
|
|
3717
|
+
metadata: {
|
|
3718
|
+
...metadata,
|
|
3719
|
+
splitType: split.type,
|
|
3720
|
+
transactionCategory: transaction.category
|
|
3721
|
+
}
|
|
3722
|
+
});
|
|
3723
|
+
settlements.push(settlement);
|
|
3724
|
+
}
|
|
3725
|
+
this.events.emit("settlement.created", {
|
|
3726
|
+
settlements,
|
|
3727
|
+
transactionId,
|
|
3728
|
+
count: settlements.length
|
|
3729
|
+
});
|
|
3730
|
+
this.logger.info("Created settlements from splits", {
|
|
3731
|
+
transactionId,
|
|
3732
|
+
count: settlements.length
|
|
3733
|
+
});
|
|
3734
|
+
return settlements;
|
|
3735
|
+
}
|
|
3736
|
+
/**
|
|
3737
|
+
* Schedule a payout
|
|
3738
|
+
*
|
|
3739
|
+
* @param params - Settlement parameters
|
|
3740
|
+
* @returns Created settlement
|
|
3741
|
+
*/
|
|
3742
|
+
async schedule(params) {
|
|
3743
|
+
if (!this.models.Settlement) {
|
|
3744
|
+
throw new ModelNotRegisteredError("Settlement");
|
|
3745
|
+
}
|
|
3746
|
+
const {
|
|
3747
|
+
organizationId,
|
|
3748
|
+
recipientId,
|
|
3749
|
+
recipientType,
|
|
3750
|
+
type,
|
|
3751
|
+
amount,
|
|
3752
|
+
currency = "USD",
|
|
3753
|
+
payoutMethod,
|
|
3754
|
+
sourceTransactionIds = [],
|
|
3755
|
+
sourceSplitIds = [],
|
|
3756
|
+
scheduledAt = /* @__PURE__ */ new Date(),
|
|
3757
|
+
bankTransferDetails,
|
|
3758
|
+
mobileWalletDetails,
|
|
3759
|
+
cryptoDetails,
|
|
3760
|
+
notes,
|
|
3761
|
+
metadata = {}
|
|
3762
|
+
} = params;
|
|
3763
|
+
if (amount <= 0) {
|
|
3764
|
+
throw new ValidationError("Settlement amount must be positive", { amount });
|
|
3765
|
+
}
|
|
3766
|
+
const SettlementModel = this.models.Settlement;
|
|
3767
|
+
const settlement = await SettlementModel.create({
|
|
3768
|
+
organizationId,
|
|
3769
|
+
recipientId,
|
|
3770
|
+
recipientType,
|
|
3771
|
+
type,
|
|
3772
|
+
status: SETTLEMENT_STATUS.PENDING,
|
|
3773
|
+
payoutMethod,
|
|
3774
|
+
amount,
|
|
3775
|
+
currency,
|
|
3776
|
+
sourceTransactionIds,
|
|
3777
|
+
sourceSplitIds,
|
|
3778
|
+
scheduledAt,
|
|
3779
|
+
bankTransferDetails,
|
|
3780
|
+
mobileWalletDetails,
|
|
3781
|
+
cryptoDetails,
|
|
3782
|
+
notes,
|
|
3783
|
+
metadata
|
|
3784
|
+
});
|
|
3785
|
+
this.events.emit("settlement.scheduled", {
|
|
3786
|
+
settlement,
|
|
3787
|
+
scheduledAt
|
|
3788
|
+
});
|
|
3789
|
+
this.logger.info("Settlement scheduled", {
|
|
3790
|
+
settlementId: settlement._id,
|
|
3791
|
+
recipientId,
|
|
3792
|
+
amount
|
|
3793
|
+
});
|
|
3794
|
+
return settlement;
|
|
3795
|
+
}
|
|
3796
|
+
/**
|
|
3797
|
+
* Process pending settlements
|
|
3798
|
+
* Batch process settlements that are due
|
|
3799
|
+
*
|
|
3800
|
+
* @param options - Processing options
|
|
3801
|
+
* @returns Processing result
|
|
3802
|
+
*/
|
|
3803
|
+
async processPending(options = {}) {
|
|
3804
|
+
if (!this.models.Settlement) {
|
|
3805
|
+
throw new ModelNotRegisteredError("Settlement");
|
|
3806
|
+
}
|
|
3807
|
+
const {
|
|
3808
|
+
limit = 100,
|
|
3809
|
+
organizationId,
|
|
3810
|
+
payoutMethod,
|
|
3811
|
+
dryRun = false
|
|
3812
|
+
} = options;
|
|
3813
|
+
const SettlementModel = this.models.Settlement;
|
|
3814
|
+
const query = {
|
|
3815
|
+
status: SETTLEMENT_STATUS.PENDING,
|
|
3816
|
+
scheduledAt: { $lte: /* @__PURE__ */ new Date() }
|
|
3817
|
+
};
|
|
3818
|
+
if (organizationId) query.organizationId = organizationId;
|
|
3819
|
+
if (payoutMethod) query.payoutMethod = payoutMethod;
|
|
3820
|
+
const settlements = await SettlementModel.find(query).limit(limit).sort({ scheduledAt: 1 });
|
|
3821
|
+
const result = {
|
|
3822
|
+
processed: 0,
|
|
3823
|
+
succeeded: 0,
|
|
3824
|
+
failed: 0,
|
|
3825
|
+
settlements: [],
|
|
3826
|
+
errors: []
|
|
3827
|
+
};
|
|
3828
|
+
if (dryRun) {
|
|
3829
|
+
this.logger.info("Dry run: would process settlements", { count: settlements.length });
|
|
3830
|
+
result.settlements = settlements;
|
|
3831
|
+
return result;
|
|
3832
|
+
}
|
|
3833
|
+
for (const settlement of settlements) {
|
|
3834
|
+
result.processed++;
|
|
3835
|
+
try {
|
|
3836
|
+
const auditEvent = SETTLEMENT_STATE_MACHINE.validateAndCreateAuditEvent(
|
|
3837
|
+
settlement.status,
|
|
3838
|
+
SETTLEMENT_STATUS.PROCESSING,
|
|
3839
|
+
settlement._id.toString(),
|
|
3840
|
+
{
|
|
3841
|
+
changedBy: "system",
|
|
3842
|
+
reason: "Settlement processing started",
|
|
3843
|
+
metadata: { recipientId: settlement.recipientId, amount: settlement.amount }
|
|
3844
|
+
}
|
|
3845
|
+
);
|
|
3846
|
+
settlement.status = SETTLEMENT_STATUS.PROCESSING;
|
|
3847
|
+
settlement.processedAt = /* @__PURE__ */ new Date();
|
|
3848
|
+
Object.assign(settlement, appendAuditEvent(settlement, auditEvent));
|
|
3849
|
+
await settlement.save();
|
|
3850
|
+
result.succeeded++;
|
|
3851
|
+
result.settlements.push(settlement);
|
|
3852
|
+
this.events.emit("settlement.processing", {
|
|
3853
|
+
settlement,
|
|
3854
|
+
processedAt: settlement.processedAt
|
|
3855
|
+
});
|
|
3856
|
+
} catch (error) {
|
|
3857
|
+
result.failed++;
|
|
3858
|
+
result.errors.push({
|
|
3859
|
+
settlementId: settlement._id.toString(),
|
|
3860
|
+
error: error.message
|
|
3861
|
+
});
|
|
3862
|
+
this.logger.error("Failed to process settlement", {
|
|
3863
|
+
settlementId: settlement._id,
|
|
3864
|
+
error
|
|
3865
|
+
});
|
|
3866
|
+
}
|
|
3867
|
+
}
|
|
3868
|
+
this.logger.info("Processed settlements", result);
|
|
3869
|
+
return result;
|
|
3870
|
+
}
|
|
3871
|
+
/**
|
|
3872
|
+
* Mark settlement as completed
|
|
3873
|
+
* Call this after bank confirms the transfer
|
|
3874
|
+
*
|
|
3875
|
+
* @param settlementId - Settlement ID
|
|
3876
|
+
* @param details - Completion details
|
|
3877
|
+
* @returns Updated settlement
|
|
3878
|
+
*/
|
|
3879
|
+
async complete(settlementId, details = {}) {
|
|
3880
|
+
if (!this.models.Settlement) {
|
|
3881
|
+
throw new ModelNotRegisteredError("Settlement");
|
|
3882
|
+
}
|
|
3883
|
+
const SettlementModel = this.models.Settlement;
|
|
3884
|
+
const settlement = await SettlementModel.findById(settlementId);
|
|
3885
|
+
if (!settlement) {
|
|
3886
|
+
throw new ValidationError("Settlement not found", { settlementId });
|
|
3887
|
+
}
|
|
3888
|
+
if (settlement.status !== SETTLEMENT_STATUS.PROCESSING && settlement.status !== SETTLEMENT_STATUS.PENDING) {
|
|
3889
|
+
throw new InvalidStateTransitionError(
|
|
3890
|
+
"complete",
|
|
3891
|
+
SETTLEMENT_STATUS.PROCESSING,
|
|
3892
|
+
settlement.status,
|
|
3893
|
+
"Only processing or pending settlements can be completed"
|
|
3894
|
+
);
|
|
3895
|
+
}
|
|
3896
|
+
const {
|
|
3897
|
+
transferReference,
|
|
3898
|
+
transferredAt = /* @__PURE__ */ new Date(),
|
|
3899
|
+
transactionHash,
|
|
3900
|
+
notes,
|
|
3901
|
+
metadata = {}
|
|
3902
|
+
} = details;
|
|
3903
|
+
const auditEvent = SETTLEMENT_STATE_MACHINE.validateAndCreateAuditEvent(
|
|
3904
|
+
settlement.status,
|
|
3905
|
+
SETTLEMENT_STATUS.COMPLETED,
|
|
3906
|
+
settlement._id.toString(),
|
|
3907
|
+
{
|
|
3908
|
+
changedBy: "system",
|
|
3909
|
+
reason: "Settlement completed successfully",
|
|
3910
|
+
metadata: {
|
|
3911
|
+
transferReference,
|
|
3912
|
+
transferredAt,
|
|
3913
|
+
transactionHash,
|
|
3914
|
+
payoutMethod: settlement.payoutMethod,
|
|
3915
|
+
amount: settlement.amount
|
|
3916
|
+
}
|
|
3917
|
+
}
|
|
3918
|
+
);
|
|
3919
|
+
settlement.status = SETTLEMENT_STATUS.COMPLETED;
|
|
3920
|
+
settlement.completedAt = /* @__PURE__ */ new Date();
|
|
3921
|
+
if (settlement.payoutMethod === "bank_transfer" && transferReference) {
|
|
3922
|
+
settlement.bankTransferDetails = {
|
|
3923
|
+
...settlement.bankTransferDetails,
|
|
3924
|
+
transferReference,
|
|
3925
|
+
transferredAt
|
|
3926
|
+
};
|
|
3927
|
+
} else if (settlement.payoutMethod === "crypto" && transactionHash) {
|
|
3928
|
+
settlement.cryptoDetails = {
|
|
3929
|
+
...settlement.cryptoDetails,
|
|
3930
|
+
transactionHash,
|
|
3931
|
+
transferredAt
|
|
3932
|
+
};
|
|
3933
|
+
} else if (settlement.payoutMethod === "mobile_wallet") {
|
|
3934
|
+
settlement.mobileWalletDetails = {
|
|
3935
|
+
...settlement.mobileWalletDetails,
|
|
3936
|
+
transferredAt
|
|
3937
|
+
};
|
|
3938
|
+
} else if (settlement.payoutMethod === "platform_balance") {
|
|
3939
|
+
settlement.platformBalanceDetails = {
|
|
3940
|
+
...settlement.platformBalanceDetails,
|
|
3941
|
+
appliedAt: transferredAt
|
|
3942
|
+
};
|
|
3943
|
+
}
|
|
3944
|
+
if (notes) settlement.notes = notes;
|
|
3945
|
+
settlement.metadata = { ...settlement.metadata, ...metadata };
|
|
3946
|
+
Object.assign(settlement, appendAuditEvent(settlement, auditEvent));
|
|
3947
|
+
await settlement.save();
|
|
3948
|
+
this.events.emit("settlement.completed", {
|
|
3949
|
+
settlement,
|
|
3950
|
+
completedAt: settlement.completedAt
|
|
3951
|
+
});
|
|
3952
|
+
this.logger.info("Settlement completed", {
|
|
3953
|
+
settlementId: settlement._id,
|
|
3954
|
+
recipientId: settlement.recipientId,
|
|
3955
|
+
amount: settlement.amount
|
|
3956
|
+
});
|
|
3957
|
+
return settlement;
|
|
3958
|
+
}
|
|
3959
|
+
/**
|
|
3960
|
+
* Mark settlement as failed
|
|
3961
|
+
*
|
|
3962
|
+
* @param settlementId - Settlement ID
|
|
3963
|
+
* @param reason - Failure reason
|
|
3964
|
+
* @returns Updated settlement
|
|
3965
|
+
*/
|
|
3966
|
+
async fail(settlementId, reason, options = {}) {
|
|
3967
|
+
if (!this.models.Settlement) {
|
|
3968
|
+
throw new ModelNotRegisteredError("Settlement");
|
|
3969
|
+
}
|
|
3970
|
+
const SettlementModel = this.models.Settlement;
|
|
3971
|
+
const settlement = await SettlementModel.findById(settlementId);
|
|
3972
|
+
if (!settlement) {
|
|
3973
|
+
throw new ValidationError("Settlement not found", { settlementId });
|
|
3974
|
+
}
|
|
3975
|
+
const { code, retry: retry2 = false } = options;
|
|
3976
|
+
if (retry2) {
|
|
3977
|
+
const auditEvent = SETTLEMENT_STATE_MACHINE.validateAndCreateAuditEvent(
|
|
3978
|
+
settlement.status,
|
|
3979
|
+
SETTLEMENT_STATUS.PENDING,
|
|
3980
|
+
settlement._id.toString(),
|
|
3981
|
+
{
|
|
3982
|
+
changedBy: "system",
|
|
3983
|
+
reason: `Settlement failed, retrying: ${reason}`,
|
|
3984
|
+
metadata: {
|
|
3985
|
+
failureReason: reason,
|
|
3986
|
+
failureCode: code,
|
|
3987
|
+
retryCount: (settlement.retryCount || 0) + 1,
|
|
3988
|
+
scheduledAt: new Date(Date.now() + 60 * 60 * 1e3)
|
|
3989
|
+
}
|
|
3990
|
+
}
|
|
3991
|
+
);
|
|
3992
|
+
settlement.status = SETTLEMENT_STATUS.PENDING;
|
|
3993
|
+
settlement.retryCount = (settlement.retryCount || 0) + 1;
|
|
3994
|
+
settlement.scheduledAt = new Date(Date.now() + 60 * 60 * 1e3);
|
|
3995
|
+
Object.assign(settlement, appendAuditEvent(settlement, auditEvent));
|
|
3996
|
+
} else {
|
|
3997
|
+
const auditEvent = SETTLEMENT_STATE_MACHINE.validateAndCreateAuditEvent(
|
|
3998
|
+
settlement.status,
|
|
3999
|
+
SETTLEMENT_STATUS.FAILED,
|
|
4000
|
+
settlement._id.toString(),
|
|
4001
|
+
{
|
|
4002
|
+
changedBy: "system",
|
|
4003
|
+
reason: `Settlement failed: ${reason}`,
|
|
4004
|
+
metadata: {
|
|
4005
|
+
failureReason: reason,
|
|
4006
|
+
failureCode: code
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
);
|
|
4010
|
+
settlement.status = SETTLEMENT_STATUS.FAILED;
|
|
4011
|
+
settlement.failedAt = /* @__PURE__ */ new Date();
|
|
4012
|
+
Object.assign(settlement, appendAuditEvent(settlement, auditEvent));
|
|
4013
|
+
}
|
|
4014
|
+
settlement.failureReason = reason;
|
|
4015
|
+
if (code) settlement.failureCode = code;
|
|
4016
|
+
await settlement.save();
|
|
4017
|
+
this.events.emit("settlement.failed", {
|
|
4018
|
+
settlement,
|
|
4019
|
+
reason,
|
|
4020
|
+
code,
|
|
4021
|
+
retry: retry2
|
|
4022
|
+
});
|
|
4023
|
+
this.logger.warn("Settlement failed", {
|
|
4024
|
+
settlementId: settlement._id,
|
|
4025
|
+
reason,
|
|
4026
|
+
retry: retry2
|
|
4027
|
+
});
|
|
4028
|
+
return settlement;
|
|
4029
|
+
}
|
|
4030
|
+
/**
|
|
4031
|
+
* List settlements with filters
|
|
4032
|
+
*
|
|
4033
|
+
* @param filters - Query filters
|
|
4034
|
+
* @returns Settlements
|
|
4035
|
+
*/
|
|
4036
|
+
async list(filters = {}) {
|
|
4037
|
+
if (!this.models.Settlement) {
|
|
4038
|
+
throw new ModelNotRegisteredError("Settlement");
|
|
4039
|
+
}
|
|
4040
|
+
const SettlementModel = this.models.Settlement;
|
|
4041
|
+
const {
|
|
4042
|
+
organizationId,
|
|
4043
|
+
recipientId,
|
|
4044
|
+
status,
|
|
4045
|
+
type,
|
|
4046
|
+
payoutMethod,
|
|
4047
|
+
scheduledAfter,
|
|
4048
|
+
scheduledBefore,
|
|
4049
|
+
limit = 50,
|
|
4050
|
+
skip = 0,
|
|
4051
|
+
sort = { createdAt: -1 }
|
|
4052
|
+
} = filters;
|
|
4053
|
+
const query = {};
|
|
4054
|
+
if (organizationId) query.organizationId = organizationId;
|
|
4055
|
+
if (recipientId) query.recipientId = recipientId;
|
|
4056
|
+
if (status) query.status = Array.isArray(status) ? { $in: status } : status;
|
|
4057
|
+
if (type) query.type = type;
|
|
4058
|
+
if (payoutMethod) query.payoutMethod = payoutMethod;
|
|
4059
|
+
if (scheduledAfter || scheduledBefore) {
|
|
4060
|
+
query.scheduledAt = {};
|
|
4061
|
+
if (scheduledAfter) query.scheduledAt.$gte = scheduledAfter;
|
|
4062
|
+
if (scheduledBefore) query.scheduledAt.$lte = scheduledBefore;
|
|
4063
|
+
}
|
|
4064
|
+
const settlements = await SettlementModel.find(query).limit(limit).skip(skip).sort(sort);
|
|
4065
|
+
return settlements;
|
|
4066
|
+
}
|
|
4067
|
+
/**
|
|
4068
|
+
* Get payout summary for recipient
|
|
4069
|
+
*
|
|
4070
|
+
* @param recipientId - Recipient ID
|
|
4071
|
+
* @param options - Summary options
|
|
4072
|
+
* @returns Settlement summary
|
|
4073
|
+
*/
|
|
4074
|
+
async getSummary(recipientId, options = {}) {
|
|
4075
|
+
if (!this.models.Settlement) {
|
|
4076
|
+
throw new ModelNotRegisteredError("Settlement");
|
|
4077
|
+
}
|
|
4078
|
+
const { organizationId, startDate, endDate } = options;
|
|
4079
|
+
const SettlementModel = this.models.Settlement;
|
|
4080
|
+
const query = { recipientId };
|
|
4081
|
+
if (organizationId) query.organizationId = organizationId;
|
|
4082
|
+
if (startDate || endDate) {
|
|
4083
|
+
query.createdAt = {};
|
|
4084
|
+
if (startDate) query.createdAt.$gte = startDate;
|
|
4085
|
+
if (endDate) query.createdAt.$lte = endDate;
|
|
4086
|
+
}
|
|
4087
|
+
const settlements = await SettlementModel.find(query);
|
|
4088
|
+
const summary = {
|
|
4089
|
+
recipientId,
|
|
4090
|
+
totalPending: 0,
|
|
4091
|
+
totalProcessing: 0,
|
|
4092
|
+
totalCompleted: 0,
|
|
4093
|
+
totalFailed: 0,
|
|
4094
|
+
amountPending: 0,
|
|
4095
|
+
amountCompleted: 0,
|
|
4096
|
+
amountFailed: 0,
|
|
4097
|
+
currency: settlements[0]?.currency || "USD",
|
|
4098
|
+
settlements: {
|
|
4099
|
+
pending: 0,
|
|
4100
|
+
processing: 0,
|
|
4101
|
+
completed: 0,
|
|
4102
|
+
failed: 0,
|
|
4103
|
+
cancelled: 0
|
|
4104
|
+
}
|
|
4105
|
+
};
|
|
4106
|
+
for (const settlement of settlements) {
|
|
4107
|
+
summary.settlements[settlement.status]++;
|
|
4108
|
+
if (settlement.status === SETTLEMENT_STATUS.PENDING) {
|
|
4109
|
+
summary.totalPending++;
|
|
4110
|
+
summary.amountPending += settlement.amount;
|
|
4111
|
+
} else if (settlement.status === SETTLEMENT_STATUS.PROCESSING) {
|
|
4112
|
+
summary.totalProcessing++;
|
|
4113
|
+
} else if (settlement.status === SETTLEMENT_STATUS.COMPLETED) {
|
|
4114
|
+
summary.totalCompleted++;
|
|
4115
|
+
summary.amountCompleted += settlement.amount;
|
|
4116
|
+
if (!summary.lastSettlementDate || settlement.completedAt > summary.lastSettlementDate) {
|
|
4117
|
+
summary.lastSettlementDate = settlement.completedAt;
|
|
4118
|
+
}
|
|
4119
|
+
} else if (settlement.status === SETTLEMENT_STATUS.FAILED) {
|
|
4120
|
+
summary.totalFailed++;
|
|
4121
|
+
summary.amountFailed += settlement.amount;
|
|
4122
|
+
}
|
|
4123
|
+
}
|
|
4124
|
+
return summary;
|
|
4125
|
+
}
|
|
4126
|
+
/**
|
|
4127
|
+
* Get settlement by ID
|
|
4128
|
+
*
|
|
4129
|
+
* @param settlementId - Settlement ID
|
|
4130
|
+
* @returns Settlement
|
|
4131
|
+
*/
|
|
4132
|
+
async get(settlementId) {
|
|
4133
|
+
if (!this.models.Settlement) {
|
|
4134
|
+
throw new ModelNotRegisteredError("Settlement");
|
|
4135
|
+
}
|
|
4136
|
+
const SettlementModel = this.models.Settlement;
|
|
4137
|
+
const settlement = await SettlementModel.findById(settlementId);
|
|
4138
|
+
if (!settlement) {
|
|
4139
|
+
throw new ValidationError("Settlement not found", { settlementId });
|
|
4140
|
+
}
|
|
4141
|
+
return settlement;
|
|
2579
4142
|
}
|
|
2580
4143
|
};
|
|
2581
4144
|
|
|
@@ -2600,6 +4163,8 @@ var Revenue = class {
|
|
|
2600
4163
|
transactions;
|
|
2601
4164
|
/** Escrow service - hold, release, splits */
|
|
2602
4165
|
escrow;
|
|
4166
|
+
/** Settlement service - payout tracking */
|
|
4167
|
+
settlement;
|
|
2603
4168
|
constructor(container, events, plugins, options, providers, config) {
|
|
2604
4169
|
this._container = container;
|
|
2605
4170
|
this._events = events;
|
|
@@ -2612,16 +4177,20 @@ var Revenue = class {
|
|
|
2612
4177
|
ttl: options.idempotencyTtl
|
|
2613
4178
|
});
|
|
2614
4179
|
if (options.circuitBreaker) {
|
|
2615
|
-
|
|
4180
|
+
const cbConfig = typeof options.circuitBreaker === "boolean" ? {} : options.circuitBreaker;
|
|
4181
|
+
this._circuitBreaker = createCircuitBreaker(cbConfig);
|
|
2616
4182
|
}
|
|
2617
4183
|
container.singleton("events", events);
|
|
2618
4184
|
container.singleton("plugins", plugins);
|
|
2619
4185
|
container.singleton("idempotency", this._idempotency);
|
|
2620
4186
|
container.singleton("logger", this._logger);
|
|
4187
|
+
container.singleton("retryConfig", options.retry || {});
|
|
4188
|
+
container.singleton("circuitBreaker", this._circuitBreaker || null);
|
|
2621
4189
|
this.monetization = new MonetizationService(container);
|
|
2622
4190
|
this.payments = new PaymentService(container);
|
|
2623
4191
|
this.transactions = new TransactionService(container);
|
|
2624
4192
|
this.escrow = new EscrowService(container);
|
|
4193
|
+
this.settlement = new SettlementService(container);
|
|
2625
4194
|
Object.freeze(this._providers);
|
|
2626
4195
|
Object.freeze(this._config);
|
|
2627
4196
|
}
|
|
@@ -2697,8 +4266,8 @@ var Revenue = class {
|
|
|
2697
4266
|
*
|
|
2698
4267
|
* @example
|
|
2699
4268
|
* ```typescript
|
|
2700
|
-
* revenue.on('payment.
|
|
2701
|
-
* console.log('Payment:', event.
|
|
4269
|
+
* revenue.on('payment.verified', (event) => {
|
|
4270
|
+
* console.log('Payment:', event.transaction._id);
|
|
2702
4271
|
* });
|
|
2703
4272
|
* ```
|
|
2704
4273
|
*/
|
|
@@ -2755,7 +4324,6 @@ var Revenue = class {
|
|
|
2755
4324
|
return {
|
|
2756
4325
|
events: this._events,
|
|
2757
4326
|
logger: this._logger,
|
|
2758
|
-
get: (key) => this._container.get(key),
|
|
2759
4327
|
storage: /* @__PURE__ */ new Map(),
|
|
2760
4328
|
meta: {
|
|
2761
4329
|
...meta,
|
|
@@ -2770,6 +4338,7 @@ var Revenue = class {
|
|
|
2770
4338
|
async destroy() {
|
|
2771
4339
|
await this._plugins.destroy();
|
|
2772
4340
|
this._events.clear();
|
|
4341
|
+
this._idempotency.destroy();
|
|
2773
4342
|
}
|
|
2774
4343
|
};
|
|
2775
4344
|
var RevenueBuilder = class {
|
|
@@ -2777,8 +4346,8 @@ var RevenueBuilder = class {
|
|
|
2777
4346
|
models = null;
|
|
2778
4347
|
providers = {};
|
|
2779
4348
|
plugins = [];
|
|
2780
|
-
hooks = {};
|
|
2781
4349
|
categoryMappings = {};
|
|
4350
|
+
transactionTypeMapping = {};
|
|
2782
4351
|
constructor(options = {}) {
|
|
2783
4352
|
this.options = options;
|
|
2784
4353
|
}
|
|
@@ -2802,7 +4371,7 @@ var RevenueBuilder = class {
|
|
|
2802
4371
|
*/
|
|
2803
4372
|
withModel(name, model) {
|
|
2804
4373
|
if (!this.models) {
|
|
2805
|
-
this.models = {
|
|
4374
|
+
this.models = {};
|
|
2806
4375
|
}
|
|
2807
4376
|
this.models[name] = model;
|
|
2808
4377
|
return this;
|
|
@@ -2847,15 +4416,6 @@ var RevenueBuilder = class {
|
|
|
2847
4416
|
this.plugins.push(...plugins);
|
|
2848
4417
|
return this;
|
|
2849
4418
|
}
|
|
2850
|
-
withHooks(hooks) {
|
|
2851
|
-
const normalized = {};
|
|
2852
|
-
for (const [event, handlerOrHandlers] of Object.entries(hooks)) {
|
|
2853
|
-
if (!handlerOrHandlers) continue;
|
|
2854
|
-
normalized[event] = Array.isArray(handlerOrHandlers) ? handlerOrHandlers : [handlerOrHandlers];
|
|
2855
|
-
}
|
|
2856
|
-
this.hooks = { ...this.hooks, ...normalized };
|
|
2857
|
-
return this;
|
|
2858
|
-
}
|
|
2859
4419
|
/**
|
|
2860
4420
|
* Set retry configuration
|
|
2861
4421
|
*
|
|
@@ -2897,7 +4457,15 @@ var RevenueBuilder = class {
|
|
|
2897
4457
|
return this;
|
|
2898
4458
|
}
|
|
2899
4459
|
/**
|
|
2900
|
-
* Set commission rate (0-
|
|
4460
|
+
* Set commission rate as decimal (0-1, e.g., 0.10 for 10%)
|
|
4461
|
+
*
|
|
4462
|
+
* @param rate - Commission rate (0-1 decimal format)
|
|
4463
|
+
* @param gatewayFeeRate - Gateway fee rate (0-1 decimal format, e.g., 0.029 for 2.9%)
|
|
4464
|
+
*
|
|
4465
|
+
* @example
|
|
4466
|
+
* ```typescript
|
|
4467
|
+
* .withCommission(0.10, 0.029) // 10% commission + 2.9% gateway fee
|
|
4468
|
+
* ```
|
|
2901
4469
|
*/
|
|
2902
4470
|
withCommission(rate, gatewayFeeRate = 0) {
|
|
2903
4471
|
this.options.commissionRate = rate;
|
|
@@ -2920,6 +4488,22 @@ var RevenueBuilder = class {
|
|
|
2920
4488
|
this.categoryMappings = { ...this.categoryMappings, ...mappings };
|
|
2921
4489
|
return this;
|
|
2922
4490
|
}
|
|
4491
|
+
/**
|
|
4492
|
+
* Set transaction flow mapping by category or monetization type
|
|
4493
|
+
*
|
|
4494
|
+
* @example
|
|
4495
|
+
* ```typescript
|
|
4496
|
+
* .withTransactionTypeMapping({
|
|
4497
|
+
* platform_subscription: 'inflow',
|
|
4498
|
+
* subscription: 'inflow',
|
|
4499
|
+
* refund: 'outflow',
|
|
4500
|
+
* })
|
|
4501
|
+
* ```
|
|
4502
|
+
*/
|
|
4503
|
+
withTransactionTypeMapping(mapping) {
|
|
4504
|
+
this.transactionTypeMapping = { ...this.transactionTypeMapping, ...mapping };
|
|
4505
|
+
return this;
|
|
4506
|
+
}
|
|
2923
4507
|
/**
|
|
2924
4508
|
* Build the Revenue instance
|
|
2925
4509
|
*/
|
|
@@ -2958,14 +4542,19 @@ var RevenueBuilder = class {
|
|
|
2958
4542
|
gatewayFeeRate: this.options.gatewayFeeRate ?? 0
|
|
2959
4543
|
};
|
|
2960
4544
|
const config = {
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
categoryMappings: this.categoryMappings
|
|
4545
|
+
...resolveConfig(resolvedOptions),
|
|
4546
|
+
// Converts singular → plural with '*' global default
|
|
4547
|
+
targetModels: [],
|
|
4548
|
+
categoryMappings: this.categoryMappings,
|
|
4549
|
+
transactionTypeMapping: this.transactionTypeMapping
|
|
2965
4550
|
};
|
|
4551
|
+
for (const provider of Object.values(this.providers)) {
|
|
4552
|
+
if (typeof provider.setDefaultCurrency === "function") {
|
|
4553
|
+
provider.setDefaultCurrency(resolvedOptions.defaultCurrency);
|
|
4554
|
+
}
|
|
4555
|
+
}
|
|
2966
4556
|
container.singleton("models", this.models);
|
|
2967
4557
|
container.singleton("providers", this.providers);
|
|
2968
|
-
container.singleton("hooks", this.hooks);
|
|
2969
4558
|
container.singleton("config", config);
|
|
2970
4559
|
const events = createEventBus();
|
|
2971
4560
|
const pluginManager = new PluginManager();
|
|
@@ -2994,12 +4583,9 @@ function createRevenue(config) {
|
|
|
2994
4583
|
if (config.plugins) {
|
|
2995
4584
|
builder = builder.withPlugins(config.plugins);
|
|
2996
4585
|
}
|
|
2997
|
-
if (config.hooks) {
|
|
2998
|
-
builder = builder.withHooks(config.hooks);
|
|
2999
|
-
}
|
|
3000
4586
|
return builder.build();
|
|
3001
4587
|
}
|
|
3002
4588
|
|
|
3003
|
-
export { AlreadyVerifiedError, ConfigurationError, Container, ERROR_CODES, EventBus, InvalidAmountError, InvalidStateTransitionError, MissingRequiredFieldError, ModelNotRegisteredError, NotFoundError, OperationError, PaymentIntentCreationError, PaymentVerificationError, PluginManager, ProviderCapabilityError, ProviderError, ProviderNotFoundError, RefundError, RefundNotSupportedError, Result, Revenue, RevenueBuilder, RevenueError, StateError, SubscriptionNotActiveError, SubscriptionNotFoundError, TransactionNotFoundError, ValidationError, all, auditPlugin, createEventBus, createRevenue, definePlugin, err, flatMap, isErr, isOk, isRetryable, isRevenueError, loggingPlugin, map, mapErr, match, metricsPlugin, ok, tryCatch, tryCatchSync, unwrap, unwrapOr };
|
|
4589
|
+
export { AlreadyVerifiedError, ConfigurationError, Container, ERROR_CODES, EventBus, HOLD_STATE_MACHINE, InvalidAmountError, InvalidStateTransitionError, MissingRequiredFieldError, ModelNotRegisteredError, NotFoundError, OperationError, PaymentIntentCreationError, PaymentVerificationError, PluginManager, ProviderCapabilityError, ProviderError, ProviderNotFoundError, RefundError, RefundNotSupportedError, Result, Revenue, RevenueBuilder, RevenueError, SETTLEMENT_STATE_MACHINE, SPLIT_STATE_MACHINE, SUBSCRIPTION_STATE_MACHINE, StateError, StateMachine, SubscriptionNotActiveError, SubscriptionNotFoundError, TRANSACTION_STATE_MACHINE, TransactionNotFoundError, ValidationError, all, auditPlugin, createEventBus, createRevenue, definePlugin, err, flatMap, isErr, isOk, isRetryable, isRevenueError, loggingPlugin, map, mapErr, match, metricsPlugin, ok, tryCatch, tryCatchSync, unwrap, unwrapOr };
|
|
3004
4590
|
//# sourceMappingURL=index.js.map
|
|
3005
4591
|
//# sourceMappingURL=index.js.map
|