@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,21 +1,27 @@
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 { logger } from "../../utils/logger";
4
+ import { countryData } from "../../config/countriesData";
4
5
  export async function frontVerification(result, env = 'PRODUCTION') {
5
6
  try {
6
7
  console.log("Front verification START", JSON.stringify({ result }, null, 2));
7
8
  logger.log("Front verification", JSON.stringify({ result }, null, 2));
8
- // SANDBOX mode: skip AI verification and return mock response
9
+ // SANDBOX mode
9
10
  if (env === 'SANDBOX') {
10
11
  console.log("SANDBOX mode: Skipping AI verification for front document");
11
12
  logger.log("SANDBOX mode: Returning mock front verification response");
12
- const mockBbox = { minX: 50, minY: 50, width: 200, height: 200 };
13
+ const mockBbox = { minX: 400, minY: 800, width: 2200, height: 1400 };
13
14
  const mockResponse = {
14
15
  result: true,
15
16
  detail: [{ confidence: 0.95 }],
16
- card_obb: { x: 50, y: 50, width: 200, height: 200 },
17
+ card_obb: [{
18
+ obb: [[400, 800], [2600, 800], [2600, 2200], [400, 2200]],
19
+ confidence: 0.95,
20
+ card_in_frame: true,
21
+ cropped_sides: []
22
+ }],
17
23
  bbox: mockBbox,
18
- ...(result.regionMapping.authMethod.includes('MRZ') ? {
24
+ ...(result.regionMapping?.authMethod?.includes('MRZ') ? {
19
25
  mrz: {
20
26
  success: true,
21
27
  parsed_data: {
@@ -29,21 +35,50 @@ export async function frontVerification(result, env = 'PRODUCTION') {
29
35
  return mockResponse;
30
36
  }
31
37
  const token = await authentification();
38
+ // Cast the response so TypeScript knows about card_obb
32
39
  const detected = await kycService.detectFaceOnId(result?.path || '', token, result?.selectedDocumentType || '', env);
33
40
  if (!detected.result) {
34
41
  throw new Error('Aucun visage détecté sur la carte. Veuillez reprendre.');
35
42
  }
36
- // If the backend indicates that the card is not fully in frame, stop early
37
- const cardInFrame = getCardInFrame(detected.card_obb);
38
- if (cardInFrame === false) {
39
- // Use a stable error code; the UI maps this to a localized i18n message (cardNotFullyInFrame)
43
+ const cardData = detected.card_obb && Array.isArray(detected.card_obb) && detected.card_obb.length > 0
44
+ ? detected.card_obb[0]
45
+ : null;
46
+ let points = null;
47
+ let isCardInFrame = true;
48
+ let hasCroppedSides = false;
49
+ // Extract data safely
50
+ if (cardData) {
51
+ points = cardData.obb;
52
+ if (typeof cardData.card_in_frame !== 'undefined') {
53
+ isCardInFrame = cardData.card_in_frame === true;
54
+ hasCroppedSides = Array.isArray(cardData.cropped_sides) && cardData.cropped_sides.length > 0;
55
+ }
56
+ }
57
+ // --- 1. STRICT FRAMING CHECK (MUST HAPPEN FIRST) ---
58
+ if (!isCardInFrame || hasCroppedSides) {
59
+ logger.log(`Front Framing failed. Cropped sides detected.`);
40
60
  throw new Error('CARD_NOT_FULLY_IN_FRAME');
41
61
  }
62
+ // --- 2. STRICT DISTANCE CHECK (ONLY RUNS IF FULLY IN FRAME) ---
63
+ if (points && points.length === 4) {
64
+ const getDistance = (p1, p2) => Math.sqrt(Math.pow(p2[0] - p1[0], 2) + Math.pow(p2[1] - p1[1], 2));
65
+ const longestCardEdge = Math.max(getDistance(points[0], points[1]), getDistance(points[1], points[2]));
66
+ const maxX = Math.max(points[0][0], points[1][0], points[2][0], points[3][0]);
67
+ const maxY = Math.max(points[0][1], points[1][1], points[2][1], points[3][1]);
68
+ const estimatedImageScale = Math.max(maxX, maxY);
69
+ const fillPercentage = longestCardEdge / estimatedImageScale;
70
+ logger.log(`Front Card Fill Percentage: ${(fillPercentage * 100).toFixed(1)}%`);
71
+ if (fillPercentage < 0.50) {
72
+ logger.log("🛑 Front Image rejected: Document is too far away.");
73
+ throw new Error("TOO_FAR_AWAY");
74
+ }
75
+ }
76
+ // Check Confidence Threshold
42
77
  const obbConfidence = getObbConfidence(detected.card_obb);
43
78
  if (obbConfidence !== null && obbConfidence < OBB_CONFIDENCE_THRESHOLD) {
44
79
  throw new Error('Carte non entièrement visible. Positionnez toute la carte dans le cadre.');
45
80
  }
46
- // Optional: crop image using card_obb for better MRZ/barcode extraction (only when confidence >= threshold)
81
+ // Optional: crop image using card_obb for better MRZ/barcode extraction
47
82
  let croppedBase64;
48
83
  let bbox;
49
84
  let mrz;
@@ -53,8 +88,9 @@ export async function frontVerification(result, env = 'PRODUCTION') {
53
88
  bbox = crop.bbox;
54
89
  }
55
90
  catch { }
56
- if (result.regionMapping.authMethod.length > 0 && result.regionMapping.authMethod.includes('MRZ')) {
57
- mrz = await kycService.extractMrzText({
91
+ // MRZ Extraction
92
+ if (result.regionMapping?.authMethod?.length > 0 && result.regionMapping.authMethod.includes('MRZ')) {
93
+ const mrzResponse = await kycService.extractMrzText({
58
94
  fileUri: result.path || '',
59
95
  docType: result?.selectedDocumentType || '',
60
96
  docRegion: result?.code || "",
@@ -63,143 +99,311 @@ export async function frontVerification(result, env = 'PRODUCTION') {
63
99
  template_path: result?.templatePath || '',
64
100
  mrz_type: result?.mrzType || ''
65
101
  }, env);
102
+ // Safety check: parse if it was returned as a raw string
103
+ mrz = typeof mrzResponse === 'string' ? JSON.parse(mrzResponse) : mrzResponse;
104
+ if (mrz && (!mrz.success || mrz.parsed_data?.status === 'FAILURE')) {
105
+ logger.log("🛑 Front MRZ Extraction Failed:", mrz.parsed_data?.status_message);
106
+ throw new Error(`MRZ illisible: ${mrz.parsed_data?.status_message || 'Veuillez nettoyer l\'objectif et réessayer'}`);
107
+ }
66
108
  }
67
109
  return { ...detected, croppedBase64, bbox, ...(mrz ? { mrz } : {}) };
68
110
  }
69
111
  catch (e) {
70
- logger.error('Error front verification:', JSON.stringify(errorMessage(e), null, 2));
112
+ logger.error('Error front verification:', e?.message);
113
+ // Do not use LogBox for background silent errors
71
114
  throw new Error(e?.message || 'Erreur de détection du visage');
72
115
  }
73
116
  }
117
+ // export async function frontVerification(
118
+ // result: { path?: string, regionMapping: { authMethod: string[], mrzTypes: string[] }, selectedDocumentType: string, code: string, currentSide: string, templatePath?: string, mrzType?: string },
119
+ // env: KycEnvironment = 'PRODUCTION'
120
+ // ) {
121
+ // try {
122
+ // console.log("Front verification START", JSON.stringify({ result }, null, 2));
123
+ // logger.log("Front verification", JSON.stringify({ result }, null, 2));
124
+ // // SANDBOX mode
125
+ // if (env === 'SANDBOX') {
126
+ // console.log("SANDBOX mode: Skipping AI verification for front document");
127
+ // logger.log("SANDBOX mode: Returning mock front verification response");
128
+ // const mockBbox: IBbox = { minX: 400, minY: 800, width: 2200, height: 1400 };
129
+ // // ==========================================
130
+ // // 🧪 TEST OVERRIDE (SANDBOX) 🧪
131
+ // // Immediately throw the error to test the UI
132
+ // // ==========================================
133
+ // throw new Error('CARD_NOT_FULLY_IN_FRAME');
134
+ // const mockResponse = {
135
+ // result: true,
136
+ // detail: [{ confidence: 0.95 }],
137
+ // card_obb: [{
138
+ // obb: [[400,800], [2600,800], [2600,2200], [400,2200]],
139
+ // confidence: 0.95,
140
+ // card_in_frame: false, // Hardcoded to false
141
+ // cropped_sides: ['left', 'top'] // Hardcoded cropped sides
142
+ // }],
143
+ // bbox: mockBbox,
144
+ // ...(result.regionMapping?.authMethod?.includes('MRZ') ? {
145
+ // mrz: {
146
+ // success: true,
147
+ // parsed_data: {
148
+ // status: 'success',
149
+ // document_type: result.selectedDocumentType,
150
+ // mrz_type: result.mrzType || 'TD1'
151
+ // }
152
+ // }
153
+ // } : {})
154
+ // };
155
+ // return mockResponse;
156
+ // }
157
+ // const token = await authentification();
158
+ // // Cast the response so TypeScript knows about card_obb
159
+ // const detected = await kycService.detectFaceOnId(result?.path || '', token, result?.selectedDocumentType || '', env) as ApiVerificationResponse;
160
+ // // ==========================================
161
+ // // 🧪 TEST OVERRIDE (PRODUCTION) 🧪
162
+ // // Hijack the real API response and force it to look cropped
163
+ // // ==========================================
164
+ // if (detected.card_obb && Array.isArray(detected.card_obb) && detected.card_obb.length > 0) {
165
+ // detected.card_obb[0].card_in_frame = false;
166
+ // detected.card_obb[0].cropped_sides = ['left', 'top'];
167
+ // console.warn("⚠️ FORCING CROPPED ERROR FOR UI TESTING ⚠️");
168
+ // }
169
+ // // ==========================================
170
+ // if (!detected.result) {
171
+ // throw new Error('Aucun visage détecté sur la carte. Veuillez reprendre.');
172
+ // }
173
+ // const cardData = detected.card_obb && Array.isArray(detected.card_obb) && detected.card_obb.length > 0
174
+ // ? detected.card_obb[0]
175
+ // : null;
176
+ // // --- STRICT FRAMING CHECK ---
177
+ // if (cardData && typeof cardData.card_in_frame !== 'undefined') {
178
+ // const isCardInFrame = cardData.card_in_frame === true;
179
+ // const hasCroppedSides = Array.isArray(cardData.cropped_sides) && cardData.cropped_sides.length > 0;
180
+ // // If it is NOT in the frame, OR if any side is cropped, block progression
181
+ // if (!isCardInFrame || hasCroppedSides) {
182
+ // logger.log(`Framing failed. Cropped sides: ${hasCroppedSides ? cardData.cropped_sides?.join(', ') : 'none'}`);
183
+ // throw new Error('CARD_NOT_FULLY_IN_FRAME');
184
+ // }
185
+ // }
186
+ // // Check Confidence Threshold
187
+ // const obbConfidence = getObbConfidence(detected.card_obb);
188
+ // if (obbConfidence !== null && obbConfidence < OBB_CONFIDENCE_THRESHOLD) {
189
+ // throw new Error('Carte non entièrement visible. Positionnez toute la carte dans le cadre.');
190
+ // }
191
+ // // Optional: crop image using card_obb for better MRZ/barcode extraction
192
+ // let croppedBase64: string | undefined;
193
+ // let bbox: IBbox | undefined;
194
+ // let mrz: any | undefined;
195
+ // try {
196
+ // const crop = await cropByObb(result?.path || '', detected.card_obb);
197
+ // croppedBase64 = crop.base64;
198
+ // bbox = crop.bbox;
199
+ // } catch { }
200
+ // // MRZ Extraction
201
+ // if (result.regionMapping?.authMethod?.length > 0 && result.regionMapping.authMethod.includes('MRZ')) {
202
+ // mrz = await kycService.extractMrzText(
203
+ // {
204
+ // fileUri: result.path || '',
205
+ // docType: result?.selectedDocumentType || '',
206
+ // docRegion: result?.code || "",
207
+ // postfix: result?.currentSide,
208
+ // token: token,
209
+ // template_path: result?.templatePath || '',
210
+ // mrz_type: result?.mrzType || ''
211
+ // },
212
+ // env
213
+ // );
214
+ // }
215
+ // return { ...detected, croppedBase64, bbox, ...(mrz ? { mrz } : {}) };
216
+ // } catch (e: any) {
217
+ // logger.error('Error front verification:', JSON.stringify(errorMessage(e), null, 2));
218
+ // throw new Error(e?.message || 'Erreur de détection du visage');
219
+ // }
220
+ // }
74
221
  export async function backVerification(result, env = 'PRODUCTION') {
75
222
  try {
76
223
  if (!result.path)
77
224
  throw new Error('No path provided');
78
225
  logger.log("result.regionMapping", result.regionMapping, result.currentSide, result.code);
79
- // SANDBOX mode: skip AI verification and return mock response
80
226
  if (env === 'SANDBOX') {
81
227
  console.log("SANDBOX mode: Skipping AI verification for back document");
82
- logger.log("SANDBOX mode: Returning mock back verification response");
83
- const mockBbox = { minX: 50, minY: 50, width: 200, height: 200 };
84
- if (result.regionMapping.authMethod.includes('MRZ')) {
228
+ const mockBbox = { minX: 400, minY: 800, width: 2200, height: 1400 };
229
+ const mockCardObb = [{
230
+ obb: [[400, 800], [2600, 800], [2600, 2200], [400, 2200]],
231
+ confidence: 0.95,
232
+ card_in_frame: true,
233
+ cropped_sides: []
234
+ }];
235
+ if (result.regionMapping?.authMethod?.includes('MRZ')) {
85
236
  return {
86
237
  success: true,
87
- parsed_data: {
88
- status: 'success',
89
- document_type: result.selectedDocumentType,
90
- mrz_type: result.mrzType || 'TD1'
238
+ mrz: {
239
+ success: true,
240
+ parsed_data: {
241
+ status: 'SUCCESS',
242
+ document_type: result.selectedDocumentType,
243
+ mrz_type: result.mrzType || 'TD1'
244
+ }
91
245
  },
92
246
  bbox: mockBbox,
93
- card_obb: { x: 50, y: 50, width: 200, height: 200 }
247
+ card_obb: mockCardObb
94
248
  };
95
249
  }
96
- else if (result.regionMapping.authMethod.includes('2D_barcode')) {
250
+ else if (result.regionMapping?.authMethod?.includes('2D_barcode')) {
97
251
  return {
98
- barcode_data: 'SANDBOX_MOCK_BARCODE',
252
+ success: true,
253
+ barcode: { barcode_data: 'SANDBOX_MOCK_BARCODE' },
99
254
  bbox: mockBbox,
100
- card_obb: { x: 50, y: 50, width: 200, height: 200 }
255
+ card_obb: mockCardObb
101
256
  };
102
257
  }
103
- return { bbox: mockBbox };
258
+ return { success: true, bbox: mockBbox, card_obb: mockCardObb };
104
259
  }
105
260
  const token = await authentification();
106
- // Fonction helper pour essayer MRZ puis barcode en fallback
107
- const tryMrzWithBarcodeFallback = async () => {
108
- try {
109
- logger.log("Tentative d'extraction MRZ");
110
- const mrz = await kycService.extractMrzText({
111
- fileUri: result.path,
112
- docType: result?.selectedDocumentType || '',
113
- docRegion: result?.code || '',
114
- postfix: 'back',
115
- token: token,
116
- template_path: result?.templatePath || '',
117
- mrz_type: result?.mrzType || ''
118
- }, env);
119
- const mrzObbConf = getObbConfidence(mrz.card_obb);
120
- if (mrzObbConf !== null && mrzObbConf < OBB_CONFIDENCE_THRESHOLD) {
121
- throw new Error('Carte non entièrement visible. Positionnez toute la carte dans le cadre.');
122
- }
123
- let bbox;
124
- let croppedBase64;
125
- try {
126
- const crop = await cropByObb(result?.path || '', mrz.card_obb);
127
- bbox = crop.bbox;
128
- croppedBase64 = crop.base64;
129
- }
130
- catch { }
131
- return { ...mrz, bbox, croppedBase64 };
261
+ logger.log("1. Checking template and framing for back document...");
262
+ const templateResponse = await kycService.checkTemplateType({
263
+ fileUri: result.path,
264
+ docType: result.selectedDocumentType,
265
+ docRegion: result.code,
266
+ postfix: 'back',
267
+ token: token
268
+ }, env);
269
+ if (templateResponse.success === false || templateResponse.Error) {
270
+ logger.log("Backend returned an error:", templateResponse.Error);
271
+ throw new Error('Impossible de lire le document. Veuillez vous rapprocher et stabiliser la caméra.');
272
+ }
273
+ const cardData = templateResponse?.card_obb && Array.isArray(templateResponse.card_obb) && templateResponse.card_obb.length > 0
274
+ ? templateResponse.card_obb[0]
275
+ : null;
276
+ if (!cardData) {
277
+ throw new Error('CARD_NOT_FULLY_IN_FRAME');
278
+ }
279
+ let points = null;
280
+ let isCardInFrame = false;
281
+ let hasCroppedSides = true;
282
+ // Safely extract coordinates and frame status
283
+ if (cardData.obb) {
284
+ points = cardData.obb;
285
+ if (typeof cardData.card_in_frame !== 'undefined') {
286
+ isCardInFrame = cardData.card_in_frame === true;
287
+ hasCroppedSides = Array.isArray(cardData.cropped_sides) && cardData.cropped_sides.length > 0;
132
288
  }
133
- catch (mrzError) {
134
- logger.log("MRZ échoué, tentative d'extraction barcode");
135
- try {
136
- const barcode = await kycService.extractBarcode({ fileUri: result.path, token: token }, env);
137
- const barcodeObbConf = getObbConfidence(barcode.card_obb);
138
- if (barcodeObbConf !== null && barcodeObbConf < OBB_CONFIDENCE_THRESHOLD) {
139
- throw new Error('Carte non entièrement visible. Positionnez toute la carte dans le cadre.');
140
- }
141
- return barcode;
142
- }
143
- catch (barcodeError) {
144
- throw new Error(`MRZ et barcode ont échoué. MRZ: ${mrzError?.message}, Barcode: ${barcodeError?.message}`);
289
+ }
290
+ else if (Array.isArray(cardData) && Array.isArray(cardData[0])) {
291
+ points = cardData[0];
292
+ isCardInFrame = true;
293
+ hasCroppedSides = false;
294
+ if (points && points.length === 4) {
295
+ const minX = Math.min(...points.map((p) => p[0]));
296
+ const minY = Math.min(...points.map((p) => p[1]));
297
+ if (minX <= 15 || minY <= 15) {
298
+ isCardInFrame = false;
299
+ hasCroppedSides = true;
145
300
  }
146
301
  }
147
- };
148
- if (result.regionMapping.authMethod.length > 2 && (!result?.mrzType || result?.mrzType.length === 0)) {
149
- return await tryMrzWithBarcodeFallback();
150
302
  }
151
- if (result.regionMapping.authMethod.length > 0 && result.regionMapping.authMethod.includes('MRZ') && result?.mrzType && result?.mrzType.length > 0) {
303
+ // --- 2. STRICT FRAMING CHECK (MUST HAPPEN FIRST) ---
304
+ if (!isCardInFrame || hasCroppedSides) {
305
+ logger.log(`Back Framing failed. Coordinates hit image boundary.`);
306
+ throw new Error('CARD_NOT_FULLY_IN_FRAME');
307
+ }
308
+ // --- 3. STRICT DISTANCE CHECK (ONLY RUNS IF FULLY IN FRAME) ---
309
+ if (points && points.length === 4) {
310
+ const getDistance = (p1, p2) => Math.sqrt(Math.pow(p2[0] - p1[0], 2) + Math.pow(p2[1] - p1[1], 2));
311
+ const longestCardEdge = Math.max(getDistance(points[0], points[1]), getDistance(points[1], points[2]));
312
+ const maxX = Math.max(points[0][0], points[1][0], points[2][0], points[3][0]);
313
+ const maxY = Math.max(points[0][1], points[1][1], points[2][1], points[3][1]);
314
+ const estimatedImageScale = Math.max(maxX, maxY);
315
+ const fillPercentage = longestCardEdge / estimatedImageScale;
316
+ logger.log(`Back Card Fill Percentage: ${(fillPercentage * 100).toFixed(1)}%`);
317
+ if (fillPercentage < 0.50) {
318
+ logger.log("🛑 Back Image rejected: Document is too far away.");
319
+ throw new Error("TOO_FAR_AWAY");
320
+ }
321
+ }
322
+ const obbConf = getObbConfidence(templateResponse?.card_obb);
323
+ if (obbConf !== null && obbConf < OBB_CONFIDENCE_THRESHOLD) {
324
+ throw new Error('Carte non entièrement visible. Positionnez toute la carte dans le cadre.');
325
+ }
326
+ const activeTemplatePath = templateResponse.template_path || result.templatePath || '';
327
+ if (activeTemplatePath) {
328
+ const expectedCountryName = countryData[result.code]?.name_en || '';
329
+ const hasCodeMatch = activeTemplatePath.includes(`_${result.code}_`);
330
+ const hasNameMatch = expectedCountryName && activeTemplatePath.toLowerCase().includes(expectedCountryName.toLowerCase());
331
+ if (!hasCodeMatch && !hasNameMatch) {
332
+ logger.log(`Template mismatch! Expected country: ${result.code} (${expectedCountryName}), Detected: ${activeTemplatePath}`);
333
+ throw new Error(`Le document ne correspond pas au pays sélectionné (${result.code}).`);
334
+ }
335
+ }
336
+ logger.log("Framing and Country Template verified successfully. Proceeding to Data Extraction.");
337
+ const hasMrz = result.regionMapping?.authMethod?.includes('MRZ') || !result.regionMapping?.authMethod?.length;
338
+ const hasBarcode = result.regionMapping?.authMethod?.includes('2D_barcode');
339
+ let extractionResult = {};
340
+ // --- 4. Try MRZ First (If required or default) ---
341
+ if (hasMrz) {
152
342
  try {
153
- let mrz;
154
- mrz = await kycService.extractMrzText({
343
+ logger.log("Tentative d'extraction MRZ");
344
+ const mrzResponse = await kycService.extractMrzText({
155
345
  fileUri: result.path,
156
346
  docType: result?.selectedDocumentType || '',
157
347
  docRegion: result?.code || '',
158
348
  postfix: 'back',
159
349
  token: token,
160
- template_path: result?.templatePath || '',
161
- mrz_type: result?.mrzType || ''
350
+ template_path: activeTemplatePath,
351
+ mrz_type: result?.mrzType || '' // Safe to send empty string
162
352
  }, env);
163
- const mrzObbConf = getObbConfidence(mrz.card_obb);
164
- if (mrzObbConf !== null && mrzObbConf < OBB_CONFIDENCE_THRESHOLD) {
165
- throw new Error('Carte non entièrement visible. Positionnez toute la carte dans le cadre.');
353
+ const mrz = typeof mrzResponse === 'string' ? JSON.parse(mrzResponse) : mrzResponse;
354
+ if (!mrz || !mrz.success || mrz.parsed_data?.status === 'FAILURE') {
355
+ throw new Error(mrz?.parsed_data?.status_message || 'Lecture MRZ échouée');
166
356
  }
167
- let bbox;
168
- try {
169
- const crop = await cropByObb(result?.path || '', mrz.card_obb);
170
- bbox = crop.bbox;
171
- }
172
- catch { }
173
- return { ...mrz, bbox };
357
+ extractionResult = { mrz };
174
358
  }
175
359
  catch (mrzError) {
176
- throw new Error(`MRZ et barcode ont échoué. MRZ: ${mrzError?.message}, Barcode: ${mrzError?.message}`);
360
+ if (hasBarcode) {
361
+ logger.log(`MRZ échoué (${mrzError?.message}), tentative d'extraction barcode...`);
362
+ try {
363
+ const barcode = await kycService.extractBarcode({ fileUri: result.path, token: token }, env);
364
+ extractionResult = { barcode };
365
+ }
366
+ catch (barcodeError) {
367
+ throw new Error(`MRZ et Barcode ont échoué. Veuillez stabiliser la carte.`);
368
+ }
369
+ }
370
+ else {
371
+ throw new Error(`Lecture MRZ invalide: ${mrzError?.message}`);
372
+ }
177
373
  }
178
374
  }
179
- if (result.regionMapping.authMethod.length > 0 && result.regionMapping.authMethod.includes('2D_barcode')) {
375
+ // --- 5. Try Barcode Only ---
376
+ else if (hasBarcode) {
180
377
  try {
181
- logger.log("Tentative d'extraction barcode");
378
+ logger.log("Tentative d'extraction barcode seule");
182
379
  const barcode = await kycService.extractBarcode({ fileUri: result.path, token: token }, env);
183
- const barcodeObbConf = getObbConfidence(barcode.card_obb);
184
- if (barcodeObbConf !== null && barcodeObbConf < OBB_CONFIDENCE_THRESHOLD) {
185
- throw new Error('Carte non entièrement visible. Positionnez toute la carte dans le cadre.');
186
- }
187
- let bbox;
188
- try {
189
- const crop = await cropByObb(result?.path || '', barcode.card_obb);
190
- bbox = crop.bbox;
191
- }
192
- catch { }
193
- return { ...barcode, bbox };
380
+ extractionResult = { barcode };
194
381
  }
195
- catch (barcodeError) {
196
- throw new Error(`Barcode et MRZ ont échoué. Barcode: ${barcodeError?.message}, MRZ: ${barcodeError?.message}`);
382
+ catch (e) {
383
+ throw new Error(`Lecture Barcode échouée: ${e?.message}`);
197
384
  }
198
385
  }
199
- return null;
386
+ let bbox;
387
+ let croppedBase64;
388
+ try {
389
+ const crop = await cropByObb(result?.path || '', templateResponse.card_obb);
390
+ bbox = crop.bbox;
391
+ croppedBase64 = crop.base64;
392
+ }
393
+ catch { }
394
+ return {
395
+ success: true,
396
+ ...extractionResult,
397
+ bbox,
398
+ croppedBase64,
399
+ card_obb: templateResponse.card_obb
400
+ };
200
401
  }
201
402
  catch (e) {
202
- throw new Error(e?.message || 'Erreur de détection du MRZ ou barcode');
403
+ if (e?.message === 'CARD_NOT_FULLY_IN_FRAME' || e?.message === 'TOO_FAR_AWAY') {
404
+ throw e;
405
+ }
406
+ throw new Error(e?.message || 'Erreur de détection des données');
203
407
  }
204
408
  }
205
409
  /**
@@ -209,22 +413,74 @@ export async function backVerification(result, env = 'PRODUCTION') {
209
413
  */
210
414
  export async function checkTemplateType(result, env = 'PRODUCTION') {
211
415
  try {
212
- // SANDBOX mode: skip AI verification and return mock response
213
416
  if (env === 'SANDBOX') {
214
- console.log("SANDBOX mode: Skipping AI template type check");
215
- logger.log("SANDBOX mode: Returning mock template type response");
216
417
  return {
217
418
  template_path: `templates/${result.docType}_${result.docRegion}_${result.postfix}.jpg`,
218
- card_obb: { x: 50, y: 50, width: 200, height: 200 }
419
+ card_obb: [{
420
+ obb: [[400, 800], [2600, 800], [2600, 2200], [400, 2200]],
421
+ confidence: 0.95,
422
+ card_in_frame: true,
423
+ cropped_sides: []
424
+ }]
219
425
  };
220
426
  }
221
427
  const token = await authentification();
222
428
  const templateType = await kycService.checkTemplateType({ fileUri: result.path || '', docType: result?.docType, docRegion: result?.docRegion || "", postfix: result?.postfix, token: token }, env);
429
+ if (templateType.success === false || templateType.Error) {
430
+ logger.log("Backend returned an error:", templateType.Error);
431
+ throw new Error('Impossible de lire le document. Veuillez vous rapprocher et stabiliser la caméra.');
432
+ }
433
+ const cardData = templateType.card_obb && Array.isArray(templateType.card_obb) && templateType.card_obb.length > 0
434
+ ? templateType.card_obb[0]
435
+ : null;
436
+ if (!cardData) {
437
+ throw new Error('CARD_NOT_FULLY_IN_FRAME');
438
+ }
439
+ // Default to FALSE so it must prove it is in the frame
440
+ let points = null;
441
+ let isCardInFrame = false;
442
+ let hasCroppedSides = true;
443
+ // Extract data safely based on API response format
444
+ if (cardData.obb) {
445
+ points = cardData.obb;
446
+ if (typeof cardData.card_in_frame !== 'undefined') {
447
+ isCardInFrame = cardData.card_in_frame === true;
448
+ hasCroppedSides = Array.isArray(cardData.cropped_sides) && cardData.cropped_sides.length > 0;
449
+ }
450
+ }
451
+ else if (Array.isArray(cardData) && Array.isArray(cardData[0])) {
452
+ points = cardData[0];
453
+ isCardInFrame = true; // Passed initial existence check
454
+ hasCroppedSides = false;
455
+ // MATH FALLBACK: Check if the coordinates are touching the absolute edge of the photo.
456
+ if (points && points.length === 4) {
457
+ const minX = Math.min(...points.map((p) => p[0]));
458
+ const minY = Math.min(...points.map((p) => p[1]));
459
+ // If any corner is within 15 pixels of the edge, it is physically cut off!
460
+ if (minX <= 15 || minY <= 15) {
461
+ isCardInFrame = false;
462
+ hasCroppedSides = true;
463
+ }
464
+ }
465
+ }
466
+ // --- 3. STRICT FRAMING CHECK (MUST HAPPEN FIRST) ---
467
+ if (!isCardInFrame || hasCroppedSides) {
468
+ logger.log(`Template Framing failed. Coordinates hit image boundary.`);
469
+ throw new Error('CARD_NOT_FULLY_IN_FRAME');
470
+ }
471
+ const LPIPS_THRESHOLD = 0.75;
472
+ if (templateType.lpips_score !== undefined && templateType.lpips_score > LPIPS_THRESHOLD) {
473
+ logger.log(`🛑 Country Mismatch! LPIPS Score too high: ${templateType.lpips_score}`);
474
+ throw new Error(`Le document présenté ne correspond pas au pays sélectionné (${result.docRegion}).`);
475
+ }
223
476
  logger.log("templateType result", JSON.stringify(truncateFields(templateType), null, 2));
224
477
  return templateType;
225
478
  }
226
479
  catch (e) {
227
480
  logger.error('Error checking template type:', JSON.stringify(errorMessage(e), null, 2));
481
+ if (e?.message === 'CARD_NOT_FULLY_IN_FRAME' || e?.message?.includes('ne correspond pas')) {
482
+ throw e;
483
+ }
228
484
  throw new Error(e?.message || 'Erreur de vérification du template');
229
485
  }
230
486
  }