@transfergratis/react-native-sdk 0.1.29 → 0.1.31
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/build/components/KYCElements/IDCardCapture.d.ts.map +1 -1
- package/build/components/KYCElements/IDCardCapture.js +56 -25
- package/build/components/KYCElements/IDCardCapture.js.map +1 -1
- package/build/config/region_mapping.d.ts.map +1 -1
- package/build/config/region_mapping.js +10 -18
- package/build/config/region_mapping.js.map +1 -1
- package/build/modules/api/CardAuthentification.d.ts.map +1 -1
- package/build/modules/api/CardAuthentification.js +180 -89
- package/build/modules/api/CardAuthentification.js.map +1 -1
- package/build/modules/api/KYCService.d.ts +1 -1
- package/build/modules/api/KYCService.d.ts.map +1 -1
- package/build/modules/api/KYCService.js +10 -6
- package/build/modules/api/KYCService.js.map +1 -1
- package/package.json +1 -1
- package/src/components/KYCElements/IDCardCapture.tsx +78 -37
- package/src/config/region_mapping.json +10 -18
- package/src/config/region_mapping.ts +10 -18
- package/src/modules/api/CardAuthentification.ts +211 -99
- package/src/modules/api/KYCService.ts +28 -7
|
@@ -71,18 +71,47 @@ export async function frontVerification(
|
|
|
71
71
|
? detected.card_obb[0]
|
|
72
72
|
: null;
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
74
|
+
let points = null;
|
|
75
|
+
let isCardInFrame = true;
|
|
76
|
+
let hasCroppedSides = false;
|
|
77
|
+
|
|
78
|
+
// Extract data safely
|
|
79
|
+
if (cardData) {
|
|
80
|
+
points = cardData.obb;
|
|
81
|
+
if (typeof cardData.card_in_frame !== 'undefined') {
|
|
82
|
+
isCardInFrame = cardData.card_in_frame === true;
|
|
83
|
+
hasCroppedSides = Array.isArray(cardData.cropped_sides) && cardData.cropped_sides.length > 0;
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
// --- 1. STRICT FRAMING CHECK (MUST HAPPEN FIRST) ---
|
|
88
|
+
if (!isCardInFrame || hasCroppedSides) {
|
|
89
|
+
logger.log(`Front Framing failed. Cropped sides detected.`);
|
|
90
|
+
throw new Error('CARD_NOT_FULLY_IN_FRAME');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// --- 2. STRICT DISTANCE CHECK (ONLY RUNS IF FULLY IN FRAME) ---
|
|
94
|
+
if (points && points.length === 4) {
|
|
95
|
+
const getDistance = (p1: number[], p2: number[]) => Math.sqrt(Math.pow(p2[0] - p1[0], 2) + Math.pow(p2[1] - p1[1], 2));
|
|
96
|
+
const longestCardEdge = Math.max(getDistance(points[0], points[1]), getDistance(points[1], points[2]));
|
|
97
|
+
|
|
98
|
+
const maxX = Math.max(points[0][0], points[1][0], points[2][0], points[3][0]);
|
|
99
|
+
const maxY = Math.max(points[0][1], points[1][1], points[2][1], points[3][1]);
|
|
100
|
+
const estimatedImageScale = Math.max(maxX, maxY);
|
|
101
|
+
|
|
102
|
+
const fillPercentage = longestCardEdge / estimatedImageScale;
|
|
103
|
+
logger.log(`Front Card Fill Percentage: ${(fillPercentage * 100).toFixed(1)}%`);
|
|
104
|
+
|
|
105
|
+
if (fillPercentage < 0.50) {
|
|
106
|
+
logger.log("🛑 Front Image rejected: Document is too far away.");
|
|
107
|
+
throw new Error("TOO_FAR_AWAY");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
|
|
86
115
|
// Check Confidence Threshold
|
|
87
116
|
const obbConfidence = getObbConfidence(detected.card_obb);
|
|
88
117
|
if (obbConfidence !== null && obbConfidence < OBB_CONFIDENCE_THRESHOLD) {
|
|
@@ -101,7 +130,7 @@ export async function frontVerification(
|
|
|
101
130
|
|
|
102
131
|
// MRZ Extraction
|
|
103
132
|
if (result.regionMapping?.authMethod?.length > 0 && result.regionMapping.authMethod.includes('MRZ')) {
|
|
104
|
-
|
|
133
|
+
const mrzResponse = await kycService.extractMrzText(
|
|
105
134
|
{
|
|
106
135
|
fileUri: result.path || '',
|
|
107
136
|
docType: result?.selectedDocumentType || '',
|
|
@@ -113,11 +142,20 @@ export async function frontVerification(
|
|
|
113
142
|
},
|
|
114
143
|
env
|
|
115
144
|
);
|
|
145
|
+
|
|
146
|
+
// Safety check: parse if it was returned as a raw string
|
|
147
|
+
mrz = typeof mrzResponse === 'string' ? JSON.parse(mrzResponse) : mrzResponse;
|
|
148
|
+
|
|
149
|
+
if (mrz && (!mrz.success || mrz.parsed_data?.status === 'FAILURE')) {
|
|
150
|
+
logger.log("🛑 Front MRZ Extraction Failed:", mrz.parsed_data?.status_message);
|
|
151
|
+
throw new Error(`MRZ illisible: ${mrz.parsed_data?.status_message || 'Veuillez nettoyer l\'objectif et réessayer'}`);
|
|
152
|
+
}
|
|
116
153
|
}
|
|
117
154
|
|
|
118
155
|
return { ...detected, croppedBase64, bbox, ...(mrz ? { mrz } : {}) };
|
|
119
156
|
} catch (e: any) {
|
|
120
|
-
logger.error('Error front verification:',
|
|
157
|
+
logger.error('Error front verification:', e?.message);
|
|
158
|
+
// Do not use LogBox for background silent errors
|
|
121
159
|
throw new Error(e?.message || 'Erreur de détection du visage');
|
|
122
160
|
}
|
|
123
161
|
}
|
|
@@ -250,7 +288,6 @@ export async function backVerification(
|
|
|
250
288
|
if (!result.path) throw new Error('No path provided');
|
|
251
289
|
logger.log("result.regionMapping", result.regionMapping, result.currentSide, result.code);
|
|
252
290
|
|
|
253
|
-
|
|
254
291
|
if (env === 'SANDBOX') {
|
|
255
292
|
console.log("SANDBOX mode: Skipping AI verification for back document");
|
|
256
293
|
|
|
@@ -265,27 +302,30 @@ export async function backVerification(
|
|
|
265
302
|
if (result.regionMapping?.authMethod?.includes('MRZ')) {
|
|
266
303
|
return {
|
|
267
304
|
success: true,
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
305
|
+
mrz: {
|
|
306
|
+
success: true,
|
|
307
|
+
parsed_data: {
|
|
308
|
+
status: 'SUCCESS',
|
|
309
|
+
document_type: result.selectedDocumentType,
|
|
310
|
+
mrz_type: result.mrzType || 'TD1'
|
|
311
|
+
}
|
|
272
312
|
},
|
|
273
313
|
bbox: mockBbox,
|
|
274
314
|
card_obb: mockCardObb
|
|
275
315
|
};
|
|
276
316
|
} else if (result.regionMapping?.authMethod?.includes('2D_barcode')) {
|
|
277
317
|
return {
|
|
278
|
-
|
|
318
|
+
success: true,
|
|
319
|
+
barcode: { barcode_data: 'SANDBOX_MOCK_BARCODE' },
|
|
279
320
|
bbox: mockBbox,
|
|
280
321
|
card_obb: mockCardObb
|
|
281
322
|
};
|
|
282
323
|
}
|
|
283
|
-
return { bbox: mockBbox };
|
|
324
|
+
return { success: true, bbox: mockBbox, card_obb: mockCardObb };
|
|
284
325
|
}
|
|
285
326
|
|
|
286
327
|
const token = await authentification();
|
|
287
328
|
|
|
288
|
-
|
|
289
329
|
logger.log("1. Checking template and framing for back document...");
|
|
290
330
|
|
|
291
331
|
const templateResponse = await kycService.checkTemplateType({
|
|
@@ -296,18 +336,66 @@ export async function backVerification(
|
|
|
296
336
|
token: token
|
|
297
337
|
}, env) as ApiVerificationResponse;
|
|
298
338
|
|
|
299
|
-
|
|
339
|
+
if (templateResponse.success === false || templateResponse.Error) {
|
|
340
|
+
logger.log("Backend returned an error:", templateResponse.Error);
|
|
341
|
+
throw new Error('Impossible de lire le document. Veuillez vous rapprocher et stabiliser la caméra.');
|
|
342
|
+
}
|
|
343
|
+
|
|
300
344
|
const cardData = templateResponse?.card_obb && Array.isArray(templateResponse.card_obb) && templateResponse.card_obb.length > 0
|
|
301
345
|
? templateResponse.card_obb[0]
|
|
302
346
|
: null;
|
|
303
347
|
|
|
304
|
-
if (cardData
|
|
305
|
-
|
|
306
|
-
|
|
348
|
+
if (!cardData) {
|
|
349
|
+
throw new Error('CARD_NOT_FULLY_IN_FRAME');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
let points = null;
|
|
353
|
+
let isCardInFrame = false;
|
|
354
|
+
let hasCroppedSides = true;
|
|
307
355
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
356
|
+
// Safely extract coordinates and frame status
|
|
357
|
+
if (cardData.obb) {
|
|
358
|
+
points = cardData.obb;
|
|
359
|
+
if (typeof cardData.card_in_frame !== 'undefined') {
|
|
360
|
+
isCardInFrame = cardData.card_in_frame === true;
|
|
361
|
+
hasCroppedSides = Array.isArray(cardData.cropped_sides) && cardData.cropped_sides.length > 0;
|
|
362
|
+
}
|
|
363
|
+
} else if (Array.isArray(cardData) && Array.isArray(cardData[0])) {
|
|
364
|
+
points = cardData[0];
|
|
365
|
+
isCardInFrame = true;
|
|
366
|
+
hasCroppedSides = false;
|
|
367
|
+
|
|
368
|
+
if (points && points.length === 4) {
|
|
369
|
+
const minX = Math.min(...points.map((p: number[]) => p[0]));
|
|
370
|
+
const minY = Math.min(...points.map((p: number[]) => p[1]));
|
|
371
|
+
if (minX <= 15 || minY <= 15) {
|
|
372
|
+
isCardInFrame = false;
|
|
373
|
+
hasCroppedSides = true;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// --- 2. STRICT FRAMING CHECK (MUST HAPPEN FIRST) ---
|
|
379
|
+
if (!isCardInFrame || hasCroppedSides) {
|
|
380
|
+
logger.log(`Back Framing failed. Coordinates hit image boundary.`);
|
|
381
|
+
throw new Error('CARD_NOT_FULLY_IN_FRAME');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// --- 3. STRICT DISTANCE CHECK (ONLY RUNS IF FULLY IN FRAME) ---
|
|
385
|
+
if (points && points.length === 4) {
|
|
386
|
+
const getDistance = (p1: number[], p2: number[]) => Math.sqrt(Math.pow(p2[0] - p1[0], 2) + Math.pow(p2[1] - p1[1], 2));
|
|
387
|
+
const longestCardEdge = Math.max(getDistance(points[0], points[1]), getDistance(points[1], points[2]));
|
|
388
|
+
|
|
389
|
+
const maxX = Math.max(points[0][0], points[1][0], points[2][0], points[3][0]);
|
|
390
|
+
const maxY = Math.max(points[0][1], points[1][1], points[2][1], points[3][1]);
|
|
391
|
+
const estimatedImageScale = Math.max(maxX, maxY);
|
|
392
|
+
|
|
393
|
+
const fillPercentage = longestCardEdge / estimatedImageScale;
|
|
394
|
+
logger.log(`Back Card Fill Percentage: ${(fillPercentage * 100).toFixed(1)}%`);
|
|
395
|
+
|
|
396
|
+
if (fillPercentage < 0.50) {
|
|
397
|
+
logger.log("🛑 Back Image rejected: Document is too far away.");
|
|
398
|
+
throw new Error("TOO_FAR_AWAY");
|
|
311
399
|
}
|
|
312
400
|
}
|
|
313
401
|
|
|
@@ -316,12 +404,10 @@ export async function backVerification(
|
|
|
316
404
|
throw new Error('Carte non entièrement visible. Positionnez toute la carte dans le cadre.');
|
|
317
405
|
}
|
|
318
406
|
|
|
319
|
-
|
|
320
407
|
const activeTemplatePath = templateResponse.template_path || result.templatePath || '';
|
|
321
408
|
|
|
322
409
|
if (activeTemplatePath) {
|
|
323
410
|
const expectedCountryName = countryData[result.code]?.name_en || '';
|
|
324
|
-
|
|
325
411
|
const hasCodeMatch = activeTemplatePath.includes(`_${result.code}_`);
|
|
326
412
|
const hasNameMatch = expectedCountryName && activeTemplatePath.toLowerCase().includes(expectedCountryName.toLowerCase());
|
|
327
413
|
|
|
@@ -333,88 +419,80 @@ export async function backVerification(
|
|
|
333
419
|
|
|
334
420
|
logger.log("Framing and Country Template verified successfully. Proceeding to Data Extraction.");
|
|
335
421
|
|
|
336
|
-
|
|
337
|
-
const
|
|
422
|
+
const hasMrz = result.regionMapping?.authMethod?.includes('MRZ') || !result.regionMapping?.authMethod?.length;
|
|
423
|
+
const hasBarcode = result.regionMapping?.authMethod?.includes('2D_barcode');
|
|
424
|
+
|
|
425
|
+
let extractionResult: any = {};
|
|
426
|
+
|
|
427
|
+
// --- 4. Try MRZ First (If required or default) ---
|
|
428
|
+
if (hasMrz) {
|
|
338
429
|
try {
|
|
339
430
|
logger.log("Tentative d'extraction MRZ");
|
|
340
|
-
const
|
|
431
|
+
const mrzResponse = await kycService.extractMrzText({
|
|
341
432
|
fileUri: result.path!,
|
|
342
433
|
docType: result?.selectedDocumentType || '',
|
|
343
434
|
docRegion: result?.code || '',
|
|
344
435
|
postfix: 'back',
|
|
345
436
|
token: token,
|
|
346
|
-
template_path: activeTemplatePath,
|
|
347
|
-
mrz_type: result?.mrzType || ''
|
|
437
|
+
template_path: activeTemplatePath,
|
|
438
|
+
mrz_type: result?.mrzType || '' // Safe to send empty string
|
|
348
439
|
}, env);
|
|
349
440
|
|
|
350
|
-
|
|
351
|
-
let croppedBase64: string | undefined;
|
|
352
|
-
|
|
353
|
-
try {
|
|
354
|
-
// We use the OBB from our template check to ensure clean cropping
|
|
355
|
-
const crop = await cropByObb(result?.path || '', templateResponse.card_obb);
|
|
356
|
-
bbox = crop.bbox;
|
|
357
|
-
croppedBase64 = crop.base64;
|
|
358
|
-
} catch { }
|
|
359
|
-
return { ...mrz, bbox, croppedBase64 }
|
|
441
|
+
const mrz = typeof mrzResponse === 'string' ? JSON.parse(mrzResponse) : mrzResponse;
|
|
360
442
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
try {
|
|
364
|
-
const barcode = await kycService.extractBarcode({ fileUri: result.path!, token: token }, env);
|
|
365
|
-
return barcode;
|
|
366
|
-
} catch (barcodeError: any) {
|
|
367
|
-
throw new Error(`MRZ et barcode ont échoué. MRZ: ${mrzError?.message}, Barcode: ${barcodeError?.message}`);
|
|
443
|
+
if (!mrz || !mrz.success || mrz.parsed_data?.status === 'FAILURE') {
|
|
444
|
+
throw new Error(mrz?.parsed_data?.status_message || 'Lecture MRZ échouée');
|
|
368
445
|
}
|
|
369
|
-
}
|
|
370
|
-
};
|
|
371
|
-
|
|
372
|
-
if (result.regionMapping?.authMethod?.length > 2 && (!result?.mrzType || result?.mrzType.length === 0)) {
|
|
373
|
-
return await tryMrzWithBarcodeFallback();
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
if (result.regionMapping?.authMethod?.length > 0 && result.regionMapping.authMethod.includes('MRZ') && result?.mrzType && result?.mrzType.length > 0) {
|
|
377
|
-
try {
|
|
378
|
-
const mrz = await kycService.extractMrzText({
|
|
379
|
-
fileUri: result.path!,
|
|
380
|
-
docType: result?.selectedDocumentType || '',
|
|
381
|
-
docRegion: result?.code || '',
|
|
382
|
-
postfix: 'back',
|
|
383
|
-
token: token,
|
|
384
|
-
template_path: activeTemplatePath,
|
|
385
|
-
mrz_type: result?.mrzType || ''
|
|
386
|
-
}, env);
|
|
387
446
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
const crop = await cropByObb(result?.path || '', templateResponse.card_obb);
|
|
391
|
-
bbox = crop.bbox;
|
|
392
|
-
} catch { }
|
|
393
|
-
return { ...mrz, bbox };
|
|
447
|
+
extractionResult = { mrz };
|
|
448
|
+
|
|
394
449
|
} catch (mrzError: any) {
|
|
395
|
-
|
|
450
|
+
if (hasBarcode) {
|
|
451
|
+
logger.log(`MRZ échoué (${mrzError?.message}), tentative d'extraction barcode...`);
|
|
452
|
+
try {
|
|
453
|
+
const barcode = await kycService.extractBarcode({ fileUri: result.path!, token: token }, env);
|
|
454
|
+
extractionResult = { barcode };
|
|
455
|
+
} catch (barcodeError: any) {
|
|
456
|
+
throw new Error(`MRZ et Barcode ont échoué. Veuillez stabiliser la carte.`);
|
|
457
|
+
}
|
|
458
|
+
} else {
|
|
459
|
+
throw new Error(`Lecture MRZ invalide: ${mrzError?.message}`);
|
|
460
|
+
}
|
|
396
461
|
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
if (
|
|
400
|
-
|
|
401
|
-
logger.log("Tentative d'extraction barcode");
|
|
462
|
+
}
|
|
463
|
+
// --- 5. Try Barcode Only ---
|
|
464
|
+
else if (hasBarcode) {
|
|
465
|
+
try {
|
|
466
|
+
logger.log("Tentative d'extraction barcode seule");
|
|
402
467
|
const barcode = await kycService.extractBarcode({ fileUri: result.path!, token: token }, env);
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
bbox = crop.bbox;
|
|
408
|
-
} catch { }
|
|
409
|
-
return { ...barcode, bbox };
|
|
410
|
-
} catch (barcodeError: any) {
|
|
411
|
-
throw new Error(`Barcode et MRZ ont échoué. Barcode: ${barcodeError?.message}, MRZ: ${barcodeError?.message}`);
|
|
412
|
-
}
|
|
468
|
+
extractionResult = { barcode };
|
|
469
|
+
} catch (e: any) {
|
|
470
|
+
throw new Error(`Lecture Barcode échouée: ${e?.message}`);
|
|
471
|
+
}
|
|
413
472
|
}
|
|
414
|
-
|
|
473
|
+
|
|
474
|
+
let bbox: IBbox | undefined;
|
|
475
|
+
let croppedBase64: string | undefined;
|
|
476
|
+
|
|
477
|
+
try {
|
|
478
|
+
const crop = await cropByObb(result?.path || '', templateResponse.card_obb);
|
|
479
|
+
bbox = crop.bbox;
|
|
480
|
+
croppedBase64 = crop.base64;
|
|
481
|
+
} catch { }
|
|
482
|
+
|
|
483
|
+
return {
|
|
484
|
+
success: true,
|
|
485
|
+
...extractionResult,
|
|
486
|
+
bbox,
|
|
487
|
+
croppedBase64,
|
|
488
|
+
card_obb: templateResponse.card_obb
|
|
489
|
+
};
|
|
490
|
+
|
|
415
491
|
} catch (e: any) {
|
|
416
|
-
if (e?.message === 'CARD_NOT_FULLY_IN_FRAME'
|
|
417
|
-
|
|
492
|
+
if (e?.message === 'CARD_NOT_FULLY_IN_FRAME' || e?.message === 'TOO_FAR_AWAY') {
|
|
493
|
+
throw e;
|
|
494
|
+
}
|
|
495
|
+
throw new Error(e?.message || 'Erreur de détection des données');
|
|
418
496
|
}
|
|
419
497
|
}
|
|
420
498
|
|
|
@@ -441,18 +519,52 @@ export async function checkTemplateType(result: { path?: string, docType: string
|
|
|
441
519
|
const token = await authentification();
|
|
442
520
|
const templateType = await kycService.checkTemplateType({ fileUri: result.path || '', docType: result?.docType as GovernmentDocumentType, docRegion: result?.docRegion || "", postfix: result?.postfix, token: token }, env);
|
|
443
521
|
|
|
522
|
+
if (templateType.success === false || templateType.Error) {
|
|
523
|
+
logger.log("Backend returned an error:", templateType.Error);
|
|
524
|
+
throw new Error('Impossible de lire le document. Veuillez vous rapprocher et stabiliser la caméra.');
|
|
525
|
+
}
|
|
526
|
+
|
|
444
527
|
const cardData = templateType.card_obb && Array.isArray(templateType.card_obb) && templateType.card_obb.length > 0
|
|
445
528
|
? templateType.card_obb[0]
|
|
446
529
|
: null;
|
|
447
530
|
|
|
448
|
-
if (cardData
|
|
449
|
-
|
|
450
|
-
|
|
531
|
+
if (!cardData) {
|
|
532
|
+
throw new Error('CARD_NOT_FULLY_IN_FRAME');
|
|
533
|
+
}
|
|
451
534
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
535
|
+
// Default to FALSE so it must prove it is in the frame
|
|
536
|
+
let points = null;
|
|
537
|
+
let isCardInFrame = false;
|
|
538
|
+
let hasCroppedSides = true;
|
|
539
|
+
|
|
540
|
+
// Extract data safely based on API response format
|
|
541
|
+
if (cardData.obb) {
|
|
542
|
+
points = cardData.obb;
|
|
543
|
+
if (typeof cardData.card_in_frame !== 'undefined') {
|
|
544
|
+
isCardInFrame = cardData.card_in_frame === true;
|
|
545
|
+
hasCroppedSides = Array.isArray(cardData.cropped_sides) && cardData.cropped_sides.length > 0;
|
|
455
546
|
}
|
|
547
|
+
} else if (Array.isArray(cardData) && Array.isArray(cardData[0])) {
|
|
548
|
+
points = cardData[0];
|
|
549
|
+
isCardInFrame = true; // Passed initial existence check
|
|
550
|
+
hasCroppedSides = false;
|
|
551
|
+
|
|
552
|
+
// MATH FALLBACK: Check if the coordinates are touching the absolute edge of the photo.
|
|
553
|
+
if (points && points.length === 4) {
|
|
554
|
+
const minX = Math.min(...points.map((p: number[]) => p[0]));
|
|
555
|
+
const minY = Math.min(...points.map((p: number[]) => p[1]));
|
|
556
|
+
// If any corner is within 15 pixels of the edge, it is physically cut off!
|
|
557
|
+
if (minX <= 15 || minY <= 15) {
|
|
558
|
+
isCardInFrame = false;
|
|
559
|
+
hasCroppedSides = true;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// --- 3. STRICT FRAMING CHECK (MUST HAPPEN FIRST) ---
|
|
565
|
+
if (!isCardInFrame || hasCroppedSides) {
|
|
566
|
+
logger.log(`Template Framing failed. Coordinates hit image boundary.`);
|
|
567
|
+
throw new Error('CARD_NOT_FULLY_IN_FRAME');
|
|
456
568
|
}
|
|
457
569
|
|
|
458
570
|
|
|
@@ -355,7 +355,18 @@ export class KYCService {
|
|
|
355
355
|
}
|
|
356
356
|
|
|
357
357
|
// STEP 3 - MRZ TEXT EXTRACTION
|
|
358
|
-
async extractMrzText(
|
|
358
|
+
async extractMrzText(
|
|
359
|
+
params: {
|
|
360
|
+
fileUri: string;
|
|
361
|
+
docType: string;
|
|
362
|
+
docRegion: string;
|
|
363
|
+
postfix?: string;
|
|
364
|
+
token: string;
|
|
365
|
+
template_path: string;
|
|
366
|
+
mrz_type?: string;
|
|
367
|
+
},
|
|
368
|
+
env: KycEnvironment = 'PRODUCTION'
|
|
369
|
+
): Promise<any> {
|
|
359
370
|
// SANDBOX mode
|
|
360
371
|
if (env === 'SANDBOX') {
|
|
361
372
|
console.log("SANDBOX mode: Skipping AI MRZ extraction");
|
|
@@ -377,29 +388,39 @@ export class KYCService {
|
|
|
377
388
|
const { fileUri, docType, docRegion, postfix = 'back', token, template_path, mrz_type } = params;
|
|
378
389
|
const formData = new FormData();
|
|
379
390
|
|
|
380
|
-
// ✅
|
|
391
|
+
// ✅ Dynamic filename to prevent passports from crashing (since their MRZ is on the front)
|
|
381
392
|
await appendFileToFormData(formData, 'file', fileUri, `id_card_${postfix}.jpg`, 'image/jpeg');
|
|
382
393
|
|
|
383
|
-
const docTypeShorted = GovernmentDocumentTypeShorted[docType as GovernmentDocumentType];
|
|
394
|
+
const docTypeShorted = GovernmentDocumentTypeShorted[docType as GovernmentDocumentType] || docType;
|
|
384
395
|
logger.log("docTypeShorted", docTypeShorted, docRegion, postfix);
|
|
385
396
|
|
|
386
|
-
|
|
397
|
+
let url = `${this.mrzServiceURL}/extract_mrz_text/?doc_type=${encodeURIComponent(docTypeShorted)}&doc_region=${encodeURIComponent(docRegion)}&postfix=${encodeURIComponent(postfix)}&template_path=${encodeURIComponent(template_path)}`;
|
|
398
|
+
|
|
399
|
+
if (mrz_type && mrz_type.trim() !== '') {
|
|
400
|
+
url += `&mrz_type=${encodeURIComponent(mrz_type)}`;
|
|
401
|
+
}
|
|
402
|
+
|
|
387
403
|
logger.log("url", url);
|
|
388
404
|
|
|
389
405
|
const attempt = async () => {
|
|
390
406
|
try {
|
|
391
407
|
const res = await axios.post<ExtractMrzTextResponse>(url, formData, {
|
|
392
|
-
headers: { 'Content-Type': 'multipart/form-data', 'Authorization': `Bearer ${token}
|
|
393
|
-
|
|
408
|
+
headers: { 'Content-Type': 'multipart/form-data', 'Authorization': `Bearer ${token}` },
|
|
409
|
+
// Note: Reduced timeout to 10000ms (10s) based on our earlier network fix to prevent infinite hanging!
|
|
410
|
+
timeout: 10000,
|
|
394
411
|
});
|
|
412
|
+
|
|
395
413
|
logger.log('extractMrzText res', JSON.stringify(res.data, null, 2));
|
|
414
|
+
|
|
396
415
|
if (Object.keys(res.data).length === 0) throw new Error('No data found');
|
|
397
|
-
if (res.data?.success === false) throw new Error(res.data.parsed_data
|
|
416
|
+
if (res.data?.success === false) throw new Error(res.data.parsed_data?.status || 'Échec de l\'extraction MRZ');
|
|
417
|
+
|
|
398
418
|
return res.data;
|
|
399
419
|
} catch (e: any) {
|
|
400
420
|
throw new Error(e?.message || 'Erreur de détection du MRZ');
|
|
401
421
|
}
|
|
402
422
|
};
|
|
423
|
+
|
|
403
424
|
try {
|
|
404
425
|
return await attempt();
|
|
405
426
|
} catch (e) {
|