@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.
- package/build/components/EnhancedCameraView.js +1 -1
- package/build/components/EnhancedCameraView.js.map +1 -1
- package/build/components/KYCElements/CountrySelectionTemplate.d.ts.map +1 -1
- package/build/components/KYCElements/CountrySelectionTemplate.js +13 -42
- package/build/components/KYCElements/CountrySelectionTemplate.js.map +1 -1
- package/build/components/KYCElements/IDCardCapture.d.ts.map +1 -1
- package/build/components/KYCElements/IDCardCapture.js +113 -49
- package/build/components/KYCElements/IDCardCapture.js.map +1 -1
- package/build/components/KYCElements/SelfieCaptureTemplate.js +2 -2
- package/build/components/KYCElements/SelfieCaptureTemplate.js.map +1 -1
- package/build/hooks/useTemplateKYCFlow.js +1 -1
- package/build/hooks/useTemplateKYCFlow.js.map +1 -1
- package/build/modules/api/CardAuthentification.d.ts +15 -7
- package/build/modules/api/CardAuthentification.d.ts.map +1 -1
- package/build/modules/api/CardAuthentification.js +360 -104
- package/build/modules/api/CardAuthentification.js.map +1 -1
- package/build/modules/api/KYCService.d.ts +3 -1
- package/build/modules/api/KYCService.d.ts.map +1 -1
- package/build/modules/api/KYCService.js +25 -24
- package/build/modules/api/KYCService.js.map +1 -1
- package/build/modules/camera/VisionCameraModule.js +2 -2
- package/build/modules/camera/VisionCameraModule.js.map +1 -1
- package/build/utils/cropByObb.d.ts +8 -0
- package/build/utils/cropByObb.d.ts.map +1 -1
- package/build/utils/cropByObb.js +20 -3
- package/build/utils/cropByObb.js.map +1 -1
- package/build/utils/pathToBase64.js +1 -1
- package/build/utils/pathToBase64.js.map +1 -1
- package/package.json +1 -1
- package/src/components/EnhancedCameraView.tsx +1 -1
- package/src/components/KYCElements/CountrySelectionTemplate.tsx +24 -52
- package/src/components/KYCElements/IDCardCapture.tsx +179 -109
- package/src/components/KYCElements/SelfieCaptureTemplate.tsx +2 -2
- package/src/hooks/useTemplateKYCFlow.tsx +1 -1
- package/src/modules/api/CardAuthentification.ts +450 -113
- package/src/modules/api/KYCService.ts +52 -39
- package/src/modules/camera/VisionCameraModule.ts +2 -2
- package/src/utils/cropByObb.ts +22 -3
- package/src/utils/pathToBase64.ts +1 -1
|
@@ -1,25 +1,50 @@
|
|
|
1
1
|
import kycService, { authentification, errorMessage, truncateFields } from "./KYCService";
|
|
2
|
-
import { 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
|
-
|
|
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
|
|
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:
|
|
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: {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
|
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 || '',
|
|
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
|
-
|
|
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:',
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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:
|
|
314
|
+
card_obb: mockCardObb
|
|
110
315
|
};
|
|
111
|
-
} else if (result.regionMapping
|
|
316
|
+
} else if (result.regionMapping?.authMethod?.includes('2D_barcode')) {
|
|
112
317
|
return {
|
|
113
|
-
|
|
318
|
+
success: true,
|
|
319
|
+
barcode: { barcode_data: 'SANDBOX_MOCK_BARCODE' },
|
|
114
320
|
bbox: mockBbox,
|
|
115
|
-
card_obb:
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
croppedBase64 = crop.base64;
|
|
352
|
+
let points = null;
|
|
353
|
+
let isCardInFrame = false;
|
|
354
|
+
let hasCroppedSides = true;
|
|
150
355
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
171
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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
|
-
|
|
178
|
-
|
|
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:
|
|
185
|
-
mrz_type: result?.mrzType || ''
|
|
437
|
+
template_path: activeTemplatePath,
|
|
438
|
+
mrz_type: result?.mrzType || '' // Safe to send empty string
|
|
186
439
|
}, env);
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
192
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: {
|
|
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
|
}
|