@classytic/revenue 1.0.2 → 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 (53) hide show
  1. package/README.md +603 -486
  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 +2361 -705
  10. package/dist/core/index.js.map +1 -1
  11. package/dist/enums/index.d.ts +54 -25
  12. package/dist/enums/index.js +143 -14
  13. package/dist/enums/index.js.map +1 -1
  14. package/dist/escrow.enums-CE0VQsfe.d.ts +76 -0
  15. package/dist/{index-BnJWVXuw.d.ts → index-DxIK0UmZ.d.ts} +281 -26
  16. package/dist/index-EnfKzDbs.d.ts +806 -0
  17. package/dist/{index-ChVD3P9k.d.ts → index-cLJBLUvx.d.ts} +55 -81
  18. package/dist/index.d.ts +16 -15
  19. package/dist/index.js +2583 -2066
  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 +1927 -166
  35. package/dist/schemas/index.js +357 -40
  36. package/dist/schemas/index.js.map +1 -1
  37. package/dist/schemas/validation.d.ts +87 -12
  38. package/dist/schemas/validation.js +71 -17
  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 +370 -235
  46. package/dist/utils/index.js.map +1 -1
  47. package/package.json +27 -13
  48. package/dist/actions-CwG-b7fR.d.ts +0 -519
  49. package/dist/services/index.d.ts +0 -3
  50. package/dist/services/index.js +0 -1632
  51. package/dist/services/index.js.map +0 -1
  52. package/dist/split.enums-Bh24jw8p.d.ts +0 -255
  53. package/dist/split.schema-DYVP7Wu2.d.ts +0 -958
@@ -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,43 +1080,44 @@ 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
  };
1088
+ var TRANSACTION_FLOW_VALUES = Object.values(
1089
+ TRANSACTION_FLOW
1090
+ );
1089
1091
  var TRANSACTION_STATUS = {
1092
+ PENDING: "pending",
1093
+ PAYMENT_INITIATED: "payment_initiated",
1094
+ PROCESSING: "processing",
1095
+ REQUIRES_ACTION: "requires_action",
1090
1096
  VERIFIED: "verified",
1091
1097
  COMPLETED: "completed",
1092
- CANCELLED: "cancelled"};
1098
+ FAILED: "failed",
1099
+ CANCELLED: "cancelled",
1100
+ EXPIRED: "expired",
1101
+ REFUNDED: "refunded",
1102
+ PARTIALLY_REFUNDED: "partially_refunded"
1103
+ };
1104
+ var TRANSACTION_STATUS_VALUES = Object.values(
1105
+ TRANSACTION_STATUS
1106
+ );
1093
1107
  var LIBRARY_CATEGORIES = {
1094
1108
  SUBSCRIPTION: "subscription",
1095
1109
  PURCHASE: "purchase"
1096
1110
  };
1111
+ var LIBRARY_CATEGORY_VALUES = Object.values(
1112
+ LIBRARY_CATEGORIES
1113
+ );
1114
+ new Set(TRANSACTION_FLOW_VALUES);
1115
+ new Set(
1116
+ TRANSACTION_STATUS_VALUES
1117
+ );
1118
+ new Set(LIBRARY_CATEGORY_VALUES);
1097
1119
 
1098
- // src/utils/category-resolver.ts
1120
+ // src/shared/utils/validators/category-resolver.ts
1099
1121
  function resolveCategory(entity, monetizationType, categoryMappings = {}) {
1100
1122
  if (entity && categoryMappings[entity]) {
1101
1123
  return categoryMappings[entity];
@@ -1112,7 +1134,7 @@ function resolveCategory(entity, monetizationType, categoryMappings = {}) {
1112
1134
  }
1113
1135
  }
1114
1136
 
1115
- // src/utils/commission.ts
1137
+ // src/shared/utils/calculators/commission.ts
1116
1138
  function calculateCommission(amount, commissionRate, gatewayFeeRate = 0) {
1117
1139
  if (!commissionRate || commissionRate <= 0) {
1118
1140
  return null;
@@ -1126,9 +1148,9 @@ function calculateCommission(amount, commissionRate, gatewayFeeRate = 0) {
1126
1148
  if (gatewayFeeRate < 0 || gatewayFeeRate > 1) {
1127
1149
  throw new Error("Gateway fee rate must be between 0 and 1");
1128
1150
  }
1129
- const grossAmount = Math.round(amount * commissionRate * 100) / 100;
1130
- const gatewayFeeAmount = Math.round(amount * gatewayFeeRate * 100) / 100;
1131
- 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);
1132
1154
  return {
1133
1155
  rate: commissionRate,
1134
1156
  grossAmount,
@@ -1142,10 +1164,22 @@ function reverseCommission(originalCommission, originalAmount, refundAmount) {
1142
1164
  if (!originalCommission?.netAmount) {
1143
1165
  return null;
1144
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
+ }
1145
1179
  const refundRatio = refundAmount / originalAmount;
1146
- const reversedNetAmount = Math.round(originalCommission.netAmount * refundRatio * 100) / 100;
1147
- const reversedGrossAmount = Math.round(originalCommission.grossAmount * refundRatio * 100) / 100;
1148
- 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);
1149
1183
  return {
1150
1184
  rate: originalCommission.rate,
1151
1185
  grossAmount: reversedGrossAmount,
@@ -1157,26 +1191,566 @@ function reverseCommission(originalCommission, originalAmount, refundAmount) {
1157
1191
  };
1158
1192
  }
1159
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
+
1160
1229
  // src/enums/monetization.enums.ts
1161
1230
  var MONETIZATION_TYPES = {
1162
1231
  FREE: "free",
1163
1232
  PURCHASE: "purchase",
1164
1233
  SUBSCRIPTION: "subscription"
1165
1234
  };
1235
+ var MONETIZATION_TYPE_VALUES = Object.values(
1236
+ MONETIZATION_TYPES
1237
+ );
1238
+ new Set(MONETIZATION_TYPE_VALUES);
1239
+
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
+ }
1166
1698
 
1167
- // src/services/monetization.service.ts
1699
+ // src/application/services/monetization.service.ts
1168
1700
  var MonetizationService = class {
1169
1701
  models;
1170
1702
  providers;
1171
1703
  config;
1172
- hooks;
1704
+ plugins;
1173
1705
  logger;
1706
+ events;
1707
+ retryConfig;
1708
+ circuitBreaker;
1174
1709
  constructor(container) {
1175
1710
  this.models = container.get("models");
1176
1711
  this.providers = container.get("providers");
1177
1712
  this.config = container.get("config");
1178
- this.hooks = container.get("hooks");
1713
+ this.plugins = container.get("plugins");
1179
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();
1180
1754
  }
1181
1755
  /**
1182
1756
  * Create a new monetization (purchase, subscription, or free item)
@@ -1189,8 +1763,8 @@ var MonetizationService = class {
1189
1763
  * data: {
1190
1764
  * organizationId: '...',
1191
1765
  * customerId: '...',
1192
- * referenceId: order._id,
1193
- * referenceModel: 'Order',
1766
+ * sourceId: order._id,
1767
+ * sourceModel: 'Order',
1194
1768
  * },
1195
1769
  * planKey: 'one_time',
1196
1770
  * monetizationType: 'purchase',
@@ -1203,8 +1777,8 @@ var MonetizationService = class {
1203
1777
  * data: {
1204
1778
  * organizationId: '...',
1205
1779
  * customerId: '...',
1206
- * referenceId: subscription._id,
1207
- * referenceModel: 'Subscription',
1780
+ * sourceId: subscription._id,
1781
+ * sourceModel: 'Subscription',
1208
1782
  * },
1209
1783
  * planKey: 'monthly',
1210
1784
  * monetizationType: 'subscription',
@@ -1215,132 +1789,217 @@ var MonetizationService = class {
1215
1789
  * @returns Result with subscription, transaction, and paymentIntent
1216
1790
  */
1217
1791
  async create(params) {
1218
- const {
1219
- data,
1220
- planKey,
1221
- amount,
1222
- currency = "BDT",
1223
- gateway = "manual",
1224
- entity = null,
1225
- monetizationType = MONETIZATION_TYPES.SUBSCRIPTION,
1226
- paymentData,
1227
- metadata = {},
1228
- idempotencyKey = null
1229
- } = params;
1230
- if (!planKey) {
1231
- throw new MissingRequiredFieldError("planKey");
1232
- }
1233
- if (amount < 0) {
1234
- throw new InvalidAmountError(amount);
1235
- }
1236
- const isFree = amount === 0;
1237
- const provider = this.providers[gateway];
1238
- if (!provider) {
1239
- throw new ProviderNotFoundError(gateway, Object.keys(this.providers));
1240
- }
1241
- let paymentIntent = null;
1242
- let transaction = null;
1243
- if (!isFree) {
1244
- try {
1245
- paymentIntent = await provider.createIntent({
1792
+ return this.plugins.executeHook(
1793
+ "monetization.create.before",
1794
+ this.getPluginContext(params.idempotencyKey),
1795
+ params,
1796
+ async () => {
1797
+ const {
1798
+ data,
1799
+ planKey,
1246
1800
  amount,
1247
- currency,
1248
- metadata: {
1249
- ...metadata,
1250
- type: "subscription",
1251
- planKey
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);
1252
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
1253
1964
  });
1254
- } catch (error) {
1255
- throw new PaymentIntentCreationError(gateway, error);
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
+ );
1256
2001
  }
1257
- const category = resolveCategory(entity, monetizationType, this.config.categoryMappings);
1258
- const transactionType = this.config.transactionTypeMapping?.subscription ?? this.config.transactionTypeMapping?.[monetizationType] ?? TRANSACTION_TYPE.INCOME;
1259
- const commissionRate = this.config.commissionRates?.[category] ?? 0;
1260
- const gatewayFeeRate = this.config.gatewayFeeRates?.[gateway] ?? 0;
1261
- const commission = calculateCommission(amount, commissionRate, gatewayFeeRate);
1262
- const TransactionModel = this.models.Transaction;
1263
- transaction = await TransactionModel.create({
1264
- organizationId: data.organizationId,
1265
- customerId: data.customerId ?? null,
1266
- amount,
1267
- currency,
1268
- category,
1269
- type: transactionType,
1270
- method: paymentData?.method ?? "manual",
1271
- status: paymentIntent.status === "succeeded" ? "verified" : "pending",
1272
- gateway: {
1273
- type: gateway,
1274
- sessionId: paymentIntent.sessionId,
1275
- paymentIntentId: paymentIntent.paymentIntentId,
1276
- provider: paymentIntent.provider,
1277
- metadata: paymentIntent.metadata
1278
- },
1279
- paymentDetails: {
1280
- provider: gateway,
1281
- ...paymentData
1282
- },
1283
- ...commission && { commission },
1284
- // Only include if commission exists
1285
- // Polymorphic reference (top-level, not metadata)
1286
- ...data.referenceId && { referenceId: data.referenceId },
1287
- ...data.referenceModel && { referenceModel: data.referenceModel },
1288
- metadata: {
1289
- ...metadata,
1290
- planKey,
1291
- entity,
1292
- monetizationType,
1293
- paymentIntentId: paymentIntent.id
1294
- },
1295
- idempotencyKey: idempotencyKey ?? `sub_${nanoid(16)}`
1296
- });
1297
- }
1298
- let subscription = null;
1299
- if (this.models.Subscription) {
1300
- const SubscriptionModel = this.models.Subscription;
1301
- const subscriptionData = {
1302
- organizationId: data.organizationId,
1303
- customerId: data.customerId ?? null,
1304
- planKey,
1305
- amount,
1306
- currency,
1307
- status: isFree ? "active" : "pending",
1308
- isActive: isFree,
1309
- gateway,
1310
- transactionId: transaction?._id ?? null,
1311
- paymentIntentId: paymentIntent?.id ?? null,
1312
- metadata: {
1313
- ...metadata,
1314
- isFree,
1315
- entity,
1316
- monetizationType
1317
- },
1318
- ...data
1319
- };
1320
- delete subscriptionData.referenceId;
1321
- delete subscriptionData.referenceModel;
1322
- subscription = await SubscriptionModel.create(subscriptionData);
1323
- }
1324
- const eventData = {
1325
- subscription,
1326
- transaction,
1327
- paymentIntent,
1328
- isFree,
1329
- monetizationType
1330
- };
1331
- if (monetizationType === MONETIZATION_TYPES.PURCHASE) {
1332
- this._triggerHook("purchase.created", eventData);
1333
- } else if (monetizationType === MONETIZATION_TYPES.SUBSCRIPTION) {
1334
- this._triggerHook("subscription.created", eventData);
1335
- } else if (monetizationType === MONETIZATION_TYPES.FREE) {
1336
- this._triggerHook("free.created", eventData);
1337
- }
1338
- this._triggerHook("monetization.created", eventData);
1339
- return {
1340
- subscription,
1341
- transaction,
1342
- paymentIntent
1343
- };
2002
+ );
1344
2003
  }
1345
2004
  /**
1346
2005
  * Activate subscription after payment verification
@@ -1350,31 +2009,54 @@ var MonetizationService = class {
1350
2009
  * @returns Updated subscription
1351
2010
  */
1352
2011
  async activate(subscriptionId, options = {}) {
1353
- const { timestamp = /* @__PURE__ */ new Date() } = options;
1354
- if (!this.models.Subscription) {
1355
- throw new ModelNotRegisteredError("Subscription");
1356
- }
1357
- const SubscriptionModel = this.models.Subscription;
1358
- const subscription = await SubscriptionModel.findById(subscriptionId);
1359
- if (!subscription) {
1360
- throw new SubscriptionNotFoundError(subscriptionId);
1361
- }
1362
- if (subscription.isActive) {
1363
- this.logger.warn("Subscription already active", { subscriptionId });
1364
- return subscription;
1365
- }
1366
- const periodEnd = this._calculatePeriodEnd(subscription.planKey, timestamp);
1367
- subscription.isActive = true;
1368
- subscription.status = "active";
1369
- subscription.startDate = timestamp;
1370
- subscription.endDate = periodEnd;
1371
- subscription.activatedAt = timestamp;
1372
- await subscription.save();
1373
- this._triggerHook("subscription.activated", {
1374
- subscription,
1375
- activatedAt: timestamp
1376
- });
1377
- return subscription;
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
+ );
1378
2060
  }
1379
2061
  /**
1380
2062
  * Renew subscription
@@ -1424,36 +2106,48 @@ var MonetizationService = class {
1424
2106
  const effectiveEntity = entity ?? subscription.metadata?.entity;
1425
2107
  const effectiveMonetizationType = subscription.metadata?.monetizationType ?? MONETIZATION_TYPES.SUBSCRIPTION;
1426
2108
  const category = resolveCategory(effectiveEntity, effectiveMonetizationType, this.config.categoryMappings);
1427
- const transactionType = this.config.transactionTypeMapping?.subscription_renewal ?? this.config.transactionTypeMapping?.subscription ?? this.config.transactionTypeMapping?.[effectiveMonetizationType] ?? TRANSACTION_TYPE.INCOME;
1428
- const commissionRate = this.config.commissionRates?.[category] ?? 0;
1429
- 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);
1430
2112
  const commission = calculateCommission(subscription.amount, commissionRate, gatewayFeeRate);
2113
+ const feeAmount = commission?.gatewayFeeAmount || 0;
2114
+ const netAmount = subscription.amount - feeAmount;
1431
2115
  const TransactionModel = this.models.Transaction;
1432
2116
  const transaction = await TransactionModel.create({
1433
2117
  organizationId: subscription.organizationId,
1434
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
1435
2126
  amount: subscription.amount,
1436
2127
  currency: subscription.currency ?? "BDT",
1437
- category,
1438
- type: transactionType,
2128
+ fee: feeAmount,
2129
+ tax: 0,
2130
+ // Tax plugin would add this
2131
+ net: netAmount,
1439
2132
  method: paymentData?.method ?? "manual",
1440
2133
  status: paymentIntent.status === "succeeded" ? "verified" : "pending",
2134
+ // ✅ Map 'succeeded' to valid TransactionStatusValue
1441
2135
  gateway: {
1442
- type: gateway,
2136
+ provider: gateway,
1443
2137
  sessionId: paymentIntent.sessionId,
1444
2138
  paymentIntentId: paymentIntent.paymentIntentId,
1445
- provider: paymentIntent.provider,
2139
+ chargeId: paymentIntent.id,
1446
2140
  metadata: paymentIntent.metadata
1447
2141
  },
1448
2142
  paymentDetails: {
1449
2143
  provider: gateway,
1450
2144
  ...paymentData
1451
2145
  },
2146
+ // Commission (for marketplace/splits)
1452
2147
  ...commission && { commission },
1453
- // Only include if commission exists
1454
- // Polymorphic reference to subscription
1455
- referenceId: subscription._id,
1456
- referenceModel: "Subscription",
2148
+ // UNIFIED: Source reference (renamed from reference)
2149
+ sourceId: subscription._id,
2150
+ sourceModel: "Subscription",
1457
2151
  metadata: {
1458
2152
  ...metadata,
1459
2153
  subscriptionId: subscription._id.toString(),
@@ -1469,10 +2163,10 @@ var MonetizationService = class {
1469
2163
  subscription.renewalTransactionId = transaction._id;
1470
2164
  subscription.renewalCount = (subscription.renewalCount ?? 0) + 1;
1471
2165
  await subscription.save();
1472
- this._triggerHook("subscription.renewed", {
2166
+ this.events.emit("subscription.renewed", {
1473
2167
  subscription,
1474
2168
  transaction,
1475
- paymentIntent,
2169
+ paymentIntent: paymentIntent ?? void 0,
1476
2170
  renewalCount: subscription.renewalCount
1477
2171
  });
1478
2172
  return {
@@ -1489,33 +2183,56 @@ var MonetizationService = class {
1489
2183
  * @returns Updated subscription
1490
2184
  */
1491
2185
  async cancel(subscriptionId, options = {}) {
1492
- const { immediate = false, reason = null } = options;
1493
- if (!this.models.Subscription) {
1494
- throw new ModelNotRegisteredError("Subscription");
1495
- }
1496
- const SubscriptionModel = this.models.Subscription;
1497
- const subscription = await SubscriptionModel.findById(subscriptionId);
1498
- if (!subscription) {
1499
- throw new SubscriptionNotFoundError(subscriptionId);
1500
- }
1501
- const now = /* @__PURE__ */ new Date();
1502
- if (immediate) {
1503
- subscription.isActive = false;
1504
- subscription.status = "cancelled";
1505
- subscription.canceledAt = now;
1506
- subscription.cancellationReason = reason;
1507
- } else {
1508
- subscription.cancelAt = subscription.endDate ?? now;
1509
- subscription.cancellationReason = reason;
1510
- }
1511
- await subscription.save();
1512
- this._triggerHook("subscription.cancelled", {
1513
- subscription,
1514
- immediate,
1515
- reason,
1516
- canceledAt: immediate ? now : subscription.cancelAt
1517
- });
1518
- 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
+ );
1519
2236
  }
1520
2237
  /**
1521
2238
  * Pause subscription
@@ -1525,30 +2242,53 @@ var MonetizationService = class {
1525
2242
  * @returns Updated subscription
1526
2243
  */
1527
2244
  async pause(subscriptionId, options = {}) {
1528
- const { reason = null } = options;
1529
- if (!this.models.Subscription) {
1530
- throw new ModelNotRegisteredError("Subscription");
1531
- }
1532
- const SubscriptionModel = this.models.Subscription;
1533
- const subscription = await SubscriptionModel.findById(subscriptionId);
1534
- if (!subscription) {
1535
- throw new SubscriptionNotFoundError(subscriptionId);
1536
- }
1537
- if (!subscription.isActive) {
1538
- throw new SubscriptionNotActiveError(subscriptionId, "Only active subscriptions can be paused");
1539
- }
1540
- const pausedAt = /* @__PURE__ */ new Date();
1541
- subscription.isActive = false;
1542
- subscription.status = "paused";
1543
- subscription.pausedAt = pausedAt;
1544
- subscription.pauseReason = reason;
1545
- await subscription.save();
1546
- this._triggerHook("subscription.paused", {
1547
- subscription,
1548
- reason,
1549
- pausedAt
1550
- });
1551
- 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
+ );
1552
2292
  }
1553
2293
  /**
1554
2294
  * Resume subscription
@@ -1558,42 +2298,70 @@ var MonetizationService = class {
1558
2298
  * @returns Updated subscription
1559
2299
  */
1560
2300
  async resume(subscriptionId, options = {}) {
1561
- const { extendPeriod = false } = options;
1562
- if (!this.models.Subscription) {
1563
- throw new ModelNotRegisteredError("Subscription");
1564
- }
1565
- const SubscriptionModel = this.models.Subscription;
1566
- const subscription = await SubscriptionModel.findById(subscriptionId);
1567
- if (!subscription) {
1568
- throw new SubscriptionNotFoundError(subscriptionId);
1569
- }
1570
- if (!subscription.pausedAt) {
1571
- throw new InvalidStateTransitionError(
1572
- "resume",
1573
- "paused",
1574
- subscription.status,
1575
- "Only paused subscriptions can be resumed"
1576
- );
1577
- }
1578
- const now = /* @__PURE__ */ new Date();
1579
- const pausedAt = new Date(subscription.pausedAt);
1580
- const pauseDuration = now.getTime() - pausedAt.getTime();
1581
- subscription.isActive = true;
1582
- subscription.status = "active";
1583
- subscription.pausedAt = null;
1584
- subscription.pauseReason = null;
1585
- if (extendPeriod && subscription.endDate) {
1586
- const currentEnd = new Date(subscription.endDate);
1587
- subscription.endDate = new Date(currentEnd.getTime() + pauseDuration);
1588
- }
1589
- await subscription.save();
1590
- this._triggerHook("subscription.resumed", {
1591
- subscription,
1592
- extendPeriod,
1593
- pauseDuration,
1594
- resumedAt: now
1595
- });
1596
- 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
+ );
1597
2365
  }
1598
2366
  /**
1599
2367
  * List subscriptions with filters
@@ -1650,28 +2418,61 @@ var MonetizationService = class {
1650
2418
  }
1651
2419
  return end;
1652
2420
  }
1653
- /**
1654
- * Trigger event hook (fire-and-forget, non-blocking)
1655
- * @private
1656
- */
1657
- _triggerHook(event, data) {
1658
- triggerHook(this.hooks, event, data, this.logger);
1659
- }
1660
2421
  };
1661
-
1662
- // src/services/payment.service.ts
1663
2422
  var PaymentService = class {
1664
2423
  models;
1665
2424
  providers;
1666
2425
  config;
1667
- hooks;
2426
+ plugins;
1668
2427
  logger;
2428
+ events;
2429
+ retryConfig;
2430
+ circuitBreaker;
1669
2431
  constructor(container) {
1670
2432
  this.models = container.get("models");
1671
2433
  this.providers = container.get("providers");
1672
2434
  this.config = container.get("config");
1673
- this.hooks = container.get("hooks");
2435
+ this.plugins = container.get("plugins");
1674
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();
1675
2476
  }
1676
2477
  /**
1677
2478
  * Verify a payment
@@ -1681,72 +2482,132 @@ var PaymentService = class {
1681
2482
  * @returns { transaction, status }
1682
2483
  */
1683
2484
  async verify(paymentIntentId, options = {}) {
1684
- const { verifiedBy = null } = options;
1685
- const TransactionModel = this.models.Transaction;
1686
- const transaction = await this._findTransaction(TransactionModel, paymentIntentId);
1687
- if (!transaction) {
1688
- throw new TransactionNotFoundError(paymentIntentId);
1689
- }
1690
- if (transaction.status === "verified" || transaction.status === "completed") {
1691
- throw new AlreadyVerifiedError(transaction._id.toString());
1692
- }
1693
- const gatewayType = transaction.gateway?.type ?? "manual";
1694
- const provider = this.providers[gatewayType];
1695
- if (!provider) {
1696
- throw new ProviderNotFoundError(gatewayType, Object.keys(this.providers));
1697
- }
1698
- let paymentResult = null;
1699
- try {
1700
- paymentResult = await provider.verifyPayment(paymentIntentId);
1701
- } catch (error) {
1702
- this.logger.error("Payment verification failed:", error);
1703
- transaction.status = "failed";
1704
- transaction.failureReason = error.message;
1705
- transaction.metadata = {
1706
- ...transaction.metadata,
1707
- verificationError: error.message,
1708
- failedAt: (/* @__PURE__ */ new Date()).toISOString()
1709
- };
1710
- await transaction.save();
1711
- this._triggerHook("payment.failed", {
1712
- transaction,
1713
- error: error.message,
1714
- provider: gatewayType,
1715
- paymentIntentId
1716
- });
1717
- throw new PaymentVerificationError(paymentIntentId, error.message);
1718
- }
1719
- if (paymentResult.amount && paymentResult.amount !== transaction.amount) {
1720
- throw new ValidationError(
1721
- `Amount mismatch: expected ${transaction.amount}, got ${paymentResult.amount}`,
1722
- { expected: transaction.amount, actual: paymentResult.amount }
1723
- );
1724
- }
1725
- if (paymentResult.currency && paymentResult.currency.toUpperCase() !== transaction.currency.toUpperCase()) {
1726
- throw new ValidationError(
1727
- `Currency mismatch: expected ${transaction.currency}, got ${paymentResult.currency}`,
1728
- { expected: transaction.currency, actual: paymentResult.currency }
1729
- );
1730
- }
1731
- transaction.status = paymentResult.status === "succeeded" ? "verified" : paymentResult.status;
1732
- transaction.verifiedAt = paymentResult.paidAt ?? /* @__PURE__ */ new Date();
1733
- transaction.verifiedBy = verifiedBy;
1734
- transaction.gateway = {
1735
- ...transaction.gateway,
1736
- type: transaction.gateway?.type ?? "manual",
1737
- verificationData: paymentResult.metadata
1738
- };
1739
- await transaction.save();
1740
- this._triggerHook("payment.verified", {
1741
- transaction,
1742
- paymentResult,
1743
- verifiedBy
1744
- });
1745
- return {
1746
- transaction,
1747
- paymentResult,
1748
- status: transaction.status
1749
- };
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
+ );
1750
2611
  }
1751
2612
  /**
1752
2613
  * Get payment status
@@ -1767,7 +2628,11 @@ var PaymentService = class {
1767
2628
  }
1768
2629
  let paymentResult = null;
1769
2630
  try {
1770
- 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
+ );
1771
2636
  } catch (error) {
1772
2637
  this.logger.warn("Failed to get payment status from provider:", error);
1773
2638
  return {
@@ -1792,99 +2657,163 @@ var PaymentService = class {
1792
2657
  * @returns { transaction, refundResult }
1793
2658
  */
1794
2659
  async refund(paymentId, amount = null, options = {}) {
1795
- const { reason = null } = options;
1796
- const TransactionModel = this.models.Transaction;
1797
- const transaction = await this._findTransaction(TransactionModel, paymentId);
1798
- if (!transaction) {
1799
- throw new TransactionNotFoundError(paymentId);
1800
- }
1801
- if (transaction.status !== "verified" && transaction.status !== "completed") {
1802
- throw new RefundError(transaction._id.toString(), "Only verified/completed transactions can be refunded");
1803
- }
1804
- const gatewayType = transaction.gateway?.type ?? "manual";
1805
- const provider = this.providers[gatewayType];
1806
- if (!provider) {
1807
- throw new ProviderNotFoundError(gatewayType, Object.keys(this.providers));
1808
- }
1809
- const capabilities = provider.getCapabilities();
1810
- if (!capabilities.supportsRefunds) {
1811
- throw new RefundNotSupportedError(gatewayType);
1812
- }
1813
- const refundedSoFar = transaction.refundedAmount ?? 0;
1814
- const refundableAmount = transaction.amount - refundedSoFar;
1815
- const refundAmount = amount ?? refundableAmount;
1816
- if (refundAmount <= 0) {
1817
- throw new ValidationError(`Refund amount must be positive, got ${refundAmount}`);
1818
- }
1819
- if (refundAmount > refundableAmount) {
1820
- throw new ValidationError(
1821
- `Refund amount (${refundAmount}) exceeds refundable balance (${refundableAmount})`,
1822
- { refundAmount, refundableAmount, alreadyRefunded: refundedSoFar }
1823
- );
1824
- }
1825
- let refundResult;
1826
- try {
1827
- refundResult = await provider.refund(paymentId, refundAmount, { reason: reason ?? void 0 });
1828
- } catch (error) {
1829
- this.logger.error("Refund failed:", error);
1830
- throw new RefundError(paymentId, error.message);
1831
- }
1832
- const refundTransactionType = this.config.transactionTypeMapping?.refund ?? TRANSACTION_TYPE.EXPENSE;
1833
- const refundCommission = transaction.commission ? reverseCommission(transaction.commission, transaction.amount, refundAmount) : null;
1834
- const refundTransaction = await TransactionModel.create({
1835
- organizationId: transaction.organizationId,
1836
- customerId: transaction.customerId,
1837
- amount: refundAmount,
1838
- currency: transaction.currency,
1839
- category: transaction.category,
1840
- type: refundTransactionType,
1841
- // EXPENSE - money going out
1842
- method: transaction.method ?? "manual",
1843
- status: "completed",
1844
- gateway: {
1845
- type: transaction.gateway?.type ?? "manual",
1846
- paymentIntentId: refundResult.id,
1847
- provider: refundResult.provider
1848
- },
1849
- paymentDetails: transaction.paymentDetails,
1850
- ...refundCommission && { commission: refundCommission },
1851
- // Reversed commission
1852
- // Polymorphic reference (copy from original transaction)
1853
- ...transaction.referenceId && { referenceId: transaction.referenceId },
1854
- ...transaction.referenceModel && { referenceModel: transaction.referenceModel },
1855
- metadata: {
1856
- ...transaction.metadata,
1857
- isRefund: true,
1858
- originalTransactionId: transaction._id.toString(),
1859
- refundReason: reason,
1860
- refundResult: refundResult.metadata
1861
- },
1862
- idempotencyKey: `refund_${transaction._id}_${Date.now()}`
1863
- });
1864
- const isPartialRefund = refundAmount < transaction.amount;
1865
- transaction.status = isPartialRefund ? "partially_refunded" : "refunded";
1866
- transaction.refundedAmount = (transaction.refundedAmount ?? 0) + refundAmount;
1867
- transaction.refundedAt = refundResult.refundedAt ?? /* @__PURE__ */ new Date();
1868
- transaction.metadata = {
1869
- ...transaction.metadata,
1870
- refundTransactionId: refundTransaction._id.toString(),
1871
- refundReason: reason
1872
- };
1873
- await transaction.save();
1874
- this._triggerHook("payment.refunded", {
1875
- transaction,
1876
- refundTransaction,
1877
- refundResult,
1878
- refundAmount,
1879
- reason,
1880
- isPartialRefund
1881
- });
1882
- return {
1883
- transaction,
1884
- refundTransaction,
1885
- refundResult,
1886
- status: transaction.status
1887
- };
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
+ );
1888
2817
  }
1889
2818
  /**
1890
2819
  * Handle webhook from payment provider
@@ -1905,7 +2834,10 @@ var PaymentService = class {
1905
2834
  }
1906
2835
  let webhookEvent;
1907
2836
  try {
1908
- webhookEvent = await provider.handleWebhook(payload, headers);
2837
+ webhookEvent = await this.executeProviderCall(
2838
+ () => provider.handleWebhook(payload, headers),
2839
+ `${providerName}.handleWebhook`
2840
+ );
1909
2841
  } catch (error) {
1910
2842
  this.logger.error("Webhook processing failed:", error);
1911
2843
  throw new ProviderError(
@@ -1975,19 +2907,46 @@ var PaymentService = class {
1975
2907
  processedAt: /* @__PURE__ */ new Date(),
1976
2908
  data: webhookEvent.data
1977
2909
  };
2910
+ let newStatus = transaction.status;
1978
2911
  if (webhookEvent.type === "payment.succeeded") {
1979
- transaction.status = "verified";
1980
- transaction.verifiedAt = webhookEvent.createdAt;
2912
+ newStatus = "verified";
1981
2913
  } else if (webhookEvent.type === "payment.failed") {
1982
- transaction.status = "failed";
2914
+ newStatus = "failed";
1983
2915
  } else if (webhookEvent.type === "refund.succeeded") {
1984
- transaction.status = "refunded";
1985
- 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));
1986
2942
  }
1987
2943
  await transaction.save();
1988
- this._triggerHook(`payment.webhook.${webhookEvent.type}`, {
2944
+ this.events.emit("webhook.processed", {
2945
+ webhookType: webhookEvent.type,
2946
+ provider: webhookEvent.provider,
1989
2947
  event: webhookEvent,
1990
- transaction
2948
+ transaction,
2949
+ processedAt: /* @__PURE__ */ new Date()
1991
2950
  });
1992
2951
  return {
1993
2952
  event: webhookEvent,
@@ -2035,13 +2994,6 @@ var PaymentService = class {
2035
2994
  }
2036
2995
  return provider;
2037
2996
  }
2038
- /**
2039
- * Trigger event hook (fire-and-forget, non-blocking)
2040
- * @private
2041
- */
2042
- _triggerHook(event, data) {
2043
- triggerHook(this.hooks, event, data, this.logger);
2044
- }
2045
2997
  /**
2046
2998
  * Find transaction by sessionId, paymentIntentId, or transaction ID
2047
2999
  * @private
@@ -2061,17 +3013,32 @@ var PaymentService = class {
2061
3013
  return transaction;
2062
3014
  }
2063
3015
  };
2064
-
2065
- // src/services/transaction.service.ts
2066
3016
  var TransactionService = class {
2067
3017
  models;
2068
- hooks;
3018
+ plugins;
3019
+ events;
2069
3020
  logger;
2070
3021
  constructor(container) {
2071
3022
  this.models = container.get("models");
2072
- this.hooks = container.get("hooks");
3023
+ this.plugins = container.get("plugins");
3024
+ this.events = container.get("events");
2073
3025
  this.logger = container.get("logger");
2074
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
+ }
2075
3042
  /**
2076
3043
  * Get transaction by ID
2077
3044
  *
@@ -2128,59 +3095,46 @@ var TransactionService = class {
2128
3095
  * @returns Updated transaction
2129
3096
  */
2130
3097
  async update(transactionId, updates) {
2131
- const TransactionModel = this.models.Transaction;
2132
- const model = TransactionModel;
2133
- let transaction;
2134
- if (typeof model.update === "function") {
2135
- transaction = await model.update(transactionId, updates);
2136
- } else if (typeof model.findByIdAndUpdate === "function") {
2137
- transaction = await model.findByIdAndUpdate(
2138
- transactionId,
2139
- { $set: updates },
2140
- { new: true }
2141
- );
2142
- } else {
2143
- throw new Error("Transaction model does not support update operations");
2144
- }
2145
- if (!transaction) {
2146
- throw new TransactionNotFoundError(transactionId);
2147
- }
2148
- this._triggerHook("transaction.updated", {
2149
- transaction,
2150
- updates
2151
- });
2152
- return transaction;
2153
- }
2154
- /**
2155
- * Trigger event hook (fire-and-forget, non-blocking)
2156
- * @private
2157
- */
2158
- _triggerHook(event, data) {
2159
- 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
+ );
2160
3134
  }
2161
3135
  };
2162
3136
 
2163
- // src/enums/escrow.enums.ts
2164
- var HOLD_STATUS = {
2165
- HELD: "held",
2166
- RELEASED: "released",
2167
- CANCELLED: "cancelled",
2168
- PARTIALLY_RELEASED: "partially_released"
2169
- };
2170
- var RELEASE_REASON = {
2171
- PAYMENT_VERIFIED: "payment_verified"};
2172
- var HOLD_REASON = {
2173
- PAYMENT_VERIFICATION: "payment_verification"};
2174
-
2175
- // src/enums/split.enums.ts
2176
- var SPLIT_TYPE = {
2177
- CUSTOM: "custom"
2178
- };
2179
- var SPLIT_STATUS = {
2180
- PENDING: "pending",
2181
- PAID: "paid"};
2182
-
2183
- // src/utils/commission-split.ts
3137
+ // src/shared/utils/calculators/commission-split.ts
2184
3138
  function calculateSplits(amount, splitRules = [], gatewayFeeRate = 0) {
2185
3139
  if (!splitRules || splitRules.length === 0) {
2186
3140
  return [];
@@ -2199,9 +3153,9 @@ function calculateSplits(amount, splitRules = [], gatewayFeeRate = 0) {
2199
3153
  if (rule.rate < 0 || rule.rate > 1) {
2200
3154
  throw new Error(`Split rate must be between 0 and 1 for split ${index}`);
2201
3155
  }
2202
- const grossAmount = Math.round(amount * rule.rate * 100) / 100;
2203
- const gatewayFeeAmount = index === 0 && gatewayFeeRate > 0 ? Math.round(amount * gatewayFeeRate * 100) / 100 : 0;
2204
- 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);
2205
3159
  return {
2206
3160
  type: rule.type ?? SPLIT_TYPE.CUSTOM,
2207
3161
  recipientId: rule.recipientId,
@@ -2219,18 +3173,35 @@ function calculateSplits(amount, splitRules = [], gatewayFeeRate = 0) {
2219
3173
  }
2220
3174
  function calculateOrganizationPayout(amount, splits = []) {
2221
3175
  const totalSplitAmount = splits.reduce((sum, split) => sum + split.grossAmount, 0);
2222
- return Math.max(0, Math.round((amount - totalSplitAmount) * 100) / 100);
3176
+ return Math.max(0, amount - totalSplitAmount);
2223
3177
  }
2224
3178
 
2225
- // src/services/escrow.service.ts
3179
+ // src/application/services/escrow.service.ts
2226
3180
  var EscrowService = class {
2227
3181
  models;
2228
- hooks;
3182
+ plugins;
2229
3183
  logger;
3184
+ events;
2230
3185
  constructor(container) {
2231
3186
  this.models = container.get("models");
2232
- this.hooks = container.get("hooks");
3187
+ this.plugins = container.get("plugins");
2233
3188
  this.logger = container.get("logger");
3189
+ this.events = container.get("events");
3190
+ }
3191
+ /**
3192
+ * Create plugin context for hook execution
3193
+ * @private
3194
+ */
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
+ }
3204
+ };
2234
3205
  }
2235
3206
  /**
2236
3207
  * Hold funds in escrow
@@ -2240,36 +3211,54 @@ var EscrowService = class {
2240
3211
  * @returns Updated transaction
2241
3212
  */
2242
3213
  async hold(transactionId, options = {}) {
2243
- const {
2244
- reason = HOLD_REASON.PAYMENT_VERIFICATION,
2245
- holdUntil = null,
2246
- metadata = {}
2247
- } = options;
2248
- const TransactionModel = this.models.Transaction;
2249
- const transaction = await TransactionModel.findById(transactionId);
2250
- if (!transaction) {
2251
- throw new TransactionNotFoundError(transactionId);
2252
- }
2253
- if (transaction.status !== TRANSACTION_STATUS.VERIFIED) {
2254
- throw new Error(`Cannot hold transaction with status: ${transaction.status}. Must be verified.`);
2255
- }
2256
- transaction.hold = {
2257
- status: HOLD_STATUS.HELD,
2258
- heldAmount: transaction.amount,
2259
- releasedAmount: 0,
2260
- reason,
2261
- heldAt: /* @__PURE__ */ new Date(),
2262
- ...holdUntil && { holdUntil },
2263
- releases: [],
2264
- metadata
2265
- };
2266
- await transaction.save();
2267
- this._triggerHook("escrow.held", {
2268
- transaction,
2269
- heldAmount: transaction.amount,
2270
- reason
2271
- });
2272
- return transaction;
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
+ );
2273
3262
  }
2274
3263
  /**
2275
3264
  * Release funds from escrow to recipient
@@ -2279,93 +3268,185 @@ var EscrowService = class {
2279
3268
  * @returns { transaction, releaseTransaction }
2280
3269
  */
2281
3270
  async release(transactionId, options) {
2282
- const {
2283
- amount = null,
2284
- recipientId,
2285
- recipientType = "organization",
2286
- reason = RELEASE_REASON.PAYMENT_VERIFIED,
2287
- releasedBy = null,
2288
- createTransaction = true,
2289
- metadata = {}
2290
- } = options;
2291
- const TransactionModel = this.models.Transaction;
2292
- const transaction = await TransactionModel.findById(transactionId);
2293
- if (!transaction) {
2294
- throw new TransactionNotFoundError(transactionId);
2295
- }
2296
- if (!transaction.hold || transaction.hold.status !== HOLD_STATUS.HELD) {
2297
- throw new Error(`Transaction is not in held status. Current: ${transaction.hold?.status ?? "none"}`);
2298
- }
2299
- if (!recipientId) {
2300
- throw new Error("recipientId is required for release");
2301
- }
2302
- const releaseAmount = amount ?? transaction.hold.heldAmount - transaction.hold.releasedAmount;
2303
- const availableAmount = transaction.hold.heldAmount - transaction.hold.releasedAmount;
2304
- if (releaseAmount > availableAmount) {
2305
- throw new Error(`Release amount (${releaseAmount}) exceeds available held amount (${availableAmount})`);
2306
- }
2307
- const releaseRecord = {
2308
- amount: releaseAmount,
2309
- recipientId,
2310
- recipientType,
2311
- releasedAt: /* @__PURE__ */ new Date(),
2312
- releasedBy,
2313
- reason,
2314
- metadata
2315
- };
2316
- transaction.hold.releases.push(releaseRecord);
2317
- transaction.hold.releasedAmount += releaseAmount;
2318
- const isFullRelease = transaction.hold.releasedAmount >= transaction.hold.heldAmount;
2319
- const isPartialRelease = transaction.hold.releasedAmount > 0 && transaction.hold.releasedAmount < transaction.hold.heldAmount;
2320
- if (isFullRelease) {
2321
- transaction.hold.status = HOLD_STATUS.RELEASED;
2322
- transaction.hold.releasedAt = /* @__PURE__ */ new Date();
2323
- transaction.status = TRANSACTION_STATUS.COMPLETED;
2324
- } else if (isPartialRelease) {
2325
- transaction.hold.status = HOLD_STATUS.PARTIALLY_RELEASED;
2326
- }
2327
- await transaction.save();
2328
- let releaseTransaction = null;
2329
- if (createTransaction) {
2330
- releaseTransaction = await TransactionModel.create({
2331
- organizationId: transaction.organizationId,
2332
- customerId: recipientId,
2333
- amount: releaseAmount,
2334
- currency: transaction.currency,
2335
- category: transaction.category,
2336
- type: TRANSACTION_TYPE.INCOME,
2337
- method: transaction.method,
2338
- status: TRANSACTION_STATUS.COMPLETED,
2339
- gateway: transaction.gateway,
2340
- referenceId: transaction.referenceId,
2341
- referenceModel: transaction.referenceModel,
2342
- metadata: {
2343
- ...metadata,
2344
- isRelease: true,
2345
- heldTransactionId: transaction._id.toString(),
2346
- releaseReason: reason,
2347
- recipientType
2348
- },
2349
- idempotencyKey: `release_${transaction._id}_${Date.now()}`
2350
- });
2351
- }
2352
- this._triggerHook("escrow.released", {
2353
- transaction,
2354
- releaseTransaction,
2355
- releaseAmount,
2356
- recipientId,
2357
- recipientType,
2358
- reason,
2359
- isFullRelease,
2360
- isPartialRelease
2361
- });
2362
- return {
2363
- transaction,
2364
- releaseTransaction,
2365
- releaseAmount,
2366
- isFullRelease,
2367
- isPartialRelease
2368
- };
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
+ );
2369
3450
  }
2370
3451
  /**
2371
3452
  * Cancel hold and release back to customer
@@ -2382,8 +3463,23 @@ var EscrowService = class {
2382
3463
  throw new TransactionNotFoundError(transactionId);
2383
3464
  }
2384
3465
  if (!transaction.hold || transaction.hold.status !== HOLD_STATUS.HELD) {
2385
- 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
+ );
2386
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
+ );
2387
3483
  transaction.hold.status = HOLD_STATUS.CANCELLED;
2388
3484
  transaction.hold.cancelledAt = /* @__PURE__ */ new Date();
2389
3485
  transaction.hold.metadata = {
@@ -2391,9 +3487,24 @@ var EscrowService = class {
2391
3487
  ...metadata,
2392
3488
  cancelReason: reason
2393
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
+ );
2394
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
+ }
2395
3506
  await transaction.save();
2396
- this._triggerHook("escrow.cancelled", {
3507
+ this.events.emit("escrow.cancelled", {
2397
3508
  transaction,
2398
3509
  reason
2399
3510
  });
@@ -2414,10 +3525,15 @@ var EscrowService = class {
2414
3525
  throw new TransactionNotFoundError(transactionId);
2415
3526
  }
2416
3527
  if (!transaction.hold || transaction.hold.status !== HOLD_STATUS.HELD) {
2417
- 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
+ );
2418
3534
  }
2419
3535
  if (!splitRules || splitRules.length === 0) {
2420
- throw new Error("splitRules cannot be empty");
3536
+ throw new ValidationError("splitRules cannot be empty", { transactionId });
2421
3537
  }
2422
3538
  const splits = calculateSplits(
2423
3539
  transaction.amount,
@@ -2427,25 +3543,58 @@ var EscrowService = class {
2427
3543
  transaction.splits = splits;
2428
3544
  await transaction.save();
2429
3545
  const splitTransactions = [];
2430
- 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;
2431
3561
  const splitTransaction = await TransactionModel.create({
2432
3562
  organizationId: transaction.organizationId,
2433
3563
  customerId: split.recipientId,
2434
- 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)
2435
3572
  currency: transaction.currency,
2436
- category: split.type,
2437
- 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
+ },
2438
3582
  method: transaction.method,
2439
- status: TRANSACTION_STATUS.COMPLETED,
3583
+ status: "completed",
2440
3584
  gateway: transaction.gateway,
2441
- referenceId: transaction.referenceId,
2442
- referenceModel: transaction.referenceModel,
3585
+ // ✅ UNIFIED: Source reference (link to original transaction)
3586
+ sourceId: transaction._id,
3587
+ sourceModel: "Transaction",
3588
+ relatedTransactionId: transaction._id,
2443
3589
  metadata: {
2444
3590
  isSplit: true,
2445
3591
  splitType: split.type,
2446
3592
  recipientType: split.recipientType,
2447
3593
  originalTransactionId: transaction._id.toString(),
2448
- grossAmount: split.grossAmount,
3594
+ // Store split details for reference
3595
+ splitGrossAmount: split.grossAmount,
3596
+ splitNetAmount: split.netAmount,
3597
+ // Original calculation
2449
3598
  gatewayFeeAmount: split.gatewayFeeAmount
2450
3599
  },
2451
3600
  idempotencyKey: `split_${transaction._id}_${split.recipientId}_${Date.now()}`
@@ -2457,8 +3606,10 @@ var EscrowService = class {
2457
3606
  }
2458
3607
  await transaction.save();
2459
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;
2460
3611
  const organizationTransaction = await this.release(transactionId, {
2461
- amount: organizationPayout,
3612
+ amount: organizationPayoutTotal,
2462
3613
  recipientId: transaction.organizationId?.toString() ?? "",
2463
3614
  recipientType: "organization",
2464
3615
  reason: RELEASE_REASON.PAYMENT_VERIFIED,
@@ -2469,7 +3620,7 @@ var EscrowService = class {
2469
3620
  totalSplitAmount: transaction.amount - organizationPayout
2470
3621
  }
2471
3622
  });
2472
- this._triggerHook("escrow.split", {
3623
+ this.events.emit("escrow.split", {
2473
3624
  transaction,
2474
3625
  splits,
2475
3626
  splitTransactions,
@@ -2504,8 +3655,490 @@ var EscrowService = class {
2504
3655
  hasSplits: transaction.splits ? transaction.splits.length > 0 : false
2505
3656
  };
2506
3657
  }
2507
- _triggerHook(event, data) {
2508
- 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;
2509
4142
  }
2510
4143
  };
2511
4144
 
@@ -2530,6 +4163,8 @@ var Revenue = class {
2530
4163
  transactions;
2531
4164
  /** Escrow service - hold, release, splits */
2532
4165
  escrow;
4166
+ /** Settlement service - payout tracking */
4167
+ settlement;
2533
4168
  constructor(container, events, plugins, options, providers, config) {
2534
4169
  this._container = container;
2535
4170
  this._events = events;
@@ -2542,16 +4177,20 @@ var Revenue = class {
2542
4177
  ttl: options.idempotencyTtl
2543
4178
  });
2544
4179
  if (options.circuitBreaker) {
2545
- this._circuitBreaker = createCircuitBreaker();
4180
+ const cbConfig = typeof options.circuitBreaker === "boolean" ? {} : options.circuitBreaker;
4181
+ this._circuitBreaker = createCircuitBreaker(cbConfig);
2546
4182
  }
2547
4183
  container.singleton("events", events);
2548
4184
  container.singleton("plugins", plugins);
2549
4185
  container.singleton("idempotency", this._idempotency);
2550
4186
  container.singleton("logger", this._logger);
4187
+ container.singleton("retryConfig", options.retry || {});
4188
+ container.singleton("circuitBreaker", this._circuitBreaker || null);
2551
4189
  this.monetization = new MonetizationService(container);
2552
4190
  this.payments = new PaymentService(container);
2553
4191
  this.transactions = new TransactionService(container);
2554
4192
  this.escrow = new EscrowService(container);
4193
+ this.settlement = new SettlementService(container);
2555
4194
  Object.freeze(this._providers);
2556
4195
  Object.freeze(this._config);
2557
4196
  }
@@ -2627,8 +4266,8 @@ var Revenue = class {
2627
4266
  *
2628
4267
  * @example
2629
4268
  * ```typescript
2630
- * revenue.on('payment.succeeded', (event) => {
2631
- * console.log('Payment:', event.transactionId);
4269
+ * revenue.on('payment.verified', (event) => {
4270
+ * console.log('Payment:', event.transaction._id);
2632
4271
  * });
2633
4272
  * ```
2634
4273
  */
@@ -2685,7 +4324,6 @@ var Revenue = class {
2685
4324
  return {
2686
4325
  events: this._events,
2687
4326
  logger: this._logger,
2688
- get: (key) => this._container.get(key),
2689
4327
  storage: /* @__PURE__ */ new Map(),
2690
4328
  meta: {
2691
4329
  ...meta,
@@ -2700,6 +4338,7 @@ var Revenue = class {
2700
4338
  async destroy() {
2701
4339
  await this._plugins.destroy();
2702
4340
  this._events.clear();
4341
+ this._idempotency.destroy();
2703
4342
  }
2704
4343
  };
2705
4344
  var RevenueBuilder = class {
@@ -2707,8 +4346,8 @@ var RevenueBuilder = class {
2707
4346
  models = null;
2708
4347
  providers = {};
2709
4348
  plugins = [];
2710
- hooks = {};
2711
4349
  categoryMappings = {};
4350
+ transactionTypeMapping = {};
2712
4351
  constructor(options = {}) {
2713
4352
  this.options = options;
2714
4353
  }
@@ -2732,7 +4371,7 @@ var RevenueBuilder = class {
2732
4371
  */
2733
4372
  withModel(name, model) {
2734
4373
  if (!this.models) {
2735
- this.models = { Transaction: model };
4374
+ this.models = {};
2736
4375
  }
2737
4376
  this.models[name] = model;
2738
4377
  return this;
@@ -2777,15 +4416,6 @@ var RevenueBuilder = class {
2777
4416
  this.plugins.push(...plugins);
2778
4417
  return this;
2779
4418
  }
2780
- withHooks(hooks) {
2781
- const normalized = {};
2782
- for (const [event, handlerOrHandlers] of Object.entries(hooks)) {
2783
- if (!handlerOrHandlers) continue;
2784
- normalized[event] = Array.isArray(handlerOrHandlers) ? handlerOrHandlers : [handlerOrHandlers];
2785
- }
2786
- this.hooks = { ...this.hooks, ...normalized };
2787
- return this;
2788
- }
2789
4419
  /**
2790
4420
  * Set retry configuration
2791
4421
  *
@@ -2827,7 +4457,15 @@ var RevenueBuilder = class {
2827
4457
  return this;
2828
4458
  }
2829
4459
  /**
2830
- * 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
+ * ```
2831
4469
  */
2832
4470
  withCommission(rate, gatewayFeeRate = 0) {
2833
4471
  this.options.commissionRate = rate;
@@ -2850,6 +4488,22 @@ var RevenueBuilder = class {
2850
4488
  this.categoryMappings = { ...this.categoryMappings, ...mappings };
2851
4489
  return this;
2852
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
+ }
2853
4507
  /**
2854
4508
  * Build the Revenue instance
2855
4509
  */
@@ -2888,14 +4542,19 @@ var RevenueBuilder = class {
2888
4542
  gatewayFeeRate: this.options.gatewayFeeRate ?? 0
2889
4543
  };
2890
4544
  const config = {
2891
- defaultCurrency: resolvedOptions.defaultCurrency,
2892
- commissionRate: resolvedOptions.commissionRate,
2893
- gatewayFeeRate: resolvedOptions.gatewayFeeRate,
2894
- categoryMappings: this.categoryMappings
4545
+ ...resolveConfig(resolvedOptions),
4546
+ // Converts singular → plural with '*' global default
4547
+ targetModels: [],
4548
+ categoryMappings: this.categoryMappings,
4549
+ transactionTypeMapping: this.transactionTypeMapping
2895
4550
  };
4551
+ for (const provider of Object.values(this.providers)) {
4552
+ if (typeof provider.setDefaultCurrency === "function") {
4553
+ provider.setDefaultCurrency(resolvedOptions.defaultCurrency);
4554
+ }
4555
+ }
2896
4556
  container.singleton("models", this.models);
2897
4557
  container.singleton("providers", this.providers);
2898
- container.singleton("hooks", this.hooks);
2899
4558
  container.singleton("config", config);
2900
4559
  const events = createEventBus();
2901
4560
  const pluginManager = new PluginManager();
@@ -2924,12 +4583,9 @@ function createRevenue(config) {
2924
4583
  if (config.plugins) {
2925
4584
  builder = builder.withPlugins(config.plugins);
2926
4585
  }
2927
- if (config.hooks) {
2928
- builder = builder.withHooks(config.hooks);
2929
- }
2930
4586
  return builder.build();
2931
4587
  }
2932
4588
 
2933
- 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 };
2934
4590
  //# sourceMappingURL=index.js.map
2935
4591
  //# sourceMappingURL=index.js.map