Review Step: Check2FA và Ward Logic
Mục tiêu bài học
- Hiểu review step trong eKYC
- Nắm check2fa logic và ward search
- Biết cách xử lý missing fields (province, ward, address)
- Học auto-fill logic và province code mapping
Tổng quan bước xem lại
Xem lại là bước cuối trước khi gửi eKYC. Người dùng:
- Xem lại thông tin OCR
- Xem kết quả xác thực khuôn mặt
- Kiểm tra địa chỉ (check2fa + ward)
- Điền missing fields (nếu thiếu)
- Gửi
Check2FA Logic - Flow Tổng quan
Khi nào cần kiểm tra?
// Kiểm tra khi VÀO bước xem lại (không phải sau khi gửi)
watch(currentStep, async (newStep) => {
if (newStep === "review") {
await loadProvinceList(); // Load trước để map province name → code
await check2faAndWard();
}
});State Management
// Theo dõi các trường còn thiếu
const missingFields = reactive({
province: false,
ward: false,
address: false,
});
// Giá trị input cho các trường còn thiếu
const missingFieldsInput = reactive({
province: "", // Lưu mã tỉnh (code)
ward: "", // Lưu tên phường/xã
address: "", // Lưu chi tiết địa chỉ
});Flow Logic (Tóm tắt)
1. Gọi check2fa API
↓
2. Phát hiện missing fields (province, ward, address)
↓
3. Nếu thiếu ward → Tìm kiếm ward từ địa chỉ OCR
↓
4. Nếu tìm thấy ward:
- Tự động điền ward, province, address
- Kiểm tra ward mới sau sáp nhập
- Nếu có 1 kết quả → Tự động chọn
- Nếu có nhiều kết quả → Hiển thị dropdown
↓
5. Hiển thị UI cho missing fields
↓
6. User xem lại và chỉnh sửa (nếu cần)
↓
7. Submit với missing fieldsPhát hiện Missing Fields
Logic Phát hiện
Hệ thống kiểm tra từng trường trong response của check2fa API:
// Kiểm tra từng trường
const hasProvince =
data.province &&
typeof data.province === "string" &&
data.province.trim() !== "";
const hasWard =
data.ward && typeof data.ward === "string" && data.ward.trim() !== "";
const hasAddress =
data.address &&
typeof data.address === "string" &&
data.address.trim() !== "";
// Cập nhật flags
missingFields.province = !hasProvince;
missingFields.ward = !hasWard;
missingFields.address = !hasAddress;Ví dụ Response
Trường hợp 1: Đầy đủ
{ "province": "HNI", "ward": "Phường Cầu Giấy", "address": "247 Cầu Giấy" }→ missingFields = { province: false, ward: false, address: false }
Trường hợp 2: Thiếu tất cả
{ "province": "", "ward": "", "address": "" }→ missingFields = { province: true, ward: true, address: true }
Tự động điền Missing Fields
Thứ tự ưu tiên
Từ Ward Search Result (nếu có):
ward→ từfirstWard.wardNameprovince→ từfirstWard.provinceName(convert sang code)address→ từextractAddressDetail()(phần trước ward name)
Từ New Ward Options (nếu có):
ward→ từnewWard.newWardprovince→ từnewWard.newProvince(convert sang code)address→ từextractAddressDetail()vớioldWardhoặcnewWard
Từ Check2FA Data (nếu có):
- Khởi tạo
missingFieldsInputvới giá trị hiện có - Convert province name → code nếu cần
- Khởi tạo
Ví dụ Auto-fill
// Sau khi tìm thấy ward từ API
if (wardData.found && wardData.wards.length > 0) {
const firstWard = wardData.wards[0];
// Auto-fill ward
if (missingFields.ward) {
missingFieldsInput.ward = firstWard.wardName;
}
// Auto-fill province (convert name → code)
if (missingFields.province) {
const provinceCode = getProvinceCode(firstWard.provinceName);
missingFieldsInput.province = provinceCode || firstWard.provinceName;
}
// Auto-fill address detail
if (missingFields.address && frontOcrData.value?.address) {
const addressDetail = extractAddressDetail(
frontOcrData.value.address,
firstWard.wardName,
firstWard.districtName
);
if (addressDetail) {
missingFieldsInput.address = addressDetail;
}
}
}Province Code Mapping
Vấn đề
API check2fa có thể trả về province name (ví dụ: "Thành phố Hà Nội") hoặc code (ví dụ: "HNI"). Nhưng API updateInfoSso yêu cầu province code.
Giải pháp
Sử dụng getProvinceCode() để convert:
function getProvinceCode(provinceName: string): string | null {
if (!provinceName || !provinceList.value.length) return null;
const normalized = (text: string) =>
text
.toLowerCase()
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.trim();
const normalizedName = normalized(provinceName);
// 1. Exact match
const exact = provinceList.value.find(
(p) => normalized(p.name) === normalizedName
);
if (exact) return exact.value;
// 2. Fuzzy match (contains)
const fuzzy = provinceList.value.find(
(p) =>
normalized(p.name).includes(normalizedName) ||
normalizedName.includes(normalized(p.name))
);
if (fuzzy) return fuzzy.value;
return null;
}Province Input Display
UI hiển thị "Tên (Code)" nhưng lưu code:
const provinceInputDisplay = computed({
get: () => {
if (!missingFieldsInput.province) return "";
return getProvinceDisplayText(missingFieldsInput.province); // "Thành phố Hà Nội (HNI)"
},
set: (value: string) => {
// Extract code from "Name (Code)" format
const match = value.match(/\(([^)]+)\)$/);
if (match && match[1]) {
missingFieldsInput.province = match[1].trim(); // Lưu "HNI"
} else {
// Try to get code from name
const provinceCode = getProvinceCode(value);
missingFieldsInput.province = provinceCode || value.trim();
}
},
});Extract Address Detail
Mục đích
Trích xuất phần "Chi tiết địa chỉ" (số nhà, tên đường) từ địa chỉ đầy đủ bằng cách loại bỏ phần phường/xã.
Logic (Tóm tắt)
- Normalize text (remove diacritics, lowercase)
- Tìm ward name trong địa chỉ (thử nhiều patterns: với/không prefix)
- Verify district xuất hiện sau ward (nếu có)
- Extract phần trước ward name
- Remove trailing separators
Ví dụ
const fullAddress = "247 Cầu Giấy, Phường Cầu Giấy, Quận Cầu Giấy";
const wardName = "Phường Cầu Giấy";
const addressDetail = extractAddressDetail(fullAddress, wardName);
// → "247 Cầu Giấy"Country Detection (isVietnam)
Logic
Không có trường country trong OCR data. Xác định dựa trên kết quả ward API:
isVietnam = true: Tìm thấy ward → Hiển thị ward dropdownisVietnam = false: Không tìm thấy → Hiển thị manual inputisVietnam = null: Chưa xác định → Hiển thị loading/default
Ví dụ
// Sau khi gọi findWardByAddress
if (wardData.found && wardData.wards.length > 0) {
isVietnam.value = true; // Tìm thấy ward → Việt Nam
} else {
isVietnam.value = false; // Không tìm thấy → Không phải Việt Nam
}Ward Search Flow
Flow Diagram
Enter Review Step
↓
Load Province List
↓
Call check2fa()
↓
Detect Missing Fields
↓
┌─────────────────┐
│ Có địa chỉ OCR? │
└────────┬────────┘
│
┌────┴────┐
│ │
Yes No
│ │
↓ ↓
findWardBy Thử với ward
Address() input thủ công
│ │
└────┬────┘
│
┌────┴────┐
│ Tìm thấy?│
└────┬────┘
│
┌────┴────┐
│ │
Yes No
│ │
↓ ↓
Auto-fill isVietnam
Missing = false
Fields (manual input)
↓
checkNewVnProvinceWard()
↓
┌──────────────┐
│ Có kết quả? │
└──────┬───────┘
│
┌────┴────┐
│ │
1 kết quả Nhiều kết quả
│ │
↓ ↓
Auto-select Dropdown
+ Auto-fill để chọnUI Hiển thị Missing Fields
Conditional Rendering
UI hiển thị dựa trên:
missingFieldsflags (province, ward, address)isVietnamstatus (true/false/null)newWardOptions(có ward options từ API không)
Template (Tóm tắt)
<!-- Missing Fields Notice -->
<div v-if="missingFields.province || missingFields.ward || missingFields.address">
<!-- Warning Header -->
<div class="warning-header">
<Lucide icon="AlertTriangle" />
<h3>{{ isVietnam === true ? 'Thiếu thông tin địa chỉ' : 'Không phải Việt Nam' }}</h3>
</div>
<!-- Province Input -->
<div v-if="missingFields.province">
<label>Tỉnh/Thành phố *</label>
<input v-model="provinceInputDisplay" />
<!-- Hiển thị "Tên (Code)", lưu code -->
</div>
<!-- Ward Dropdown/Input -->
<div v-if="missingFields.ward">
<label>Phường/Xã *</label>
<!-- Dropdown nếu có newWardOptions -->
<a-select v-if="isVietnam === true && newWardOptions.length > 0"
v-model:value="selectedNewWard"
:options="newWardOptions.map(opt => ({
value: opt.newWard,
label: opt.newWard
}))" />
<!-- Text input nếu không có options -->
<input v-else v-model="missingFieldsInput.ward" />
</div>
<!-- Address Input -->
<div v-if="missingFields.address">
<label>Chi tiết địa chỉ *</label>
<input v-model="missingFieldsInput.address" />
</div>
</div>Handler cho Ward Selection
function handleWardSelection() {
const selected = newWardOptions.value.find(
(opt) => opt.newWard === selectedNewWard.value
);
if (selected) {
// Điền ward
if (missingFields.ward) {
missingFieldsInput.ward = selected.newWard;
}
// Điền province (convert name → code)
if (missingFields.province) {
const provinceCode = getProvinceCode(selected.newProvince);
missingFieldsInput.province = provinceCode || selected.newProvince;
}
// Tự động điền address detail
if (frontOcrData.value?.address) {
let addressDetail = "";
if (selected.oldWard) {
addressDetail = extractAddressDetail(
frontOcrData.value.address,
selected.oldWard
);
}
if (!addressDetail && selected.newWard) {
addressDetail = extractAddressDetail(
frontOcrData.value.address,
selected.newWard
);
}
if (addressDetail && missingFields.address) {
missingFieldsInput.address = addressDetail;
}
}
}
}Manual Ward Search
Khi nào cần?
- Tự động tìm phường/xã không chính xác
- Người dùng muốn tìm phường/xã khác
- Không tìm thấy phường/xã tự động
Implementation (Tóm tắt)
// Mở modal tìm kiếm thủ công
function openManualWardSearchModal() {
showManualWardSearchModal.value = true;
manualWardSearchQuery.value = "";
}
// Thực hiện tìm kiếm
async function performManualWardSearch() {
const response = await customerService.checkNewVnProvinceWard({
query: manualWardSearchQuery.value.trim(),
});
// ... xử lý response
}
// Chọn ward từ kết quả
function selectWardFromManualSearch(ward: INewWardOption) {
if (missingFields.ward) missingFieldsInput.ward = ward.newWard;
if (missingFields.province) {
const provinceCode = getProvinceCode(ward.newProvince);
missingFieldsInput.province = provinceCode || ward.newProvince;
}
// Auto-fill address detail nếu có thể
closeManualWardSearchModal();
}Submit với Missing Fields
Validation
function validateBeforeSubmit(): { valid: boolean; errors: string[] } {
const errors: string[] = [];
if (missingFields.province && !missingFieldsInput.province.trim()) {
errors.push("Tỉnh/Thành phố là bắt buộc");
}
if (missingFields.ward && !missingFieldsInput.ward.trim()) {
errors.push("Phường/Xã là bắt buộc");
}
if (missingFields.address && !missingFieldsInput.address.trim()) {
errors.push("Chi tiết địa chỉ là bắt buộc");
}
return { valid: errors.length === 0, errors };
}Mapping vào updateInfoSso
// Sau khi face verification thành công
if (isSamePerson(faceVerificationData.value)) {
const updateData: IUpdateInfoSsoRequest = {};
// Address: combine detail + ward
let fullAddress =
missingFields.address && missingFieldsInput.address.trim()
? missingFieldsInput.address.trim()
: mergedFrontOcrData.address || "";
const wardName = missingFields.ward
? missingFieldsInput.ward.trim()
: mergedFrontOcrData.precinct || "";
if (fullAddress && wardName && !fullAddress.includes(wardName)) {
fullAddress = `${fullAddress}, ${wardName}`;
}
updateData.address = fullAddress;
// Ward
updateData.ward = missingFields.ward
? missingFieldsInput.ward.trim()
: mergedFrontOcrData.precinct;
// Province (luôn dùng code)
updateData.province = missingFields.province
? missingFieldsInput.province.trim() // Đã là code
: getProvinceCode(mergedFrontOcrData.province) ||
mergedFrontOcrData.province;
// Country, Gender, BirthDay, ID Number, Phone...
updateData.country = "VN";
// ... mapping các trường khác
await customerService.updateInfoSso(updateData);
}Edge Cases
1. Không có địa chỉ từ OCR
if (!address || !address.trim()) {
isVietnam.value = null; // Chưa xác định
// Nếu ward bị thiếu NHƯNG user đã nhập ward name thủ công
if (missingFields.ward && missingFieldsInput.ward?.trim()) {
// Thử gọi checkNewVnProvinceWard trực tiếp
const newWardResponse = await customerService.checkNewVnProvinceWard({
query: missingFieldsInput.ward.trim(),
});
// ... xử lý response
}
}2. Ward Search không tìm thấy
if (!wardData?.found || !wardData.wards?.length) {
isVietnam.value = false; // Không phải Việt Nam
// Hiển thị UI cho non-Vietnam (manual input)
}3. Multiple Ward Options
if (newWardOptions.value.length > 1) {
// Hiển thị dropdown để user chọn
// Không tự động điền
}4. Province Code không tìm thấy
const provinceCode = getProvinceCode(provinceName);
if (provinceCode) {
missingFieldsInput.province = provinceCode;
} else {
// Fallback: dùng province name
missingFieldsInput.province = provinceName;
}Tổng kết
Flow Hoàn chỉnh
1. User vào Review Step
2. Load Province List
3. Gọi check2fa API
4. Phát hiện Missing Fields
5. Tìm kiếm Ward (nếu thiếu)
6. Tự động điền Missing Fields
7. Kiểm tra Ward mới sau sáp nhập
8. Hiển thị UI cho Missing Fields
9. User xem lại và chỉnh sửa
10. Validate trước khi submit
11. Submit eKYC
12. Auto-update User Info (updateInfoSso)Key Points
- Missing Fields Detection: Phát hiện tự động từ
check2faresponse - Auto-fill Logic: Ưu tiên từ Ward Search → New Ward Options → Check2FA Data
- Province Code: Hiển thị "Tên (Code)", lưu code
- Country Detection: Dựa trên kết quả ward API
- Edge Cases: Xử lý tất cả trường hợp (không có OCR, không tìm thấy ward, multiple options, etc.)