@sanctum-key/react-native-sdk 1.0.20 → 1.0.22
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/package.json +1 -1
- package/build/src/components/KYCElements/IDCardCapture.d.ts.map +1 -1
- package/build/src/components/KYCElements/IDCardCapture.js +19 -17
- package/build/src/components/KYCElements/IDCardCapture.js.map +1 -1
- package/build/src/components/KYCElements/VerificationProgressTemplate.d.ts.map +1 -1
- package/build/src/components/KYCElements/VerificationProgressTemplate.js +30 -13
- package/build/src/components/KYCElements/VerificationProgressTemplate.js.map +1 -1
- package/build/src/config/region_mapping.d.ts.map +1 -1
- package/build/src/config/region_mapping.js.map +1 -1
- package/build/src/modules/api/CardAuthentification.d.ts +4 -0
- package/build/src/modules/api/CardAuthentification.d.ts.map +1 -1
- package/build/src/modules/api/CardAuthentification.js +58 -32
- package/build/src/modules/api/CardAuthentification.js.map +1 -1
- package/build/src/modules/api/KYCService.d.ts +1 -0
- package/build/src/modules/api/KYCService.d.ts.map +1 -1
- package/build/src/modules/api/KYCService.js +10 -4
- package/build/src/modules/api/KYCService.js.map +1 -1
- package/package.json +1 -1
- package/src/components/KYCElements/IDCardCapture.tsx +28 -20
- package/src/components/KYCElements/VerificationProgressTemplate.tsx +35 -14
- package/src/config/region_mapping.ts +2 -1
- package/src/modules/api/CardAuthentification.ts +70 -44
- package/src/modules/api/KYCService.ts +25 -6
|
@@ -141,19 +141,24 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
141
141
|
const getCurrentSideVerification = (currentSide: string, countryKey: string) => {
|
|
142
142
|
const rawDocType = countrySelectionData?.documentType;
|
|
143
143
|
if (!rawDocType || !countryKey) { return { authMethod: [], mrzTypes: [], regionMapping: null, key: 'root' }; }
|
|
144
|
+
|
|
144
145
|
const rawCountryName = ISO_TO_COUNTRY_NAME[countryData?.code || ''] || countryData?.code || countryKey;
|
|
145
146
|
const baseMapping = (REGION_MAPPING as any).regionMapping || REGION_MAPPING;
|
|
147
|
+
|
|
146
148
|
let countryMapping = baseMapping[rawCountryName];
|
|
147
149
|
if (!countryMapping) {
|
|
148
150
|
const foundKey = Object.keys(baseMapping).find(k => k.toLowerCase() === rawCountryName.toLowerCase() || k.toLowerCase() === countryKey.toLowerCase());
|
|
149
151
|
if (foundKey) countryMapping = baseMapping[foundKey];
|
|
150
152
|
}
|
|
151
153
|
if (!countryMapping) { return { authMethod: [], mrzTypes: [], regionMapping: null, key: 'root' }; }
|
|
154
|
+
|
|
152
155
|
const regionMapping = countryMapping[rawDocType];
|
|
153
156
|
if (!regionMapping) { return { authMethod: [], mrzTypes: [], regionMapping: null, key: 'root' }; }
|
|
157
|
+
|
|
154
158
|
const authMethod: string[] = [];
|
|
155
159
|
const mrzTypes: string[] = [];
|
|
156
160
|
const key = countrySelectionData.region?.trim()?.length > 0 ? countrySelectionData.region.trim() : 'root';
|
|
161
|
+
|
|
157
162
|
if (regionMapping?.[key] && Array.isArray(regionMapping[key])) {
|
|
158
163
|
regionMapping[key].forEach((item: any) => {
|
|
159
164
|
if (item[currentSide]) {
|
|
@@ -163,25 +168,25 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
163
168
|
});
|
|
164
169
|
}
|
|
165
170
|
return { authMethod: removeDuplicates(authMethod), mrzTypes: removeDuplicates(mrzTypes), regionMapping, key };
|
|
166
|
-
}
|
|
171
|
+
};
|
|
167
172
|
|
|
168
|
-
const getCorrespondingMrzType = (templatePath: string, mapping: any,
|
|
169
|
-
if (!mapping || !mapping[
|
|
170
|
-
const fileName = templatePath.split("/").pop()?.replace(
|
|
173
|
+
const getCorrespondingMrzType = (templatePath: string, mapping: any, regionKey: string = "root") => {
|
|
174
|
+
if (!mapping || !mapping[regionKey]) return null;
|
|
175
|
+
const fileName = templatePath.split("/").pop()?.replace(/\.(jpg|jpeg|png|py)$/i, "");
|
|
171
176
|
if (!fileName) return null;
|
|
172
177
|
const pyName = `${fileName}.py`;
|
|
173
|
-
const found = mapping[
|
|
178
|
+
const found = mapping[regionKey].find((item: any) => item.py_file === pyName);
|
|
174
179
|
return found?.mrz_type || null;
|
|
175
|
-
}
|
|
180
|
+
};
|
|
176
181
|
|
|
177
|
-
const getCorrespondingAuthMethod = (templatePath: string, mapping: any,
|
|
178
|
-
if (!mapping || !mapping[
|
|
179
|
-
const fileName = templatePath.split("/").pop()?.replace(
|
|
182
|
+
const getCorrespondingAuthMethod = (templatePath: string, mapping: any, regionKey: string = "root", side: 'front' | 'back') => {
|
|
183
|
+
if (!mapping || !mapping[regionKey]) return null;
|
|
184
|
+
const fileName = templatePath.split("/").pop()?.replace(/\.(jpg|jpeg|png|py)$/i, "");
|
|
180
185
|
if (!fileName) return null;
|
|
181
186
|
const pyName = `${fileName}.py`;
|
|
182
|
-
const found = mapping[
|
|
187
|
+
const found = mapping[regionKey].find((item: any) => item.py_file === pyName);
|
|
183
188
|
return found?.[side] || null;
|
|
184
|
-
}
|
|
189
|
+
};
|
|
185
190
|
|
|
186
191
|
const autoCapture = async (capturePath: string, verified: ISilentCaptureResult & { region?: string }) => {
|
|
187
192
|
if (isProcessingCapture) return;
|
|
@@ -214,7 +219,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
214
219
|
...newImages,
|
|
215
220
|
country: verified.country,
|
|
216
221
|
documentType: GovernmentDocumentTypeBackend[verified.documentType as keyof typeof GovernmentDocumentTypeBackend] || '',
|
|
217
|
-
...(verified.region ? { region: verified.region } : {})
|
|
222
|
+
...(verified.region ? { region: verified.region } : {})
|
|
218
223
|
});
|
|
219
224
|
}
|
|
220
225
|
|
|
@@ -287,7 +292,10 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
287
292
|
|
|
288
293
|
verificationRes = await frontVerification({
|
|
289
294
|
path: result.path,
|
|
290
|
-
regionMapping: {
|
|
295
|
+
regionMapping: {
|
|
296
|
+
authMethod: matchedAuthMethod ? [matchedAuthMethod] : [],
|
|
297
|
+
mrzTypes: mrzType ? [mrzType] : []
|
|
298
|
+
},
|
|
291
299
|
selectedDocumentType: GovernmentDocumentTypeShorted[selectedDocumentType?.type as keyof typeof GovernmentDocumentTypeShorted] || '',
|
|
292
300
|
code: countryData?.code || '',
|
|
293
301
|
currentSide,
|
|
@@ -296,16 +304,16 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
296
304
|
issuingSubdivision: issuingSubdivision
|
|
297
305
|
}, env);
|
|
298
306
|
} else {
|
|
299
|
-
|
|
300
|
-
if (!matchedBackAuthMethod && currentSide === 'back') { matchedBackAuthMethod = 'MRZ'; }
|
|
307
|
+
const matchedBackAuthMethod = getCorrespondingAuthMethod(templatePath, regionMappings.regionMapping, regionMappings.key || '', 'back');
|
|
301
308
|
const backMrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || '';
|
|
302
|
-
|
|
303
|
-
// 🚨 EXTRACT FRONT TEMPLATE TO VALIDATE AGAINST BACK TEMPLATE
|
|
304
309
|
const frontTemplatePath = capturedImages['front']?.templatePath || '';
|
|
305
310
|
|
|
306
311
|
verificationRes = await backVerification({
|
|
307
312
|
path: result.path,
|
|
308
|
-
regionMapping: {
|
|
313
|
+
regionMapping: {
|
|
314
|
+
authMethod: matchedBackAuthMethod ? [matchedBackAuthMethod] : [],
|
|
315
|
+
mrzTypes: backMrzType ? [backMrzType] : []
|
|
316
|
+
},
|
|
309
317
|
selectedDocumentType: GovernmentDocumentTypeShorted[selectedDocumentType.type as keyof typeof GovernmentDocumentTypeShorted] || '',
|
|
310
318
|
code: countryData?.code || '',
|
|
311
319
|
currentSide,
|
|
@@ -313,7 +321,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
313
321
|
mrzType: backMrzType,
|
|
314
322
|
templateResponse,
|
|
315
323
|
issuingSubdivision: issuingSubdivision,
|
|
316
|
-
frontTemplatePath: frontTemplatePath
|
|
324
|
+
frontTemplatePath: frontTemplatePath
|
|
317
325
|
}, env);
|
|
318
326
|
}
|
|
319
327
|
|
|
@@ -348,7 +356,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
348
356
|
userFriendlyMessage = 'The document does not match your selected country.';
|
|
349
357
|
} else if (rawMessage.includes('card_not_fully_in_frame')) {
|
|
350
358
|
userFriendlyMessage = 'Move closer to the document and ensure all corners are visible.';
|
|
351
|
-
} else if (rawMessage.includes('ne correspond pas au recto')) {
|
|
359
|
+
} else if (rawMessage.includes('ne correspond pas au recto')) {
|
|
352
360
|
userFriendlyMessage = 'The back of the document does not match the front. You must scan the same ID.';
|
|
353
361
|
}
|
|
354
362
|
|
|
@@ -25,7 +25,6 @@ export const VerificationProgressTemplate: React.FC<VerificationProgressTemplate
|
|
|
25
25
|
const isSuccess = verification.status === 'success';
|
|
26
26
|
const isFailed = verification.status === 'failed';
|
|
27
27
|
|
|
28
|
-
// Fonction pour récupérer le résultat de vérification avec retry
|
|
29
28
|
const getVerificationResult = async (currentRetryCount = 0, maxRetries = 40) => {
|
|
30
29
|
try {
|
|
31
30
|
logger.log(`Starting verification check (attempt ${currentRetryCount + 1}/${maxRetries + 1})`);
|
|
@@ -42,20 +41,23 @@ export const VerificationProgressTemplate: React.FC<VerificationProgressTemplate
|
|
|
42
41
|
} as any
|
|
43
42
|
});
|
|
44
43
|
setIsRetrying(false);
|
|
45
|
-
return;
|
|
44
|
+
return;
|
|
46
45
|
}
|
|
47
46
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
let verificationResult = result?.data;
|
|
48
|
+
|
|
49
|
+
if (verificationResult && !verificationResult.verification_status && verificationResult[state.session.session_id]) {
|
|
50
|
+
const nestedData = verificationResult[state.session.session_id]?.data;
|
|
51
|
+
verificationResult = typeof nestedData === 'string' ? JSON.parse(nestedData) : nestedData;
|
|
52
|
+
}
|
|
51
53
|
|
|
52
54
|
if (!verificationResult) {
|
|
53
|
-
// If we still didn't get data and the status wasn't failed, throw to trigger a retry
|
|
54
55
|
throw new Error('Verification payload missing for current session');
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
const currentStatus = verificationResult.verification_status;
|
|
59
|
+
|
|
60
|
+
if (currentStatus === null || currentStatus === "processing" || currentStatus === "pending") {
|
|
59
61
|
if (currentRetryCount < maxRetries) {
|
|
60
62
|
const nextRetryCount = currentRetryCount + 1;
|
|
61
63
|
setRetryCount(nextRetryCount);
|
|
@@ -69,7 +71,7 @@ export const VerificationProgressTemplate: React.FC<VerificationProgressTemplate
|
|
|
69
71
|
}, 10000);
|
|
70
72
|
|
|
71
73
|
setTimeoutId(newTimeoutId as any);
|
|
72
|
-
return;
|
|
74
|
+
return;
|
|
73
75
|
} else {
|
|
74
76
|
logger.error('Max retries reached, verification still processing');
|
|
75
77
|
setIsRetrying(false);
|
|
@@ -77,13 +79,15 @@ export const VerificationProgressTemplate: React.FC<VerificationProgressTemplate
|
|
|
77
79
|
}
|
|
78
80
|
}
|
|
79
81
|
|
|
80
|
-
|
|
81
|
-
if (verificationResult.verification_status === "completed" || verificationResult.verification_status === "approved") {
|
|
82
|
+
if (currentStatus === "completed" || currentStatus === "approved") {
|
|
82
83
|
actions.setVerificationState({
|
|
83
84
|
status: 'success',
|
|
84
|
-
result:
|
|
85
|
+
result: {
|
|
86
|
+
...result,
|
|
87
|
+
referenceId: verificationResult.verification_id || verificationResult.session_id
|
|
88
|
+
} as any
|
|
85
89
|
});
|
|
86
|
-
} else if (
|
|
90
|
+
} else if (currentStatus === "failed" || currentStatus === "rejected") {
|
|
87
91
|
actions.setVerificationState({
|
|
88
92
|
status: 'failed',
|
|
89
93
|
result: {
|
|
@@ -130,6 +134,24 @@ export const VerificationProgressTemplate: React.FC<VerificationProgressTemplate
|
|
|
130
134
|
};
|
|
131
135
|
}, [timeoutId]);
|
|
132
136
|
|
|
137
|
+
// 🚨 THE FIX: Automatically trigger the webhook/completion flow on success
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
let autoSubmitTimer: ReturnType<typeof setTimeout>;
|
|
140
|
+
|
|
141
|
+
if (isSuccess) {
|
|
142
|
+
logger.log("Verification successful. Initiating auto-submit countdown...");
|
|
143
|
+
// Wait 3 seconds so the user can see the "Complete" screen, then automatically push the data
|
|
144
|
+
autoSubmitTimer = setTimeout(() => {
|
|
145
|
+
actions.submitVerification();
|
|
146
|
+
}, 3000);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Cleanup the timer if the user clicks the button manually before 3 seconds are up
|
|
150
|
+
return () => {
|
|
151
|
+
if (autoSubmitTimer) clearTimeout(autoSubmitTimer);
|
|
152
|
+
};
|
|
153
|
+
}, [isSuccess, actions]);
|
|
154
|
+
|
|
133
155
|
if (isSuccess) {
|
|
134
156
|
return (
|
|
135
157
|
<View style={styles.container}>
|
|
@@ -157,7 +179,6 @@ export const VerificationProgressTemplate: React.FC<VerificationProgressTemplate
|
|
|
157
179
|
))}
|
|
158
180
|
</View>
|
|
159
181
|
|
|
160
|
-
{/* 🚨 FIX 2: Give the user actionable options to End or Restart */}
|
|
161
182
|
<TouchableOpacity style={[styles.button, styles.primary]} onPress={() => actions.submitVerification()}>
|
|
162
183
|
<Text style={styles.buttonText}>
|
|
163
184
|
{state.currentLanguage === 'en' ? 'OK / Finish' : 'Terminer'}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ICountryMapping } from "../types/KYC.types";
|
|
2
2
|
|
|
3
|
-
export const countryMapping: Record<string, Record<string, Record<string, ICountryMapping[]>>> =
|
|
3
|
+
export const countryMapping: Record<string, Record<string, Record<string, ICountryMapping[]>>> =
|
|
4
|
+
{
|
|
4
5
|
"BE": {
|
|
5
6
|
"passport": {
|
|
6
7
|
"root": [
|
|
@@ -31,7 +31,7 @@ export async function frontVerification(
|
|
|
31
31
|
currentSide: string,
|
|
32
32
|
templatePath?: string,
|
|
33
33
|
mrzType?: string,
|
|
34
|
-
issuingSubdivision?: string
|
|
34
|
+
issuingSubdivision?: string
|
|
35
35
|
},
|
|
36
36
|
env: KycEnvironment = 'PRODUCTION'
|
|
37
37
|
) {
|
|
@@ -42,7 +42,9 @@ export async function frontVerification(
|
|
|
42
42
|
const authMethods = Array.isArray(result.regionMapping?.authMethod)
|
|
43
43
|
? result.regionMapping.authMethod.map(m => String(m).toUpperCase())
|
|
44
44
|
: [];
|
|
45
|
+
|
|
45
46
|
const hasMrz = authMethods.some(m => m.includes('MRZ'));
|
|
47
|
+
const hasBarcode = authMethods.some(m => m.includes('BARCODE') || m.includes('2D'));
|
|
46
48
|
|
|
47
49
|
if (env === 'SANDBOX') {
|
|
48
50
|
console.log("SANDBOX mode: Skipping AI verification for front document");
|
|
@@ -60,12 +62,11 @@ export async function frontVerification(
|
|
|
60
62
|
...(hasMrz ? {
|
|
61
63
|
mrz: {
|
|
62
64
|
success: true,
|
|
63
|
-
parsed_data: {
|
|
64
|
-
status: 'SUCCESS',
|
|
65
|
-
document_type: result.selectedDocumentType,
|
|
66
|
-
mrz_type: result.mrzType || 'TD1'
|
|
67
|
-
}
|
|
65
|
+
parsed_data: { status: 'SUCCESS', document_type: result.selectedDocumentType, mrz_type: result.mrzType || 'TD1' }
|
|
68
66
|
}
|
|
67
|
+
} : {}),
|
|
68
|
+
...(hasBarcode ? {
|
|
69
|
+
barcode: { barcode_data: 'SANDBOX_MOCK_BARCODE' }
|
|
69
70
|
} : {})
|
|
70
71
|
};
|
|
71
72
|
return mockResponse;
|
|
@@ -110,6 +111,7 @@ export async function frontVerification(
|
|
|
110
111
|
let croppedBase64: string | undefined;
|
|
111
112
|
let bbox: IBbox | undefined;
|
|
112
113
|
let mrz: any | undefined;
|
|
114
|
+
let barcode: any | undefined;
|
|
113
115
|
let documentData: any | undefined;
|
|
114
116
|
|
|
115
117
|
try {
|
|
@@ -126,7 +128,7 @@ export async function frontVerification(
|
|
|
126
128
|
docType: result.selectedDocumentType,
|
|
127
129
|
docRegion: result.code,
|
|
128
130
|
templatePath: result.templatePath || '',
|
|
129
|
-
issuingSubdivision: result.issuingSubdivision,
|
|
131
|
+
issuingSubdivision: result.issuingSubdivision,
|
|
130
132
|
token: token,
|
|
131
133
|
postfix: result.currentSide || 'front'
|
|
132
134
|
});
|
|
@@ -162,9 +164,28 @@ export async function frontVerification(
|
|
|
162
164
|
throw new Error(err?.message || "Erreur lors de l'extraction MRZ. Veuillez reprendre la photo.");
|
|
163
165
|
}
|
|
164
166
|
}
|
|
167
|
+
|
|
168
|
+
if (hasBarcode) {
|
|
169
|
+
try {
|
|
170
|
+
logger.log("Tentative d'extraction barcode (Front)");
|
|
171
|
+
barcode = await kycService.extractBarcode({ fileUri: result.path || '', token: token }, env);
|
|
172
|
+
} catch (e: any) {
|
|
173
|
+
logger.log("Front Barcode Extraction Error:", e?.message);
|
|
174
|
+
// If it exclusively has a barcode (no MRZ) and barcode fails, throw.
|
|
175
|
+
if (!mrz) {
|
|
176
|
+
throw new Error(`Lecture Barcode échouée: ${e?.message || 'Veuillez réessayer'}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
165
180
|
|
|
166
|
-
|
|
167
|
-
|
|
181
|
+
return {
|
|
182
|
+
...detected,
|
|
183
|
+
croppedBase64,
|
|
184
|
+
bbox,
|
|
185
|
+
...(mrz ? { mrz } : {}),
|
|
186
|
+
...(barcode ? { barcode } : {}),
|
|
187
|
+
...(documentData ? { documentData } : {})
|
|
188
|
+
};
|
|
168
189
|
|
|
169
190
|
} catch (e: any) {
|
|
170
191
|
logger.log('Error front verification:', e?.message);
|
|
@@ -182,8 +203,8 @@ export async function backVerification(
|
|
|
182
203
|
templatePath?: string,
|
|
183
204
|
mrzType?: string,
|
|
184
205
|
templateResponse?: ApiVerificationResponse,
|
|
185
|
-
issuingSubdivision?: string,
|
|
186
|
-
frontTemplatePath?: string
|
|
206
|
+
issuingSubdivision?: string,
|
|
207
|
+
frontTemplatePath?: string
|
|
187
208
|
},
|
|
188
209
|
env: KycEnvironment = 'PRODUCTION'
|
|
189
210
|
) {
|
|
@@ -213,11 +234,7 @@ export async function backVerification(
|
|
|
213
234
|
success: true,
|
|
214
235
|
mrz: {
|
|
215
236
|
success: true,
|
|
216
|
-
parsed_data: {
|
|
217
|
-
status: 'SUCCESS',
|
|
218
|
-
document_type: result.selectedDocumentType,
|
|
219
|
-
mrz_type: result.mrzType || 'TD1'
|
|
220
|
-
}
|
|
237
|
+
parsed_data: { status: 'SUCCESS', document_type: result.selectedDocumentType, mrz_type: result.mrzType || 'TD1' }
|
|
221
238
|
},
|
|
222
239
|
bbox: mockBbox,
|
|
223
240
|
card_obb: mockCardObb
|
|
@@ -295,29 +312,34 @@ export async function backVerification(
|
|
|
295
312
|
}
|
|
296
313
|
}
|
|
297
314
|
|
|
298
|
-
// 🚨 STRICT FRONT VS BACK TEMPLATE VALIDATION 🚨
|
|
299
315
|
if (result.frontTemplatePath && activeTemplatePath) {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
if (frontPy && backPy) {
|
|
304
|
-
logger.log(`Validating Front (${frontPy}) vs Back (${backPy}) for region ${result.code}`);
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
const matchValidation = await kycService.validateBackTemplate({
|
|
308
|
-
region: result.code,
|
|
309
|
-
docType: result.selectedDocumentType,
|
|
310
|
-
frontTemplateName: frontPy,
|
|
311
|
-
backTemplateName: backPy,
|
|
312
|
-
subRegion: result.issuingSubdivision || "",
|
|
313
|
-
token: token
|
|
314
|
-
}, env);
|
|
316
|
+
try {
|
|
317
|
+
const frontPy = result.frontTemplatePath.split("/").pop()?.replace(/\.(jpg|png)$/i, ".py");
|
|
318
|
+
const backPy = activeTemplatePath.split("/").pop()?.replace(/\.(jpg|png)$/i, ".py");
|
|
315
319
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
+
if (frontPy && backPy) {
|
|
321
|
+
logger.log(`Validating Front (${frontPy}) vs Back (${backPy}) for region ${result.code}`);
|
|
322
|
+
|
|
323
|
+
const isCanadianSubRegionDoc = result.code === 'CA' && ['hic'].includes(result.selectedDocumentType);
|
|
324
|
+
const safeSubRegion = isCanadianSubRegionDoc ? (result.issuingSubdivision || "") : "";
|
|
325
|
+
|
|
326
|
+
const matchValidation = await kycService.validateBackTemplate({
|
|
327
|
+
region: result.code,
|
|
328
|
+
docType: result.selectedDocumentType,
|
|
329
|
+
frontTemplateName: frontPy,
|
|
330
|
+
backTemplateName: backPy,
|
|
331
|
+
subRegion: safeSubRegion,
|
|
332
|
+
token: token
|
|
333
|
+
}, env);
|
|
334
|
+
|
|
335
|
+
if (!matchValidation || matchValidation.is_match === false) {
|
|
336
|
+
logger.log(`Front/Back Mismatch: ${matchValidation?.status_message}`);
|
|
337
|
+
throw new Error(`Le verso ne correspond pas au recto fourni. Vous avez scanné un document différent.`);
|
|
338
|
+
}
|
|
320
339
|
}
|
|
340
|
+
} catch (err: any) {
|
|
341
|
+
if (err?.message?.includes('ne correspond pas')) throw err;
|
|
342
|
+
logger.log("Template matching validation skipped/failed internally:", err?.message);
|
|
321
343
|
}
|
|
322
344
|
}
|
|
323
345
|
|
|
@@ -369,10 +391,11 @@ export async function backVerification(
|
|
|
369
391
|
|
|
370
392
|
if (hasBarcode) {
|
|
371
393
|
try {
|
|
372
|
-
logger.log("Tentative d'extraction barcode");
|
|
394
|
+
logger.log("Tentative d'extraction barcode (Back)");
|
|
373
395
|
const barcode = await kycService.extractBarcode({ fileUri: result.path!, token: token }, env);
|
|
374
396
|
extractionResult.barcode = barcode;
|
|
375
397
|
} catch (e: any) {
|
|
398
|
+
// If it explicitly expects a barcode (and not an MRZ) and fails, throw to user
|
|
376
399
|
if (!extractionResult.mrz) {
|
|
377
400
|
throw new Error(`Lecture Barcode échouée: ${e?.message}`);
|
|
378
401
|
}
|
|
@@ -403,11 +426,11 @@ export async function backVerification(
|
|
|
403
426
|
|
|
404
427
|
export async function checkTemplateType(
|
|
405
428
|
result: {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
429
|
+
path?: string,
|
|
430
|
+
docType: string,
|
|
431
|
+
docRegion: string,
|
|
432
|
+
postfix: string,
|
|
433
|
+
issuingSubdivision?: string
|
|
411
434
|
},
|
|
412
435
|
env: KycEnvironment = 'PRODUCTION'
|
|
413
436
|
) {
|
|
@@ -425,12 +448,15 @@ export async function checkTemplateType(
|
|
|
425
448
|
}
|
|
426
449
|
|
|
427
450
|
const token = await authentification();
|
|
451
|
+
const isCanada = result.docRegion?.toUpperCase() === 'CA';
|
|
452
|
+
|
|
428
453
|
const templateType = await kycService.checkTemplateType({
|
|
429
454
|
fileUri: result.path || '',
|
|
430
455
|
docType: result?.docType as GovernmentDocumentType,
|
|
431
456
|
docRegion: result?.docRegion || "",
|
|
432
457
|
postfix: result?.postfix,
|
|
433
|
-
token: token
|
|
458
|
+
token: token,
|
|
459
|
+
issuingSubdivision: isCanada ? result.issuingSubdivision : undefined
|
|
434
460
|
}, env);
|
|
435
461
|
|
|
436
462
|
if (templateType.success === false || templateType.Error) {
|
|
@@ -468,7 +494,7 @@ export async function checkTemplateType(
|
|
|
468
494
|
|
|
469
495
|
const LPIPS_THRESHOLD = 0.75;
|
|
470
496
|
if (templateType.lpips_score !== undefined && templateType.lpips_score > LPIPS_THRESHOLD) {
|
|
471
|
-
logger.log(
|
|
497
|
+
logger.log(`Country Mismatch! LPIPS: ${templateType.lpips_score}`);
|
|
472
498
|
throw new Error(`Le document présenté ne correspond pas au pays sélectionné (${result.docRegion}).`);
|
|
473
499
|
}
|
|
474
500
|
|
|
@@ -281,7 +281,18 @@ export class KYCService {
|
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
//check templatetemplate_type
|
|
284
|
-
async checkTemplateType(
|
|
284
|
+
async checkTemplateType(
|
|
285
|
+
params: {
|
|
286
|
+
fileUri: string;
|
|
287
|
+
docType: string;
|
|
288
|
+
docRegion: string;
|
|
289
|
+
token: string;
|
|
290
|
+
postfix: string;
|
|
291
|
+
issuingSubdivision?: string; // 🚨 Added parameter
|
|
292
|
+
},
|
|
293
|
+
env: KycEnvironment = 'PRODUCTION'
|
|
294
|
+
): Promise<any> {
|
|
295
|
+
|
|
285
296
|
// SANDBOX mode
|
|
286
297
|
if (env === 'SANDBOX') {
|
|
287
298
|
console.log("SANDBOX mode: Skipping AI template type check");
|
|
@@ -293,15 +304,24 @@ export class KYCService {
|
|
|
293
304
|
};
|
|
294
305
|
}
|
|
295
306
|
|
|
296
|
-
|
|
307
|
+
// 🚨 Destructure issuingSubdivision
|
|
308
|
+
const { fileUri, docType, docRegion, token, postfix, issuingSubdivision } = params;
|
|
297
309
|
const formData = new FormData();
|
|
298
310
|
|
|
299
311
|
await appendFileToFormData(formData, 'file', fileUri, `id_card_${postfix}.jpg`, 'image/jpeg');
|
|
300
312
|
|
|
301
313
|
const docTypeShorted = GovernmentDocumentTypeShorted[docType as GovernmentDocumentType];
|
|
302
|
-
|
|
314
|
+
|
|
315
|
+
// Build the base URL
|
|
316
|
+
let url = `${this.mrzServiceURL}/get_template_version/?doc_type=${encodeURIComponent(docTypeShorted)}&doc_region=${encodeURIComponent(docRegion)}&postfix=${postfix}`;
|
|
317
|
+
|
|
318
|
+
// 🚨 Append the subdivision strictly for Canada/regional docs
|
|
319
|
+
if (issuingSubdivision) {
|
|
320
|
+
url += `&issuing_subdivision=${encodeURIComponent(issuingSubdivision)}`;
|
|
321
|
+
}
|
|
303
322
|
|
|
304
|
-
logger.log('checkTemplateType params', this.mrzServiceURL, JSON.stringify({ fileUri, docTypeShorted, docRegion, token, postfix, url }, null, 2));
|
|
323
|
+
logger.log('checkTemplateType params', this.mrzServiceURL, JSON.stringify({ fileUri, docTypeShorted, docRegion, issuingSubdivision, token, postfix, url }, null, 2));
|
|
324
|
+
|
|
305
325
|
try {
|
|
306
326
|
const res = await axios.post<CheckTemplateTypeResponse>(url, formData, {
|
|
307
327
|
headers: { 'Content-Type': 'multipart/form-data', 'Authorization': `Bearer ${token}`, },
|
|
@@ -310,10 +330,9 @@ export class KYCService {
|
|
|
310
330
|
logger.log('checkTemplateType res', JSON.stringify(res.data, null, 2));
|
|
311
331
|
return res.data;
|
|
312
332
|
} catch (e: any) {
|
|
313
|
-
logger.error('Error
|
|
333
|
+
logger.error('Error checking template type:', JSON.stringify(e));
|
|
314
334
|
throw e;
|
|
315
335
|
}
|
|
316
|
-
|
|
317
336
|
}
|
|
318
337
|
|
|
319
338
|
async extractDocumentInformation(params: {
|