ts-client-lib 0.0.7
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 +76 -0
- package/auth/TSJWT.d.ts +29 -0
- package/auth/TSJWT.js +44 -0
- package/auth/TSOAuth.d.ts +132 -0
- package/auth/TSOAuth.js +230 -0
- package/devices/TSCordova.d.ts +5 -0
- package/devices/TSCordova.js +52 -0
- package/entities/TSCountries.d.ts +14 -0
- package/entities/TSCountries.js +1188 -0
- package/entities/TSCurrencies.d.ts +35 -0
- package/entities/TSCurrencies.js +604 -0
- package/entities/TSMobilePhones.d.ts +167 -0
- package/entities/TSMobilePhones.js +206 -0
- package/entities/TSMoney.d.ts +149 -0
- package/entities/TSMoney.js +311 -0
- package/entities/currency-amount.d.ts +13 -0
- package/entities/currency-amount.js +43 -0
- package/finance/TSBonus.d.ts +197 -0
- package/finance/TSBonus.js +530 -0
- package/finance/TSKYC.d.ts +563 -0
- package/finance/TSKYC.js +1066 -0
- package/finance/TSTax.d.ts +49 -0
- package/finance/TSTax.js +106 -0
- package/finance/bonus-money.d.ts +41 -0
- package/finance/bonus-money.js +61 -0
- package/games/TSBetSlip.d.ts +72 -0
- package/games/TSBetSlip.js +179 -0
- package/games/TSBetSystem.d.ts +4 -0
- package/games/TSBetSystem.js +48 -0
- package/games/TSLotto.d.ts +35 -0
- package/games/TSLotto.js +205 -0
- package/games/TSPool.d.ts +28 -0
- package/games/TSPool.js +88 -0
- package/package.json +93 -0
- package/utils/TSArray.d.ts +9 -0
- package/utils/TSArray.js +87 -0
- package/utils/TSBoolean.d.ts +4 -0
- package/utils/TSBoolean.js +24 -0
- package/utils/TSCache.d.ts +167 -0
- package/utils/TSCache.js +531 -0
- package/utils/TSDate.d.ts +8 -0
- package/utils/TSDate.js +67 -0
- package/utils/TSHeuristic.d.ts +20 -0
- package/utils/TSHeuristic.js +197 -0
- package/utils/TSLZS.d.ts +42 -0
- package/utils/TSLZS.js +343 -0
- package/utils/TSLog.d.ts +40 -0
- package/utils/TSLog.js +110 -0
- package/utils/TSNumber.d.ts +6 -0
- package/utils/TSNumber.js +68 -0
- package/utils/TSObject.d.ts +29 -0
- package/utils/TSObject.js +312 -0
- package/utils/TSPagination.d.ts +282 -0
- package/utils/TSPagination.js +425 -0
- package/utils/TSPaginationMulti.d.ts +77 -0
- package/utils/TSPaginationMulti.js +356 -0
- package/utils/TSString.d.ts +10 -0
- package/utils/TSString.js +107 -0
- package/utils/TSValidator.d.ts +16 -0
- package/utils/TSValidator.js +74 -0
- package/utils/TSWorker.d.ts +3 -0
- package/utils/TSWorker.js +32 -0
- package/utils/diacritics-removal-map.d.ts +5 -0
- package/utils/diacritics-removal-map.js +341 -0
package/finance/TSKYC.js
ADDED
|
@@ -0,0 +1,1066 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* KYCEligibility - Client-Safe KYC Validation & Eligibility Checker
|
|
4
|
+
*
|
|
5
|
+
* A static class for checking KYC eligibility, limits, and requirements on client-side.
|
|
6
|
+
* No database dependencies - pure functions only.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { KYCEligibility } from 'ts-client-lib/finance/TSKyc';
|
|
11
|
+
*
|
|
12
|
+
* // Check if user can make a transaction
|
|
13
|
+
* const result = KYCEligibility.checkTransaction(limits, {
|
|
14
|
+
* currentTier: 'basic',
|
|
15
|
+
* transactionType: 'withdrawal',
|
|
16
|
+
* amount: 5000,
|
|
17
|
+
* currency: 'EUR',
|
|
18
|
+
* usedToday: 1000,
|
|
19
|
+
* usedThisMonth: 10000,
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* if (!result.allowed) {
|
|
23
|
+
* console.log(result.reason);
|
|
24
|
+
* console.log(`Remaining: ${result.remaining}`);
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* // Get requirements for next tier
|
|
28
|
+
* const requirements = KYCEligibility.getTierRequirements('enhanced');
|
|
29
|
+
*
|
|
30
|
+
* // Check if user can access a feature
|
|
31
|
+
* const canWithdraw = KYCEligibility.canPerformAction('withdrawal', 'basic', 'standard');
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
var __assign = (this && this.__assign) || function () {
|
|
35
|
+
__assign = Object.assign || function(t) {
|
|
36
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
37
|
+
s = arguments[i];
|
|
38
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
39
|
+
t[p] = s[p];
|
|
40
|
+
}
|
|
41
|
+
return t;
|
|
42
|
+
};
|
|
43
|
+
return __assign.apply(this, arguments);
|
|
44
|
+
};
|
|
45
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
|
+
exports.prevalidateKYCTransaction = exports.getRemainingLimits = exports.tierMeetsRequirement = exports.canPerformAction = exports.checkTierEligibility = exports.getTierRequirements = exports.checkKYCTransaction = exports.KYCEligibility = exports.KYC_VALIDATOR_ERROR_CODES = exports.KYC_VALIDATOR_ERRORS = void 0;
|
|
47
|
+
exports.canonicalLimitKey = canonicalLimitKey;
|
|
48
|
+
exports.KYC_VALIDATOR_ERRORS = {
|
|
49
|
+
UnknownKYCTier: 'MSKYCUnknownKYCTier',
|
|
50
|
+
FileTooLarge: 'MSKYCFileTooLarge',
|
|
51
|
+
FileTypeNotAllowed: 'MSKYCFileTypeNotAllowed',
|
|
52
|
+
FileExtensionNotAllowed: 'MSKYCFileExtensionNotAllowed',
|
|
53
|
+
// ─── Pre-validation rejection codes (client + server share these so i18n stays consistent) ───
|
|
54
|
+
/** KYC status is not approved or in-review (e.g. rejected/suspended/expired). */
|
|
55
|
+
KycStatusNotAllowed: 'MSKYCStatusNotAllowed',
|
|
56
|
+
/** Stored `expiresAt` is past. Distinct from `KycStatusNotAllowed` so UIs can prompt re-verification. */
|
|
57
|
+
KycExpired: 'MSKYCExpired',
|
|
58
|
+
/** Account flagged suspended at the operator/auth layer. */
|
|
59
|
+
AccountSuspended: 'MSKYCAccountSuspended',
|
|
60
|
+
/** Self-exclusion window is active. */
|
|
61
|
+
SelfExcluded: 'MSKYCSelfExcluded',
|
|
62
|
+
/** Wallet status is not 'active' (suspended/closed). */
|
|
63
|
+
WalletNotActive: 'MSKYCWalletNotActive',
|
|
64
|
+
/** Wallet balance < amount on an outflow operation. */
|
|
65
|
+
InsufficientBalance: 'MSKYCInsufficientBalance',
|
|
66
|
+
/** Operation type has no limit profile at this tier (e.g. transfer at tier 'none'). */
|
|
67
|
+
TierNotPermitted: 'MSKYCTierNotPermitted',
|
|
68
|
+
/** Amount below the per-transaction min. */
|
|
69
|
+
AmountBelowMin: 'MSKYCAmountBelowMin',
|
|
70
|
+
/** Amount above the per-transaction max (upgrade may help). */
|
|
71
|
+
AmountAboveMax: 'MSKYCAmountAboveMax',
|
|
72
|
+
/** Daily aggregate cap would be exceeded. */
|
|
73
|
+
DailyLimitExceeded: 'MSKYCDailyLimitExceeded',
|
|
74
|
+
/** Weekly aggregate cap would be exceeded. */
|
|
75
|
+
WeeklyLimitExceeded: 'MSKYCWeeklyLimitExceeded',
|
|
76
|
+
/** Monthly aggregate cap would be exceeded. */
|
|
77
|
+
MonthlyLimitExceeded: 'MSKYCMonthlyLimitExceeded',
|
|
78
|
+
/** Per-period transaction-count cap reached. */
|
|
79
|
+
TransactionCountExceeded: 'MSKYCTransactionCountExceeded',
|
|
80
|
+
/** Deposit would push wallet balance above the tier's max-balance ceiling. */
|
|
81
|
+
MaxBalanceExceeded: 'MSKYCMaxBalanceExceeded'
|
|
82
|
+
};
|
|
83
|
+
exports.KYC_VALIDATOR_ERROR_CODES = [
|
|
84
|
+
exports.KYC_VALIDATOR_ERRORS.UnknownKYCTier,
|
|
85
|
+
exports.KYC_VALIDATOR_ERRORS.FileTooLarge,
|
|
86
|
+
exports.KYC_VALIDATOR_ERRORS.FileTypeNotAllowed,
|
|
87
|
+
exports.KYC_VALIDATOR_ERRORS.FileExtensionNotAllowed,
|
|
88
|
+
exports.KYC_VALIDATOR_ERRORS.KycStatusNotAllowed,
|
|
89
|
+
exports.KYC_VALIDATOR_ERRORS.KycExpired,
|
|
90
|
+
exports.KYC_VALIDATOR_ERRORS.AccountSuspended,
|
|
91
|
+
exports.KYC_VALIDATOR_ERRORS.SelfExcluded,
|
|
92
|
+
exports.KYC_VALIDATOR_ERRORS.WalletNotActive,
|
|
93
|
+
exports.KYC_VALIDATOR_ERRORS.InsufficientBalance,
|
|
94
|
+
exports.KYC_VALIDATOR_ERRORS.TierNotPermitted,
|
|
95
|
+
exports.KYC_VALIDATOR_ERRORS.AmountBelowMin,
|
|
96
|
+
exports.KYC_VALIDATOR_ERRORS.AmountAboveMax,
|
|
97
|
+
exports.KYC_VALIDATOR_ERRORS.DailyLimitExceeded,
|
|
98
|
+
exports.KYC_VALIDATOR_ERRORS.WeeklyLimitExceeded,
|
|
99
|
+
exports.KYC_VALIDATOR_ERRORS.MonthlyLimitExceeded,
|
|
100
|
+
exports.KYC_VALIDATOR_ERRORS.TransactionCountExceeded,
|
|
101
|
+
exports.KYC_VALIDATOR_ERRORS.MaxBalanceExceeded
|
|
102
|
+
];
|
|
103
|
+
/**
|
|
104
|
+
* Map operation type → tier-limit profile key. `KYCTierLimits` stores three profiles
|
|
105
|
+
* (deposit / withdrawal / transfer); `order` reuses `deposit`. Anything else is invalid
|
|
106
|
+
* at the type level so this is exhaustive.
|
|
107
|
+
*
|
|
108
|
+
* **PXE-7** — `tierLimits[X]` MUST go through this helper. Raw `tierLimits[type]` returns
|
|
109
|
+
* undefined for `'order'` and silently bypasses tier-limit enforcement.
|
|
110
|
+
* See graphify/pre-execution/cross-product-admission-canonical.md.
|
|
111
|
+
*/
|
|
112
|
+
function canonicalLimitKey(t) {
|
|
113
|
+
return t === 'order' ? 'deposit' : t;
|
|
114
|
+
}
|
|
115
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
116
|
+
// CONSTANTS
|
|
117
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
118
|
+
/**
|
|
119
|
+
* Tier hierarchy (lower index = lower tier)
|
|
120
|
+
*/
|
|
121
|
+
var TIER_LEVELS = {
|
|
122
|
+
'none': 0,
|
|
123
|
+
'basic': 1,
|
|
124
|
+
'standard': 2,
|
|
125
|
+
'enhanced': 3,
|
|
126
|
+
'full': 4,
|
|
127
|
+
'professional': 5
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* Tier display names
|
|
131
|
+
*/
|
|
132
|
+
var TIER_NAMES = {
|
|
133
|
+
'none': 'Unverified',
|
|
134
|
+
'basic': 'Basic',
|
|
135
|
+
'standard': 'Standard',
|
|
136
|
+
'enhanced': 'Enhanced',
|
|
137
|
+
'full': 'Full',
|
|
138
|
+
'professional': 'Professional'
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* Tier descriptions
|
|
142
|
+
*/
|
|
143
|
+
var TIER_DESCRIPTIONS = {
|
|
144
|
+
'none': 'No verification completed',
|
|
145
|
+
'basic': 'Email and phone verified',
|
|
146
|
+
'standard': 'Government-issued ID verified',
|
|
147
|
+
'enhanced': 'ID and address verified',
|
|
148
|
+
'full': 'Full KYC with source of funds',
|
|
149
|
+
'professional': 'Corporate/institutional verification'
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* Default tier requirements
|
|
153
|
+
*/
|
|
154
|
+
var DEFAULT_TIER_REQUIREMENTS = {
|
|
155
|
+
none: {
|
|
156
|
+
tier: 'none',
|
|
157
|
+
displayName: TIER_NAMES.none,
|
|
158
|
+
description: TIER_DESCRIPTIONS.none,
|
|
159
|
+
documents: [],
|
|
160
|
+
checks: [],
|
|
161
|
+
information: []
|
|
162
|
+
},
|
|
163
|
+
basic: {
|
|
164
|
+
tier: 'basic',
|
|
165
|
+
displayName: TIER_NAMES.basic,
|
|
166
|
+
description: TIER_DESCRIPTIONS.basic,
|
|
167
|
+
documents: [],
|
|
168
|
+
checks: [
|
|
169
|
+
{ id: 'aml_basic', name: 'AML Screening', type: 'aml', required: true }
|
|
170
|
+
],
|
|
171
|
+
information: [
|
|
172
|
+
{ id: 'firstName', fieldPath: 'personalInfo.firstName', displayName: 'First Name', required: true },
|
|
173
|
+
{ id: 'lastName', fieldPath: 'personalInfo.lastName', displayName: 'Last Name', required: true },
|
|
174
|
+
{ id: 'dob', fieldPath: 'personalInfo.dateOfBirth', displayName: 'Date of Birth', required: true },
|
|
175
|
+
{ id: 'email', fieldPath: 'personalInfo.email', displayName: 'Email Address', required: true }
|
|
176
|
+
]
|
|
177
|
+
},
|
|
178
|
+
standard: {
|
|
179
|
+
tier: 'standard',
|
|
180
|
+
displayName: TIER_NAMES.standard,
|
|
181
|
+
description: TIER_DESCRIPTIONS.standard,
|
|
182
|
+
prerequisiteTier: 'basic',
|
|
183
|
+
documents: [
|
|
184
|
+
{
|
|
185
|
+
id: 'identity',
|
|
186
|
+
name: 'Government-issued ID',
|
|
187
|
+
category: 'identity',
|
|
188
|
+
acceptedTypes: ['passport', 'national_id', 'drivers_license'],
|
|
189
|
+
required: true,
|
|
190
|
+
mustNotBeExpired: true
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
id: 'selfie',
|
|
194
|
+
name: 'Selfie Photo',
|
|
195
|
+
category: 'biometric',
|
|
196
|
+
acceptedTypes: ['selfie'],
|
|
197
|
+
required: true
|
|
198
|
+
}
|
|
199
|
+
],
|
|
200
|
+
checks: [
|
|
201
|
+
{ id: 'aml', name: 'AML Screening', type: 'aml', required: true },
|
|
202
|
+
{ id: 'pep', name: 'PEP Screening', type: 'pep', required: true },
|
|
203
|
+
{ id: 'liveness', name: 'Liveness Check', type: 'liveness', required: true },
|
|
204
|
+
{ id: 'face_match', name: 'Face Match', type: 'face_match', required: true }
|
|
205
|
+
],
|
|
206
|
+
information: [
|
|
207
|
+
{ id: 'nationality', fieldPath: 'personalInfo.nationality', displayName: 'Nationality', required: true },
|
|
208
|
+
{ id: 'residence', fieldPath: 'personalInfo.countryOfResidence', displayName: 'Country of Residence', required: true }
|
|
209
|
+
]
|
|
210
|
+
},
|
|
211
|
+
enhanced: {
|
|
212
|
+
tier: 'enhanced',
|
|
213
|
+
displayName: TIER_NAMES.enhanced,
|
|
214
|
+
description: TIER_DESCRIPTIONS.enhanced,
|
|
215
|
+
prerequisiteTier: 'standard',
|
|
216
|
+
documents: [
|
|
217
|
+
{
|
|
218
|
+
id: 'address',
|
|
219
|
+
name: 'Proof of Address',
|
|
220
|
+
category: 'address',
|
|
221
|
+
acceptedTypes: ['utility_bill', 'bank_statement', 'tax_document', 'government_letter'],
|
|
222
|
+
required: true,
|
|
223
|
+
maxAgeDays: 90
|
|
224
|
+
}
|
|
225
|
+
],
|
|
226
|
+
checks: [
|
|
227
|
+
{ id: 'sanctions', name: 'Sanctions Screening', type: 'sanctions', required: true },
|
|
228
|
+
{ id: 'address_verify', name: 'Address Verification', type: 'address_verification', required: true }
|
|
229
|
+
],
|
|
230
|
+
information: [
|
|
231
|
+
{ id: 'address', fieldPath: 'addresses', displayName: 'Residential Address', required: true }
|
|
232
|
+
]
|
|
233
|
+
},
|
|
234
|
+
full: {
|
|
235
|
+
tier: 'full',
|
|
236
|
+
displayName: TIER_NAMES.full,
|
|
237
|
+
description: TIER_DESCRIPTIONS.full,
|
|
238
|
+
prerequisiteTier: 'enhanced',
|
|
239
|
+
documents: [
|
|
240
|
+
{
|
|
241
|
+
id: 'financial',
|
|
242
|
+
name: 'Source of Funds Documentation',
|
|
243
|
+
category: 'financial',
|
|
244
|
+
acceptedTypes: ['proof_of_income', 'tax_return', 'bank_statement', 'employment_letter'],
|
|
245
|
+
required: true
|
|
246
|
+
}
|
|
247
|
+
],
|
|
248
|
+
checks: [],
|
|
249
|
+
information: [
|
|
250
|
+
{ id: 'occupation', fieldPath: 'personalInfo.occupation', displayName: 'Occupation', required: true },
|
|
251
|
+
{ id: 'sof', fieldPath: 'sourceOfFunds', displayName: 'Source of Funds', required: true }
|
|
252
|
+
]
|
|
253
|
+
},
|
|
254
|
+
professional: {
|
|
255
|
+
tier: 'professional',
|
|
256
|
+
displayName: TIER_NAMES.professional,
|
|
257
|
+
description: TIER_DESCRIPTIONS.professional,
|
|
258
|
+
prerequisiteTier: 'enhanced',
|
|
259
|
+
documents: [
|
|
260
|
+
{
|
|
261
|
+
id: 'company_reg',
|
|
262
|
+
name: 'Company Registration Documents',
|
|
263
|
+
category: 'corporate',
|
|
264
|
+
acceptedTypes: ['company_registration', 'articles_of_incorporation'],
|
|
265
|
+
required: true
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
id: 'ubo',
|
|
269
|
+
name: 'Beneficial Owner Documentation',
|
|
270
|
+
category: 'corporate',
|
|
271
|
+
acceptedTypes: ['shareholder_register'],
|
|
272
|
+
required: true
|
|
273
|
+
}
|
|
274
|
+
],
|
|
275
|
+
checks: [],
|
|
276
|
+
information: [
|
|
277
|
+
{ id: 'company_name', fieldPath: 'businessInfo.companyName', displayName: 'Company Name', required: true },
|
|
278
|
+
{ id: 'reg_number', fieldPath: 'businessInfo.registrationNumber', displayName: 'Registration Number', required: true },
|
|
279
|
+
{ id: 'ubos', fieldPath: 'businessInfo.beneficialOwners', displayName: 'Beneficial Owners', required: true }
|
|
280
|
+
]
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
/**
|
|
284
|
+
* Default tier limits (EUR)
|
|
285
|
+
*/
|
|
286
|
+
var DEFAULT_TIER_LIMITS = {
|
|
287
|
+
none: {
|
|
288
|
+
currency: 'EUR',
|
|
289
|
+
deposit: { minAmount: 10, maxAmount: 0, dailyLimit: 0, monthlyLimit: 0 },
|
|
290
|
+
withdrawal: { minAmount: 10, maxAmount: 0, dailyLimit: 0, monthlyLimit: 0 }
|
|
291
|
+
},
|
|
292
|
+
basic: {
|
|
293
|
+
currency: 'EUR',
|
|
294
|
+
deposit: { minAmount: 10, maxAmount: 1000, dailyLimit: 2000, monthlyLimit: 5000 },
|
|
295
|
+
withdrawal: { minAmount: 10, maxAmount: 500, dailyLimit: 1000, monthlyLimit: 2500 },
|
|
296
|
+
transfer: { minAmount: 10, maxAmount: 500, dailyLimit: 1000, monthlyLimit: 2500 },
|
|
297
|
+
maxBalance: 5000
|
|
298
|
+
},
|
|
299
|
+
standard: {
|
|
300
|
+
currency: 'EUR',
|
|
301
|
+
deposit: { minAmount: 10, maxAmount: 5000, dailyLimit: 10000, monthlyLimit: 25000 },
|
|
302
|
+
withdrawal: { minAmount: 10, maxAmount: 2500, dailyLimit: 5000, monthlyLimit: 15000 },
|
|
303
|
+
transfer: { minAmount: 10, maxAmount: 2500, dailyLimit: 5000, monthlyLimit: 15000 },
|
|
304
|
+
maxBalance: 50000
|
|
305
|
+
},
|
|
306
|
+
enhanced: {
|
|
307
|
+
currency: 'EUR',
|
|
308
|
+
deposit: { minAmount: 10, maxAmount: 25000, dailyLimit: 50000, monthlyLimit: 150000 },
|
|
309
|
+
withdrawal: { minAmount: 10, maxAmount: 15000, dailyLimit: 30000, monthlyLimit: 100000 },
|
|
310
|
+
transfer: { minAmount: 10, maxAmount: 15000, dailyLimit: 30000, monthlyLimit: 100000 },
|
|
311
|
+
maxBalance: 250000
|
|
312
|
+
},
|
|
313
|
+
full: {
|
|
314
|
+
currency: 'EUR',
|
|
315
|
+
deposit: { minAmount: 10, maxAmount: 100000, dailyLimit: 250000, monthlyLimit: 1000000 },
|
|
316
|
+
withdrawal: { minAmount: 10, maxAmount: 50000, dailyLimit: 150000, monthlyLimit: 500000 },
|
|
317
|
+
transfer: { minAmount: 10, maxAmount: 50000, dailyLimit: 150000, monthlyLimit: 500000 }
|
|
318
|
+
},
|
|
319
|
+
professional: {
|
|
320
|
+
currency: 'EUR',
|
|
321
|
+
deposit: { minAmount: 100, maxAmount: 1000000, dailyLimit: 5000000, monthlyLimit: 50000000 },
|
|
322
|
+
withdrawal: { minAmount: 100, maxAmount: 500000, dailyLimit: 2500000, monthlyLimit: 25000000 },
|
|
323
|
+
transfer: { minAmount: 100, maxAmount: 500000, dailyLimit: 2500000, monthlyLimit: 25000000 }
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
/**
|
|
327
|
+
* Minimum tier required for actions
|
|
328
|
+
*/
|
|
329
|
+
var DEFAULT_ACTION_TIERS = {
|
|
330
|
+
'deposit': 'basic',
|
|
331
|
+
'withdrawal': 'standard',
|
|
332
|
+
'transfer': 'standard',
|
|
333
|
+
'trade': 'standard',
|
|
334
|
+
'bet': 'basic',
|
|
335
|
+
'purchase': 'basic',
|
|
336
|
+
'claim_bonus': 'basic',
|
|
337
|
+
'crypto_withdrawal': 'enhanced',
|
|
338
|
+
'high_value_transaction': 'full',
|
|
339
|
+
'corporate_account': 'professional'
|
|
340
|
+
};
|
|
341
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
342
|
+
// STATIC CLASS
|
|
343
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
344
|
+
var KYCEligibility = /** @class */ (function () {
|
|
345
|
+
function KYCEligibility() {
|
|
346
|
+
}
|
|
347
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
348
|
+
// TIER UTILITIES
|
|
349
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
350
|
+
/**
|
|
351
|
+
* Get numeric level for a tier (for comparisons)
|
|
352
|
+
*/
|
|
353
|
+
KYCEligibility.getTierLevel = function (tier) {
|
|
354
|
+
var _a;
|
|
355
|
+
return (_a = TIER_LEVELS[tier]) !== null && _a !== void 0 ? _a : 0;
|
|
356
|
+
};
|
|
357
|
+
/**
|
|
358
|
+
* Compare two tiers. Returns negative if a < b, 0 if equal, positive if a > b
|
|
359
|
+
*/
|
|
360
|
+
KYCEligibility.compareTiers = function (a, b) {
|
|
361
|
+
return this.getTierLevel(a) - this.getTierLevel(b);
|
|
362
|
+
};
|
|
363
|
+
/**
|
|
364
|
+
* Check if tier meets minimum requirement
|
|
365
|
+
*/
|
|
366
|
+
KYCEligibility.tierMeetsRequirement = function (currentTier, requiredTier) {
|
|
367
|
+
return this.getTierLevel(currentTier) >= this.getTierLevel(requiredTier);
|
|
368
|
+
};
|
|
369
|
+
/**
|
|
370
|
+
* Get display name for a tier
|
|
371
|
+
*/
|
|
372
|
+
KYCEligibility.getTierDisplayName = function (tier) {
|
|
373
|
+
var _a;
|
|
374
|
+
return (_a = TIER_NAMES[tier]) !== null && _a !== void 0 ? _a : tier;
|
|
375
|
+
};
|
|
376
|
+
/**
|
|
377
|
+
* Get description for a tier
|
|
378
|
+
*/
|
|
379
|
+
KYCEligibility.getTierDescription = function (tier) {
|
|
380
|
+
var _a;
|
|
381
|
+
return (_a = TIER_DESCRIPTIONS[tier]) !== null && _a !== void 0 ? _a : '';
|
|
382
|
+
};
|
|
383
|
+
/**
|
|
384
|
+
* Get next tier in the hierarchy
|
|
385
|
+
*/
|
|
386
|
+
KYCEligibility.getNextTier = function (currentTier) {
|
|
387
|
+
var tiers = ['none', 'basic', 'standard', 'enhanced', 'full', 'professional'];
|
|
388
|
+
var currentIndex = tiers.indexOf(currentTier);
|
|
389
|
+
if (currentIndex === -1 || currentIndex >= tiers.length - 1)
|
|
390
|
+
return null;
|
|
391
|
+
return tiers[currentIndex + 1];
|
|
392
|
+
};
|
|
393
|
+
/**
|
|
394
|
+
* Get all tiers in order
|
|
395
|
+
*/
|
|
396
|
+
KYCEligibility.getAllTiers = function () {
|
|
397
|
+
return ['none', 'basic', 'standard', 'enhanced', 'full', 'professional'];
|
|
398
|
+
};
|
|
399
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
400
|
+
// TIER REQUIREMENTS
|
|
401
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
402
|
+
/**
|
|
403
|
+
* Get requirements for a tier
|
|
404
|
+
*/
|
|
405
|
+
KYCEligibility.getTierRequirements = function (tier) {
|
|
406
|
+
return DEFAULT_TIER_REQUIREMENTS[tier];
|
|
407
|
+
};
|
|
408
|
+
/**
|
|
409
|
+
* Get all requirements needed to reach a tier from current tier
|
|
410
|
+
*/
|
|
411
|
+
KYCEligibility.getRequirementsToReachTier = function (currentTier, targetTier) {
|
|
412
|
+
var requirements = [];
|
|
413
|
+
var tiers = this.getAllTiers();
|
|
414
|
+
var currentIndex = tiers.indexOf(currentTier);
|
|
415
|
+
var targetIndex = tiers.indexOf(targetTier);
|
|
416
|
+
if (targetIndex <= currentIndex)
|
|
417
|
+
return [];
|
|
418
|
+
for (var i = currentIndex + 1; i <= targetIndex; i++) {
|
|
419
|
+
requirements.push(this.getTierRequirements(tiers[i]));
|
|
420
|
+
}
|
|
421
|
+
return requirements;
|
|
422
|
+
};
|
|
423
|
+
/**
|
|
424
|
+
* Check eligibility for a target tier
|
|
425
|
+
*/
|
|
426
|
+
KYCEligibility.checkTierEligibility = function (targetTier, context) {
|
|
427
|
+
var requirements = this.getTierRequirements(targetTier);
|
|
428
|
+
var missingRequirements = [];
|
|
429
|
+
var completedRequirements = [];
|
|
430
|
+
this.collectTierPrerequisite(requirements, context, missingRequirements, completedRequirements);
|
|
431
|
+
this.collectTierDocuments(requirements, context, missingRequirements, completedRequirements);
|
|
432
|
+
this.collectTierVerificationChecks(requirements, context, missingRequirements, completedRequirements);
|
|
433
|
+
this.collectTierMinimumAge(requirements, context, missingRequirements, completedRequirements);
|
|
434
|
+
var total = missingRequirements.length + completedRequirements.length;
|
|
435
|
+
var progress = total > 0 ? Math.round((completedRequirements.length / total) * 100) : 0;
|
|
436
|
+
return {
|
|
437
|
+
currentTier: context.currentTier,
|
|
438
|
+
targetTier: targetTier,
|
|
439
|
+
eligible: missingRequirements.length === 0,
|
|
440
|
+
missingRequirements: missingRequirements,
|
|
441
|
+
completedRequirements: completedRequirements,
|
|
442
|
+
progress: progress
|
|
443
|
+
};
|
|
444
|
+
};
|
|
445
|
+
KYCEligibility.collectTierPrerequisite = function (requirements, context, missingRequirements, completedRequirements) {
|
|
446
|
+
if (!requirements.prerequisiteTier)
|
|
447
|
+
return;
|
|
448
|
+
if (!this.tierMeetsRequirement(context.currentTier, requirements.prerequisiteTier)) {
|
|
449
|
+
missingRequirements.push("Requires ".concat(this.getTierDisplayName(requirements.prerequisiteTier), " tier first"));
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
completedRequirements.push("".concat(this.getTierDisplayName(requirements.prerequisiteTier), " tier"));
|
|
453
|
+
};
|
|
454
|
+
KYCEligibility.collectTierDocuments = function (requirements, context, missingRequirements, completedRequirements) {
|
|
455
|
+
for (var _i = 0, _a = requirements.documents; _i < _a.length; _i++) {
|
|
456
|
+
var doc = _a[_i];
|
|
457
|
+
if (!doc.required)
|
|
458
|
+
continue;
|
|
459
|
+
var hasDoc = (doc.category === 'identity' && Boolean(context.hasIdentityDoc)) ||
|
|
460
|
+
(doc.category === 'address' && Boolean(context.hasAddressDoc)) ||
|
|
461
|
+
(doc.category === 'financial' && Boolean(context.hasFinancialDoc));
|
|
462
|
+
if (hasDoc)
|
|
463
|
+
completedRequirements.push(doc.name);
|
|
464
|
+
else
|
|
465
|
+
missingRequirements.push(doc.name);
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
KYCEligibility.collectTierVerificationChecks = function (requirements, context, missingRequirements, completedRequirements) {
|
|
469
|
+
for (var _i = 0, _a = requirements.checks; _i < _a.length; _i++) {
|
|
470
|
+
var check = _a[_i];
|
|
471
|
+
if (!check.required)
|
|
472
|
+
continue;
|
|
473
|
+
var passed = (check.type === 'aml' && Boolean(context.amlCheckPassed)) ||
|
|
474
|
+
(check.type === 'pep' && Boolean(context.pepCheckPassed)) ||
|
|
475
|
+
(check.type === 'sanctions' && Boolean(context.sanctionsCheckPassed)) ||
|
|
476
|
+
(check.type === 'liveness' && Boolean(context.livenessCheckPassed));
|
|
477
|
+
if (passed)
|
|
478
|
+
completedRequirements.push(check.name);
|
|
479
|
+
else
|
|
480
|
+
missingRequirements.push(check.name);
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
KYCEligibility.collectTierMinimumAge = function (requirements, context, missingRequirements, completedRequirements) {
|
|
484
|
+
if (!requirements.minimumAge || context.userAge == null)
|
|
485
|
+
return;
|
|
486
|
+
if (context.userAge < requirements.minimumAge) {
|
|
487
|
+
missingRequirements.push("Minimum age ".concat(requirements.minimumAge));
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
completedRequirements.push('Age requirement');
|
|
491
|
+
};
|
|
492
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
493
|
+
// PRE-VALIDATION — bulletproof client + server admission control
|
|
494
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
495
|
+
/**
|
|
496
|
+
* Run the full admission gate against a single input. Pure — no I/O. Same function
|
|
497
|
+
* intended to run on the client (before submit) and on the server (in pre-transaction
|
|
498
|
+
* middleware or the wallet.order handler) for defence-in-depth.
|
|
499
|
+
*
|
|
500
|
+
* Order of checks (first failure wins; same order on client and server):
|
|
501
|
+
*
|
|
502
|
+
* 1. **Account preconditions** — suspended? self-excluded?
|
|
503
|
+
* 2. **Wallet preconditions** — status === 'active'?
|
|
504
|
+
* 3. **KYC snapshot** — status approved/in_review? expiresAt > now?
|
|
505
|
+
* 4. **Balance** (outflows only) — wallet.balance >= amount?
|
|
506
|
+
* 5. **Tier limits** — delegates to `checkTransaction` (min/max per-tx, daily/monthly
|
|
507
|
+
* aggregate, transaction count, max-balance ceiling). Bounded `code` flows through.
|
|
508
|
+
*
|
|
509
|
+
* The client app supplies `wallet`, `account`, usage, and KYC snapshot from API
|
|
510
|
+
* responses it already has; the server supplies the same fields from its source of
|
|
511
|
+
* truth. Both sides produce identical results.
|
|
512
|
+
*
|
|
513
|
+
* Why this exists alongside `checkTransaction`: that method only handles tier-limit
|
|
514
|
+
* math and assumes a valid KYC snapshot. `prevalidate` is the high-level umbrella —
|
|
515
|
+
* checks account / wallet / status / expiry first, then delegates limits. UI code
|
|
516
|
+
* should always call `prevalidate`, never `checkTransaction` directly.
|
|
517
|
+
*/
|
|
518
|
+
KYCEligibility.prevalidate = function (input) {
|
|
519
|
+
var _a, _b;
|
|
520
|
+
// 1. Account
|
|
521
|
+
var accountReject = this.preRejectAccount(input);
|
|
522
|
+
if (accountReject)
|
|
523
|
+
return accountReject;
|
|
524
|
+
// 2. Wallet status
|
|
525
|
+
var walletStatusReject = this.preRejectWalletStatus(input);
|
|
526
|
+
if (walletStatusReject)
|
|
527
|
+
return walletStatusReject;
|
|
528
|
+
// 3. KYC snapshot
|
|
529
|
+
var kycReject = this.preRejectKyc(input);
|
|
530
|
+
if (kycReject)
|
|
531
|
+
return kycReject;
|
|
532
|
+
// 4. Balance (outflow only)
|
|
533
|
+
var balanceReject = this.preRejectInsufficientBalance(input);
|
|
534
|
+
if (balanceReject)
|
|
535
|
+
return balanceReject;
|
|
536
|
+
// 5. Tier limits — delegate to checkTransaction. The bounded `code` set in each
|
|
537
|
+
// txReject* method flows back here unchanged; we just promote remaining fields
|
|
538
|
+
// into the KYCPrevalidationResult shape for UI consumption.
|
|
539
|
+
var limitsLadder = (_a = input.jurisdictionLimits) !== null && _a !== void 0 ? _a : DEFAULT_TIER_LIMITS;
|
|
540
|
+
var txResult = this.checkTransaction(limitsLadder, {
|
|
541
|
+
currentTier: input.currentTier,
|
|
542
|
+
kycStatus: input.kycStatus,
|
|
543
|
+
transactionType: input.operation,
|
|
544
|
+
amount: input.amount,
|
|
545
|
+
currency: input.currency,
|
|
546
|
+
usedToday: input.usedToday,
|
|
547
|
+
usedThisWeek: input.usedThisWeek,
|
|
548
|
+
usedThisMonth: input.usedThisMonth,
|
|
549
|
+
transactionsToday: input.transactionsToday,
|
|
550
|
+
transactionsThisMonth: input.transactionsThisMonth,
|
|
551
|
+
currentBalance: (_b = input.wallet) === null || _b === void 0 ? void 0 : _b.balance,
|
|
552
|
+
jurisdictionLimits: input.jurisdictionLimits
|
|
553
|
+
});
|
|
554
|
+
return __assign(__assign(__assign(__assign(__assign(__assign(__assign(__assign({ allowed: txResult.allowed }, (txResult.code ? { code: txResult.code } : {})), (txResult.reason ? { reason: txResult.reason } : {})), (txResult.details ? { details: txResult.details } : {})), (txResult.maxAllowed !== undefined ? { maxAllowed: txResult.maxAllowed } : {})), (txResult.dailyRemaining !== undefined ? { dailyRemaining: txResult.dailyRemaining } : {})), (txResult.monthlyRemaining !== undefined ? { monthlyRemaining: txResult.monthlyRemaining } : {})), (txResult.requiredTier ? { upgradeToTier: txResult.requiredTier } : {})), (txResult.upgradeMessage ? { upgradeMessage: txResult.upgradeMessage } : {}));
|
|
555
|
+
};
|
|
556
|
+
KYCEligibility.preRejectAccount = function (input) {
|
|
557
|
+
var _a, _b;
|
|
558
|
+
if ((_a = input.account) === null || _a === void 0 ? void 0 : _a.suspended) {
|
|
559
|
+
return {
|
|
560
|
+
allowed: false,
|
|
561
|
+
code: exports.KYC_VALIDATOR_ERRORS.AccountSuspended,
|
|
562
|
+
reason: 'Account is suspended'
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
if ((_b = input.account) === null || _b === void 0 ? void 0 : _b.selfExcludedUntil) {
|
|
566
|
+
var until = new Date(input.account.selfExcludedUntil);
|
|
567
|
+
var now = Date.now();
|
|
568
|
+
if (until.getTime() > now) {
|
|
569
|
+
return {
|
|
570
|
+
allowed: false,
|
|
571
|
+
code: exports.KYC_VALIDATOR_ERRORS.SelfExcluded,
|
|
572
|
+
reason: "Self-exclusion active until ".concat(until.toISOString()),
|
|
573
|
+
details: { until: until.toISOString() }
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return null;
|
|
578
|
+
};
|
|
579
|
+
KYCEligibility.preRejectWalletStatus = function (input) {
|
|
580
|
+
var _a;
|
|
581
|
+
if (((_a = input.wallet) === null || _a === void 0 ? void 0 : _a.status) && input.wallet.status !== 'active') {
|
|
582
|
+
return {
|
|
583
|
+
allowed: false,
|
|
584
|
+
code: exports.KYC_VALIDATOR_ERRORS.WalletNotActive,
|
|
585
|
+
reason: "Wallet is ".concat(input.wallet.status),
|
|
586
|
+
details: { walletStatus: input.wallet.status }
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
return null;
|
|
590
|
+
};
|
|
591
|
+
KYCEligibility.preRejectKyc = function (input) {
|
|
592
|
+
// 'none' tier is the unverified state — the limit profile (which may be very tight)
|
|
593
|
+
// gates the check inside checkTransaction. Don't reject here purely on status.
|
|
594
|
+
if (input.currentTier !== 'none'
|
|
595
|
+
&& input.kycStatus !== 'approved'
|
|
596
|
+
&& input.kycStatus !== 'in_review') {
|
|
597
|
+
return {
|
|
598
|
+
allowed: false,
|
|
599
|
+
code: exports.KYC_VALIDATOR_ERRORS.KycStatusNotAllowed,
|
|
600
|
+
reason: "KYC status '".concat(input.kycStatus, "' does not allow transactions"),
|
|
601
|
+
details: { kycStatus: input.kycStatus }
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
if (input.expiresAt) {
|
|
605
|
+
var exp = new Date(input.expiresAt);
|
|
606
|
+
if (exp.getTime() < Date.now()) {
|
|
607
|
+
return {
|
|
608
|
+
allowed: false,
|
|
609
|
+
code: exports.KYC_VALIDATOR_ERRORS.KycExpired,
|
|
610
|
+
reason: 'KYC verification has expired',
|
|
611
|
+
details: { expiresAt: exp.toISOString() }
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return null;
|
|
616
|
+
};
|
|
617
|
+
KYCEligibility.preRejectInsufficientBalance = function (input) {
|
|
618
|
+
var _a;
|
|
619
|
+
// Inflow operations don't consume wallet balance.
|
|
620
|
+
if (input.operation === 'deposit')
|
|
621
|
+
return null;
|
|
622
|
+
var balance = (_a = input.wallet) === null || _a === void 0 ? void 0 : _a.balance;
|
|
623
|
+
if (balance == null)
|
|
624
|
+
return null; // caller opted out of balance check
|
|
625
|
+
if (input.amount <= balance)
|
|
626
|
+
return null;
|
|
627
|
+
return {
|
|
628
|
+
allowed: false,
|
|
629
|
+
code: exports.KYC_VALIDATOR_ERRORS.InsufficientBalance,
|
|
630
|
+
reason: "Insufficient balance: have ".concat(balance, " ").concat(input.currency, ", need ").concat(input.amount),
|
|
631
|
+
details: { balance: balance, amount: input.amount, currency: input.currency },
|
|
632
|
+
maxAllowed: Math.max(0, balance)
|
|
633
|
+
};
|
|
634
|
+
};
|
|
635
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
636
|
+
// TRANSACTION LIMITS
|
|
637
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
638
|
+
/**
|
|
639
|
+
* Get limits for a tier
|
|
640
|
+
*/
|
|
641
|
+
KYCEligibility.getTierLimits = function (tier, customLimits) {
|
|
642
|
+
var limits = customLimits !== null && customLimits !== void 0 ? customLimits : DEFAULT_TIER_LIMITS;
|
|
643
|
+
return limits[tier];
|
|
644
|
+
};
|
|
645
|
+
/**
|
|
646
|
+
* Check if a transaction is allowed
|
|
647
|
+
*/
|
|
648
|
+
KYCEligibility.checkTransaction = function (limits, context) {
|
|
649
|
+
var _a, _b;
|
|
650
|
+
var tierLimits = this.resolveTransactionTierLimits(limits, context.currentTier);
|
|
651
|
+
if (!tierLimits) {
|
|
652
|
+
return {
|
|
653
|
+
allowed: false,
|
|
654
|
+
reason: 'Unknown KYC tier',
|
|
655
|
+
code: exports.KYC_VALIDATOR_ERRORS.UnknownKYCTier,
|
|
656
|
+
details: { tier: context.currentTier }
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
var kycStatusReject = this.txRejectInvalidKycStatus(context);
|
|
660
|
+
if (kycStatusReject)
|
|
661
|
+
return kycStatusReject;
|
|
662
|
+
var opResolved = this.resolveTransactionOpLimits(tierLimits, context);
|
|
663
|
+
if (!('opLimits' in opResolved))
|
|
664
|
+
return opResolved;
|
|
665
|
+
var opLimits = opResolved.opLimits;
|
|
666
|
+
var minReject = this.txRejectBelowMinAmount(context, opLimits);
|
|
667
|
+
if (minReject)
|
|
668
|
+
return minReject;
|
|
669
|
+
var maxReject = this.txRejectAboveMaxPerTransaction(context, opLimits);
|
|
670
|
+
if (maxReject)
|
|
671
|
+
return maxReject;
|
|
672
|
+
var usedToday = (_a = context.usedToday) !== null && _a !== void 0 ? _a : 0;
|
|
673
|
+
var dailyRemaining = opLimits.dailyLimit - usedToday;
|
|
674
|
+
var dailyReject = this.txRejectExceedsDailyBudget(context, opLimits, dailyRemaining);
|
|
675
|
+
if (dailyReject)
|
|
676
|
+
return dailyReject;
|
|
677
|
+
var usedThisMonth = (_b = context.usedThisMonth) !== null && _b !== void 0 ? _b : 0;
|
|
678
|
+
var monthlyRemaining = opLimits.monthlyLimit - usedThisMonth;
|
|
679
|
+
var monthlyReject = this.txRejectExceedsMonthlyBudget(context, opLimits, monthlyRemaining);
|
|
680
|
+
if (monthlyReject)
|
|
681
|
+
return monthlyReject;
|
|
682
|
+
var weeklyReject = this.txRejectExceedsWeeklyBudget(context, opLimits);
|
|
683
|
+
if (weeklyReject)
|
|
684
|
+
return weeklyReject;
|
|
685
|
+
var txCountReject = this.txRejectDailyTransactionCount(context, opLimits);
|
|
686
|
+
if (txCountReject)
|
|
687
|
+
return txCountReject;
|
|
688
|
+
var balanceReject = this.txRejectDepositOverMaxBalance(tierLimits, context);
|
|
689
|
+
if (balanceReject)
|
|
690
|
+
return balanceReject;
|
|
691
|
+
return this.txBuildAllowedResult(context, opLimits, dailyRemaining, monthlyRemaining);
|
|
692
|
+
};
|
|
693
|
+
KYCEligibility.resolveTransactionTierLimits = function (limits, currentTier) {
|
|
694
|
+
var _a;
|
|
695
|
+
if ('deposit' in limits)
|
|
696
|
+
return limits;
|
|
697
|
+
return (_a = limits[currentTier]) !== null && _a !== void 0 ? _a : null;
|
|
698
|
+
};
|
|
699
|
+
KYCEligibility.txRejectInvalidKycStatus = function (context) {
|
|
700
|
+
if (context.kycStatus && ['approved', 'in_review'].indexOf(context.kycStatus) === -1) {
|
|
701
|
+
return {
|
|
702
|
+
allowed: false,
|
|
703
|
+
reason: "KYC status '".concat(context.kycStatus, "' does not allow transactions"),
|
|
704
|
+
code: exports.KYC_VALIDATOR_ERRORS.KycStatusNotAllowed,
|
|
705
|
+
details: { kycStatus: context.kycStatus }
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
return null;
|
|
709
|
+
};
|
|
710
|
+
KYCEligibility.resolveTransactionOpLimits = function (tierLimits, context) {
|
|
711
|
+
var _a;
|
|
712
|
+
var opType = canonicalLimitKey(context.transactionType);
|
|
713
|
+
var opLimits = tierLimits[opType];
|
|
714
|
+
if (!opLimits) {
|
|
715
|
+
return {
|
|
716
|
+
allowed: false,
|
|
717
|
+
reason: "Transaction type '".concat(context.transactionType, "' not allowed at this tier"),
|
|
718
|
+
code: exports.KYC_VALIDATOR_ERRORS.TierNotPermitted,
|
|
719
|
+
requiredTier: (_a = this.getNextTier(context.currentTier)) !== null && _a !== void 0 ? _a : undefined
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
return { opLimits: opLimits };
|
|
723
|
+
};
|
|
724
|
+
KYCEligibility.txRejectBelowMinAmount = function (context, opLimits) {
|
|
725
|
+
if (opLimits.minAmount && context.amount < opLimits.minAmount) {
|
|
726
|
+
return {
|
|
727
|
+
allowed: false,
|
|
728
|
+
reason: "Minimum amount is ".concat(opLimits.minAmount, " ").concat(context.currency),
|
|
729
|
+
code: exports.KYC_VALIDATOR_ERRORS.AmountBelowMin,
|
|
730
|
+
details: { minAmount: opLimits.minAmount, amount: context.amount, currency: context.currency },
|
|
731
|
+
maxAllowed: opLimits.minAmount
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
return null;
|
|
735
|
+
};
|
|
736
|
+
KYCEligibility.txRejectAboveMaxPerTransaction = function (context, opLimits) {
|
|
737
|
+
if (context.amount <= opLimits.maxAmount)
|
|
738
|
+
return null;
|
|
739
|
+
var requiredTier = this.findTierForAmount(context.amount, context.transactionType, context.jurisdictionLimits);
|
|
740
|
+
return {
|
|
741
|
+
allowed: false,
|
|
742
|
+
reason: "Maximum amount per transaction is ".concat(opLimits.maxAmount, " ").concat(context.currency),
|
|
743
|
+
code: exports.KYC_VALIDATOR_ERRORS.AmountAboveMax,
|
|
744
|
+
details: { maxAmount: opLimits.maxAmount, amount: context.amount, currency: context.currency },
|
|
745
|
+
maxAllowed: opLimits.maxAmount,
|
|
746
|
+
requiredTier: requiredTier !== null && requiredTier !== void 0 ? requiredTier : undefined,
|
|
747
|
+
upgradeMessage: requiredTier
|
|
748
|
+
? "Upgrade to ".concat(this.getTierDisplayName(requiredTier), " for higher limits")
|
|
749
|
+
: undefined
|
|
750
|
+
};
|
|
751
|
+
};
|
|
752
|
+
KYCEligibility.txRejectExceedsDailyBudget = function (context, opLimits, dailyRemaining) {
|
|
753
|
+
if (context.amount <= dailyRemaining)
|
|
754
|
+
return null;
|
|
755
|
+
return {
|
|
756
|
+
allowed: false,
|
|
757
|
+
reason: "Daily limit exceeded. Remaining today: ".concat(Math.max(0, dailyRemaining), " ").concat(context.currency),
|
|
758
|
+
code: exports.KYC_VALIDATOR_ERRORS.DailyLimitExceeded,
|
|
759
|
+
details: { dailyLimit: opLimits.dailyLimit, dailyRemaining: Math.max(0, dailyRemaining), currency: context.currency },
|
|
760
|
+
remaining: Math.max(0, dailyRemaining),
|
|
761
|
+
dailyRemaining: Math.max(0, dailyRemaining),
|
|
762
|
+
maxAllowed: Math.min(opLimits.maxAmount, dailyRemaining)
|
|
763
|
+
};
|
|
764
|
+
};
|
|
765
|
+
KYCEligibility.txRejectExceedsMonthlyBudget = function (context, opLimits, monthlyRemaining) {
|
|
766
|
+
if (context.amount <= monthlyRemaining)
|
|
767
|
+
return null;
|
|
768
|
+
return {
|
|
769
|
+
allowed: false,
|
|
770
|
+
reason: "Monthly limit exceeded. Remaining this month: ".concat(Math.max(0, monthlyRemaining), " ").concat(context.currency),
|
|
771
|
+
code: exports.KYC_VALIDATOR_ERRORS.MonthlyLimitExceeded,
|
|
772
|
+
details: { monthlyLimit: opLimits.monthlyLimit, monthlyRemaining: Math.max(0, monthlyRemaining), currency: context.currency },
|
|
773
|
+
remaining: Math.max(0, monthlyRemaining),
|
|
774
|
+
monthlyRemaining: Math.max(0, monthlyRemaining),
|
|
775
|
+
maxAllowed: Math.min(opLimits.maxAmount, monthlyRemaining)
|
|
776
|
+
};
|
|
777
|
+
};
|
|
778
|
+
KYCEligibility.txRejectExceedsWeeklyBudget = function (context, opLimits) {
|
|
779
|
+
var _a;
|
|
780
|
+
if (!opLimits.weeklyLimit)
|
|
781
|
+
return null;
|
|
782
|
+
var usedThisWeek = (_a = context.usedThisWeek) !== null && _a !== void 0 ? _a : 0;
|
|
783
|
+
var weeklyRemaining = opLimits.weeklyLimit - usedThisWeek;
|
|
784
|
+
if (context.amount <= weeklyRemaining)
|
|
785
|
+
return null;
|
|
786
|
+
return {
|
|
787
|
+
allowed: false,
|
|
788
|
+
reason: "Weekly limit exceeded. Remaining this week: ".concat(Math.max(0, weeklyRemaining), " ").concat(context.currency),
|
|
789
|
+
code: exports.KYC_VALIDATOR_ERRORS.WeeklyLimitExceeded,
|
|
790
|
+
details: { weeklyLimit: opLimits.weeklyLimit, weeklyRemaining: Math.max(0, weeklyRemaining), currency: context.currency },
|
|
791
|
+
remaining: Math.max(0, weeklyRemaining),
|
|
792
|
+
maxAllowed: Math.min(opLimits.maxAmount, weeklyRemaining)
|
|
793
|
+
};
|
|
794
|
+
};
|
|
795
|
+
KYCEligibility.txRejectDailyTransactionCount = function (context, opLimits) {
|
|
796
|
+
var _a;
|
|
797
|
+
if (!opLimits.maxDailyTransactions)
|
|
798
|
+
return null;
|
|
799
|
+
var txToday = (_a = context.transactionsToday) !== null && _a !== void 0 ? _a : 0;
|
|
800
|
+
if (txToday < opLimits.maxDailyTransactions)
|
|
801
|
+
return null;
|
|
802
|
+
return {
|
|
803
|
+
allowed: false,
|
|
804
|
+
reason: "Daily transaction limit reached (".concat(opLimits.maxDailyTransactions, " transactions)"),
|
|
805
|
+
code: exports.KYC_VALIDATOR_ERRORS.TransactionCountExceeded,
|
|
806
|
+
details: { maxDailyTransactions: opLimits.maxDailyTransactions, transactionsToday: txToday }
|
|
807
|
+
};
|
|
808
|
+
};
|
|
809
|
+
KYCEligibility.txRejectDepositOverMaxBalance = function (tierLimits, context) {
|
|
810
|
+
var _a;
|
|
811
|
+
if (context.transactionType !== 'deposit' || !tierLimits.maxBalance)
|
|
812
|
+
return null;
|
|
813
|
+
var currentBalance = (_a = context.currentBalance) !== null && _a !== void 0 ? _a : 0;
|
|
814
|
+
var newBalance = currentBalance + context.amount;
|
|
815
|
+
if (newBalance <= tierLimits.maxBalance)
|
|
816
|
+
return null;
|
|
817
|
+
var maxDeposit = tierLimits.maxBalance - currentBalance;
|
|
818
|
+
return {
|
|
819
|
+
allowed: false,
|
|
820
|
+
reason: "Would exceed maximum balance of ".concat(tierLimits.maxBalance, " ").concat(context.currency),
|
|
821
|
+
code: exports.KYC_VALIDATOR_ERRORS.MaxBalanceExceeded,
|
|
822
|
+
details: { maxBalance: tierLimits.maxBalance, currentBalance: currentBalance, currency: context.currency },
|
|
823
|
+
maxAllowed: Math.max(0, maxDeposit)
|
|
824
|
+
};
|
|
825
|
+
};
|
|
826
|
+
KYCEligibility.txBuildAllowedResult = function (context, opLimits, dailyRemaining, monthlyRemaining) {
|
|
827
|
+
return {
|
|
828
|
+
allowed: true,
|
|
829
|
+
remaining: Math.min(dailyRemaining, monthlyRemaining) - context.amount,
|
|
830
|
+
dailyRemaining: dailyRemaining - context.amount,
|
|
831
|
+
monthlyRemaining: monthlyRemaining - context.amount,
|
|
832
|
+
maxAllowed: Math.min(opLimits.maxAmount, dailyRemaining, monthlyRemaining)
|
|
833
|
+
};
|
|
834
|
+
};
|
|
835
|
+
/**
|
|
836
|
+
* Get remaining limits for a user
|
|
837
|
+
*/
|
|
838
|
+
KYCEligibility.getRemainingLimits = function (tier, transactionType, usage, customLimits) {
|
|
839
|
+
var _a, _b, _c;
|
|
840
|
+
var tierLimits = this.getTierLimits(tier, customLimits);
|
|
841
|
+
var opType = canonicalLimitKey(transactionType);
|
|
842
|
+
var opLimits = tierLimits[opType];
|
|
843
|
+
if (!opLimits) {
|
|
844
|
+
return { daily: 0, monthly: 0, perTransaction: 0 };
|
|
845
|
+
}
|
|
846
|
+
return {
|
|
847
|
+
daily: Math.max(0, opLimits.dailyLimit - ((_a = usage.daily) !== null && _a !== void 0 ? _a : 0)),
|
|
848
|
+
weekly: opLimits.weeklyLimit
|
|
849
|
+
? Math.max(0, opLimits.weeklyLimit - ((_b = usage.weekly) !== null && _b !== void 0 ? _b : 0))
|
|
850
|
+
: undefined,
|
|
851
|
+
monthly: Math.max(0, opLimits.monthlyLimit - ((_c = usage.monthly) !== null && _c !== void 0 ? _c : 0)),
|
|
852
|
+
perTransaction: opLimits.maxAmount
|
|
853
|
+
};
|
|
854
|
+
};
|
|
855
|
+
/**
|
|
856
|
+
* Find the minimum tier that allows a specific amount
|
|
857
|
+
*/
|
|
858
|
+
KYCEligibility.findTierForAmount = function (amount, transactionType, customLimits) {
|
|
859
|
+
var limits = customLimits !== null && customLimits !== void 0 ? customLimits : DEFAULT_TIER_LIMITS;
|
|
860
|
+
var tiers = this.getAllTiers();
|
|
861
|
+
var opType = canonicalLimitKey(transactionType);
|
|
862
|
+
for (var _i = 0, tiers_1 = tiers; _i < tiers_1.length; _i++) {
|
|
863
|
+
var tier = tiers_1[_i];
|
|
864
|
+
var tierLimits = limits[tier];
|
|
865
|
+
var opLimits = tierLimits[opType];
|
|
866
|
+
if (opLimits && opLimits.maxAmount >= amount) {
|
|
867
|
+
return tier;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
return null;
|
|
871
|
+
};
|
|
872
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
873
|
+
// ACTION ELIGIBILITY
|
|
874
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
875
|
+
/**
|
|
876
|
+
* Check if user can perform an action based on their tier
|
|
877
|
+
*/
|
|
878
|
+
KYCEligibility.canPerformAction = function (action, currentTier, customActionTiers) {
|
|
879
|
+
var actionTiers = customActionTiers !== null && customActionTiers !== void 0 ? customActionTiers : DEFAULT_ACTION_TIERS;
|
|
880
|
+
var requiredTier = actionTiers[action];
|
|
881
|
+
if (!requiredTier) {
|
|
882
|
+
// Action not defined, allow by default
|
|
883
|
+
return { allowed: true };
|
|
884
|
+
}
|
|
885
|
+
var allowed = this.tierMeetsRequirement(currentTier, requiredTier);
|
|
886
|
+
return {
|
|
887
|
+
allowed: allowed,
|
|
888
|
+
reason: allowed ? undefined : "Requires ".concat(this.getTierDisplayName(requiredTier), " tier"),
|
|
889
|
+
requiredTier: allowed ? undefined : requiredTier
|
|
890
|
+
};
|
|
891
|
+
};
|
|
892
|
+
/**
|
|
893
|
+
* Get all actions a tier can perform
|
|
894
|
+
*/
|
|
895
|
+
KYCEligibility.getAvailableActions = function (tier, customActionTiers) {
|
|
896
|
+
var actionTiers = customActionTiers !== null && customActionTiers !== void 0 ? customActionTiers : DEFAULT_ACTION_TIERS;
|
|
897
|
+
var available = [];
|
|
898
|
+
for (var _i = 0, _a = Object.keys(actionTiers); _i < _a.length; _i++) {
|
|
899
|
+
var action = _a[_i];
|
|
900
|
+
var requiredTier = actionTiers[action];
|
|
901
|
+
if (this.tierMeetsRequirement(tier, requiredTier)) {
|
|
902
|
+
available.push(action);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
return available;
|
|
906
|
+
};
|
|
907
|
+
/**
|
|
908
|
+
* Get actions that require upgrade
|
|
909
|
+
*/
|
|
910
|
+
KYCEligibility.getLockedActions = function (tier, customActionTiers) {
|
|
911
|
+
var actionTiers = customActionTiers !== null && customActionTiers !== void 0 ? customActionTiers : DEFAULT_ACTION_TIERS;
|
|
912
|
+
var locked = [];
|
|
913
|
+
for (var _i = 0, _a = Object.keys(actionTiers); _i < _a.length; _i++) {
|
|
914
|
+
var action = _a[_i];
|
|
915
|
+
var requiredTier = actionTiers[action];
|
|
916
|
+
if (!this.tierMeetsRequirement(tier, requiredTier)) {
|
|
917
|
+
locked.push({ action: action, requiredTier: requiredTier });
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return locked;
|
|
921
|
+
};
|
|
922
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
923
|
+
// JURISDICTION RULES
|
|
924
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
925
|
+
/**
|
|
926
|
+
* Check if a country is blocked
|
|
927
|
+
*/
|
|
928
|
+
KYCEligibility.isCountryBlocked = function (country, rules) {
|
|
929
|
+
return rules.blockedCountries.indexOf(country) !== -1;
|
|
930
|
+
};
|
|
931
|
+
/**
|
|
932
|
+
* Check if a country is high risk
|
|
933
|
+
*/
|
|
934
|
+
KYCEligibility.isCountryHighRisk = function (country, rules) {
|
|
935
|
+
return rules.highRiskCountries.indexOf(country) !== -1;
|
|
936
|
+
};
|
|
937
|
+
/**
|
|
938
|
+
* Check age eligibility for a jurisdiction
|
|
939
|
+
*/
|
|
940
|
+
KYCEligibility.checkAgeEligibility = function (userAge, rules) {
|
|
941
|
+
if (userAge < rules.minimumAge) {
|
|
942
|
+
return {
|
|
943
|
+
eligible: false,
|
|
944
|
+
reason: "Minimum age is ".concat(rules.minimumAge, " years")
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
return { eligible: true };
|
|
948
|
+
};
|
|
949
|
+
/**
|
|
950
|
+
* Check nationality eligibility
|
|
951
|
+
*/
|
|
952
|
+
KYCEligibility.checkNationalityEligibility = function (nationality, rules) {
|
|
953
|
+
var _a;
|
|
954
|
+
if (((_a = rules.restrictedNationalities) === null || _a === void 0 ? void 0 : _a.indexOf(nationality)) !== -1) {
|
|
955
|
+
return {
|
|
956
|
+
eligible: false,
|
|
957
|
+
reason: "Nationality '".concat(nationality, "' is restricted")
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
return { eligible: true };
|
|
961
|
+
};
|
|
962
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
963
|
+
// DOCUMENT VALIDATION (Client-side)
|
|
964
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
965
|
+
/**
|
|
966
|
+
* Check if a document is expired
|
|
967
|
+
*/
|
|
968
|
+
KYCEligibility.isDocumentExpired = function (expiryDate, currentDate) {
|
|
969
|
+
var now = currentDate !== null && currentDate !== void 0 ? currentDate : new Date();
|
|
970
|
+
return new Date(expiryDate) < now;
|
|
971
|
+
};
|
|
972
|
+
/**
|
|
973
|
+
* Check if a document is too old (e.g., utility bill older than 3 months)
|
|
974
|
+
*/
|
|
975
|
+
KYCEligibility.isDocumentTooOld = function (issuedDate, maxAgeDays, currentDate) {
|
|
976
|
+
var now = currentDate !== null && currentDate !== void 0 ? currentDate : new Date();
|
|
977
|
+
var ageMs = now.getTime() - new Date(issuedDate).getTime();
|
|
978
|
+
var ageDays = Math.floor(ageMs / (1000 * 60 * 60 * 24));
|
|
979
|
+
return ageDays > maxAgeDays;
|
|
980
|
+
};
|
|
981
|
+
/**
|
|
982
|
+
* Validate document file (client-side pre-validation)
|
|
983
|
+
*/
|
|
984
|
+
KYCEligibility.validateDocumentFile = function (file, options) {
|
|
985
|
+
var _a;
|
|
986
|
+
if (options === void 0) { options = {}; }
|
|
987
|
+
var _b = options.maxSizeMB, maxSizeMB = _b === void 0 ? 10 : _b, _c = options.allowedTypes, allowedTypes = _c === void 0 ? ['image/jpeg', 'image/png', 'application/pdf'] : _c, _d = options.allowedExtensions, allowedExtensions = _d === void 0 ? ['.jpg', '.jpeg', '.png', '.pdf'] : _d;
|
|
988
|
+
// Check file size
|
|
989
|
+
var sizeMB = file.size / (1024 * 1024);
|
|
990
|
+
if (sizeMB > maxSizeMB) {
|
|
991
|
+
return {
|
|
992
|
+
valid: false,
|
|
993
|
+
reason: "File too large. Maximum size is ".concat(maxSizeMB, "MB"),
|
|
994
|
+
code: exports.KYC_VALIDATOR_ERRORS.FileTooLarge,
|
|
995
|
+
details: { maxSizeMB: maxSizeMB, sizeMB: sizeMB }
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
// Check mime type
|
|
999
|
+
if (allowedTypes.indexOf(file.type) === -1) {
|
|
1000
|
+
return {
|
|
1001
|
+
valid: false,
|
|
1002
|
+
reason: "File type '".concat(file.type, "' not allowed"),
|
|
1003
|
+
code: exports.KYC_VALIDATOR_ERRORS.FileTypeNotAllowed,
|
|
1004
|
+
details: { fileType: file.type, allowedTypes: allowedTypes }
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
// Check extension
|
|
1008
|
+
var ext = '.' + ((_a = file.name.split('.').pop()) === null || _a === void 0 ? void 0 : _a.toLowerCase());
|
|
1009
|
+
if (allowedExtensions.indexOf(ext) === -1) {
|
|
1010
|
+
return {
|
|
1011
|
+
valid: false,
|
|
1012
|
+
reason: "File extension '".concat(ext, "' not allowed"),
|
|
1013
|
+
code: exports.KYC_VALIDATOR_ERRORS.FileExtensionNotAllowed,
|
|
1014
|
+
details: { extension: ext, allowedExtensions: allowedExtensions }
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
return { valid: true };
|
|
1018
|
+
};
|
|
1019
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1020
|
+
// RISK LEVEL UTILITIES
|
|
1021
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1022
|
+
/**
|
|
1023
|
+
* Calculate risk level from score
|
|
1024
|
+
*/
|
|
1025
|
+
KYCEligibility.getRiskLevelFromScore = function (score, thresholds) {
|
|
1026
|
+
if (thresholds === void 0) { thresholds = { low: 30, medium: 50, high: 70 }; }
|
|
1027
|
+
if (score >= thresholds.high)
|
|
1028
|
+
return 'critical';
|
|
1029
|
+
if (score >= thresholds.medium)
|
|
1030
|
+
return 'high';
|
|
1031
|
+
if (score >= thresholds.low)
|
|
1032
|
+
return 'medium';
|
|
1033
|
+
return 'low';
|
|
1034
|
+
};
|
|
1035
|
+
/**
|
|
1036
|
+
* Check if enhanced due diligence is required
|
|
1037
|
+
*/
|
|
1038
|
+
KYCEligibility.requiresEnhancedDueDiligence = function (context) {
|
|
1039
|
+
// PEP always requires EDD
|
|
1040
|
+
if (context.isPEP)
|
|
1041
|
+
return true;
|
|
1042
|
+
// High/critical risk requires EDD
|
|
1043
|
+
if (context.riskLevel === 'high' || context.riskLevel === 'critical')
|
|
1044
|
+
return true;
|
|
1045
|
+
// High risk score requires EDD
|
|
1046
|
+
if (context.riskScore && context.riskScore >= 70)
|
|
1047
|
+
return true;
|
|
1048
|
+
return false;
|
|
1049
|
+
};
|
|
1050
|
+
return KYCEligibility;
|
|
1051
|
+
}());
|
|
1052
|
+
exports.KYCEligibility = KYCEligibility;
|
|
1053
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1054
|
+
// CONVENIENCE EXPORTS
|
|
1055
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1056
|
+
exports.checkKYCTransaction = KYCEligibility.checkTransaction.bind(KYCEligibility);
|
|
1057
|
+
exports.getTierRequirements = KYCEligibility.getTierRequirements.bind(KYCEligibility);
|
|
1058
|
+
exports.checkTierEligibility = KYCEligibility.checkTierEligibility.bind(KYCEligibility);
|
|
1059
|
+
exports.canPerformAction = KYCEligibility.canPerformAction.bind(KYCEligibility);
|
|
1060
|
+
exports.tierMeetsRequirement = KYCEligibility.tierMeetsRequirement.bind(KYCEligibility);
|
|
1061
|
+
exports.getRemainingLimits = KYCEligibility.getRemainingLimits.bind(KYCEligibility);
|
|
1062
|
+
/**
|
|
1063
|
+
* Top-level pre-validation helper — see {@link KYCEligibility.prevalidate}.
|
|
1064
|
+
* Bulletproof client + server admission control. Pure function; no I/O.
|
|
1065
|
+
*/
|
|
1066
|
+
exports.prevalidateKYCTransaction = KYCEligibility.prevalidate.bind(KYCEligibility);
|