feat(migrations): add national_code_verified flag to users and normalize bank_account representation

- Added a new column `national_code_verified` to the `users` table.
- Normalized the `bank_account` field in the `representations` table from a single object to an array of IBANs with a default `verified` status of false.

feat(ApiIrService): implement identity verification client for api.ir

- Created `ApiIrService` to handle identity verification via api.ir.
- Implemented methods for matching national code with mobile and IBAN with national code and birth date.
- Added error handling and logging for external API requests.
This commit is contained in:
hamed
2026-06-25 19:38:47 +03:30
parent 9b608aaeac
commit c2c6ae4d02
515 changed files with 194899 additions and 55 deletions
+32 -10
View File
@@ -78,7 +78,7 @@ export default function SettlementDetailPage() {
onError: (e: Error) => toast.error(e.message),
});
// آپلود رسید → سپس ثبت پرداخت نهایی (paid). مبلغ قبلاً هنگام درخواست از کیف‌پول کسر شده.
// آپلود رسید (بدون نهایی‌سازی). نهایی‌سازی با دکمه‌ی جداگانه‌ی «تکمیل» انجام می‌شود.
const handleReceiptUpload = async (file: File) => {
setPaying(true);
try {
@@ -94,15 +94,26 @@ export default function SettlementDetailPage() {
const json = await res.json();
const url = json?.data?.url;
if (!url) throw new Error('آپلود رسید ناموفق بود');
await api.post<ApiResponse<null>>(`/api/v1/settlement/${uuid}/paid`, { receipt: url });
toast.success('پرداخت ثبت شد');
// رسید را در همان رکورد ذخیره می‌کنیم تا دکمه‌ی «تکمیل» فعال شود (وضعیت همچنان approved).
await api.post<ApiResponse<null>>(`/api/v1/settlement/${uuid}/receipt`, { receipt: url });
toast.success('رسید آپلود شد');
qc.invalidateQueries({ queryKey: ['settlement-detail', uuid] });
qc.invalidateQueries({ queryKey: ['settlements'] });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) { toast.error(e?.message ?? 'خطا در ثبت پرداخت'); }
} catch (e: any) { toast.error(e?.message ?? 'خطا در آپلود رسید'); }
finally { setPaying(false); }
};
// تکمیل: نهایی‌سازی پرداخت (وضعیت → paid). مبلغ قبلاً هنگام درخواست از کیف‌پول کسر شده.
const completeMut = useMutation({
mutationFn: () => api.post<ApiResponse<null>>(`/api/v1/settlement/${uuid}/paid`, { receipt: s?.receipt }),
onSuccess: () => {
toast.success('تسویه تکمیل شد');
qc.invalidateQueries({ queryKey: ['settlement-detail', uuid] });
qc.invalidateQueries({ queryKey: ['settlements'] });
},
onError: (e: Error) => toast.error(e.message),
});
if (isLoading) {
return (
<div className="fade-in">
@@ -186,16 +197,27 @@ export default function SettlementDetailPage() {
)}
{s.status === 'approved' && (
<div style={{ marginTop: 14 }}>
<div style={{ marginTop: 14, display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
<input ref={receiptInputRef} type="file" accept="image/*" style={{ display: 'none' }}
onChange={e => e.target.files?.[0] && handleReceiptUpload(e.target.files[0])} />
<button className="btn primary sm" disabled={paying}
<button className="btn ghost sm" disabled={paying}
onClick={() => receiptInputRef.current?.click()}>
<ArrowUpTrayIcon style={{ width: 15, height: 15 }} />
{paying ? 'در حال ثبت...' : 'آپلود رسید و ثبت پرداخت'}
{paying ? 'در حال آپلود...' : (s.receipt ? 'تغییر رسید' : 'آپلود رسید')}
</button>
<p className="muted" style={{ fontSize: 12, marginTop: 8 }}>
با آپلود رسید، وضعیت به «پرداخت شده» تغییر میکند. مبلغ هنگام ثبت درخواست از کیفپول کسر شده است.
{/* دکمه‌ی تکمیل فقط بعد از آپلود رسید فعال می‌شود */}
<button className="btn primary sm" disabled={!s.receipt || completeMut.isPending}
onClick={() => completeMut.mutate()}
title={!s.receipt ? 'ابتدا رسید را آپلود کنید' : undefined}>
<CheckIcon style={{ width: 15, height: 15 }} />
{completeMut.isPending ? 'در حال تکمیل...' : 'تکمیل'}
</button>
<p className="muted" style={{ fontSize: 12, width: '100%' }}>
{s.receipt
? 'با زدن «تکمیل»، وضعیت به «پرداخت شده» تغییر می‌کند. مبلغ هنگام ثبت درخواست از کیف‌پول کسر شده است.'
: 'ابتدا رسید پرداخت را آپلود کنید، سپس دکمه‌ی «تکمیل» فعال می‌شود.'}
</p>
</div>
)}