@classytic/revenue 0.2.3 → 1.0.0

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 (111) hide show
  1. package/README.md +498 -499
  2. package/dist/actions-CwG-b7fR.d.ts +519 -0
  3. package/dist/core/index.d.ts +884 -0
  4. package/dist/core/index.js +2941 -0
  5. package/dist/core/index.js.map +1 -0
  6. package/dist/enums/index.d.ts +130 -0
  7. package/dist/enums/index.js +167 -0
  8. package/dist/enums/index.js.map +1 -0
  9. package/dist/index-BnJWVXuw.d.ts +378 -0
  10. package/dist/index-ChVD3P9k.d.ts +504 -0
  11. package/dist/index.d.ts +42 -0
  12. package/dist/index.js +4353 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/providers/index.d.ts +132 -0
  15. package/dist/providers/index.js +122 -0
  16. package/dist/providers/index.js.map +1 -0
  17. package/dist/retry-80lBCmSe.d.ts +234 -0
  18. package/dist/schemas/index.d.ts +894 -0
  19. package/dist/schemas/index.js +524 -0
  20. package/dist/schemas/index.js.map +1 -0
  21. package/dist/schemas/validation.d.ts +309 -0
  22. package/dist/schemas/validation.js +249 -0
  23. package/dist/schemas/validation.js.map +1 -0
  24. package/dist/services/index.d.ts +3 -0
  25. package/dist/services/index.js +1632 -0
  26. package/dist/services/index.js.map +1 -0
  27. package/dist/split.enums-DHdM1YAV.d.ts +255 -0
  28. package/dist/split.schema-BPdFZMbU.d.ts +958 -0
  29. package/dist/utils/index.d.ts +24 -0
  30. package/dist/utils/index.js +1067 -0
  31. package/dist/utils/index.js.map +1 -0
  32. package/package.json +48 -32
  33. package/core/builder.js +0 -219
  34. package/core/container.js +0 -119
  35. package/core/errors.js +0 -262
  36. package/dist/types/core/builder.d.ts +0 -97
  37. package/dist/types/core/container.d.ts +0 -57
  38. package/dist/types/core/errors.d.ts +0 -122
  39. package/dist/types/enums/escrow.enums.d.ts +0 -24
  40. package/dist/types/enums/index.d.ts +0 -69
  41. package/dist/types/enums/monetization.enums.d.ts +0 -6
  42. package/dist/types/enums/payment.enums.d.ts +0 -16
  43. package/dist/types/enums/split.enums.d.ts +0 -25
  44. package/dist/types/enums/subscription.enums.d.ts +0 -15
  45. package/dist/types/enums/transaction.enums.d.ts +0 -24
  46. package/dist/types/index.d.ts +0 -22
  47. package/dist/types/providers/base.d.ts +0 -126
  48. package/dist/types/schemas/escrow/hold.schema.d.ts +0 -54
  49. package/dist/types/schemas/escrow/index.d.ts +0 -6
  50. package/dist/types/schemas/index.d.ts +0 -506
  51. package/dist/types/schemas/split/index.d.ts +0 -8
  52. package/dist/types/schemas/split/split.schema.d.ts +0 -142
  53. package/dist/types/schemas/subscription/index.d.ts +0 -152
  54. package/dist/types/schemas/subscription/info.schema.d.ts +0 -128
  55. package/dist/types/schemas/subscription/plan.schema.d.ts +0 -39
  56. package/dist/types/schemas/transaction/common.schema.d.ts +0 -12
  57. package/dist/types/schemas/transaction/gateway.schema.d.ts +0 -86
  58. package/dist/types/schemas/transaction/index.d.ts +0 -202
  59. package/dist/types/schemas/transaction/payment.schema.d.ts +0 -145
  60. package/dist/types/services/escrow.service.d.ts +0 -51
  61. package/dist/types/services/monetization.service.d.ts +0 -193
  62. package/dist/types/services/payment.service.d.ts +0 -112
  63. package/dist/types/services/transaction.service.d.ts +0 -40
  64. package/dist/types/utils/category-resolver.d.ts +0 -46
  65. package/dist/types/utils/commission-split.d.ts +0 -56
  66. package/dist/types/utils/commission.d.ts +0 -29
  67. package/dist/types/utils/hooks.d.ts +0 -17
  68. package/dist/types/utils/index.d.ts +0 -6
  69. package/dist/types/utils/logger.d.ts +0 -12
  70. package/dist/types/utils/subscription/actions.d.ts +0 -28
  71. package/dist/types/utils/subscription/index.d.ts +0 -2
  72. package/dist/types/utils/subscription/period.d.ts +0 -47
  73. package/dist/types/utils/transaction-type.d.ts +0 -102
  74. package/enums/escrow.enums.js +0 -36
  75. package/enums/index.d.ts +0 -116
  76. package/enums/index.js +0 -110
  77. package/enums/monetization.enums.js +0 -15
  78. package/enums/payment.enums.js +0 -64
  79. package/enums/split.enums.js +0 -37
  80. package/enums/subscription.enums.js +0 -33
  81. package/enums/transaction.enums.js +0 -69
  82. package/index.js +0 -76
  83. package/providers/base.js +0 -162
  84. package/schemas/escrow/hold.schema.js +0 -62
  85. package/schemas/escrow/index.js +0 -15
  86. package/schemas/index.d.ts +0 -33
  87. package/schemas/index.js +0 -27
  88. package/schemas/split/index.js +0 -16
  89. package/schemas/split/split.schema.js +0 -86
  90. package/schemas/subscription/index.js +0 -17
  91. package/schemas/subscription/info.schema.js +0 -115
  92. package/schemas/subscription/plan.schema.js +0 -48
  93. package/schemas/transaction/common.schema.js +0 -22
  94. package/schemas/transaction/gateway.schema.js +0 -69
  95. package/schemas/transaction/index.js +0 -20
  96. package/schemas/transaction/payment.schema.js +0 -110
  97. package/services/escrow.service.js +0 -353
  98. package/services/monetization.service.js +0 -671
  99. package/services/payment.service.js +0 -517
  100. package/services/transaction.service.js +0 -142
  101. package/utils/category-resolver.js +0 -74
  102. package/utils/commission-split.js +0 -180
  103. package/utils/commission.js +0 -83
  104. package/utils/hooks.js +0 -44
  105. package/utils/index.d.ts +0 -164
  106. package/utils/index.js +0 -16
  107. package/utils/logger.js +0 -36
  108. package/utils/subscription/actions.js +0 -68
  109. package/utils/subscription/index.js +0 -20
  110. package/utils/subscription/period.js +0 -123
  111. package/utils/transaction-type.js +0 -254
@@ -0,0 +1,1067 @@
1
+ import { nanoid } from 'nanoid';
2
+
3
+ // @classytic/revenue - Enterprise Revenue Management System
4
+
5
+ // src/utils/money.ts
6
+ var CURRENCIES = {
7
+ USD: { code: "USD", decimals: 2, symbol: "$", name: "US Dollar" },
8
+ EUR: { code: "EUR", decimals: 2, symbol: "\u20AC", name: "Euro" },
9
+ GBP: { code: "GBP", decimals: 2, symbol: "\xA3", name: "British Pound" },
10
+ BDT: { code: "BDT", decimals: 2, symbol: "\u09F3", name: "Bangladeshi Taka" },
11
+ INR: { code: "INR", decimals: 2, symbol: "\u20B9", name: "Indian Rupee" },
12
+ JPY: { code: "JPY", decimals: 0, symbol: "\xA5", name: "Japanese Yen" },
13
+ CNY: { code: "CNY", decimals: 2, symbol: "\xA5", name: "Chinese Yuan" },
14
+ AED: { code: "AED", decimals: 2, symbol: "\u062F.\u0625", name: "UAE Dirham" },
15
+ SAR: { code: "SAR", decimals: 2, symbol: "\uFDFC", name: "Saudi Riyal" },
16
+ SGD: { code: "SGD", decimals: 2, symbol: "S$", name: "Singapore Dollar" },
17
+ AUD: { code: "AUD", decimals: 2, symbol: "A$", name: "Australian Dollar" },
18
+ CAD: { code: "CAD", decimals: 2, symbol: "C$", name: "Canadian Dollar" }
19
+ };
20
+ var Money = class _Money {
21
+ amount;
22
+ currency;
23
+ constructor(amount, currency) {
24
+ if (!Number.isInteger(amount)) {
25
+ throw new Error(`Money amount must be integer (smallest unit). Got: ${amount}`);
26
+ }
27
+ this.amount = amount;
28
+ this.currency = currency.toUpperCase();
29
+ }
30
+ // ============ FACTORY METHODS ============
31
+ /**
32
+ * Create money from smallest unit (cents, paisa)
33
+ * @example Money.cents(1999, 'USD') // $19.99
34
+ */
35
+ static cents(amount, currency = "USD") {
36
+ return new _Money(Math.round(amount), currency);
37
+ }
38
+ /**
39
+ * Create money from major unit (dollars, taka)
40
+ * @example Money.of(19.99, 'USD') // $19.99 (stored as 1999 cents)
41
+ */
42
+ static of(amount, currency = "USD") {
43
+ const config = CURRENCIES[currency.toUpperCase()] ?? { decimals: 2 };
44
+ const multiplier = Math.pow(10, config.decimals);
45
+ return new _Money(Math.round(amount * multiplier), currency);
46
+ }
47
+ /**
48
+ * Create zero money
49
+ */
50
+ static zero(currency = "USD") {
51
+ return new _Money(0, currency);
52
+ }
53
+ // ============ SHORTHAND FACTORIES ============
54
+ static usd(cents) {
55
+ return _Money.cents(cents, "USD");
56
+ }
57
+ static eur(cents) {
58
+ return _Money.cents(cents, "EUR");
59
+ }
60
+ static gbp(pence) {
61
+ return _Money.cents(pence, "GBP");
62
+ }
63
+ static bdt(paisa) {
64
+ return _Money.cents(paisa, "BDT");
65
+ }
66
+ static inr(paisa) {
67
+ return _Money.cents(paisa, "INR");
68
+ }
69
+ static jpy(yen) {
70
+ return _Money.cents(yen, "JPY");
71
+ }
72
+ // ============ ARITHMETIC ============
73
+ /**
74
+ * Add two money values (must be same currency)
75
+ */
76
+ add(other) {
77
+ this.assertSameCurrency(other);
78
+ return new _Money(this.amount + other.amount, this.currency);
79
+ }
80
+ /**
81
+ * Subtract money (must be same currency)
82
+ */
83
+ subtract(other) {
84
+ this.assertSameCurrency(other);
85
+ return new _Money(this.amount - other.amount, this.currency);
86
+ }
87
+ /**
88
+ * Multiply by a factor (rounds to nearest integer)
89
+ */
90
+ multiply(factor) {
91
+ return new _Money(Math.round(this.amount * factor), this.currency);
92
+ }
93
+ /**
94
+ * Divide by a divisor (rounds to nearest integer)
95
+ */
96
+ divide(divisor) {
97
+ if (divisor === 0) throw new Error("Cannot divide by zero");
98
+ return new _Money(Math.round(this.amount / divisor), this.currency);
99
+ }
100
+ /**
101
+ * Calculate percentage
102
+ * @example money.percentage(10) // 10% of money
103
+ */
104
+ percentage(percent) {
105
+ return this.multiply(percent / 100);
106
+ }
107
+ /**
108
+ * Allocate money among recipients (handles rounding)
109
+ * @example Money.usd(100).allocate([1, 1, 1]) // [34, 33, 33] cents
110
+ */
111
+ allocate(ratios) {
112
+ const total = ratios.reduce((a, b) => a + b, 0);
113
+ if (total === 0) throw new Error("Ratios must sum to more than 0");
114
+ let remainder = this.amount;
115
+ const results = [];
116
+ for (let i = 0; i < ratios.length; i++) {
117
+ const share = Math.floor(this.amount * ratios[i] / total);
118
+ results.push(new _Money(share, this.currency));
119
+ remainder -= share;
120
+ }
121
+ for (let i = 0; i < remainder; i++) {
122
+ results[i] = new _Money(results[i].amount + 1, this.currency);
123
+ }
124
+ return results;
125
+ }
126
+ /**
127
+ * Split equally among n recipients
128
+ */
129
+ split(parts) {
130
+ return this.allocate(Array(parts).fill(1));
131
+ }
132
+ // ============ COMPARISON ============
133
+ isZero() {
134
+ return this.amount === 0;
135
+ }
136
+ isPositive() {
137
+ return this.amount > 0;
138
+ }
139
+ isNegative() {
140
+ return this.amount < 0;
141
+ }
142
+ equals(other) {
143
+ return this.amount === other.amount && this.currency === other.currency;
144
+ }
145
+ greaterThan(other) {
146
+ this.assertSameCurrency(other);
147
+ return this.amount > other.amount;
148
+ }
149
+ lessThan(other) {
150
+ this.assertSameCurrency(other);
151
+ return this.amount < other.amount;
152
+ }
153
+ greaterThanOrEqual(other) {
154
+ return this.greaterThan(other) || this.equals(other);
155
+ }
156
+ lessThanOrEqual(other) {
157
+ return this.lessThan(other) || this.equals(other);
158
+ }
159
+ // ============ FORMATTING ============
160
+ /**
161
+ * Get amount in major unit (dollars, taka)
162
+ */
163
+ toUnit() {
164
+ const config = CURRENCIES[this.currency] ?? { decimals: 2 };
165
+ return this.amount / Math.pow(10, config.decimals);
166
+ }
167
+ /**
168
+ * Format for display
169
+ * @example Money.usd(1999).format() // "$19.99"
170
+ */
171
+ format(locale = "en-US") {
172
+ return new Intl.NumberFormat(locale, {
173
+ style: "currency",
174
+ currency: this.currency
175
+ }).format(this.toUnit());
176
+ }
177
+ /**
178
+ * Format without currency symbol
179
+ */
180
+ formatAmount(locale = "en-US") {
181
+ const config = CURRENCIES[this.currency] ?? { decimals: 2 };
182
+ return new Intl.NumberFormat(locale, {
183
+ minimumFractionDigits: config.decimals,
184
+ maximumFractionDigits: config.decimals
185
+ }).format(this.toUnit());
186
+ }
187
+ /**
188
+ * Convert to JSON-serializable object
189
+ */
190
+ toJSON() {
191
+ return { amount: this.amount, currency: this.currency };
192
+ }
193
+ /**
194
+ * Create from JSON
195
+ */
196
+ static fromJSON(json) {
197
+ return new _Money(json.amount, json.currency);
198
+ }
199
+ toString() {
200
+ return `${this.currency} ${this.amount}`;
201
+ }
202
+ // ============ HELPERS ============
203
+ assertSameCurrency(other) {
204
+ if (this.currency !== other.currency) {
205
+ throw new Error(
206
+ `Currency mismatch: ${this.currency} vs ${other.currency}. Convert first.`
207
+ );
208
+ }
209
+ }
210
+ };
211
+ function toSmallestUnit(amount, currency = "USD") {
212
+ return Money.of(amount, currency).amount;
213
+ }
214
+ function fromSmallestUnit(amount, currency = "USD") {
215
+ return Money.cents(amount, currency).toUnit();
216
+ }
217
+
218
+ // src/core/result.ts
219
+ function ok(value) {
220
+ return { ok: true, value };
221
+ }
222
+ function err(error) {
223
+ return { ok: false, error };
224
+ }
225
+
226
+ // src/utils/retry.ts
227
+ var DEFAULT_CONFIG = {
228
+ maxAttempts: 3,
229
+ baseDelay: 1e3,
230
+ maxDelay: 3e4,
231
+ backoffMultiplier: 2,
232
+ jitter: 0.1,
233
+ retryIf: isRetryableError
234
+ };
235
+ function calculateDelay(attempt, config) {
236
+ const exponentialDelay = config.baseDelay * Math.pow(config.backoffMultiplier, attempt);
237
+ const cappedDelay = Math.min(exponentialDelay, config.maxDelay);
238
+ const jitterRange = cappedDelay * config.jitter;
239
+ const jitter = Math.random() * jitterRange * 2 - jitterRange;
240
+ return Math.round(Math.max(0, cappedDelay + jitter));
241
+ }
242
+ function sleep(ms) {
243
+ return new Promise((resolve) => setTimeout(resolve, ms));
244
+ }
245
+ function isRetryableError(error) {
246
+ if (!(error instanceof Error)) return false;
247
+ if (error.message.includes("ECONNREFUSED")) return true;
248
+ if (error.message.includes("ETIMEDOUT")) return true;
249
+ if (error.message.includes("ENOTFOUND")) return true;
250
+ if (error.message.includes("network")) return true;
251
+ if (error.message.includes("timeout")) return true;
252
+ if (error.message.includes("429")) return true;
253
+ if (error.message.includes("rate limit")) return true;
254
+ if (error.message.includes("500")) return true;
255
+ if (error.message.includes("502")) return true;
256
+ if (error.message.includes("503")) return true;
257
+ if (error.message.includes("504")) return true;
258
+ if ("retryable" in error && error.retryable === true) return true;
259
+ return false;
260
+ }
261
+ async function retry(operation, config = {}) {
262
+ const fullConfig = { ...DEFAULT_CONFIG, ...config };
263
+ const state = {
264
+ attempt: 0,
265
+ totalDelay: 0,
266
+ errors: []
267
+ };
268
+ while (state.attempt < fullConfig.maxAttempts) {
269
+ try {
270
+ return await operation();
271
+ } catch (error) {
272
+ state.errors.push(error instanceof Error ? error : new Error(String(error)));
273
+ state.attempt++;
274
+ const shouldRetry = fullConfig.retryIf?.(error) ?? isRetryableError(error);
275
+ if (!shouldRetry || state.attempt >= fullConfig.maxAttempts) {
276
+ throw new RetryExhaustedError(
277
+ `Operation failed after ${state.attempt} attempts`,
278
+ state.errors
279
+ );
280
+ }
281
+ const delay = calculateDelay(state.attempt - 1, fullConfig);
282
+ state.totalDelay += delay;
283
+ fullConfig.onRetry?.(error, state.attempt, delay);
284
+ await sleep(delay);
285
+ }
286
+ }
287
+ throw new RetryExhaustedError(
288
+ `Operation failed after ${state.attempt} attempts`,
289
+ state.errors
290
+ );
291
+ }
292
+ async function retryWithResult(operation, config = {}) {
293
+ try {
294
+ const result = await retry(operation, config);
295
+ return ok(result);
296
+ } catch (error) {
297
+ if (error instanceof RetryExhaustedError) {
298
+ return err(error);
299
+ }
300
+ return err(new RetryExhaustedError("Operation failed", [
301
+ error instanceof Error ? error : new Error(String(error))
302
+ ]));
303
+ }
304
+ }
305
+ var RetryExhaustedError = class extends Error {
306
+ attempts;
307
+ errors;
308
+ constructor(message, errors) {
309
+ super(message);
310
+ this.name = "RetryExhaustedError";
311
+ this.attempts = errors.length;
312
+ this.errors = errors;
313
+ }
314
+ /**
315
+ * Get the last error
316
+ */
317
+ get lastError() {
318
+ return this.errors[this.errors.length - 1];
319
+ }
320
+ /**
321
+ * Get the first error
322
+ */
323
+ get firstError() {
324
+ return this.errors[0];
325
+ }
326
+ };
327
+ var DEFAULT_CIRCUIT_CONFIG = {
328
+ failureThreshold: 5,
329
+ resetTimeout: 3e4,
330
+ successThreshold: 3,
331
+ monitorWindow: 6e4
332
+ };
333
+ var CircuitBreaker = class {
334
+ state = "closed";
335
+ failures = [];
336
+ successes = 0;
337
+ lastFailure;
338
+ config;
339
+ constructor(config = {}) {
340
+ this.config = { ...DEFAULT_CIRCUIT_CONFIG, ...config };
341
+ }
342
+ /**
343
+ * Execute operation through circuit breaker
344
+ */
345
+ async execute(operation) {
346
+ if (this.state === "open") {
347
+ if (this.shouldAttemptReset()) {
348
+ this.state = "half-open";
349
+ } else {
350
+ throw new CircuitOpenError("Circuit is open, request rejected");
351
+ }
352
+ }
353
+ try {
354
+ const result = await operation();
355
+ this.onSuccess();
356
+ return result;
357
+ } catch (error) {
358
+ this.onFailure();
359
+ throw error;
360
+ }
361
+ }
362
+ /**
363
+ * Execute with Result type
364
+ */
365
+ async executeWithResult(operation) {
366
+ try {
367
+ const result = await this.execute(operation);
368
+ return ok(result);
369
+ } catch (error) {
370
+ return err(error);
371
+ }
372
+ }
373
+ onSuccess() {
374
+ if (this.state === "half-open") {
375
+ this.successes++;
376
+ if (this.successes >= this.config.successThreshold) {
377
+ this.reset();
378
+ }
379
+ }
380
+ this.cleanOldFailures();
381
+ }
382
+ onFailure() {
383
+ this.failures.push(/* @__PURE__ */ new Date());
384
+ this.lastFailure = /* @__PURE__ */ new Date();
385
+ this.successes = 0;
386
+ this.cleanOldFailures();
387
+ if (this.failures.length >= this.config.failureThreshold) {
388
+ this.state = "open";
389
+ }
390
+ }
391
+ shouldAttemptReset() {
392
+ if (!this.lastFailure) return true;
393
+ return Date.now() - this.lastFailure.getTime() >= this.config.resetTimeout;
394
+ }
395
+ cleanOldFailures() {
396
+ const cutoff = Date.now() - this.config.monitorWindow;
397
+ this.failures = this.failures.filter((f) => f.getTime() > cutoff);
398
+ }
399
+ reset() {
400
+ this.state = "closed";
401
+ this.failures = [];
402
+ this.successes = 0;
403
+ }
404
+ /**
405
+ * Get current circuit state
406
+ */
407
+ getState() {
408
+ return this.state;
409
+ }
410
+ /**
411
+ * Manually reset circuit
412
+ */
413
+ forceReset() {
414
+ this.reset();
415
+ }
416
+ /**
417
+ * Get circuit statistics
418
+ */
419
+ getStats() {
420
+ return {
421
+ state: this.state,
422
+ failures: this.failures.length,
423
+ successes: this.successes,
424
+ lastFailure: this.lastFailure
425
+ };
426
+ }
427
+ };
428
+ var CircuitOpenError = class extends Error {
429
+ constructor(message) {
430
+ super(message);
431
+ this.name = "CircuitOpenError";
432
+ }
433
+ };
434
+ function createCircuitBreaker(config) {
435
+ return new CircuitBreaker(config);
436
+ }
437
+ async function resilientExecute(operation, options = {}) {
438
+ const { retry: retryConfig, circuitBreaker } = options;
439
+ const wrappedOperation = async () => {
440
+ if (circuitBreaker) {
441
+ return circuitBreaker.execute(operation);
442
+ }
443
+ return operation();
444
+ };
445
+ if (retryConfig) {
446
+ return retry(wrappedOperation, retryConfig);
447
+ }
448
+ return wrappedOperation();
449
+ }
450
+ var MemoryIdempotencyStore = class {
451
+ records = /* @__PURE__ */ new Map();
452
+ cleanupInterval = null;
453
+ constructor(cleanupIntervalMs = 6e4) {
454
+ this.cleanupInterval = setInterval(() => {
455
+ this.cleanup();
456
+ }, cleanupIntervalMs);
457
+ }
458
+ async get(key) {
459
+ const record = this.records.get(key);
460
+ if (!record) return null;
461
+ if (record.expiresAt < /* @__PURE__ */ new Date()) {
462
+ this.records.delete(key);
463
+ return null;
464
+ }
465
+ return record;
466
+ }
467
+ async set(key, record) {
468
+ this.records.set(key, record);
469
+ }
470
+ async delete(key) {
471
+ this.records.delete(key);
472
+ }
473
+ async exists(key) {
474
+ const record = await this.get(key);
475
+ return record !== null;
476
+ }
477
+ cleanup() {
478
+ const now = /* @__PURE__ */ new Date();
479
+ for (const [key, record] of this.records) {
480
+ if (record.expiresAt < now) {
481
+ this.records.delete(key);
482
+ }
483
+ }
484
+ }
485
+ destroy() {
486
+ if (this.cleanupInterval) {
487
+ clearInterval(this.cleanupInterval);
488
+ this.cleanupInterval = null;
489
+ }
490
+ this.records.clear();
491
+ }
492
+ };
493
+ var IdempotencyError = class extends Error {
494
+ constructor(message, code) {
495
+ super(message);
496
+ this.code = code;
497
+ this.name = "IdempotencyError";
498
+ }
499
+ };
500
+ var IdempotencyManager = class {
501
+ store;
502
+ ttl;
503
+ prefix;
504
+ constructor(config = {}) {
505
+ this.store = config.store ?? new MemoryIdempotencyStore();
506
+ this.ttl = config.ttl ?? 24 * 60 * 60 * 1e3;
507
+ this.prefix = config.prefix ?? "idem:";
508
+ }
509
+ /**
510
+ * Generate a unique idempotency key
511
+ */
512
+ generateKey() {
513
+ return `${this.prefix}${nanoid(21)}`;
514
+ }
515
+ /**
516
+ * Hash request parameters for validation
517
+ */
518
+ hashRequest(params) {
519
+ const json = JSON.stringify(params, Object.keys(params).sort());
520
+ let hash = 0;
521
+ for (let i = 0; i < json.length; i++) {
522
+ const char = json.charCodeAt(i);
523
+ hash = (hash << 5) - hash + char;
524
+ hash = hash & hash;
525
+ }
526
+ return hash.toString(36);
527
+ }
528
+ /**
529
+ * Execute operation with idempotency protection
530
+ */
531
+ async execute(key, params, operation) {
532
+ const fullKey = key.startsWith(this.prefix) ? key : `${this.prefix}${key}`;
533
+ const requestHash = this.hashRequest(params);
534
+ const existing = await this.store.get(fullKey);
535
+ if (existing) {
536
+ if (existing.requestHash !== requestHash) {
537
+ return err(new IdempotencyError(
538
+ "Idempotency key used with different request parameters",
539
+ "REQUEST_MISMATCH"
540
+ ));
541
+ }
542
+ if (existing.status === "completed" && existing.result !== void 0) {
543
+ return ok(existing.result);
544
+ }
545
+ if (existing.status === "pending") {
546
+ return err(new IdempotencyError(
547
+ "Request with this idempotency key is already in progress",
548
+ "REQUEST_IN_PROGRESS"
549
+ ));
550
+ }
551
+ if (existing.status === "failed") {
552
+ await this.store.delete(fullKey);
553
+ }
554
+ }
555
+ const record = {
556
+ key: fullKey,
557
+ status: "pending",
558
+ createdAt: /* @__PURE__ */ new Date(),
559
+ requestHash,
560
+ expiresAt: new Date(Date.now() + this.ttl)
561
+ };
562
+ await this.store.set(fullKey, record);
563
+ try {
564
+ const result = await operation();
565
+ record.status = "completed";
566
+ record.result = result;
567
+ record.completedAt = /* @__PURE__ */ new Date();
568
+ await this.store.set(fullKey, record);
569
+ return ok(result);
570
+ } catch (error) {
571
+ record.status = "failed";
572
+ await this.store.set(fullKey, record);
573
+ throw error;
574
+ }
575
+ }
576
+ /**
577
+ * Check if operation with key was already completed
578
+ */
579
+ async wasCompleted(key) {
580
+ const fullKey = key.startsWith(this.prefix) ? key : `${this.prefix}${key}`;
581
+ const record = await this.store.get(fullKey);
582
+ return record?.status === "completed";
583
+ }
584
+ /**
585
+ * Get cached result for key
586
+ */
587
+ async getCached(key) {
588
+ const fullKey = key.startsWith(this.prefix) ? key : `${this.prefix}${key}`;
589
+ const record = await this.store.get(fullKey);
590
+ return record?.status === "completed" ? record.result ?? null : null;
591
+ }
592
+ /**
593
+ * Invalidate a key (force re-execution on next call)
594
+ */
595
+ async invalidate(key) {
596
+ const fullKey = key.startsWith(this.prefix) ? key : `${this.prefix}${key}`;
597
+ await this.store.delete(fullKey);
598
+ }
599
+ };
600
+ function createIdempotencyManager(config) {
601
+ return new IdempotencyManager(config);
602
+ }
603
+
604
+ // src/utils/transaction-type.ts
605
+ var TRANSACTION_MANAGEMENT_TYPE = {
606
+ MONETIZATION: "monetization",
607
+ // Library-managed (subscriptions, purchases)
608
+ MANUAL: "manual"
609
+ // Admin-managed (expenses, income, adjustments)
610
+ };
611
+ var DEFAULT_MONETIZATION_CATEGORIES = [
612
+ "subscription",
613
+ "purchase"
614
+ ];
615
+ function isMonetizationCategory(category, additionalCategories = []) {
616
+ const allCategories = [...DEFAULT_MONETIZATION_CATEGORIES, ...additionalCategories];
617
+ return allCategories.includes(category);
618
+ }
619
+ function isMonetizationTransaction(transaction, options = {}) {
620
+ const {
621
+ targetModels = ["Subscription", "Membership"],
622
+ additionalCategories = []
623
+ } = options;
624
+ if (transaction.referenceModel && targetModels.includes(transaction.referenceModel)) {
625
+ return true;
626
+ }
627
+ if (transaction.category) {
628
+ return isMonetizationCategory(transaction.category, additionalCategories);
629
+ }
630
+ return false;
631
+ }
632
+ function isManualTransaction(transaction, options = {}) {
633
+ return !isMonetizationTransaction(transaction, options);
634
+ }
635
+ function getTransactionType(transaction, options = {}) {
636
+ return isMonetizationTransaction(transaction, options) ? TRANSACTION_MANAGEMENT_TYPE.MONETIZATION : TRANSACTION_MANAGEMENT_TYPE.MANUAL;
637
+ }
638
+ var PROTECTED_MONETIZATION_FIELDS = [
639
+ "status",
640
+ "amount",
641
+ "platformCommission",
642
+ "netAmount",
643
+ "verifiedAt",
644
+ "verifiedBy",
645
+ "gateway",
646
+ "webhook",
647
+ "metadata.commission",
648
+ "metadata.gateway",
649
+ "type",
650
+ "category",
651
+ "referenceModel",
652
+ "referenceId"
653
+ ];
654
+ var EDITABLE_MONETIZATION_FIELDS_PRE_VERIFICATION = [
655
+ "reference",
656
+ "paymentDetails",
657
+ "notes"
658
+ ];
659
+ var MANUAL_TRANSACTION_CREATE_FIELDS = [
660
+ "organizationId",
661
+ "type",
662
+ "category",
663
+ "amount",
664
+ "method",
665
+ "reference",
666
+ "paymentDetails",
667
+ "notes",
668
+ "date",
669
+ // Transaction date (can be backdated)
670
+ "description"
671
+ ];
672
+ var MANUAL_TRANSACTION_UPDATE_FIELDS = [
673
+ "amount",
674
+ "method",
675
+ "reference",
676
+ "paymentDetails",
677
+ "notes",
678
+ "date",
679
+ "description"
680
+ ];
681
+ function getAllowedUpdateFields(transaction, options = {}) {
682
+ const type = getTransactionType(transaction, options);
683
+ if (type === TRANSACTION_MANAGEMENT_TYPE.MONETIZATION) {
684
+ if (transaction.status === "pending") {
685
+ return EDITABLE_MONETIZATION_FIELDS_PRE_VERIFICATION;
686
+ }
687
+ return [];
688
+ }
689
+ if (transaction.status === "verified" || transaction.status === "completed") {
690
+ return ["notes"];
691
+ }
692
+ return MANUAL_TRANSACTION_UPDATE_FIELDS;
693
+ }
694
+ function validateFieldUpdate(transaction, fieldName, options = {}) {
695
+ const allowedFields = getAllowedUpdateFields(transaction, options);
696
+ if (allowedFields.includes(fieldName)) {
697
+ return { allowed: true };
698
+ }
699
+ const type = getTransactionType(transaction, options);
700
+ if (type === TRANSACTION_MANAGEMENT_TYPE.MONETIZATION) {
701
+ if (PROTECTED_MONETIZATION_FIELDS.includes(fieldName)) {
702
+ return {
703
+ allowed: false,
704
+ reason: `Field "${fieldName}" is protected for monetization transactions. Updates must go through payment flow.`
705
+ };
706
+ }
707
+ }
708
+ return {
709
+ allowed: false,
710
+ reason: `Field "${fieldName}" cannot be updated for ${transaction.status} transactions.`
711
+ };
712
+ }
713
+ function canSelfVerify(transaction, options = {}) {
714
+ const type = getTransactionType(transaction, options);
715
+ if (type === TRANSACTION_MANAGEMENT_TYPE.MANUAL) {
716
+ return transaction.status === "pending";
717
+ }
718
+ return false;
719
+ }
720
+
721
+ // src/utils/logger.ts
722
+ var _logger = console;
723
+ function setLogger(customLogger) {
724
+ _logger = customLogger;
725
+ }
726
+ var logger = {
727
+ info: (...args) => {
728
+ (_logger.info ?? _logger.log)?.call(_logger, ...args);
729
+ },
730
+ warn: (...args) => {
731
+ (_logger.warn ?? _logger.log)?.call(_logger, "WARN:", ...args);
732
+ },
733
+ error: (...args) => {
734
+ (_logger.error ?? _logger.log)?.call(_logger, "ERROR:", ...args);
735
+ },
736
+ debug: (...args) => {
737
+ (_logger.debug ?? _logger.log)?.call(_logger, "DEBUG:", ...args);
738
+ }
739
+ };
740
+ var logger_default = logger;
741
+
742
+ // src/utils/hooks.ts
743
+ function triggerHook(hooks, event, data, logger2) {
744
+ const handlers = hooks[event] ?? [];
745
+ if (handlers.length === 0) {
746
+ return;
747
+ }
748
+ Promise.all(
749
+ handlers.map(
750
+ (handler) => Promise.resolve(handler(data)).catch((error) => {
751
+ logger2.error(`Hook "${event}" failed:`, {
752
+ error: error.message,
753
+ stack: error.stack,
754
+ event,
755
+ // Don't log full data (could be huge)
756
+ dataKeys: Object.keys(data)
757
+ });
758
+ })
759
+ )
760
+ ).catch(() => {
761
+ });
762
+ }
763
+
764
+ // src/utils/commission.ts
765
+ function calculateCommission(amount, commissionRate, gatewayFeeRate = 0) {
766
+ if (!commissionRate || commissionRate <= 0) {
767
+ return null;
768
+ }
769
+ if (amount < 0) {
770
+ throw new Error("Transaction amount cannot be negative");
771
+ }
772
+ if (commissionRate < 0 || commissionRate > 1) {
773
+ throw new Error("Commission rate must be between 0 and 1");
774
+ }
775
+ if (gatewayFeeRate < 0 || gatewayFeeRate > 1) {
776
+ throw new Error("Gateway fee rate must be between 0 and 1");
777
+ }
778
+ const grossAmount = Math.round(amount * commissionRate * 100) / 100;
779
+ const gatewayFeeAmount = Math.round(amount * gatewayFeeRate * 100) / 100;
780
+ const netAmount = Math.max(0, Math.round((grossAmount - gatewayFeeAmount) * 100) / 100);
781
+ return {
782
+ rate: commissionRate,
783
+ grossAmount,
784
+ gatewayFeeRate,
785
+ gatewayFeeAmount,
786
+ netAmount,
787
+ status: "pending"
788
+ };
789
+ }
790
+ function reverseCommission(originalCommission, originalAmount, refundAmount) {
791
+ if (!originalCommission?.netAmount) {
792
+ return null;
793
+ }
794
+ const refundRatio = refundAmount / originalAmount;
795
+ const reversedNetAmount = Math.round(originalCommission.netAmount * refundRatio * 100) / 100;
796
+ const reversedGrossAmount = Math.round(originalCommission.grossAmount * refundRatio * 100) / 100;
797
+ const reversedGatewayFee = Math.round(originalCommission.gatewayFeeAmount * refundRatio * 100) / 100;
798
+ return {
799
+ rate: originalCommission.rate,
800
+ grossAmount: reversedGrossAmount,
801
+ gatewayFeeRate: originalCommission.gatewayFeeRate,
802
+ gatewayFeeAmount: reversedGatewayFee,
803
+ netAmount: reversedNetAmount,
804
+ status: "waived"
805
+ // Commission waived due to refund
806
+ };
807
+ }
808
+
809
+ // src/enums/split.enums.ts
810
+ var SPLIT_TYPE = {
811
+ PLATFORM_COMMISSION: "platform_commission",
812
+ AFFILIATE_COMMISSION: "affiliate_commission",
813
+ CUSTOM: "custom"
814
+ };
815
+ var SPLIT_STATUS = {
816
+ PENDING: "pending",
817
+ WAIVED: "waived"};
818
+
819
+ // src/utils/commission-split.ts
820
+ function calculateSplits(amount, splitRules = [], gatewayFeeRate = 0) {
821
+ if (!splitRules || splitRules.length === 0) {
822
+ return [];
823
+ }
824
+ if (amount < 0) {
825
+ throw new Error("Transaction amount cannot be negative");
826
+ }
827
+ if (gatewayFeeRate < 0 || gatewayFeeRate > 1) {
828
+ throw new Error("Gateway fee rate must be between 0 and 1");
829
+ }
830
+ const totalRate = splitRules.reduce((sum, rule) => sum + rule.rate, 0);
831
+ if (totalRate > 1) {
832
+ throw new Error(`Total split rate (${totalRate}) cannot exceed 1.0`);
833
+ }
834
+ return splitRules.map((rule, index) => {
835
+ if (rule.rate < 0 || rule.rate > 1) {
836
+ throw new Error(`Split rate must be between 0 and 1 for split ${index}`);
837
+ }
838
+ const grossAmount = Math.round(amount * rule.rate * 100) / 100;
839
+ const gatewayFeeAmount = index === 0 && gatewayFeeRate > 0 ? Math.round(amount * gatewayFeeRate * 100) / 100 : 0;
840
+ const netAmount = Math.max(0, Math.round((grossAmount - gatewayFeeAmount) * 100) / 100);
841
+ return {
842
+ type: rule.type ?? SPLIT_TYPE.CUSTOM,
843
+ recipientId: rule.recipientId,
844
+ recipientType: rule.recipientType,
845
+ rate: rule.rate,
846
+ grossAmount,
847
+ gatewayFeeRate: gatewayFeeAmount > 0 ? gatewayFeeRate : 0,
848
+ gatewayFeeAmount,
849
+ netAmount,
850
+ status: SPLIT_STATUS.PENDING,
851
+ dueDate: rule.dueDate ?? null,
852
+ metadata: rule.metadata ?? {}
853
+ };
854
+ });
855
+ }
856
+ function calculateOrganizationPayout(amount, splits = []) {
857
+ const totalSplitAmount = splits.reduce((sum, split) => sum + split.grossAmount, 0);
858
+ return Math.max(0, Math.round((amount - totalSplitAmount) * 100) / 100);
859
+ }
860
+ function reverseSplits(originalSplits, originalAmount, refundAmount) {
861
+ if (!originalSplits || originalSplits.length === 0) {
862
+ return [];
863
+ }
864
+ const refundRatio = refundAmount / originalAmount;
865
+ return originalSplits.map((split) => ({
866
+ ...split,
867
+ grossAmount: Math.round(split.grossAmount * refundRatio * 100) / 100,
868
+ gatewayFeeAmount: Math.round(split.gatewayFeeAmount * refundRatio * 100) / 100,
869
+ netAmount: Math.round(split.netAmount * refundRatio * 100) / 100,
870
+ status: SPLIT_STATUS.WAIVED
871
+ }));
872
+ }
873
+ function calculateCommissionWithSplits(amount, commissionRate, gatewayFeeRate = 0, options = {}) {
874
+ const { affiliateRate = 0, affiliateId = null, affiliateType = "user" } = options;
875
+ if (commissionRate <= 0 && affiliateRate <= 0) {
876
+ return null;
877
+ }
878
+ const splitRules = [];
879
+ if (commissionRate > 0) {
880
+ splitRules.push({
881
+ type: SPLIT_TYPE.PLATFORM_COMMISSION,
882
+ recipientId: "platform",
883
+ recipientType: "platform",
884
+ rate: commissionRate
885
+ });
886
+ }
887
+ if (affiliateRate > 0 && affiliateId) {
888
+ splitRules.push({
889
+ type: SPLIT_TYPE.AFFILIATE_COMMISSION,
890
+ recipientId: affiliateId,
891
+ recipientType: affiliateType,
892
+ rate: affiliateRate
893
+ });
894
+ }
895
+ const splits = calculateSplits(amount, splitRules, gatewayFeeRate);
896
+ const platformSplit = splits.find((s) => s.type === SPLIT_TYPE.PLATFORM_COMMISSION);
897
+ const affiliateSplit = splits.find((s) => s.type === SPLIT_TYPE.AFFILIATE_COMMISSION);
898
+ return {
899
+ rate: commissionRate,
900
+ grossAmount: platformSplit?.grossAmount ?? 0,
901
+ gatewayFeeRate: platformSplit?.gatewayFeeRate ?? 0,
902
+ gatewayFeeAmount: platformSplit?.gatewayFeeAmount ?? 0,
903
+ netAmount: platformSplit?.netAmount ?? 0,
904
+ status: "pending",
905
+ ...splits.length > 0 && { splits },
906
+ ...affiliateSplit && {
907
+ affiliate: {
908
+ recipientId: affiliateSplit.recipientId,
909
+ recipientType: affiliateSplit.recipientType,
910
+ rate: affiliateSplit.rate,
911
+ grossAmount: affiliateSplit.grossAmount,
912
+ netAmount: affiliateSplit.netAmount
913
+ }
914
+ }
915
+ };
916
+ }
917
+ var LIBRARY_CATEGORIES = {
918
+ SUBSCRIPTION: "subscription",
919
+ PURCHASE: "purchase"
920
+ };
921
+
922
+ // src/utils/category-resolver.ts
923
+ function resolveCategory(entity, monetizationType, categoryMappings = {}) {
924
+ if (entity && categoryMappings[entity]) {
925
+ return categoryMappings[entity];
926
+ }
927
+ switch (monetizationType) {
928
+ case "subscription":
929
+ return LIBRARY_CATEGORIES.SUBSCRIPTION;
930
+ // 'subscription'
931
+ case "purchase":
932
+ return LIBRARY_CATEGORIES.PURCHASE;
933
+ // 'purchase'
934
+ default:
935
+ return LIBRARY_CATEGORIES.SUBSCRIPTION;
936
+ }
937
+ }
938
+ function isCategoryValid(category, allowedCategories = []) {
939
+ return allowedCategories.includes(category);
940
+ }
941
+
942
+ // src/utils/subscription/period.ts
943
+ function addDuration(startDate, duration, unit = "days") {
944
+ const date = new Date(startDate);
945
+ switch (unit) {
946
+ case "months":
947
+ case "month":
948
+ date.setMonth(date.getMonth() + duration);
949
+ return date;
950
+ case "years":
951
+ case "year":
952
+ date.setFullYear(date.getFullYear() + duration);
953
+ return date;
954
+ case "weeks":
955
+ case "week":
956
+ date.setDate(date.getDate() + duration * 7);
957
+ return date;
958
+ case "days":
959
+ case "day":
960
+ default:
961
+ date.setDate(date.getDate() + duration);
962
+ return date;
963
+ }
964
+ }
965
+ function calculatePeriodRange(params) {
966
+ const {
967
+ currentEndDate = null,
968
+ startDate = null,
969
+ duration,
970
+ unit = "days",
971
+ now = /* @__PURE__ */ new Date()
972
+ } = params;
973
+ let periodStart;
974
+ if (startDate) {
975
+ periodStart = new Date(startDate);
976
+ } else if (currentEndDate) {
977
+ const end = new Date(currentEndDate);
978
+ periodStart = end > now ? end : now;
979
+ } else {
980
+ periodStart = now;
981
+ }
982
+ const periodEnd = addDuration(periodStart, duration, unit);
983
+ return { startDate: periodStart, endDate: periodEnd };
984
+ }
985
+ function calculateProratedAmount(params) {
986
+ const {
987
+ amountPaid,
988
+ startDate,
989
+ endDate,
990
+ asOfDate = /* @__PURE__ */ new Date(),
991
+ precision = 2
992
+ } = params;
993
+ if (!amountPaid || amountPaid <= 0) return 0;
994
+ const start = new Date(startDate);
995
+ const end = new Date(endDate);
996
+ const asOf = new Date(asOfDate);
997
+ const totalMs = end.getTime() - start.getTime();
998
+ if (totalMs <= 0) return 0;
999
+ const remainingMs = Math.max(0, end.getTime() - asOf.getTime());
1000
+ if (remainingMs <= 0) return 0;
1001
+ const ratio = remainingMs / totalMs;
1002
+ const amount = amountPaid * ratio;
1003
+ const factor = 10 ** precision;
1004
+ return Math.round(amount * factor) / factor;
1005
+ }
1006
+ function resolveIntervalToDuration(interval = "month", intervalCount = 1) {
1007
+ const normalized = (interval || "month").toLowerCase();
1008
+ const count = Number(intervalCount) > 0 ? Number(intervalCount) : 1;
1009
+ switch (normalized) {
1010
+ case "year":
1011
+ case "years":
1012
+ return { duration: count, unit: "years" };
1013
+ case "week":
1014
+ case "weeks":
1015
+ return { duration: count, unit: "weeks" };
1016
+ case "quarter":
1017
+ case "quarters":
1018
+ return { duration: count * 3, unit: "months" };
1019
+ case "day":
1020
+ case "days":
1021
+ return { duration: count, unit: "days" };
1022
+ case "month":
1023
+ case "months":
1024
+ default:
1025
+ return { duration: count, unit: "months" };
1026
+ }
1027
+ }
1028
+
1029
+ // src/enums/subscription.enums.ts
1030
+ var SUBSCRIPTION_STATUS = {
1031
+ PAUSED: "paused",
1032
+ CANCELLED: "cancelled"};
1033
+
1034
+ // src/utils/subscription/actions.ts
1035
+ function isSubscriptionActive(subscription) {
1036
+ if (!subscription) return false;
1037
+ if (!subscription.isActive) return false;
1038
+ if (subscription.endDate) {
1039
+ const now = /* @__PURE__ */ new Date();
1040
+ const endDate = new Date(subscription.endDate);
1041
+ if (endDate < now) return false;
1042
+ }
1043
+ return true;
1044
+ }
1045
+ function canRenewSubscription(entity) {
1046
+ if (!entity?.subscription) return false;
1047
+ return isSubscriptionActive(entity.subscription);
1048
+ }
1049
+ function canCancelSubscription(entity) {
1050
+ if (!entity?.subscription) return false;
1051
+ if (!isSubscriptionActive(entity.subscription)) return false;
1052
+ return !entity.subscription.canceledAt;
1053
+ }
1054
+ function canPauseSubscription(entity) {
1055
+ if (!entity?.subscription) return false;
1056
+ if (entity.status === SUBSCRIPTION_STATUS.PAUSED) return false;
1057
+ if (entity.status === SUBSCRIPTION_STATUS.CANCELLED) return false;
1058
+ return isSubscriptionActive(entity.subscription);
1059
+ }
1060
+ function canResumeSubscription(entity) {
1061
+ if (!entity?.subscription) return false;
1062
+ return entity.status === SUBSCRIPTION_STATUS.PAUSED;
1063
+ }
1064
+
1065
+ export { CircuitBreaker, CircuitOpenError, EDITABLE_MONETIZATION_FIELDS_PRE_VERIFICATION, IdempotencyError, IdempotencyManager, MANUAL_TRANSACTION_CREATE_FIELDS, MANUAL_TRANSACTION_UPDATE_FIELDS, MemoryIdempotencyStore, Money, PROTECTED_MONETIZATION_FIELDS, RetryExhaustedError, TRANSACTION_MANAGEMENT_TYPE, addDuration, calculateCommission, calculateCommissionWithSplits, calculateDelay, calculateOrganizationPayout, calculatePeriodRange, calculateProratedAmount, calculateSplits, canCancelSubscription, canPauseSubscription, canRenewSubscription, canResumeSubscription, canSelfVerify, createCircuitBreaker, createIdempotencyManager, fromSmallestUnit, getAllowedUpdateFields, getTransactionType, isCategoryValid, isManualTransaction, isMonetizationTransaction, isRetryableError, isSubscriptionActive, logger, logger_default as loggerDefault, resilientExecute, resolveCategory, resolveIntervalToDuration, retry, retryWithResult, reverseCommission, reverseSplits, setLogger, toSmallestUnit, triggerHook, validateFieldUpdate };
1066
+ //# sourceMappingURL=index.js.map
1067
+ //# sourceMappingURL=index.js.map