@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,21 +1,27 @@
|
|
|
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 { 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
|
|
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:
|
|
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: {
|
|
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
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
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
|
-
|
|
57
|
-
|
|
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:',
|
|
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
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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:
|
|
247
|
+
card_obb: mockCardObb
|
|
94
248
|
};
|
|
95
249
|
}
|
|
96
|
-
else if (result.regionMapping
|
|
250
|
+
else if (result.regionMapping?.authMethod?.includes('2D_barcode')) {
|
|
97
251
|
return {
|
|
98
|
-
|
|
252
|
+
success: true,
|
|
253
|
+
barcode: { barcode_data: 'SANDBOX_MOCK_BARCODE' },
|
|
99
254
|
bbox: mockBbox,
|
|
100
|
-
card_obb:
|
|
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
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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
|
-
|
|
154
|
-
|
|
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:
|
|
161
|
-
mrz_type: result?.mrzType || ''
|
|
350
|
+
template_path: activeTemplatePath,
|
|
351
|
+
mrz_type: result?.mrzType || '' // Safe to send empty string
|
|
162
352
|
}, env);
|
|
163
|
-
const
|
|
164
|
-
if (
|
|
165
|
-
throw new Error(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
196
|
-
throw new Error(`
|
|
382
|
+
catch (e) {
|
|
383
|
+
throw new Error(`Lecture Barcode échouée: ${e?.message}`);
|
|
197
384
|
}
|
|
198
385
|
}
|
|
199
|
-
|
|
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
|
-
|
|
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: {
|
|
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
|
}
|