@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.
Files changed (54) hide show
  1. package/README.md +581 -633
  2. package/dist/application/services/index.d.ts +6 -0
  3. package/dist/application/services/index.js +3288 -0
  4. package/dist/application/services/index.js.map +1 -0
  5. package/dist/core/events.d.ts +455 -0
  6. package/dist/core/events.js +122 -0
  7. package/dist/core/events.js.map +1 -0
  8. package/dist/core/index.d.ts +12 -889
  9. package/dist/core/index.js +2372 -786
  10. package/dist/core/index.js.map +1 -1
  11. package/dist/enums/index.d.ts +29 -8
  12. package/dist/enums/index.js +41 -8
  13. package/dist/enums/index.js.map +1 -1
  14. package/dist/escrow.enums-CE0VQsfe.d.ts +76 -0
  15. package/dist/{index-BnEXsnLJ.d.ts → index-DxIK0UmZ.d.ts} +281 -26
  16. package/dist/index-EnfKzDbs.d.ts +806 -0
  17. package/dist/{index-C5SsOrV0.d.ts → index-cLJBLUvx.d.ts} +55 -111
  18. package/dist/index.d.ts +16 -16
  19. package/dist/index.js +2558 -2192
  20. package/dist/index.js.map +1 -1
  21. package/dist/infrastructure/plugins/index.d.ts +267 -0
  22. package/dist/infrastructure/plugins/index.js +292 -0
  23. package/dist/infrastructure/plugins/index.js.map +1 -0
  24. package/dist/money-widWVD7r.d.ts +111 -0
  25. package/dist/payment.enums-C1BiGlRa.d.ts +69 -0
  26. package/dist/plugin-Bb9HOE10.d.ts +336 -0
  27. package/dist/providers/index.d.ts +19 -6
  28. package/dist/providers/index.js +22 -3
  29. package/dist/providers/index.js.map +1 -1
  30. package/dist/reconciliation/index.d.ts +215 -0
  31. package/dist/reconciliation/index.js +140 -0
  32. package/dist/reconciliation/index.js.map +1 -0
  33. package/dist/{retry-80lBCmSe.d.ts → retry-D4hFUwVk.d.ts} +1 -41
  34. package/dist/schemas/index.d.ts +1653 -49
  35. package/dist/schemas/index.js +233 -19
  36. package/dist/schemas/index.js.map +1 -1
  37. package/dist/schemas/validation.d.ts +4 -4
  38. package/dist/schemas/validation.js +16 -15
  39. package/dist/schemas/validation.js.map +1 -1
  40. package/dist/settlement.enums-ByC1x0ye.d.ts +130 -0
  41. package/dist/settlement.schema-CpamV7ZY.d.ts +343 -0
  42. package/dist/split.enums-DG3TxQf9.d.ts +42 -0
  43. package/dist/tax-CV8A0sxl.d.ts +60 -0
  44. package/dist/utils/index.d.ts +487 -13
  45. package/dist/utils/index.js +351 -289
  46. package/dist/utils/index.js.map +1 -1
  47. package/package.json +22 -9
  48. package/dist/actions-Ctf2XUL-.d.ts +0 -519
  49. package/dist/payment.enums-B_RwB8iR.d.ts +0 -184
  50. package/dist/services/index.d.ts +0 -3
  51. package/dist/services/index.js +0 -1702
  52. package/dist/services/index.js.map +0 -1
  53. package/dist/split.schema-DLVF3XBI.d.ts +0 -1122
  54. package/dist/transaction.enums-7uBnuswI.d.ts +0 -87
@@ -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
- ...payload,
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.before": async (ctx, input, next) => {
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", { transactionId: result?.transactionId });
329
+ ctx.logger[level]("Payment created", { paymentIntentId: result?.paymentIntentId });
331
330
  return result;
332
331
  },
333
- "payment.verify.before": async (ctx, input, next) => {
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.before": async (ctx, input, next) => {
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
- const json = JSON.stringify(params, Object.keys(params).sort());
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 TRANSACTION_TYPE = {
1086
- INCOME: "income",
1087
- EXPENSE: "expense"
1084
+ var TRANSACTION_FLOW = {
1085
+ INFLOW: "inflow",
1086
+ OUTFLOW: "outflow"
1088
1087
  };
1089
- var TRANSACTION_TYPE_VALUES = Object.values(
1090
- TRANSACTION_TYPE
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(TRANSACTION_TYPE_VALUES);
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 * 100) / 100;
1153
- const gatewayFeeAmount = Math.round(amount * gatewayFeeRate * 100) / 100;
1154
- const netAmount = Math.max(0, Math.round((grossAmount - gatewayFeeAmount) * 100) / 100);
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 * 100) / 100;
1170
- const reversedGrossAmount = Math.round(originalCommission.grossAmount * refundRatio * 100) / 100;
1171
- const reversedGatewayFee = Math.round(originalCommission.gatewayFeeAmount * refundRatio * 100) / 100;
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/services/monetization.service.ts
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
- hooks;
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.hooks = container.get("hooks");
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
- * referenceId: order._id,
1220
- * referenceModel: 'Order',
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
- * referenceId: subscription._id,
1234
- * referenceModel: 'Subscription',
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 (!planKey) {
1258
- throw new MissingRequiredFieldError("planKey");
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 < 0) {
1261
- throw new InvalidAmountError(amount);
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 transactionType = this.config.transactionTypeMapping?.subscription_renewal ?? this.config.transactionTypeMapping?.subscription ?? this.config.transactionTypeMapping?.[effectiveMonetizationType] ?? TRANSACTION_TYPE.INCOME;
1455
- const commissionRate = this.config.commissionRates?.[category] ?? 0;
1456
- const gatewayFeeRate = this.config.gatewayFeeRates?.[gateway] ?? 0;
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
- category,
1465
- type: transactionType,
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
- type: gateway,
2136
+ provider: gateway,
1470
2137
  sessionId: paymentIntent.sessionId,
1471
2138
  paymentIntentId: paymentIntent.paymentIntentId,
1472
- provider: paymentIntent.provider,
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
- // Only include if commission exists
1481
- // Polymorphic reference to subscription
1482
- referenceId: subscription._id,
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._triggerHook("subscription.renewed", {
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
- const { immediate = false, reason = null } = options;
1520
- if (!this.models.Subscription) {
1521
- throw new ModelNotRegisteredError("Subscription");
1522
- }
1523
- const SubscriptionModel = this.models.Subscription;
1524
- const subscription = await SubscriptionModel.findById(subscriptionId);
1525
- if (!subscription) {
1526
- throw new SubscriptionNotFoundError(subscriptionId);
1527
- }
1528
- const now = /* @__PURE__ */ new Date();
1529
- if (immediate) {
1530
- subscription.isActive = false;
1531
- subscription.status = "cancelled";
1532
- subscription.canceledAt = now;
1533
- subscription.cancellationReason = reason;
1534
- } else {
1535
- subscription.cancelAt = subscription.endDate ?? now;
1536
- subscription.cancellationReason = reason;
1537
- }
1538
- await subscription.save();
1539
- this._triggerHook("subscription.cancelled", {
1540
- subscription,
1541
- immediate,
1542
- reason,
1543
- canceledAt: immediate ? now : subscription.cancelAt
1544
- });
1545
- return subscription;
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
- const { reason = null } = options;
1556
- if (!this.models.Subscription) {
1557
- throw new ModelNotRegisteredError("Subscription");
1558
- }
1559
- const SubscriptionModel = this.models.Subscription;
1560
- const subscription = await SubscriptionModel.findById(subscriptionId);
1561
- if (!subscription) {
1562
- throw new SubscriptionNotFoundError(subscriptionId);
1563
- }
1564
- if (!subscription.isActive) {
1565
- throw new SubscriptionNotActiveError(subscriptionId, "Only active subscriptions can be paused");
1566
- }
1567
- const pausedAt = /* @__PURE__ */ new Date();
1568
- subscription.isActive = false;
1569
- subscription.status = "paused";
1570
- subscription.pausedAt = pausedAt;
1571
- subscription.pauseReason = reason;
1572
- await subscription.save();
1573
- this._triggerHook("subscription.paused", {
1574
- subscription,
1575
- reason,
1576
- pausedAt
1577
- });
1578
- return subscription;
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
- const { extendPeriod = false } = options;
1589
- if (!this.models.Subscription) {
1590
- throw new ModelNotRegisteredError("Subscription");
1591
- }
1592
- const SubscriptionModel = this.models.Subscription;
1593
- const subscription = await SubscriptionModel.findById(subscriptionId);
1594
- if (!subscription) {
1595
- throw new SubscriptionNotFoundError(subscriptionId);
1596
- }
1597
- if (!subscription.pausedAt) {
1598
- throw new InvalidStateTransitionError(
1599
- "resume",
1600
- "paused",
1601
- subscription.status,
1602
- "Only paused subscriptions can be resumed"
1603
- );
1604
- }
1605
- const now = /* @__PURE__ */ new Date();
1606
- const pausedAt = new Date(subscription.pausedAt);
1607
- const pauseDuration = now.getTime() - pausedAt.getTime();
1608
- subscription.isActive = true;
1609
- subscription.status = "active";
1610
- subscription.pausedAt = null;
1611
- subscription.pauseReason = null;
1612
- if (extendPeriod && subscription.endDate) {
1613
- const currentEnd = new Date(subscription.endDate);
1614
- subscription.endDate = new Date(currentEnd.getTime() + pauseDuration);
1615
- }
1616
- await subscription.save();
1617
- this._triggerHook("subscription.resumed", {
1618
- subscription,
1619
- extendPeriod,
1620
- pauseDuration,
1621
- resumedAt: now
1622
- });
1623
- return subscription;
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
- hooks;
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.hooks = container.get("hooks");
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
- const { verifiedBy = null } = options;
1712
- const TransactionModel = this.models.Transaction;
1713
- const transaction = await this._findTransaction(TransactionModel, paymentIntentId);
1714
- if (!transaction) {
1715
- throw new TransactionNotFoundError(paymentIntentId);
1716
- }
1717
- if (transaction.status === "verified" || transaction.status === "completed") {
1718
- throw new AlreadyVerifiedError(transaction._id.toString());
1719
- }
1720
- const gatewayType = transaction.gateway?.type ?? "manual";
1721
- const provider = this.providers[gatewayType];
1722
- if (!provider) {
1723
- throw new ProviderNotFoundError(gatewayType, Object.keys(this.providers));
1724
- }
1725
- let paymentResult = null;
1726
- try {
1727
- paymentResult = await provider.verifyPayment(paymentIntentId);
1728
- } catch (error) {
1729
- this.logger.error("Payment verification failed:", error);
1730
- transaction.status = "failed";
1731
- transaction.failureReason = error.message;
1732
- transaction.metadata = {
1733
- ...transaction.metadata,
1734
- verificationError: error.message,
1735
- failedAt: (/* @__PURE__ */ new Date()).toISOString()
1736
- };
1737
- await transaction.save();
1738
- this._triggerHook("payment.failed", {
1739
- transaction,
1740
- error: error.message,
1741
- provider: gatewayType,
1742
- paymentIntentId
1743
- });
1744
- throw new PaymentVerificationError(paymentIntentId, error.message);
1745
- }
1746
- if (paymentResult.amount && paymentResult.amount !== transaction.amount) {
1747
- throw new ValidationError(
1748
- `Amount mismatch: expected ${transaction.amount}, got ${paymentResult.amount}`,
1749
- { expected: transaction.amount, actual: paymentResult.amount }
1750
- );
1751
- }
1752
- if (paymentResult.currency && paymentResult.currency.toUpperCase() !== transaction.currency.toUpperCase()) {
1753
- throw new ValidationError(
1754
- `Currency mismatch: expected ${transaction.currency}, got ${paymentResult.currency}`,
1755
- { expected: transaction.currency, actual: paymentResult.currency }
1756
- );
1757
- }
1758
- transaction.status = paymentResult.status === "succeeded" ? "verified" : paymentResult.status;
1759
- transaction.verifiedAt = paymentResult.paidAt ?? /* @__PURE__ */ new Date();
1760
- transaction.verifiedBy = verifiedBy;
1761
- transaction.gateway = {
1762
- ...transaction.gateway,
1763
- type: transaction.gateway?.type ?? "manual",
1764
- verificationData: paymentResult.metadata
1765
- };
1766
- await transaction.save();
1767
- this._triggerHook("payment.verified", {
1768
- transaction,
1769
- paymentResult,
1770
- verifiedBy
1771
- });
1772
- return {
1773
- transaction,
1774
- paymentResult,
1775
- status: transaction.status
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
- paymentResult = await provider.getStatus(paymentIntentId);
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
- const { reason = null } = options;
1823
- const TransactionModel = this.models.Transaction;
1824
- const transaction = await this._findTransaction(TransactionModel, paymentId);
1825
- if (!transaction) {
1826
- throw new TransactionNotFoundError(paymentId);
1827
- }
1828
- if (transaction.status !== "verified" && transaction.status !== "completed") {
1829
- throw new RefundError(transaction._id.toString(), "Only verified/completed transactions can be refunded");
1830
- }
1831
- const gatewayType = transaction.gateway?.type ?? "manual";
1832
- const provider = this.providers[gatewayType];
1833
- if (!provider) {
1834
- throw new ProviderNotFoundError(gatewayType, Object.keys(this.providers));
1835
- }
1836
- const capabilities = provider.getCapabilities();
1837
- if (!capabilities.supportsRefunds) {
1838
- throw new RefundNotSupportedError(gatewayType);
1839
- }
1840
- const refundedSoFar = transaction.refundedAmount ?? 0;
1841
- const refundableAmount = transaction.amount - refundedSoFar;
1842
- const refundAmount = amount ?? refundableAmount;
1843
- if (refundAmount <= 0) {
1844
- throw new ValidationError(`Refund amount must be positive, got ${refundAmount}`);
1845
- }
1846
- if (refundAmount > refundableAmount) {
1847
- throw new ValidationError(
1848
- `Refund amount (${refundAmount}) exceeds refundable balance (${refundableAmount})`,
1849
- { refundAmount, refundableAmount, alreadyRefunded: refundedSoFar }
1850
- );
1851
- }
1852
- let refundResult;
1853
- try {
1854
- refundResult = await provider.refund(paymentId, refundAmount, { reason: reason ?? void 0 });
1855
- } catch (error) {
1856
- this.logger.error("Refund failed:", error);
1857
- throw new RefundError(paymentId, error.message);
1858
- }
1859
- const refundTransactionType = this.config.transactionTypeMapping?.refund ?? TRANSACTION_TYPE.EXPENSE;
1860
- const refundCommission = transaction.commission ? reverseCommission(transaction.commission, transaction.amount, refundAmount) : null;
1861
- const refundTransaction = await TransactionModel.create({
1862
- organizationId: transaction.organizationId,
1863
- customerId: transaction.customerId,
1864
- amount: refundAmount,
1865
- currency: transaction.currency,
1866
- category: transaction.category,
1867
- type: refundTransactionType,
1868
- // EXPENSE - money going out
1869
- method: transaction.method ?? "manual",
1870
- status: "completed",
1871
- gateway: {
1872
- type: transaction.gateway?.type ?? "manual",
1873
- paymentIntentId: refundResult.id,
1874
- provider: refundResult.provider
1875
- },
1876
- paymentDetails: transaction.paymentDetails,
1877
- ...refundCommission && { commission: refundCommission },
1878
- // Reversed commission
1879
- // Polymorphic reference (copy from original transaction)
1880
- ...transaction.referenceId && { referenceId: transaction.referenceId },
1881
- ...transaction.referenceModel && { referenceModel: transaction.referenceModel },
1882
- metadata: {
1883
- ...transaction.metadata,
1884
- isRefund: true,
1885
- originalTransactionId: transaction._id.toString(),
1886
- refundReason: reason,
1887
- refundResult: refundResult.metadata
1888
- },
1889
- idempotencyKey: `refund_${transaction._id}_${Date.now()}`
1890
- });
1891
- const isPartialRefund = refundAmount < transaction.amount;
1892
- transaction.status = isPartialRefund ? "partially_refunded" : "refunded";
1893
- transaction.refundedAmount = (transaction.refundedAmount ?? 0) + refundAmount;
1894
- transaction.refundedAt = refundResult.refundedAt ?? /* @__PURE__ */ new Date();
1895
- transaction.metadata = {
1896
- ...transaction.metadata,
1897
- refundTransactionId: refundTransaction._id.toString(),
1898
- refundReason: reason
1899
- };
1900
- await transaction.save();
1901
- this._triggerHook("payment.refunded", {
1902
- transaction,
1903
- refundTransaction,
1904
- refundResult,
1905
- refundAmount,
1906
- reason,
1907
- isPartialRefund
1908
- });
1909
- return {
1910
- transaction,
1911
- refundTransaction,
1912
- refundResult,
1913
- status: transaction.status
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 provider.handleWebhook(payload, headers);
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
- transaction.status = "verified";
2007
- transaction.verifiedAt = webhookEvent.createdAt;
2912
+ newStatus = "verified";
2008
2913
  } else if (webhookEvent.type === "payment.failed") {
2009
- transaction.status = "failed";
2914
+ newStatus = "failed";
2010
2915
  } else if (webhookEvent.type === "refund.succeeded") {
2011
- transaction.status = "refunded";
2012
- transaction.refundedAt = webhookEvent.createdAt;
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._triggerHook(`payment.webhook.${webhookEvent.type}`, {
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
- hooks;
3018
+ plugins;
3019
+ events;
2096
3020
  logger;
2097
3021
  constructor(container) {
2098
3022
  this.models = container.get("models");
2099
- this.hooks = container.get("hooks");
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 TransactionModel = this.models.Transaction;
2159
- const model = TransactionModel;
2160
- let transaction;
2161
- if (typeof model.update === "function") {
2162
- transaction = await model.update(transactionId, updates);
2163
- } else if (typeof model.findByIdAndUpdate === "function") {
2164
- transaction = await model.findByIdAndUpdate(
2165
- transactionId,
2166
- { $set: updates },
2167
- { new: true }
2168
- );
2169
- } else {
2170
- throw new Error("Transaction model does not support update operations");
2171
- }
2172
- if (!transaction) {
2173
- throw new TransactionNotFoundError(transactionId);
2174
- }
2175
- this._triggerHook("transaction.updated", {
2176
- transaction,
2177
- updates
2178
- });
2179
- return transaction;
2180
- }
2181
- /**
2182
- * Trigger event hook (fire-and-forget, non-blocking)
2183
- * @private
2184
- */
2185
- _triggerHook(event, data) {
2186
- triggerHook(this.hooks, event, data, this.logger);
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/enums/escrow.enums.ts
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 * 100) / 100;
2273
- const gatewayFeeAmount = index === 0 && gatewayFeeRate > 0 ? Math.round(amount * gatewayFeeRate * 100) / 100 : 0;
2274
- const netAmount = Math.max(0, Math.round((grossAmount - gatewayFeeAmount) * 100) / 100);
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, Math.round((amount - totalSplitAmount) * 100) / 100);
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
- hooks;
3182
+ plugins;
2299
3183
  logger;
3184
+ events;
2300
3185
  constructor(container) {
2301
3186
  this.models = container.get("models");
2302
- this.hooks = container.get("hooks");
3187
+ this.plugins = container.get("plugins");
2303
3188
  this.logger = container.get("logger");
3189
+ this.events = container.get("events");
2304
3190
  }
2305
3191
  /**
2306
- * Hold funds in escrow
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
- async hold(transactionId, options = {}) {
2313
- const {
2314
- reason = HOLD_REASON.PAYMENT_VERIFICATION,
2315
- holdUntil = null,
2316
- metadata = {}
2317
- } = options;
2318
- const TransactionModel = this.models.Transaction;
2319
- const transaction = await TransactionModel.findById(transactionId);
2320
- if (!transaction) {
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
- await transaction.save();
2337
- this._triggerHook("escrow.held", {
2338
- transaction,
2339
- heldAmount: transaction.amount,
2340
- reason
2341
- });
2342
- return transaction;
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
- const {
2353
- amount = null,
2354
- recipientId,
2355
- recipientType = "organization",
2356
- reason = RELEASE_REASON.PAYMENT_VERIFIED,
2357
- releasedBy = null,
2358
- createTransaction = true,
2359
- metadata = {}
2360
- } = options;
2361
- const TransactionModel = this.models.Transaction;
2362
- const transaction = await TransactionModel.findById(transactionId);
2363
- if (!transaction) {
2364
- throw new TransactionNotFoundError(transactionId);
2365
- }
2366
- if (!transaction.hold || transaction.hold.status !== HOLD_STATUS.HELD) {
2367
- throw new Error(`Transaction is not in held status. Current: ${transaction.hold?.status ?? "none"}`);
2368
- }
2369
- if (!recipientId) {
2370
- throw new Error("recipientId is required for release");
2371
- }
2372
- const releaseAmount = amount ?? transaction.hold.heldAmount - transaction.hold.releasedAmount;
2373
- const availableAmount = transaction.hold.heldAmount - transaction.hold.releasedAmount;
2374
- if (releaseAmount > availableAmount) {
2375
- throw new Error(`Release amount (${releaseAmount}) exceeds available held amount (${availableAmount})`);
2376
- }
2377
- const releaseRecord = {
2378
- amount: releaseAmount,
2379
- recipientId,
2380
- recipientType,
2381
- releasedAt: /* @__PURE__ */ new Date(),
2382
- releasedBy,
2383
- reason,
2384
- metadata
2385
- };
2386
- transaction.hold.releases.push(releaseRecord);
2387
- transaction.hold.releasedAmount += releaseAmount;
2388
- const isFullRelease = transaction.hold.releasedAmount >= transaction.hold.heldAmount;
2389
- const isPartialRelease = transaction.hold.releasedAmount > 0 && transaction.hold.releasedAmount < transaction.hold.heldAmount;
2390
- if (isFullRelease) {
2391
- transaction.hold.status = HOLD_STATUS.RELEASED;
2392
- transaction.hold.releasedAt = /* @__PURE__ */ new Date();
2393
- transaction.status = TRANSACTION_STATUS.COMPLETED;
2394
- } else if (isPartialRelease) {
2395
- transaction.hold.status = HOLD_STATUS.PARTIALLY_RELEASED;
2396
- }
2397
- await transaction.save();
2398
- let releaseTransaction = null;
2399
- if (createTransaction) {
2400
- releaseTransaction = await TransactionModel.create({
2401
- organizationId: transaction.organizationId,
2402
- customerId: recipientId,
2403
- amount: releaseAmount,
2404
- currency: transaction.currency,
2405
- category: transaction.category,
2406
- type: TRANSACTION_TYPE.INCOME,
2407
- method: transaction.method,
2408
- status: TRANSACTION_STATUS.COMPLETED,
2409
- gateway: transaction.gateway,
2410
- referenceId: transaction.referenceId,
2411
- referenceModel: transaction.referenceModel,
2412
- metadata: {
2413
- ...metadata,
2414
- isRelease: true,
2415
- heldTransactionId: transaction._id.toString(),
2416
- releaseReason: reason,
2417
- recipientType
2418
- },
2419
- idempotencyKey: `release_${transaction._id}_${Date.now()}`
2420
- });
2421
- }
2422
- this._triggerHook("escrow.released", {
2423
- transaction,
2424
- releaseTransaction,
2425
- releaseAmount,
2426
- recipientId,
2427
- recipientType,
2428
- reason,
2429
- isFullRelease,
2430
- isPartialRelease
2431
- });
2432
- return {
2433
- transaction,
2434
- releaseTransaction,
2435
- releaseAmount,
2436
- isFullRelease,
2437
- isPartialRelease
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 Error(`Transaction is not in held status. Current: ${transaction.hold?.status ?? "none"}`);
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._triggerHook("escrow.cancelled", {
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 Error(`Transaction must be held before splitting. Current: ${transaction.hold?.status ?? "none"}`);
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 Error("splitRules cannot be empty");
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
- for (const split of splits) {
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
- amount: split.netAmount,
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
- category: split.type,
2507
- type: TRANSACTION_TYPE.EXPENSE,
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: TRANSACTION_STATUS.COMPLETED,
3583
+ status: "completed",
2510
3584
  gateway: transaction.gateway,
2511
- referenceId: transaction.referenceId,
2512
- referenceModel: transaction.referenceModel,
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
- grossAmount: split.grossAmount,
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: organizationPayout,
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._triggerHook("escrow.split", {
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
- _triggerHook(event, data) {
2578
- triggerHook(this.hooks, event, data, this.logger);
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
- this._circuitBreaker = createCircuitBreaker();
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.succeeded', (event) => {
2701
- * console.log('Payment:', event.transactionId);
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 = { Transaction: model };
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-100)
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
- defaultCurrency: resolvedOptions.defaultCurrency,
2962
- commissionRate: resolvedOptions.commissionRate,
2963
- gatewayFeeRate: resolvedOptions.gatewayFeeRate,
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