@classytic/revenue 1.1.2 → 1.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -7
- package/dist/application/services/index.d.mts +4 -0
- package/dist/application/services/index.mjs +3 -0
- package/dist/base-CsTlVQJe.d.mts +136 -0
- package/dist/base-DCoyIUj6.mjs +152 -0
- package/dist/category-resolver-DV83N8ok.mjs +284 -0
- package/dist/commission-split-BzB8cd39.mjs +485 -0
- package/dist/core/events.d.mts +294 -0
- package/dist/core/events.mjs +100 -0
- package/dist/core/index.d.mts +9 -0
- package/dist/core/index.mjs +8 -0
- package/dist/enums/index.d.mts +157 -0
- package/dist/enums/index.mjs +56 -0
- package/dist/errors-rRdOqnWx.d.mts +787 -0
- package/dist/escrow.enums-CZGrrdg7.mjs +101 -0
- package/dist/{escrow.enums-CE0VQsfe.d.ts → escrow.enums-DwdLuuve.d.mts} +30 -28
- package/dist/idempotency-DaYcUGY1.mjs +172 -0
- package/dist/index-Dsp7H5Wb.d.mts +471 -0
- package/dist/index.d.mts +9 -0
- package/dist/index.mjs +38 -0
- package/dist/infrastructure/plugins/{index.d.ts → index.d.mts} +81 -109
- package/dist/infrastructure/plugins/index.mjs +345 -0
- package/dist/money-CvrDOijQ.mjs +271 -0
- package/dist/money-DPG8AtJ8.d.mts +112 -0
- package/dist/{payment.enums-C1BiGlRa.d.ts → payment.enums-HAuAS9Pp.d.mts} +14 -13
- package/dist/payment.enums-tEFVa-Xp.mjs +69 -0
- package/dist/plugin-BbK0OVHy.d.mts +327 -0
- package/dist/plugin-Cd_V04Em.mjs +210 -0
- package/dist/providers/index.d.mts +3 -0
- package/dist/providers/index.mjs +3 -0
- package/dist/reconciliation/{index.d.ts → index.d.mts} +90 -112
- package/dist/reconciliation/index.mjs +192 -0
- package/dist/retry-HHCOXYdn.d.mts +186 -0
- package/dist/revenue-BhdS7nXh.mjs +553 -0
- package/dist/schemas/index.d.mts +2665 -0
- package/dist/schemas/index.mjs +717 -0
- package/dist/schemas/validation.d.mts +375 -0
- package/dist/schemas/validation.mjs +325 -0
- package/dist/{settlement.enums-ByC1x0ye.d.ts → settlement.enums-DFhkqZEY.d.mts} +31 -29
- package/dist/settlement.schema-DnNSFpGd.d.mts +344 -0
- package/dist/settlement.service-DjzAjezU.d.mts +594 -0
- package/dist/settlement.service-DmdKv0Zu.mjs +2511 -0
- package/dist/split.enums-BrjabxIX.mjs +86 -0
- package/dist/split.enums-DmskfLOM.d.mts +43 -0
- package/dist/tax-BoCt5cEd.d.mts +61 -0
- package/dist/tax-EQ15DO81.mjs +162 -0
- package/dist/transaction.enums-pCyMFT4Z.mjs +96 -0
- package/dist/utils/{index.d.ts → index.d.mts} +91 -161
- package/dist/utils/index.mjs +346 -0
- package/package.json +39 -37
- package/dist/application/services/index.d.ts +0 -6
- package/dist/application/services/index.js +0 -3288
- package/dist/application/services/index.js.map +0 -1
- package/dist/core/events.d.ts +0 -455
- package/dist/core/events.js +0 -122
- package/dist/core/events.js.map +0 -1
- package/dist/core/index.d.ts +0 -13
- package/dist/core/index.js +0 -4591
- package/dist/core/index.js.map +0 -1
- package/dist/enums/index.d.ts +0 -159
- package/dist/enums/index.js +0 -296
- package/dist/enums/index.js.map +0 -1
- package/dist/index-DxIK0UmZ.d.ts +0 -633
- package/dist/index-EnfKzDbs.d.ts +0 -806
- package/dist/index-cLJBLUvx.d.ts +0 -478
- package/dist/index.d.ts +0 -43
- package/dist/index.js +0 -4864
- package/dist/index.js.map +0 -1
- package/dist/infrastructure/plugins/index.js +0 -292
- package/dist/infrastructure/plugins/index.js.map +0 -1
- package/dist/money-widWVD7r.d.ts +0 -111
- package/dist/plugin-Bb9HOE10.d.ts +0 -336
- package/dist/providers/index.d.ts +0 -145
- package/dist/providers/index.js +0 -141
- package/dist/providers/index.js.map +0 -1
- package/dist/reconciliation/index.js +0 -140
- package/dist/reconciliation/index.js.map +0 -1
- package/dist/retry-D4hFUwVk.d.ts +0 -194
- package/dist/schemas/index.d.ts +0 -2655
- package/dist/schemas/index.js +0 -841
- package/dist/schemas/index.js.map +0 -1
- package/dist/schemas/validation.d.ts +0 -384
- package/dist/schemas/validation.js +0 -303
- package/dist/schemas/validation.js.map +0 -1
- package/dist/settlement.schema-CpamV7ZY.d.ts +0 -343
- package/dist/split.enums-DG3TxQf9.d.ts +0 -42
- package/dist/tax-CV8A0sxl.d.ts +0 -60
- package/dist/utils/index.js +0 -1202
- package/dist/utils/index.js.map +0 -1
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import { C as ValidationError } from "./category-resolver-DV83N8ok.mjs";
|
|
2
|
+
import { a as SPLIT_TYPE, r as SPLIT_STATUS } from "./split.enums-BrjabxIX.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/core/result.ts
|
|
5
|
+
/**
|
|
6
|
+
* Create a success result
|
|
7
|
+
*/
|
|
8
|
+
function ok(value) {
|
|
9
|
+
return {
|
|
10
|
+
ok: true,
|
|
11
|
+
value
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Create an error result
|
|
16
|
+
*/
|
|
17
|
+
function err(error) {
|
|
18
|
+
return {
|
|
19
|
+
ok: false,
|
|
20
|
+
error
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Check if result is Ok
|
|
25
|
+
*/
|
|
26
|
+
function isOk(result) {
|
|
27
|
+
return result.ok === true;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Check if result is Err
|
|
31
|
+
*/
|
|
32
|
+
function isErr(result) {
|
|
33
|
+
return result.ok === false;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Unwrap a result, throwing if it's an error
|
|
37
|
+
* Use sparingly - prefer pattern matching
|
|
38
|
+
*/
|
|
39
|
+
function unwrap(result) {
|
|
40
|
+
if (result.ok) return result.value;
|
|
41
|
+
throw result.error;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Unwrap a result with a default value
|
|
45
|
+
*/
|
|
46
|
+
function unwrapOr(result, defaultValue) {
|
|
47
|
+
return result.ok ? result.value : defaultValue;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Map over a successful result
|
|
51
|
+
*/
|
|
52
|
+
function map(result, fn) {
|
|
53
|
+
return result.ok ? ok(fn(result.value)) : result;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Map over an error result
|
|
57
|
+
*/
|
|
58
|
+
function mapErr(result, fn) {
|
|
59
|
+
return result.ok ? result : err(fn(result.error));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Flat map (chain) results
|
|
63
|
+
*/
|
|
64
|
+
function flatMap(result, fn) {
|
|
65
|
+
return result.ok ? fn(result.value) : result;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Try-catch wrapper that returns Result
|
|
69
|
+
*/
|
|
70
|
+
async function tryCatch(fn, mapError) {
|
|
71
|
+
try {
|
|
72
|
+
return ok(await fn());
|
|
73
|
+
} catch (e) {
|
|
74
|
+
return err(mapError ? mapError(e) : e);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Synchronous try-catch wrapper
|
|
79
|
+
*/
|
|
80
|
+
function tryCatchSync(fn, mapError) {
|
|
81
|
+
try {
|
|
82
|
+
return ok(fn());
|
|
83
|
+
} catch (e) {
|
|
84
|
+
return err(mapError ? mapError(e) : e);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Combine multiple results - all must succeed
|
|
89
|
+
*/
|
|
90
|
+
function all(results) {
|
|
91
|
+
const values = [];
|
|
92
|
+
for (const result of results) {
|
|
93
|
+
if (!result.ok) return result;
|
|
94
|
+
values.push(result.value);
|
|
95
|
+
}
|
|
96
|
+
return ok(values);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Pattern match on result
|
|
100
|
+
*/
|
|
101
|
+
function match(result, handlers) {
|
|
102
|
+
return result.ok ? handlers.ok(result.value) : handlers.err(result.error);
|
|
103
|
+
}
|
|
104
|
+
const Result = {
|
|
105
|
+
ok,
|
|
106
|
+
err,
|
|
107
|
+
isOk,
|
|
108
|
+
isErr,
|
|
109
|
+
unwrap,
|
|
110
|
+
unwrapOr,
|
|
111
|
+
map,
|
|
112
|
+
mapErr,
|
|
113
|
+
flatMap,
|
|
114
|
+
tryCatch,
|
|
115
|
+
tryCatchSync,
|
|
116
|
+
all,
|
|
117
|
+
match
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region src/shared/utils/resilience/retry.ts
|
|
122
|
+
/**
|
|
123
|
+
* Retry Utilities
|
|
124
|
+
* @classytic/revenue
|
|
125
|
+
*
|
|
126
|
+
* Exponential backoff with jitter for resilient operations
|
|
127
|
+
* Inspired by: AWS SDK retry, Netflix Hystrix, resilience4j
|
|
128
|
+
*/
|
|
129
|
+
const DEFAULT_CONFIG = {
|
|
130
|
+
maxAttempts: 3,
|
|
131
|
+
baseDelay: 1e3,
|
|
132
|
+
maxDelay: 3e4,
|
|
133
|
+
backoffMultiplier: 2,
|
|
134
|
+
jitter: .1,
|
|
135
|
+
retryIf: isRetryableError
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* Calculate delay with exponential backoff and jitter
|
|
139
|
+
*/
|
|
140
|
+
function calculateDelay(attempt, config) {
|
|
141
|
+
const exponentialDelay = config.baseDelay * Math.pow(config.backoffMultiplier, attempt);
|
|
142
|
+
const cappedDelay = Math.min(exponentialDelay, config.maxDelay);
|
|
143
|
+
const jitterRange = cappedDelay * config.jitter;
|
|
144
|
+
const jitter = Math.random() * jitterRange * 2 - jitterRange;
|
|
145
|
+
return Math.round(Math.max(0, cappedDelay + jitter));
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Sleep for specified milliseconds
|
|
149
|
+
*/
|
|
150
|
+
function sleep(ms) {
|
|
151
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Check if error is retryable by default
|
|
155
|
+
*/
|
|
156
|
+
function isRetryableError(error) {
|
|
157
|
+
if (!(error instanceof Error)) return false;
|
|
158
|
+
if (error.message.includes("ECONNREFUSED")) return true;
|
|
159
|
+
if (error.message.includes("ETIMEDOUT")) return true;
|
|
160
|
+
if (error.message.includes("ENOTFOUND")) return true;
|
|
161
|
+
if (error.message.includes("network")) return true;
|
|
162
|
+
if (error.message.includes("timeout")) return true;
|
|
163
|
+
if (error.message.includes("429")) return true;
|
|
164
|
+
if (error.message.includes("rate limit")) return true;
|
|
165
|
+
if (error.message.includes("500")) return true;
|
|
166
|
+
if (error.message.includes("502")) return true;
|
|
167
|
+
if (error.message.includes("503")) return true;
|
|
168
|
+
if (error.message.includes("504")) return true;
|
|
169
|
+
if ("retryable" in error && error.retryable === true) return true;
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Execute operation with retry logic
|
|
174
|
+
*/
|
|
175
|
+
async function retry(operation, config = {}) {
|
|
176
|
+
const fullConfig = {
|
|
177
|
+
...DEFAULT_CONFIG,
|
|
178
|
+
...config
|
|
179
|
+
};
|
|
180
|
+
const state = {
|
|
181
|
+
attempt: 0,
|
|
182
|
+
totalDelay: 0,
|
|
183
|
+
errors: []
|
|
184
|
+
};
|
|
185
|
+
while (state.attempt < fullConfig.maxAttempts) try {
|
|
186
|
+
return await operation();
|
|
187
|
+
} catch (error) {
|
|
188
|
+
state.errors.push(error instanceof Error ? error : new Error(String(error)));
|
|
189
|
+
state.attempt++;
|
|
190
|
+
if (!(fullConfig.retryIf?.(error) ?? isRetryableError(error)) || state.attempt >= fullConfig.maxAttempts) throw new RetryExhaustedError(`Operation failed after ${state.attempt} attempts`, state.errors);
|
|
191
|
+
const delay = calculateDelay(state.attempt - 1, fullConfig);
|
|
192
|
+
state.totalDelay += delay;
|
|
193
|
+
fullConfig.onRetry?.(error, state.attempt, delay);
|
|
194
|
+
await sleep(delay);
|
|
195
|
+
}
|
|
196
|
+
throw new RetryExhaustedError(`Operation failed after ${state.attempt} attempts`, state.errors);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Error thrown when all retries are exhausted
|
|
200
|
+
*/
|
|
201
|
+
var RetryExhaustedError = class extends Error {
|
|
202
|
+
attempts;
|
|
203
|
+
errors;
|
|
204
|
+
constructor(message, errors) {
|
|
205
|
+
super(message);
|
|
206
|
+
this.name = "RetryExhaustedError";
|
|
207
|
+
this.attempts = errors.length;
|
|
208
|
+
this.errors = errors;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Get the last error
|
|
212
|
+
*/
|
|
213
|
+
get lastError() {
|
|
214
|
+
return this.errors[this.errors.length - 1];
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Get the first error
|
|
218
|
+
*/
|
|
219
|
+
get firstError() {
|
|
220
|
+
return this.errors[0];
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
const DEFAULT_CIRCUIT_CONFIG = {
|
|
224
|
+
failureThreshold: 5,
|
|
225
|
+
resetTimeout: 3e4,
|
|
226
|
+
successThreshold: 3,
|
|
227
|
+
monitorWindow: 6e4
|
|
228
|
+
};
|
|
229
|
+
/**
|
|
230
|
+
* Circuit breaker for preventing cascade failures
|
|
231
|
+
* Inspired by: Netflix Hystrix, resilience4j
|
|
232
|
+
*/
|
|
233
|
+
var CircuitBreaker = class {
|
|
234
|
+
state = "closed";
|
|
235
|
+
failures = [];
|
|
236
|
+
successes = 0;
|
|
237
|
+
lastFailure;
|
|
238
|
+
config;
|
|
239
|
+
constructor(config = {}) {
|
|
240
|
+
this.config = {
|
|
241
|
+
...DEFAULT_CIRCUIT_CONFIG,
|
|
242
|
+
...config
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Execute operation through circuit breaker
|
|
247
|
+
*/
|
|
248
|
+
async execute(operation) {
|
|
249
|
+
if (this.state === "open") if (this.shouldAttemptReset()) this.state = "half-open";
|
|
250
|
+
else throw new CircuitOpenError("Circuit is open, request rejected");
|
|
251
|
+
try {
|
|
252
|
+
const result = await operation();
|
|
253
|
+
this.onSuccess();
|
|
254
|
+
return result;
|
|
255
|
+
} catch (error) {
|
|
256
|
+
this.onFailure();
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Execute with Result type
|
|
262
|
+
*/
|
|
263
|
+
async executeWithResult(operation) {
|
|
264
|
+
try {
|
|
265
|
+
return ok(await this.execute(operation));
|
|
266
|
+
} catch (error) {
|
|
267
|
+
return err(error);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
onSuccess() {
|
|
271
|
+
if (this.state === "half-open") {
|
|
272
|
+
this.successes++;
|
|
273
|
+
if (this.successes >= this.config.successThreshold) this.reset();
|
|
274
|
+
}
|
|
275
|
+
this.cleanOldFailures();
|
|
276
|
+
}
|
|
277
|
+
onFailure() {
|
|
278
|
+
this.failures.push(/* @__PURE__ */ new Date());
|
|
279
|
+
this.lastFailure = /* @__PURE__ */ new Date();
|
|
280
|
+
this.successes = 0;
|
|
281
|
+
this.cleanOldFailures();
|
|
282
|
+
if (this.failures.length >= this.config.failureThreshold) this.state = "open";
|
|
283
|
+
}
|
|
284
|
+
shouldAttemptReset() {
|
|
285
|
+
if (!this.lastFailure) return true;
|
|
286
|
+
return Date.now() - this.lastFailure.getTime() >= this.config.resetTimeout;
|
|
287
|
+
}
|
|
288
|
+
cleanOldFailures() {
|
|
289
|
+
const cutoff = Date.now() - this.config.monitorWindow;
|
|
290
|
+
this.failures = this.failures.filter((f) => f.getTime() > cutoff);
|
|
291
|
+
}
|
|
292
|
+
reset() {
|
|
293
|
+
this.state = "closed";
|
|
294
|
+
this.failures = [];
|
|
295
|
+
this.successes = 0;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Get current circuit state
|
|
299
|
+
*/
|
|
300
|
+
getState() {
|
|
301
|
+
return this.state;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Manually reset circuit
|
|
305
|
+
*/
|
|
306
|
+
forceReset() {
|
|
307
|
+
this.reset();
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Get circuit statistics
|
|
311
|
+
*/
|
|
312
|
+
getStats() {
|
|
313
|
+
return {
|
|
314
|
+
state: this.state,
|
|
315
|
+
failures: this.failures.length,
|
|
316
|
+
successes: this.successes,
|
|
317
|
+
lastFailure: this.lastFailure
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
/**
|
|
322
|
+
* Error thrown when circuit is open
|
|
323
|
+
*/
|
|
324
|
+
var CircuitOpenError = class extends Error {
|
|
325
|
+
constructor(message) {
|
|
326
|
+
super(message);
|
|
327
|
+
this.name = "CircuitOpenError";
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
/**
|
|
331
|
+
* Create a circuit breaker
|
|
332
|
+
*/
|
|
333
|
+
function createCircuitBreaker(config) {
|
|
334
|
+
return new CircuitBreaker(config);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
//#endregion
|
|
338
|
+
//#region src/shared/utils/calculators/commission.ts
|
|
339
|
+
/**
|
|
340
|
+
* Build commission object for transaction
|
|
341
|
+
*
|
|
342
|
+
* @param amount - Transaction amount
|
|
343
|
+
* @param commissionRate - Commission rate (0 to 1, e.g., 0.10 for 10%)
|
|
344
|
+
* @param gatewayFeeRate - Gateway fee rate (0 to 1, e.g., 0.018 for 1.8%)
|
|
345
|
+
* @returns Commission object or null
|
|
346
|
+
*/
|
|
347
|
+
function calculateCommission(amount, commissionRate, gatewayFeeRate = 0) {
|
|
348
|
+
if (!commissionRate || commissionRate <= 0) return null;
|
|
349
|
+
if (amount < 0) throw new Error("Transaction amount cannot be negative");
|
|
350
|
+
if (commissionRate < 0 || commissionRate > 1) throw new Error("Commission rate must be between 0 and 1");
|
|
351
|
+
if (gatewayFeeRate < 0 || gatewayFeeRate > 1) throw new Error("Gateway fee rate must be between 0 and 1");
|
|
352
|
+
const grossAmount = Math.round(amount * commissionRate);
|
|
353
|
+
const gatewayFeeAmount = Math.round(amount * gatewayFeeRate);
|
|
354
|
+
return {
|
|
355
|
+
rate: commissionRate,
|
|
356
|
+
grossAmount,
|
|
357
|
+
gatewayFeeRate,
|
|
358
|
+
gatewayFeeAmount,
|
|
359
|
+
netAmount: Math.max(0, grossAmount - gatewayFeeAmount),
|
|
360
|
+
status: "pending"
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Reverse commission on refund (proportional)
|
|
365
|
+
*
|
|
366
|
+
* @param originalCommission - Original commission object
|
|
367
|
+
* @param originalAmount - Original transaction amount
|
|
368
|
+
* @param refundAmount - Amount being refunded
|
|
369
|
+
* @returns Reversed commission or null
|
|
370
|
+
*/
|
|
371
|
+
function reverseCommission(originalCommission, originalAmount, refundAmount) {
|
|
372
|
+
if (!originalCommission?.netAmount) return null;
|
|
373
|
+
if (!originalAmount || originalAmount <= 0) throw new ValidationError("Original amount must be greater than 0", { originalAmount });
|
|
374
|
+
if (refundAmount < 0) throw new ValidationError("Refund amount cannot be negative", { refundAmount });
|
|
375
|
+
if (refundAmount > originalAmount) throw new ValidationError(`Refund amount (${refundAmount}) exceeds original amount (${originalAmount})`, {
|
|
376
|
+
refundAmount,
|
|
377
|
+
originalAmount
|
|
378
|
+
});
|
|
379
|
+
const refundRatio = refundAmount / originalAmount;
|
|
380
|
+
const reversedNetAmount = Math.round(originalCommission.netAmount * refundRatio);
|
|
381
|
+
const reversedGrossAmount = Math.round(originalCommission.grossAmount * refundRatio);
|
|
382
|
+
const reversedGatewayFee = Math.round(originalCommission.gatewayFeeAmount * refundRatio);
|
|
383
|
+
return {
|
|
384
|
+
rate: originalCommission.rate,
|
|
385
|
+
grossAmount: reversedGrossAmount,
|
|
386
|
+
gatewayFeeRate: originalCommission.gatewayFeeRate,
|
|
387
|
+
gatewayFeeAmount: reversedGatewayFee,
|
|
388
|
+
netAmount: reversedNetAmount,
|
|
389
|
+
status: "waived"
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
//#endregion
|
|
394
|
+
//#region src/shared/utils/calculators/commission-split.ts
|
|
395
|
+
/**
|
|
396
|
+
* Commission Split Utilities
|
|
397
|
+
* @classytic/revenue
|
|
398
|
+
*
|
|
399
|
+
* Multi-party commission split calculation for affiliate/referral systems
|
|
400
|
+
*/
|
|
401
|
+
/**
|
|
402
|
+
* Calculate multi-party commission splits
|
|
403
|
+
*
|
|
404
|
+
* @param amount - Transaction amount
|
|
405
|
+
* @param splitRules - Split configuration
|
|
406
|
+
* @param gatewayFeeRate - Gateway fee rate (optional)
|
|
407
|
+
* @returns Split objects
|
|
408
|
+
*
|
|
409
|
+
* @example
|
|
410
|
+
* calculateSplits(1000, [
|
|
411
|
+
* { type: 'platform_commission', recipientId: 'platform', recipientType: 'platform', rate: 0.10 },
|
|
412
|
+
* { type: 'affiliate_commission', recipientId: 'affiliate-123', recipientType: 'user', rate: 0.02 },
|
|
413
|
+
* ], 0.018);
|
|
414
|
+
*
|
|
415
|
+
* Returns:
|
|
416
|
+
* [
|
|
417
|
+
* { type: 'platform_commission', recipientId: 'platform', grossAmount: 100, gatewayFeeAmount: 18, netAmount: 82, ... },
|
|
418
|
+
* { type: 'affiliate_commission', recipientId: 'affiliate-123', grossAmount: 20, gatewayFeeAmount: 0, netAmount: 20, ... },
|
|
419
|
+
* ]
|
|
420
|
+
*/
|
|
421
|
+
function calculateSplits(amount, splitRules = [], gatewayFeeRate = 0) {
|
|
422
|
+
if (!splitRules || splitRules.length === 0) return [];
|
|
423
|
+
if (amount < 0) throw new Error("Transaction amount cannot be negative");
|
|
424
|
+
if (gatewayFeeRate < 0 || gatewayFeeRate > 1) throw new Error("Gateway fee rate must be between 0 and 1");
|
|
425
|
+
const totalRate = splitRules.reduce((sum, rule) => sum + rule.rate, 0);
|
|
426
|
+
if (totalRate > 1) throw new Error(`Total split rate (${totalRate}) cannot exceed 1.0`);
|
|
427
|
+
return splitRules.map((rule, index) => {
|
|
428
|
+
if (rule.rate < 0 || rule.rate > 1) throw new Error(`Split rate must be between 0 and 1 for split ${index}`);
|
|
429
|
+
const grossAmount = Math.round(amount * rule.rate);
|
|
430
|
+
const gatewayFeeAmount = index === 0 && gatewayFeeRate > 0 ? Math.round(amount * gatewayFeeRate) : 0;
|
|
431
|
+
const netAmount = Math.max(0, grossAmount - gatewayFeeAmount);
|
|
432
|
+
return {
|
|
433
|
+
type: rule.type ?? SPLIT_TYPE.CUSTOM,
|
|
434
|
+
recipientId: rule.recipientId,
|
|
435
|
+
recipientType: rule.recipientType,
|
|
436
|
+
rate: rule.rate,
|
|
437
|
+
grossAmount,
|
|
438
|
+
gatewayFeeRate: gatewayFeeAmount > 0 ? gatewayFeeRate : 0,
|
|
439
|
+
gatewayFeeAmount,
|
|
440
|
+
netAmount,
|
|
441
|
+
status: SPLIT_STATUS.PENDING,
|
|
442
|
+
dueDate: rule.dueDate ?? null,
|
|
443
|
+
metadata: rule.metadata ?? {}
|
|
444
|
+
};
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Calculate organization payout after splits
|
|
449
|
+
*
|
|
450
|
+
* @param amount - Total transaction amount
|
|
451
|
+
* @param splits - Calculated splits
|
|
452
|
+
* @returns Amount organization receives
|
|
453
|
+
*/
|
|
454
|
+
function calculateOrganizationPayout(amount, splits = []) {
|
|
455
|
+
const totalSplitAmount = splits.reduce((sum, split) => sum + split.grossAmount, 0);
|
|
456
|
+
return Math.max(0, amount - totalSplitAmount);
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Reverse splits proportionally on refund
|
|
460
|
+
*
|
|
461
|
+
* @param originalSplits - Original split objects
|
|
462
|
+
* @param originalAmount - Original transaction amount
|
|
463
|
+
* @param refundAmount - Amount being refunded
|
|
464
|
+
* @returns Reversed splits
|
|
465
|
+
*/
|
|
466
|
+
function reverseSplits(originalSplits, originalAmount, refundAmount) {
|
|
467
|
+
if (!originalSplits || originalSplits.length === 0) return [];
|
|
468
|
+
if (!originalAmount || originalAmount <= 0) throw new ValidationError("Original amount must be greater than 0", { originalAmount });
|
|
469
|
+
if (refundAmount < 0) throw new ValidationError("Refund amount cannot be negative", { refundAmount });
|
|
470
|
+
if (refundAmount > originalAmount) throw new ValidationError(`Refund amount (${refundAmount}) exceeds original amount (${originalAmount})`, {
|
|
471
|
+
refundAmount,
|
|
472
|
+
originalAmount
|
|
473
|
+
});
|
|
474
|
+
const refundRatio = refundAmount / originalAmount;
|
|
475
|
+
return originalSplits.map((split) => ({
|
|
476
|
+
...split,
|
|
477
|
+
grossAmount: Math.round(split.grossAmount * refundRatio),
|
|
478
|
+
gatewayFeeAmount: Math.round(split.gatewayFeeAmount * refundRatio),
|
|
479
|
+
netAmount: Math.round(split.netAmount * refundRatio),
|
|
480
|
+
status: SPLIT_STATUS.WAIVED
|
|
481
|
+
}));
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
//#endregion
|
|
485
|
+
export { unwrapOr as S, match as _, reverseCommission as a, tryCatchSync as b, retry as c, err as d, flatMap as f, mapErr as g, map as h, calculateCommission as i, Result as l, isOk as m, calculateSplits as n, CircuitBreaker as o, isErr as p, reverseSplits as r, createCircuitBreaker as s, calculateOrganizationPayout as t, all as u, ok as v, unwrap as x, tryCatch as y };
|