@transfergratis/react-native-sdk 0.1.28 → 0.1.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/build/components/EnhancedCameraView.js +1 -1
  2. package/build/components/EnhancedCameraView.js.map +1 -1
  3. package/build/components/KYCElements/CountrySelectionTemplate.d.ts.map +1 -1
  4. package/build/components/KYCElements/CountrySelectionTemplate.js +13 -42
  5. package/build/components/KYCElements/CountrySelectionTemplate.js.map +1 -1
  6. package/build/components/KYCElements/IDCardCapture.d.ts.map +1 -1
  7. package/build/components/KYCElements/IDCardCapture.js +113 -49
  8. package/build/components/KYCElements/IDCardCapture.js.map +1 -1
  9. package/build/components/KYCElements/SelfieCaptureTemplate.js +2 -2
  10. package/build/components/KYCElements/SelfieCaptureTemplate.js.map +1 -1
  11. package/build/hooks/useTemplateKYCFlow.js +1 -1
  12. package/build/hooks/useTemplateKYCFlow.js.map +1 -1
  13. package/build/modules/api/CardAuthentification.d.ts +15 -7
  14. package/build/modules/api/CardAuthentification.d.ts.map +1 -1
  15. package/build/modules/api/CardAuthentification.js +360 -104
  16. package/build/modules/api/CardAuthentification.js.map +1 -1
  17. package/build/modules/api/KYCService.d.ts +3 -1
  18. package/build/modules/api/KYCService.d.ts.map +1 -1
  19. package/build/modules/api/KYCService.js +25 -24
  20. package/build/modules/api/KYCService.js.map +1 -1
  21. package/build/modules/camera/VisionCameraModule.js +2 -2
  22. package/build/modules/camera/VisionCameraModule.js.map +1 -1
  23. package/build/utils/cropByObb.d.ts +8 -0
  24. package/build/utils/cropByObb.d.ts.map +1 -1
  25. package/build/utils/cropByObb.js +20 -3
  26. package/build/utils/cropByObb.js.map +1 -1
  27. package/build/utils/pathToBase64.js +1 -1
  28. package/build/utils/pathToBase64.js.map +1 -1
  29. package/package.json +1 -1
  30. package/src/components/EnhancedCameraView.tsx +1 -1
  31. package/src/components/KYCElements/CountrySelectionTemplate.tsx +24 -52
  32. package/src/components/KYCElements/IDCardCapture.tsx +179 -109
  33. package/src/components/KYCElements/SelfieCaptureTemplate.tsx +2 -2
  34. package/src/hooks/useTemplateKYCFlow.tsx +1 -1
  35. package/src/modules/api/CardAuthentification.ts +450 -113
  36. package/src/modules/api/KYCService.ts +52 -39
  37. package/src/modules/camera/VisionCameraModule.ts +2 -2
  38. package/src/utils/cropByObb.ts +22 -3
  39. package/src/utils/pathToBase64.ts +1 -1
@@ -1,25 +1,50 @@
1
1
  import kycService, { authentification, errorMessage, truncateFields } from "./KYCService";
2
- import { cropByObb, getCardInFrame, getObbConfidence, OBB_CONFIDENCE_THRESHOLD } from "../../utils/cropByObb";
2
+ import { cropByObb, getObbConfidence, OBB_CONFIDENCE_THRESHOLD } from "../../utils/cropByObb";
3
3
  import { GovernmentDocumentType, IBbox } from "../../types/KYC.types";
4
4
  import { KycEnvironment } from "../../types/env.types";
5
5
  import { logger } from "../../utils/logger";
6
+ import { countryData } from "../../config/countriesData";
6
7
 
7
- export async function frontVerification(result: { path?: string, regionMapping: { authMethod: string[], mrzTypes: string[] }, selectedDocumentType: string, code: string, currentSide: string, templatePath?: string, mrzType?: string }, env: KycEnvironment = 'PRODUCTION') {
8
+ // 1. Add this interface to tell TypeScript what the AI response actually looks like
9
+ interface CardObbData {
10
+ obb: number[][];
11
+ confidence: number;
12
+ card_in_frame?: boolean;
13
+ cropped_sides?: string[];
14
+ }
15
+
16
+ interface ApiVerificationResponse {
17
+ result?: boolean;
18
+ detail?: any[];
19
+ card_obb?: CardObbData[] | any;
20
+ [key: string]: any;
21
+ }
22
+
23
+ export async function frontVerification(
24
+ result: { path?: string, regionMapping: { authMethod: string[], mrzTypes: string[] }, selectedDocumentType: string, code: string, currentSide: string, templatePath?: string, mrzType?: string },
25
+ env: KycEnvironment = 'PRODUCTION'
26
+ ) {
8
27
  try {
9
28
  console.log("Front verification START", JSON.stringify({ result }, null, 2));
10
29
  logger.log("Front verification", JSON.stringify({ result }, null, 2));
11
30
 
12
- // SANDBOX mode: skip AI verification and return mock response
31
+ // SANDBOX mode
13
32
  if (env === 'SANDBOX') {
14
33
  console.log("SANDBOX mode: Skipping AI verification for front document");
15
34
  logger.log("SANDBOX mode: Returning mock front verification response");
16
- const mockBbox: IBbox = { minX: 50, minY: 50, width: 200, height: 200 };
35
+ const mockBbox: IBbox = { minX: 400, minY: 800, width: 2200, height: 1400 };
36
+
17
37
  const mockResponse = {
18
38
  result: true,
19
39
  detail: [{ confidence: 0.95 }],
20
- card_obb: { x: 50, y: 50, width: 200, height: 200 },
40
+ card_obb: [{
41
+ obb: [[400,800], [2600,800], [2600,2200], [400,2200]],
42
+ confidence: 0.95,
43
+ card_in_frame: true,
44
+ cropped_sides: []
45
+ }],
21
46
  bbox: mockBbox,
22
- ...(result.regionMapping.authMethod.includes('MRZ') ? {
47
+ ...(result.regionMapping?.authMethod?.includes('MRZ') ? {
23
48
  mrz: {
24
49
  success: true,
25
50
  parsed_data: {
@@ -34,38 +59,78 @@ export async function frontVerification(result: { path?: string, regionMapping:
34
59
  }
35
60
 
36
61
  const token = await authentification();
37
- const detected = await kycService.detectFaceOnId(result?.path || '', token, result?.selectedDocumentType || '', env)
62
+
63
+ // Cast the response so TypeScript knows about card_obb
64
+ const detected = await kycService.detectFaceOnId(result?.path || '', token, result?.selectedDocumentType || '', env) as ApiVerificationResponse;
38
65
 
39
66
  if (!detected.result) {
40
67
  throw new Error('Aucun visage détecté sur la carte. Veuillez reprendre.');
41
68
  }
42
69
 
43
- // If the backend indicates that the card is not fully in frame, stop early
44
- const cardInFrame = getCardInFrame((detected as any).card_obb);
45
- if (cardInFrame === false) {
46
- // Use a stable error code; the UI maps this to a localized i18n message (cardNotFullyInFrame)
70
+ const cardData = detected.card_obb && Array.isArray(detected.card_obb) && detected.card_obb.length > 0
71
+ ? detected.card_obb[0]
72
+ : null;
73
+
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;
84
+ }
85
+ }
86
+
87
+ // --- 1. STRICT FRAMING CHECK (MUST HAPPEN FIRST) ---
88
+ if (!isCardInFrame || hasCroppedSides) {
89
+ logger.log(`Front Framing failed. Cropped sides detected.`);
47
90
  throw new Error('CARD_NOT_FULLY_IN_FRAME');
48
91
  }
49
92
 
50
- const obbConfidence = getObbConfidence((detected as any).card_obb);
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
+
115
+ // Check Confidence Threshold
116
+ const obbConfidence = getObbConfidence(detected.card_obb);
51
117
  if (obbConfidence !== null && obbConfidence < OBB_CONFIDENCE_THRESHOLD) {
52
118
  throw new Error('Carte non entièrement visible. Positionnez toute la carte dans le cadre.');
53
119
  }
54
120
 
55
- // Optional: crop image using card_obb for better MRZ/barcode extraction (only when confidence >= threshold)
121
+ // Optional: crop image using card_obb for better MRZ/barcode extraction
56
122
  let croppedBase64: string | undefined;
57
123
  let bbox: IBbox | undefined;
58
124
  let mrz: any | undefined;
59
125
  try {
60
- const crop = await cropByObb(result?.path || '', (detected as any).card_obb);
126
+ const crop = await cropByObb(result?.path || '', detected.card_obb);
61
127
  croppedBase64 = crop.base64;
62
128
  bbox = crop.bbox;
63
129
  } catch { }
64
130
 
65
- if (result.regionMapping.authMethod.length > 0 && result.regionMapping.authMethod.includes('MRZ')) {
66
-
67
-
68
- mrz = await kycService.extractMrzText(
131
+ // MRZ Extraction
132
+ if (result.regionMapping?.authMethod?.length > 0 && result.regionMapping.authMethod.includes('MRZ')) {
133
+ const mrzResponse = await kycService.extractMrzText(
69
134
  {
70
135
  fileUri: result.path || '',
71
136
  docType: result?.selectedDocumentType || '',
@@ -75,154 +140,363 @@ export async function frontVerification(result: { path?: string, regionMapping:
75
140
  template_path: result?.templatePath || '',
76
141
  mrz_type: result?.mrzType || ''
77
142
  },
78
- env)
143
+ env
144
+ );
79
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
+ }
80
153
  }
81
154
 
82
155
  return { ...detected, croppedBase64, bbox, ...(mrz ? { mrz } : {}) };
83
156
  } catch (e: any) {
84
- logger.error('Error front verification:', JSON.stringify(errorMessage(e), null, 2));
157
+ logger.error('Error front verification:', e?.message);
158
+ // Do not use LogBox for background silent errors
85
159
  throw new Error(e?.message || 'Erreur de détection du visage');
86
160
  }
87
161
  }
88
162
 
89
- export async function backVerification(result: { path?: string, regionMapping: { authMethod: string[], mrzTypes: string[] }, selectedDocumentType: string, code: string, currentSide: string, templatePath?: string, mrzType?: string }, env: KycEnvironment = 'PRODUCTION') {
163
+ // export async function frontVerification(
164
+ // result: { path?: string, regionMapping: { authMethod: string[], mrzTypes: string[] }, selectedDocumentType: string, code: string, currentSide: string, templatePath?: string, mrzType?: string },
165
+ // env: KycEnvironment = 'PRODUCTION'
166
+ // ) {
167
+ // try {
168
+ // console.log("Front verification START", JSON.stringify({ result }, null, 2));
169
+ // logger.log("Front verification", JSON.stringify({ result }, null, 2));
170
+
171
+ // // SANDBOX mode
172
+ // if (env === 'SANDBOX') {
173
+ // console.log("SANDBOX mode: Skipping AI verification for front document");
174
+ // logger.log("SANDBOX mode: Returning mock front verification response");
175
+ // const mockBbox: IBbox = { minX: 400, minY: 800, width: 2200, height: 1400 };
176
+
177
+ // // ==========================================
178
+ // // 🧪 TEST OVERRIDE (SANDBOX) 🧪
179
+ // // Immediately throw the error to test the UI
180
+ // // ==========================================
181
+ // throw new Error('CARD_NOT_FULLY_IN_FRAME');
182
+
183
+ // const mockResponse = {
184
+ // result: true,
185
+ // detail: [{ confidence: 0.95 }],
186
+ // card_obb: [{
187
+ // obb: [[400,800], [2600,800], [2600,2200], [400,2200]],
188
+ // confidence: 0.95,
189
+ // card_in_frame: false, // Hardcoded to false
190
+ // cropped_sides: ['left', 'top'] // Hardcoded cropped sides
191
+ // }],
192
+ // bbox: mockBbox,
193
+ // ...(result.regionMapping?.authMethod?.includes('MRZ') ? {
194
+ // mrz: {
195
+ // success: true,
196
+ // parsed_data: {
197
+ // status: 'success',
198
+ // document_type: result.selectedDocumentType,
199
+ // mrz_type: result.mrzType || 'TD1'
200
+ // }
201
+ // }
202
+ // } : {})
203
+ // };
204
+ // return mockResponse;
205
+ // }
206
+
207
+ // const token = await authentification();
208
+
209
+ // // Cast the response so TypeScript knows about card_obb
210
+ // const detected = await kycService.detectFaceOnId(result?.path || '', token, result?.selectedDocumentType || '', env) as ApiVerificationResponse;
211
+
212
+ // // ==========================================
213
+ // // 🧪 TEST OVERRIDE (PRODUCTION) 🧪
214
+ // // Hijack the real API response and force it to look cropped
215
+ // // ==========================================
216
+ // if (detected.card_obb && Array.isArray(detected.card_obb) && detected.card_obb.length > 0) {
217
+ // detected.card_obb[0].card_in_frame = false;
218
+ // detected.card_obb[0].cropped_sides = ['left', 'top'];
219
+ // console.warn("⚠️ FORCING CROPPED ERROR FOR UI TESTING ⚠️");
220
+ // }
221
+ // // ==========================================
222
+
223
+ // if (!detected.result) {
224
+ // throw new Error('Aucun visage détecté sur la carte. Veuillez reprendre.');
225
+ // }
226
+
227
+ // const cardData = detected.card_obb && Array.isArray(detected.card_obb) && detected.card_obb.length > 0
228
+ // ? detected.card_obb[0]
229
+ // : null;
230
+
231
+ // // --- STRICT FRAMING CHECK ---
232
+ // if (cardData && typeof cardData.card_in_frame !== 'undefined') {
233
+ // const isCardInFrame = cardData.card_in_frame === true;
234
+ // const hasCroppedSides = Array.isArray(cardData.cropped_sides) && cardData.cropped_sides.length > 0;
235
+
236
+ // // If it is NOT in the frame, OR if any side is cropped, block progression
237
+ // if (!isCardInFrame || hasCroppedSides) {
238
+ // logger.log(`Framing failed. Cropped sides: ${hasCroppedSides ? cardData.cropped_sides?.join(', ') : 'none'}`);
239
+ // throw new Error('CARD_NOT_FULLY_IN_FRAME');
240
+ // }
241
+ // }
242
+
243
+ // // Check Confidence Threshold
244
+ // const obbConfidence = getObbConfidence(detected.card_obb);
245
+ // if (obbConfidence !== null && obbConfidence < OBB_CONFIDENCE_THRESHOLD) {
246
+ // throw new Error('Carte non entièrement visible. Positionnez toute la carte dans le cadre.');
247
+ // }
248
+
249
+ // // Optional: crop image using card_obb for better MRZ/barcode extraction
250
+ // let croppedBase64: string | undefined;
251
+ // let bbox: IBbox | undefined;
252
+ // let mrz: any | undefined;
253
+ // try {
254
+ // const crop = await cropByObb(result?.path || '', detected.card_obb);
255
+ // croppedBase64 = crop.base64;
256
+ // bbox = crop.bbox;
257
+ // } catch { }
258
+
259
+ // // MRZ Extraction
260
+ // if (result.regionMapping?.authMethod?.length > 0 && result.regionMapping.authMethod.includes('MRZ')) {
261
+ // mrz = await kycService.extractMrzText(
262
+ // {
263
+ // fileUri: result.path || '',
264
+ // docType: result?.selectedDocumentType || '',
265
+ // docRegion: result?.code || "",
266
+ // postfix: result?.currentSide,
267
+ // token: token,
268
+ // template_path: result?.templatePath || '',
269
+ // mrz_type: result?.mrzType || ''
270
+ // },
271
+ // env
272
+ // );
273
+ // }
274
+
275
+ // return { ...detected, croppedBase64, bbox, ...(mrz ? { mrz } : {}) };
276
+ // } catch (e: any) {
277
+ // logger.error('Error front verification:', JSON.stringify(errorMessage(e), null, 2));
278
+ // throw new Error(e?.message || 'Erreur de détection du visage');
279
+ // }
280
+ // }
281
+
282
+
283
+ export async function backVerification(
284
+ result: { path?: string, regionMapping: { authMethod: string[], mrzTypes: string[] }, selectedDocumentType: string, code: string, currentSide: string, templatePath?: string, mrzType?: string },
285
+ env: KycEnvironment = 'PRODUCTION'
286
+ ) {
90
287
  try {
91
288
  if (!result.path) throw new Error('No path provided');
92
289
  logger.log("result.regionMapping", result.regionMapping, result.currentSide, result.code);
93
290
 
94
- // SANDBOX mode: skip AI verification and return mock response
95
291
  if (env === 'SANDBOX') {
96
292
  console.log("SANDBOX mode: Skipping AI verification for back document");
97
- logger.log("SANDBOX mode: Returning mock back verification response");
98
- const mockBbox: IBbox = { minX: 50, minY: 50, width: 200, height: 200 };
99
293
 
100
- if (result.regionMapping.authMethod.includes('MRZ')) {
294
+ const mockBbox: IBbox = { minX: 400, minY: 800, width: 2200, height: 1400 };
295
+ const mockCardObb = [{
296
+ obb: [[400,800], [2600,800], [2600,2200], [400,2200]],
297
+ confidence: 0.95,
298
+ card_in_frame: true,
299
+ cropped_sides: []
300
+ }];
301
+
302
+ if (result.regionMapping?.authMethod?.includes('MRZ')) {
101
303
  return {
102
304
  success: true,
103
- parsed_data: {
104
- status: 'success',
105
- document_type: result.selectedDocumentType,
106
- mrz_type: result.mrzType || 'TD1'
305
+ mrz: {
306
+ success: true,
307
+ parsed_data: {
308
+ status: 'SUCCESS',
309
+ document_type: result.selectedDocumentType,
310
+ mrz_type: result.mrzType || 'TD1'
311
+ }
107
312
  },
108
313
  bbox: mockBbox,
109
- card_obb: { x: 50, y: 50, width: 200, height: 200 }
314
+ card_obb: mockCardObb
110
315
  };
111
- } else if (result.regionMapping.authMethod.includes('2D_barcode')) {
316
+ } else if (result.regionMapping?.authMethod?.includes('2D_barcode')) {
112
317
  return {
113
- barcode_data: 'SANDBOX_MOCK_BARCODE',
318
+ success: true,
319
+ barcode: { barcode_data: 'SANDBOX_MOCK_BARCODE' },
114
320
  bbox: mockBbox,
115
- card_obb: { x: 50, y: 50, width: 200, height: 200 }
321
+ card_obb: mockCardObb
116
322
  };
117
323
  }
118
- return { bbox: mockBbox };
324
+ return { success: true, bbox: mockBbox, card_obb: mockCardObb };
119
325
  }
120
326
 
121
327
  const token = await authentification();
122
328
 
329
+ logger.log("1. Checking template and framing for back document...");
330
+
331
+ const templateResponse = await kycService.checkTemplateType({
332
+ fileUri: result.path,
333
+ docType: result.selectedDocumentType as any,
334
+ docRegion: result.code,
335
+ postfix: 'back',
336
+ token: token
337
+ }, env) as ApiVerificationResponse;
123
338
 
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
+ }
124
343
 
125
- // Fonction helper pour essayer MRZ puis barcode en fallback
126
- const tryMrzWithBarcodeFallback = async () => {
127
- try {
344
+ const cardData = templateResponse?.card_obb && Array.isArray(templateResponse.card_obb) && templateResponse.card_obb.length > 0
345
+ ? templateResponse.card_obb[0]
346
+ : null;
128
347
 
129
- logger.log("Tentative d'extraction MRZ");
130
- const mrz = await kycService.extractMrzText({
131
- fileUri: result.path!,
132
- docType: result?.selectedDocumentType || '',
133
- docRegion: result?.code || '',
134
- postfix: 'back',
135
- token: token,
136
- template_path: result?.templatePath || '',
137
- mrz_type: result?.mrzType || ''
138
- }, env);
139
- const mrzObbConf = getObbConfidence((mrz as any).card_obb);
140
- if (mrzObbConf !== null && mrzObbConf < OBB_CONFIDENCE_THRESHOLD) {
141
- throw new Error('Carte non entièrement visible. Positionnez toute la carte dans le cadre.');
142
- }
143
- let bbox: IBbox | undefined;
144
- let croppedBase64: string | undefined;
348
+ if (!cardData) {
349
+ throw new Error('CARD_NOT_FULLY_IN_FRAME');
350
+ }
145
351
 
146
- try {
147
- const crop = await cropByObb(result?.path || '', (mrz as any).card_obb);
148
- bbox = crop.bbox;
149
- croppedBase64 = crop.base64;
352
+ let points = null;
353
+ let isCardInFrame = false;
354
+ let hasCroppedSides = true;
150
355
 
151
- } catch { }
152
- return { ...mrz, bbox, croppedBase64 }
153
- } catch (mrzError: any) {
154
- logger.log("MRZ échoué, tentative d'extraction barcode");
155
- try {
156
- const barcode = await kycService.extractBarcode({ fileUri: result.path!, token: token }, env);
157
- const barcodeObbConf = getObbConfidence((barcode as any).card_obb);
158
- if (barcodeObbConf !== null && barcodeObbConf < OBB_CONFIDENCE_THRESHOLD) {
159
- throw new Error('Carte non entièrement visible. Positionnez toute la carte dans le cadre.');
160
- }
161
- return barcode;
162
- } catch (barcodeError: any) {
163
- throw new Error(`MRZ et barcode ont échoué. MRZ: ${mrzError?.message}, Barcode: ${barcodeError?.message}`);
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;
164
374
  }
165
375
  }
166
- };
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
+ }
167
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");
399
+ }
400
+ }
401
+
402
+ const obbConf = getObbConfidence(templateResponse?.card_obb);
403
+ if (obbConf !== null && obbConf < OBB_CONFIDENCE_THRESHOLD) {
404
+ throw new Error('Carte non entièrement visible. Positionnez toute la carte dans le cadre.');
405
+ }
168
406
 
407
+ const activeTemplatePath = templateResponse.template_path || result.templatePath || '';
408
+
409
+ if (activeTemplatePath) {
410
+ const expectedCountryName = countryData[result.code]?.name_en || '';
411
+ const hasCodeMatch = activeTemplatePath.includes(`_${result.code}_`);
412
+ const hasNameMatch = expectedCountryName && activeTemplatePath.toLowerCase().includes(expectedCountryName.toLowerCase());
169
413
 
170
- if (result.regionMapping.authMethod.length > 2 && (!result?.mrzType || result?.mrzType.length === 0)) {
171
- return await tryMrzWithBarcodeFallback();
414
+ if (!hasCodeMatch && !hasNameMatch) {
415
+ logger.log(`Template mismatch! Expected country: ${result.code} (${expectedCountryName}), Detected: ${activeTemplatePath}`);
416
+ throw new Error(`Le document ne correspond pas au pays sélectionné (${result.code}).`);
417
+ }
172
418
  }
173
419
 
174
- if (result.regionMapping.authMethod.length > 0 && result.regionMapping.authMethod.includes('MRZ') && result?.mrzType && result?.mrzType.length > 0) {
175
- try {
420
+ logger.log("Framing and Country Template verified successfully. Proceeding to Data Extraction.");
421
+
422
+ const hasMrz = result.regionMapping?.authMethod?.includes('MRZ') || !result.regionMapping?.authMethod?.length;
423
+ const hasBarcode = result.regionMapping?.authMethod?.includes('2D_barcode');
176
424
 
177
- let mrz: any | undefined;
178
- mrz = await kycService.extractMrzText({
425
+ let extractionResult: any = {};
426
+
427
+ // --- 4. Try MRZ First (If required or default) ---
428
+ if (hasMrz) {
429
+ try {
430
+ logger.log("Tentative d'extraction MRZ");
431
+ const mrzResponse = await kycService.extractMrzText({
179
432
  fileUri: result.path!,
180
433
  docType: result?.selectedDocumentType || '',
181
434
  docRegion: result?.code || '',
182
435
  postfix: 'back',
183
436
  token: token,
184
- template_path: result?.templatePath || '',
185
- mrz_type: result?.mrzType || ''
437
+ template_path: activeTemplatePath,
438
+ mrz_type: result?.mrzType || '' // Safe to send empty string
186
439
  }, env);
187
- const mrzObbConf = getObbConfidence((mrz as any).card_obb);
188
- if (mrzObbConf !== null && mrzObbConf < OBB_CONFIDENCE_THRESHOLD) {
189
- throw new Error('Carte non entièrement visible. Positionnez toute la carte dans le cadre.');
440
+
441
+ const mrz = typeof mrzResponse === 'string' ? JSON.parse(mrzResponse) : mrzResponse;
442
+
443
+ if (!mrz || !mrz.success || mrz.parsed_data?.status === 'FAILURE') {
444
+ throw new Error(mrz?.parsed_data?.status_message || 'Lecture MRZ échouée');
190
445
  }
191
- let bbox: IBbox | undefined;
192
- try {
193
- const crop = await cropByObb(result?.path || '', (mrz as any).card_obb);
194
- bbox = crop.bbox;
195
- } catch { }
196
- return { ...mrz, bbox };
197
- } catch (mrzError: any) {
198
- throw new Error(`MRZ et barcode ont échoué. MRZ: ${mrzError?.message}, Barcode: ${mrzError?.message}`);
199
- }
200
- }
446
+
447
+ extractionResult = { mrz };
201
448
 
202
- if (result.regionMapping.authMethod.length > 0 && result.regionMapping.authMethod.includes('2D_barcode')) {
203
- try {
204
- logger.log("Tentative d'extraction barcode");
205
- const barcode = await kycService.extractBarcode({ fileUri: result.path!, token: token }, env);
206
- const barcodeObbConf = getObbConfidence((barcode as any).card_obb);
207
- if (barcodeObbConf !== null && barcodeObbConf < OBB_CONFIDENCE_THRESHOLD) {
208
- throw new Error('Carte non entièrement visible. Positionnez toute la carte dans le cadre.');
449
+ } catch (mrzError: any) {
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}`);
209
460
  }
210
- let bbox: IBbox | undefined;
211
- try {
212
- const crop = await cropByObb(result?.path || '', (barcode as any).card_obb);
213
- bbox = crop.bbox;
214
- } catch { }
215
- return { ...barcode, bbox };
216
- } catch (barcodeError: any) {
217
- throw new Error(`Barcode et MRZ ont échoué. Barcode: ${barcodeError?.message}, MRZ: ${barcodeError?.message}`);
218
461
  }
462
+ }
463
+ // --- 5. Try Barcode Only ---
464
+ else if (hasBarcode) {
465
+ try {
466
+ logger.log("Tentative d'extraction barcode seule");
467
+ const barcode = await kycService.extractBarcode({ fileUri: result.path!, token: token }, env);
468
+ extractionResult = { barcode };
469
+ } catch (e: any) {
470
+ throw new Error(`Lecture Barcode échouée: ${e?.message}`);
471
+ }
219
472
  }
220
- return null;
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
+
221
491
  } catch (e: any) {
222
- throw new Error(e?.message || 'Erreur de détection du MRZ ou barcode');
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');
223
496
  }
224
497
  }
225
498
 
499
+
226
500
  /**
227
501
  * Check template type
228
502
  * @param result
@@ -230,23 +504,86 @@ export async function backVerification(result: { path?: string, regionMapping: {
230
504
  */
231
505
  export async function checkTemplateType(result: { path?: string, docType: string, docRegion: string, postfix: string }, env: KycEnvironment = 'PRODUCTION') {
232
506
  try {
233
- // SANDBOX mode: skip AI verification and return mock response
234
507
  if (env === 'SANDBOX') {
235
- console.log("SANDBOX mode: Skipping AI template type check");
236
- logger.log("SANDBOX mode: Returning mock template type response");
237
508
  return {
238
509
  template_path: `templates/${result.docType}_${result.docRegion}_${result.postfix}.jpg`,
239
- card_obb: { x: 50, y: 50, width: 200, height: 200 }
510
+ card_obb: [{
511
+ obb: [[400,800], [2600,800], [2600,2200], [400,2200]],
512
+ confidence: 0.95,
513
+ card_in_frame: true,
514
+ cropped_sides: []
515
+ }]
240
516
  };
241
517
  }
242
518
 
243
519
  const token = await authentification();
244
520
  const templateType = await kycService.checkTemplateType({ fileUri: result.path || '', docType: result?.docType as GovernmentDocumentType, docRegion: result?.docRegion || "", postfix: result?.postfix, token: token }, env);
245
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
+
527
+ const cardData = templateType.card_obb && Array.isArray(templateType.card_obb) && templateType.card_obb.length > 0
528
+ ? templateType.card_obb[0]
529
+ : null;
530
+
531
+ if (!cardData) {
532
+ throw new Error('CARD_NOT_FULLY_IN_FRAME');
533
+ }
534
+
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;
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');
568
+ }
569
+
570
+
571
+ const LPIPS_THRESHOLD = 0.75;
572
+
573
+ if (templateType.lpips_score !== undefined && templateType.lpips_score > LPIPS_THRESHOLD) {
574
+ logger.log(`🛑 Country Mismatch! LPIPS Score too high: ${templateType.lpips_score}`);
575
+ throw new Error(`Le document présenté ne correspond pas au pays sélectionné (${result.docRegion}).`);
576
+ }
577
+
246
578
  logger.log("templateType result", JSON.stringify(truncateFields(templateType), null, 2));
247
579
  return templateType;
248
580
  } catch (e: any) {
249
581
  logger.error('Error checking template type:', JSON.stringify(errorMessage(e), null, 2));
582
+
583
+ if (e?.message === 'CARD_NOT_FULLY_IN_FRAME' || e?.message?.includes('ne correspond pas')) {
584
+ throw e;
585
+ }
586
+
250
587
  throw new Error(e?.message || 'Erreur de vérification du template');
251
588
  }
252
589
  }