@africode/core 5.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.
- package/AFRICODE_FRAMEWORK_GUIDE.md +707 -0
- package/LICENSE +623 -0
- package/README.md +442 -0
- package/bin/africode.js +73 -0
- package/bin/africode.js.1758507140 +343 -0
- package/bin/cli.ts +83 -0
- package/bin/create-africode.js +158 -0
- package/bin/scaffold.ts +219 -0
- package/components/accordion.js +183 -0
- package/components/alert.js +131 -0
- package/components/auth.js +172 -0
- package/components/avatar.js +117 -0
- package/components/badge.js +104 -0
- package/components/base.d.ts +139 -0
- package/components/base.js +184 -0
- package/components/button.js +164 -0
- package/components/card.js +137 -0
- package/components/cultural-card.js +243 -0
- package/components/divider.js +83 -0
- package/components/dropdown.js +171 -0
- package/components/error-boundary.js +155 -0
- package/components/form.js +131 -0
- package/components/grid.js +273 -0
- package/components/hero.js +138 -0
- package/components/icon.js +36 -0
- package/components/index.js +57 -0
- package/components/input.js +256 -0
- package/components/kanga-card.js +185 -0
- package/components/language-switcher.js +108 -0
- package/components/loader.js +80 -0
- package/components/modal.js +262 -0
- package/components/motion.js +84 -0
- package/components/navbar.js +236 -0
- package/components/pattern-showcase.js +225 -0
- package/components/progress.js +134 -0
- package/components/react.js +111 -0
- package/components/section.js +54 -0
- package/components/select.js +322 -0
- package/components/sidebar.js +180 -0
- package/components/skeleton.js +85 -0
- package/components/table.js +181 -0
- package/components/tabs.js +202 -0
- package/components/theme-toggle.js +82 -0
- package/components/toast.js +139 -0
- package/components/tooltip.js +167 -0
- package/core/a2ui-schema-manager.js +344 -0
- package/core/a2ui.js +431 -0
- package/core/bun-runtime.js +799 -0
- package/core/cli/commands/add.js +23 -0
- package/core/cli/commands/audit.js +58 -0
- package/core/cli/commands/build.js +137 -0
- package/core/cli/commands/create-plugin.js +241 -0
- package/core/cli/commands/dev.js +228 -0
- package/core/cli/commands/lint.js +23 -0
- package/core/cli/commands/test.js +34 -0
- package/core/cli/migrator.js +71 -0
- package/core/cli/ui.js +46 -0
- package/core/compliance.js +628 -0
- package/core/config.js +263 -0
- package/core/db-advanced.js +481 -0
- package/core/db.js +284 -0
- package/core/enhanced-hmr.js +404 -0
- package/core/errors.js +222 -0
- package/core/file-router.js +290 -0
- package/core/heartbeat.js +64 -0
- package/core/hmr-client.js +204 -0
- package/core/hmr.js +196 -0
- package/core/html.d.ts +116 -0
- package/core/html.js +160 -0
- package/core/hydration.js +52 -0
- package/core/lipa-namba-journey.js +572 -0
- package/core/motion.js +106 -0
- package/core/nida-cig-middleware.js +455 -0
- package/core/patterns.d.ts +124 -0
- package/core/patterns.js +833 -0
- package/core/plugins/index.js +312 -0
- package/core/router.js +387 -0
- package/core/sdk-client.js +62 -0
- package/core/sdk.d.ts +133 -0
- package/core/sdk.js +123 -0
- package/core/seo.js +76 -0
- package/core/server/auth-endpoints.js +339 -0
- package/core/server/auth.js +180 -0
- package/core/server/csrf.js +206 -0
- package/core/server/db.js +39 -0
- package/core/server/middleware.js +324 -0
- package/core/server/rate-limit.js +238 -0
- package/core/server/render.js +69 -0
- package/core/server/router.js +120 -0
- package/core/shim.js +28 -0
- package/core/state.d.ts +86 -0
- package/core/state.js +242 -0
- package/core/store.d.ts +122 -0
- package/core/store.js +61 -0
- package/core/validation.d.ts +233 -0
- package/core/validation.js +590 -0
- package/core/websocket.js +639 -0
- package/dist/africode.js +2905 -0
- package/dist/africode.js.map +61 -0
- package/dist/build-info.json +23 -0
- package/dist/components.js +2888 -0
- package/dist/components.js.map +58 -0
- package/dist/styles/africanity.css +322 -0
- package/dist/styles/typography.css +141 -0
- package/docs/IDE-Guide.md +50 -0
- package/package.json +110 -0
- package/src/index.ts +196 -0
- package/styles/africanity.css +322 -0
- package/styles/typography.css +141 -0
- package/templates/starter/.env.example +15 -0
- package/templates/starter/africode.config.js +40 -0
- package/templates/starter/package.json +14 -0
- package/templates/starter/src/pages/index.html +46 -0
- package/templates/starter/src/pages/index.js +32 -0
- package/templates/starter/src/styles/main.css +4 -0
- package/templates/starter-3d/.env.example +7 -0
- package/templates/starter-3d/africode.config.js +29 -0
- package/templates/starter-3d/components/af-model-viewer.js +125 -0
- package/templates/starter-3d/package.json +15 -0
- package/templates/starter-3d/src/pages/index.html +46 -0
- package/templates/starter-3d/src/pages/index.js +50 -0
- package/templates/starter-3d/src/styles/main.css +4 -0
- package/templates/starter-react/.env.example +15 -0
- package/templates/starter-react/africode.config.js +40 -0
- package/templates/starter-react/package.json +16 -0
- package/templates/starter-react/src/pages/index.html +46 -0
- package/templates/starter-react/src/pages/index.js +68 -0
- package/templates/starter-react/src/styles/main.css +4 -0
- package/templates/starter-tailwind/.env.example +15 -0
- package/templates/starter-tailwind/africode.config.js +40 -0
- package/templates/starter-tailwind/package.json +20 -0
- package/templates/starter-tailwind/src/pages/index.html +46 -0
- package/templates/starter-tailwind/src/pages/index.js +37 -0
- package/templates/starter-tailwind/src/styles/main.css +4 -0
- package/templates/starter-tailwind/src/styles/tailwind.css +1 -0
- package/templates/starter-tailwind/src/tailwind-loader.js +30 -0
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fintech Compliance Engine
|
|
3
|
+
* Tanzanian regulatory compliance for NIDA, TIPS, and AML/FIU
|
|
4
|
+
*
|
|
5
|
+
* Implements the "operating system" approach to compliance with
|
|
6
|
+
* automated identity verification, transaction monitoring, and
|
|
7
|
+
* regulatory reporting.
|
|
8
|
+
*
|
|
9
|
+
* @module core/compliance
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* NIDA Identity Infrastructure Integration
|
|
16
|
+
* National Identification Authority verification system
|
|
17
|
+
*/
|
|
18
|
+
export class NIDAClient {
|
|
19
|
+
constructor(config = {}) {
|
|
20
|
+
this.baseUrl = config.baseUrl || 'https://api.nida.go.tz';
|
|
21
|
+
this.apiKey = config.apiKey;
|
|
22
|
+
this.certificate = config.certificate;
|
|
23
|
+
this.timeout = config.timeout || 30000;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Verify National ID Number (NIN)
|
|
28
|
+
* @param {string} nin - 20-digit NIN
|
|
29
|
+
* @param {Object} options - Verification options
|
|
30
|
+
*/
|
|
31
|
+
async verifyNIN(nin, options = {}) {
|
|
32
|
+
const schema = z.string().length(20).regex(/^\d{20}$/);
|
|
33
|
+
schema.parse(nin);
|
|
34
|
+
|
|
35
|
+
const response = await this._makeRequest('/verify/nin', {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
body: JSON.stringify({
|
|
38
|
+
nin,
|
|
39
|
+
includeBiometrics: options.includeBiometrics || false,
|
|
40
|
+
includeDemographics: options.includeDemographics || true
|
|
41
|
+
})
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
verified: response.verified,
|
|
46
|
+
demographics: response.demographics,
|
|
47
|
+
biometrics: response.biometrics,
|
|
48
|
+
confidence: response.confidence,
|
|
49
|
+
timestamp: new Date().toISOString()
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Perform biometric verification
|
|
55
|
+
* @param {string} nin - NIN to verify against
|
|
56
|
+
* @param {Object} biometrics - Biometric data
|
|
57
|
+
*/
|
|
58
|
+
async verifyBiometrics(nin, biometrics) {
|
|
59
|
+
const response = await this._makeRequest('/verify/biometrics', {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
body: JSON.stringify({
|
|
62
|
+
nin,
|
|
63
|
+
fingerprint: biometrics.fingerprint,
|
|
64
|
+
facialImage: biometrics.facialImage,
|
|
65
|
+
livenessData: biometrics.livenessData
|
|
66
|
+
})
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
match: response.match,
|
|
71
|
+
confidence: response.confidence,
|
|
72
|
+
level: response.level // ISO 30107-3 PAD level
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Extract data from ID document
|
|
78
|
+
* @param {File|Buffer} document - ID document image
|
|
79
|
+
* @param {string} type - Document type (1-7 Tanzanian ID types)
|
|
80
|
+
*/
|
|
81
|
+
async extractDocumentData(document, type) {
|
|
82
|
+
const formData = new FormData();
|
|
83
|
+
formData.append('document', document);
|
|
84
|
+
formData.append('type', type);
|
|
85
|
+
|
|
86
|
+
const response = await this._makeRequest('/extract/document', {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
body: formData
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
extractedData: response.data,
|
|
93
|
+
confidence: response.confidence,
|
|
94
|
+
ocrText: response.ocrText
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Make authenticated request to NIDA API
|
|
100
|
+
*/
|
|
101
|
+
async _makeRequest(endpoint, options = {}) {
|
|
102
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
103
|
+
|
|
104
|
+
const headers = {
|
|
105
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
106
|
+
'Content-Type': 'application/json',
|
|
107
|
+
...options.headers
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Add certificate-based authentication for production
|
|
111
|
+
if (this.certificate) {
|
|
112
|
+
headers['X-Client-Certificate'] = this.certificate;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const response = await fetch(url, {
|
|
116
|
+
...options,
|
|
117
|
+
headers,
|
|
118
|
+
signal: AbortSignal.timeout(this.timeout)
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
if (!response.ok) {
|
|
122
|
+
throw new Error(`NIDA API error: ${response.status} ${response.statusText}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return response.json();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* TIPS Payment System Integration
|
|
131
|
+
* Tanzania Instant Payment System for real-time transactions
|
|
132
|
+
*/
|
|
133
|
+
export class TIPSClient {
|
|
134
|
+
constructor(config = {}) {
|
|
135
|
+
this.baseUrl = config.baseUrl || 'https://api.tips.go.tz';
|
|
136
|
+
this.apiKey = config.apiKey;
|
|
137
|
+
this.clientId = config.clientId;
|
|
138
|
+
this.privateKey = config.privateKey;
|
|
139
|
+
this.timeout = config.timeout || 10000; // TIPS requires fast responses
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Process P2P payment
|
|
144
|
+
* @param {Object} payment - Payment details
|
|
145
|
+
*/
|
|
146
|
+
async processP2P(payment) {
|
|
147
|
+
const schema = z.object({
|
|
148
|
+
fromAccount: z.string(),
|
|
149
|
+
toAccount: z.string(),
|
|
150
|
+
amount: z.number().positive(),
|
|
151
|
+
currency: z.string().default('TZS'),
|
|
152
|
+
description: z.string().optional()
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const validatedPayment = schema.parse(payment);
|
|
156
|
+
|
|
157
|
+
const response = await this._makeRequest('/payments/p2p', {
|
|
158
|
+
method: 'POST',
|
|
159
|
+
body: JSON.stringify({
|
|
160
|
+
...validatedPayment,
|
|
161
|
+
timestamp: new Date().toISOString(),
|
|
162
|
+
idempotencyKey: this._generateIdempotencyKey()
|
|
163
|
+
})
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
transactionId: response.transactionId,
|
|
168
|
+
status: response.status,
|
|
169
|
+
processedAt: response.processedAt,
|
|
170
|
+
fee: response.fee
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Process P2B payment (merchant payment)
|
|
176
|
+
* @param {Object} payment - Payment details
|
|
177
|
+
*/
|
|
178
|
+
async processP2B(payment) {
|
|
179
|
+
const schema = z.object({
|
|
180
|
+
customerAccount: z.string(),
|
|
181
|
+
merchantAccount: z.string(),
|
|
182
|
+
amount: z.number().positive(),
|
|
183
|
+
reference: z.string(),
|
|
184
|
+
description: z.string().optional()
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const validatedPayment = schema.parse(payment);
|
|
188
|
+
|
|
189
|
+
const response = await this._makeRequest('/payments/p2b', {
|
|
190
|
+
method: 'POST',
|
|
191
|
+
body: JSON.stringify(validatedPayment)
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
transactionId: response.transactionId,
|
|
196
|
+
status: response.status,
|
|
197
|
+
confirmationCode: response.confirmationCode
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Request to Pay (RTP) - merchant requests payment from customer
|
|
203
|
+
* @param {Object} request - RTP details
|
|
204
|
+
*/
|
|
205
|
+
async requestToPay(request) {
|
|
206
|
+
const schema = z.object({
|
|
207
|
+
merchantAccount: z.string(),
|
|
208
|
+
customerAccount: z.string(),
|
|
209
|
+
amount: z.number().positive(),
|
|
210
|
+
expiryMinutes: z.number().min(1).max(60).default(15),
|
|
211
|
+
description: z.string()
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const validatedRequest = schema.parse(request);
|
|
215
|
+
|
|
216
|
+
const response = await this._makeRequest('/payments/rtp', {
|
|
217
|
+
method: 'POST',
|
|
218
|
+
body: JSON.stringify(validatedRequest)
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
requestId: response.requestId,
|
|
223
|
+
status: response.status,
|
|
224
|
+
expiryAt: response.expiryAt
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Reverse transaction
|
|
230
|
+
* @param {string} transactionId - Transaction to reverse
|
|
231
|
+
* @param {string} reason - Reversal reason
|
|
232
|
+
*/
|
|
233
|
+
async reverseTransaction(transactionId, reason) {
|
|
234
|
+
const response = await this._makeRequest('/payments/reverse', {
|
|
235
|
+
method: 'POST',
|
|
236
|
+
body: JSON.stringify({
|
|
237
|
+
transactionId,
|
|
238
|
+
reason,
|
|
239
|
+
timestamp: new Date().toISOString()
|
|
240
|
+
})
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
reversalId: response.reversalId,
|
|
245
|
+
status: response.status,
|
|
246
|
+
reversedAt: response.reversedAt
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Get transaction status
|
|
252
|
+
* @param {string} transactionId - Transaction ID
|
|
253
|
+
*/
|
|
254
|
+
async getTransactionStatus(transactionId) {
|
|
255
|
+
const response = await this._makeRequest(`/payments/${transactionId}/status`);
|
|
256
|
+
return response;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Generate idempotency key for request deduplication
|
|
261
|
+
*/
|
|
262
|
+
_generateIdempotencyKey() {
|
|
263
|
+
return `tips_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Make authenticated request to TIPS API
|
|
268
|
+
*/
|
|
269
|
+
async _makeRequest(endpoint, options = {}) {
|
|
270
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
271
|
+
|
|
272
|
+
// Create signature for request authentication
|
|
273
|
+
const timestamp = new Date().toISOString();
|
|
274
|
+
const signature = await this._createSignature(options.body || '', timestamp);
|
|
275
|
+
|
|
276
|
+
const headers = {
|
|
277
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
278
|
+
'X-Client-ID': this.clientId,
|
|
279
|
+
'X-Timestamp': timestamp,
|
|
280
|
+
'X-Signature': signature,
|
|
281
|
+
'Content-Type': 'application/json',
|
|
282
|
+
...options.headers
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const response = await fetch(url, {
|
|
286
|
+
...options,
|
|
287
|
+
headers,
|
|
288
|
+
signal: AbortSignal.timeout(this.timeout)
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
if (!response.ok) {
|
|
292
|
+
throw new Error(`TIPS API error: ${response.status} ${response.statusText}`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return response.json();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Create cryptographic signature for TIPS authentication
|
|
300
|
+
*/
|
|
301
|
+
async _createSignature(body, timestamp) {
|
|
302
|
+
const message = `${timestamp}${body}`;
|
|
303
|
+
const encoder = new TextEncoder();
|
|
304
|
+
const data = encoder.encode(message);
|
|
305
|
+
|
|
306
|
+
// Use Web Crypto API for signing
|
|
307
|
+
const key = await crypto.subtle.importKey(
|
|
308
|
+
'pkcs8',
|
|
309
|
+
this._base64ToArrayBuffer(this.privateKey),
|
|
310
|
+
{
|
|
311
|
+
name: 'RSASSA-PKCS1-v1_5',
|
|
312
|
+
hash: 'SHA-256'
|
|
313
|
+
},
|
|
314
|
+
false,
|
|
315
|
+
['sign']
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
const signature = await crypto.subtle.sign('RSASSA-PKCS1-v1_5', key, data);
|
|
319
|
+
return this._arrayBufferToBase64(signature);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
_base64ToArrayBuffer(base64) {
|
|
323
|
+
const binaryString = atob(base64);
|
|
324
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
325
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
326
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
327
|
+
}
|
|
328
|
+
return bytes.buffer;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
_arrayBufferToBase64(buffer) {
|
|
332
|
+
const bytes = new Uint8Array(buffer);
|
|
333
|
+
let binary = '';
|
|
334
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
335
|
+
binary += String.fromCharCode(bytes[i]);
|
|
336
|
+
}
|
|
337
|
+
return btoa(binary);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* AML/FIU Compliance Engine
|
|
343
|
+
* Automated Anti-Money Laundering and Financial Intelligence Unit integration
|
|
344
|
+
*/
|
|
345
|
+
export class AMLComplianceEngine {
|
|
346
|
+
constructor(config = {}) {
|
|
347
|
+
this.fiuEndpoint = config.fiuEndpoint || 'https://api.fiu.go.tz';
|
|
348
|
+
this.reportingWindow = config.reportingWindow || 24; // hours
|
|
349
|
+
this.monitoringRules = config.monitoringRules || this._defaultRules();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Evaluate transaction for suspicious activity
|
|
354
|
+
* @param {Object} transaction - Transaction details
|
|
355
|
+
* @param {Object} customer - Customer profile
|
|
356
|
+
*/
|
|
357
|
+
async evaluateTransaction(transaction, customer) {
|
|
358
|
+
const riskScore = await this._calculateRiskScore(transaction, customer);
|
|
359
|
+
const flags = await this._checkMonitoringRules(transaction, customer);
|
|
360
|
+
|
|
361
|
+
const isSuspicious = riskScore > 0.7 || flags.length > 0;
|
|
362
|
+
|
|
363
|
+
if (isSuspicious) {
|
|
364
|
+
await this._queueForReporting(transaction, customer, riskScore, flags);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
riskScore,
|
|
369
|
+
flags,
|
|
370
|
+
suspicious: isSuspicious,
|
|
371
|
+
evaluatedAt: new Date().toISOString()
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Submit suspicious activity report to FIU
|
|
377
|
+
* @param {Object} report - SAR details
|
|
378
|
+
*/
|
|
379
|
+
async submitSAR(report) {
|
|
380
|
+
const schema = z.object({
|
|
381
|
+
transactionId: z.string(),
|
|
382
|
+
customerId: z.string(),
|
|
383
|
+
activityType: z.string(),
|
|
384
|
+
description: z.string(),
|
|
385
|
+
riskScore: z.number(),
|
|
386
|
+
flags: z.array(z.string()),
|
|
387
|
+
evidence: z.array(z.object({
|
|
388
|
+
type: z.string(),
|
|
389
|
+
data: z.any()
|
|
390
|
+
}))
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
const validatedReport = schema.parse(report);
|
|
394
|
+
|
|
395
|
+
const response = await fetch(`${this.fiuEndpoint}/sar`, {
|
|
396
|
+
method: 'POST',
|
|
397
|
+
headers: {
|
|
398
|
+
'Content-Type': 'application/json',
|
|
399
|
+
'Authorization': `Bearer ${process.env.FIU_API_KEY}`
|
|
400
|
+
},
|
|
401
|
+
body: JSON.stringify({
|
|
402
|
+
...validatedReport,
|
|
403
|
+
submittedAt: new Date().toISOString(),
|
|
404
|
+
reportingEntity: process.env.FINTECH_LICENSE_ID
|
|
405
|
+
})
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
if (!response.ok) {
|
|
409
|
+
throw new Error(`FIU submission failed: ${response.status}`);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return response.json();
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Calculate risk score for transaction
|
|
417
|
+
*/
|
|
418
|
+
async _calculateRiskScore(transaction, customer) {
|
|
419
|
+
let score = 0;
|
|
420
|
+
|
|
421
|
+
// Amount-based scoring
|
|
422
|
+
if (transaction.amount > 1000000) {
|
|
423
|
+
score += 0.3; // > 1M TZS
|
|
424
|
+
} else if (transaction.amount > 500000) {
|
|
425
|
+
score += 0.2;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Velocity checks
|
|
429
|
+
const recentTransactions = await this._getRecentTransactions(customer.id, 24);
|
|
430
|
+
if (recentTransactions.length > 10) {
|
|
431
|
+
score += 0.2;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Geographic anomalies
|
|
435
|
+
if (this._isGeographicAnomaly(transaction, customer)) {
|
|
436
|
+
score += 0.3;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Customer risk profile
|
|
440
|
+
if (customer.riskLevel === 'high') {
|
|
441
|
+
score += 0.4;
|
|
442
|
+
} else if (customer.riskLevel === 'medium') {
|
|
443
|
+
score += 0.2;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return Math.min(score, 1.0);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Check transaction against monitoring rules
|
|
451
|
+
*/
|
|
452
|
+
async _checkMonitoringRules(transaction, customer) {
|
|
453
|
+
const flags = [];
|
|
454
|
+
|
|
455
|
+
for (const rule of this.monitoringRules) {
|
|
456
|
+
if (await rule.check(transaction, customer)) {
|
|
457
|
+
flags.push(rule.name);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return flags;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Queue suspicious activity for FIU reporting
|
|
466
|
+
*/
|
|
467
|
+
async _queueForReporting(transaction, customer, riskScore, flags) {
|
|
468
|
+
const report = {
|
|
469
|
+
transactionId: transaction.id,
|
|
470
|
+
customerId: customer.id,
|
|
471
|
+
activityType: 'suspicious_transaction',
|
|
472
|
+
description: `High-risk transaction detected: ${flags.join(', ')}`,
|
|
473
|
+
riskScore,
|
|
474
|
+
flags,
|
|
475
|
+
evidence: [
|
|
476
|
+
{ type: 'transaction', data: transaction },
|
|
477
|
+
{ type: 'customer', data: customer }
|
|
478
|
+
]
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
// Queue for reporting within 24-hour window
|
|
482
|
+
await this._scheduleReporting(report, this.reportingWindow);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Default AML monitoring rules
|
|
487
|
+
*/
|
|
488
|
+
_defaultRules() {
|
|
489
|
+
return [
|
|
490
|
+
{
|
|
491
|
+
name: 'large_amount',
|
|
492
|
+
check: (tx) => tx.amount > 2000000 // > 2M TZS
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
name: 'rapid_transactions',
|
|
496
|
+
check: async (tx, customer) => {
|
|
497
|
+
const recent = await this._getRecentTransactions(customer.id, 1);
|
|
498
|
+
return recent.length > 5;
|
|
499
|
+
}
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
name: 'unusual_location',
|
|
503
|
+
check: (tx, customer) => this._isGeographicAnomaly(tx, customer)
|
|
504
|
+
}
|
|
505
|
+
];
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Check for geographic anomalies
|
|
510
|
+
*/
|
|
511
|
+
_isGeographicAnomaly(transaction, customer) {
|
|
512
|
+
// Simplified geographic check
|
|
513
|
+
const customerCountry = customer.address?.country || 'TZ';
|
|
514
|
+
const transactionCountry = transaction.location?.country || 'TZ';
|
|
515
|
+
|
|
516
|
+
return customerCountry !== transactionCountry;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Get recent transactions for customer
|
|
521
|
+
*/
|
|
522
|
+
async _getRecentTransactions(_customerId, _hours) {
|
|
523
|
+
// Implementation would query transaction database
|
|
524
|
+
// Placeholder for demo
|
|
525
|
+
void _customerId;
|
|
526
|
+
void _hours;
|
|
527
|
+
return [];
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Schedule reporting within compliance window
|
|
532
|
+
*/
|
|
533
|
+
async _scheduleReporting(report, hours) {
|
|
534
|
+
// Implementation would use job queue system
|
|
535
|
+
console.warn(`Scheduling FIU report in ${hours} hours for transaction ${report.transactionId}`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Compliance Middleware
|
|
541
|
+
* Framework-level compliance enforcement
|
|
542
|
+
*/
|
|
543
|
+
export class ComplianceMiddleware {
|
|
544
|
+
constructor(config = {}) {
|
|
545
|
+
this.nida = config.nida ? new NIDAClient(config.nida) : null;
|
|
546
|
+
this.tips = config.tips ? new TIPSClient(config.tips) : null;
|
|
547
|
+
this.aml = new AMLComplianceEngine(config.aml);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* KYC middleware for identity verification
|
|
552
|
+
*/
|
|
553
|
+
kycMiddleware() {
|
|
554
|
+
return async (request, next) => {
|
|
555
|
+
if (!this.nida) {
|
|
556
|
+
return next(request);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const userId = request.session?.userId;
|
|
560
|
+
if (!userId) {
|
|
561
|
+
return next(request);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Check if user needs KYC verification
|
|
565
|
+
const user = await this._getUser(userId);
|
|
566
|
+
if (!user.kycVerified) {
|
|
567
|
+
// Perform NIDA verification
|
|
568
|
+
try {
|
|
569
|
+
const verification = await this.nida.verifyNIN(user.nin);
|
|
570
|
+
if (verification.verified) {
|
|
571
|
+
await this._updateUserKYC(userId, verification);
|
|
572
|
+
}
|
|
573
|
+
} catch (error) {
|
|
574
|
+
console.error('NIDA verification failed:', error);
|
|
575
|
+
// Allow request to continue but log failure
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return next(request);
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Transaction monitoring middleware
|
|
585
|
+
*/
|
|
586
|
+
transactionMiddleware() {
|
|
587
|
+
return async (transaction, next) => {
|
|
588
|
+
const customer = await this._getCustomer(transaction.customerId);
|
|
589
|
+
|
|
590
|
+
const evaluation = await this.aml.evaluateTransaction(transaction, customer);
|
|
591
|
+
|
|
592
|
+
if (evaluation.suspicious) {
|
|
593
|
+
console.warn(`Suspicious transaction detected: ${transaction.id}`);
|
|
594
|
+
// Could block transaction or flag for review
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return next(transaction);
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Get user by ID
|
|
603
|
+
*/
|
|
604
|
+
async _getUser(userId) {
|
|
605
|
+
// Implementation would query user database
|
|
606
|
+
return { id: userId, kycVerified: false, nin: '12345678901234567890' };
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Update user KYC status
|
|
611
|
+
*/
|
|
612
|
+
async _updateUserKYC(userId, verification) {
|
|
613
|
+
// Implementation would update user database
|
|
614
|
+
console.warn(`Updated KYC for user ${userId}:`, verification);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Get customer by ID
|
|
619
|
+
*/
|
|
620
|
+
async _getCustomer(customerId) {
|
|
621
|
+
// Implementation would query customer database
|
|
622
|
+
return {
|
|
623
|
+
id: customerId,
|
|
624
|
+
riskLevel: 'low',
|
|
625
|
+
address: { country: 'TZ' }
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
}
|