This commit is contained in:
alisaza 2026-09-05 16:04:36 +03:30
parent 0bb22059d0
commit 78e4312f6c
6 changed files with 716 additions and 2870 deletions

View File

@ -37,9 +37,7 @@ interface BankAccountRow {
id: string
userId: string
ownerType: 'guest' | 'organizer'
accountHolder: string
iban: string
bankName: string | null
nationalCode: string | null
verificationStatus: BankAccountVerificationStatus
rejectionReason: string | null
@ -129,7 +127,8 @@ const BankAccountsPage = () => {
return
}
showAlert(`حساب «${row.accountHolder}» تأیید شود؟`, () =>
const userName = formatPersonName(row.user?.firstName, row.user?.lastName) || row.iban
showAlert(`حساب «${userName}» تأیید شود؟`, () =>
runAction(row.id, () => axiosInstance.patch(API_ROUTES.BANK_ACCOUNTS.ADMIN_APPROVE(row.id)), 'حساب بانکی تأیید شد')
)
}

View File

@ -35,8 +35,6 @@ interface WithdrawalRequestUserSummary {
interface WithdrawalRequestBankAccountSummary {
iban: string
bankName: string | null
accountHolder: string | null
}
interface WithdrawalRequestRow {
@ -213,16 +211,12 @@ const WithdrawalRequestsPage = () => {
}
return (
<div className="flex flex-col gap-1">
<span
className="text-xs font-mono"
dir="ltr"
>
{bankAccount.iban || '—'}
</span>
<span className="text-text-muted text-xs">{bankAccount.bankName ?? '—'}</span>
<span className="text-text-muted text-xs">{bankAccount.accountHolder ?? '—'}</span>
</div>
)
},
actions: (row) => {

File diff suppressed because it is too large Load Diff

View File

@ -10,8 +10,8 @@ export const wallet = {
tabTransactions: 'تراکنش‌ها',
tabSettlements: 'تسویه‌های میزبانی',
addAccount: 'افزودن حساب',
editAccount: 'ویرایش حساب',
noBankAccounts: 'هنوز حساب بانکی ثبت نکرده‌اید',
defaultBadge: 'پیش‌فرض',
noWithdrawals: 'درخواست برداشتی ثبت نشده است',
noTransactions: 'تراکنشی ثبت نشده است',
noSettlements: 'هنوز تسویه‌ای برای شما ثبت نشده است',
@ -19,23 +19,17 @@ export const wallet = {
viewPaymentReceipt: 'مشاهده رسید پرداخت',
addBankAccountTitle: 'افزودن حساب بانکی',
ownerTypeLabel: 'نوع حساب',
nationalCodeHint: 'برای حساب مهمان، کد ملی لازم است. حساب میزبان از احراز هویت خوانده می‌شود.',
hostIdentityRequired: 'برای ثبت حساب میزبانی، ابتدا کد ملی را در احراز هویت ارسال کنید.',
rejectedHint: 'رد شده',
onlyApprovedWithdrawal: 'فقط حساب تأییدشده قابل برداشت است',
editBankAccountTitle: 'ویرایش حساب بانکی',
nationalCodeHint: 'کد ملی برای استعلام شبا لازم است. پس از تأیید احراز هویت میزبان، از روی پرونده هویت خوانده می‌شود.',
withdrawalNeedsBankAccount: 'ابتدا یک حساب بانکی ثبت کنید؛ پس از تأیید حساب می‌توانید درخواست برداشت بدهید',
withdrawalNeedsApprovedBankAccount: 'حساب بانکی شما هنوز تأیید نشده است؛ پس از تأیید ادمین می‌توانید درخواست برداشت بدهید',
submitAccount: 'ثبت حساب',
accountHolder: 'نام صاحب حساب',
accountHolderPlaceholder: 'نام و نام خانوادگی',
saveAccount: 'ذخیره تغییرات',
iban: 'شماره شبا',
bankNameOptional: 'نام بانک (اختیاری)',
bankNamePlaceholder: 'مثلاً ملت',
submitRequest: 'ثبت درخواست',
bankAccount: 'حساب بانکی',
bankAccountOption: '{holder} — {iban}',
bankAccountOption: '{iban}',
maxAmount: 'حداکثر: {amount}',
amountToman: 'مبلغ (تومان)',
@ -45,6 +39,7 @@ export const wallet = {
txDeposit: 'واریز',
bankAccountCreated: 'حساب بانکی ثبت شد',
bankAccountUpdated: 'حساب بانکی به‌روز شد و دوباره در صف بررسی قرار گرفت',
bankAccountCreateFailed: 'ثبت حساب بانکی ناموفق بود',
addBankAccountFirst: 'ابتدا یک حساب بانکی اضافه کنید',
withdrawalCreated: 'درخواست برداشت ثبت شد',

View File

@ -4,18 +4,33 @@ import { CreateBankAccountFormValidation, CreateWithdrawalRequestFormValidation
describe('financial form validation', () => {
it('normalizes and validates Iranian IBAN values', () => {
const result = CreateBankAccountFormValidation.parse({
ownerType: 'organizer',
accountHolder: 'علی رضایی',
const result = CreateBankAccountFormValidation(false).parse({
iban: 'ir12 3456 7890 1234 5678 9012 34',
bankName: 'بانک',
nationalCode: '',
})
expect(result.iban).toBe('IR123456789012345678901234')
})
it.each(['IR123', 'DE123456789012345678901234', 'IR12345678901234567890123A'])('rejects an invalid IBAN: %s', (iban) => {
expect(CreateBankAccountFormValidation.safeParse({ ownerType: 'organizer', accountHolder: 'علی رضایی', iban }).success).toBe(false)
expect(CreateBankAccountFormValidation(false).safeParse({ iban, nationalCode: '' }).success).toBe(false)
})
it('normalizes Persian digits for IBAN and nationalCode', () => {
const result = CreateBankAccountFormValidation(true).parse({
iban: 'ir۱۲ ۳۴۵۶ ۷۸۹۰ ۱۲۳۴ ۵۶۷۸ ۹۰۱۲ ۳۴',
nationalCode: '۰۰۱۲۳۴۵۶۷۸',
})
expect(result.iban).toBe('IR123456789012345678901234')
expect(result.nationalCode).toBe('0012345678')
})
it('requires nationalCode when identity is not verified', () => {
expect(CreateBankAccountFormValidation(true).safeParse({ iban: 'IR123456789012345678901234' }).success).toBe(false)
expect(
CreateBankAccountFormValidation(true).safeParse({ iban: 'IR123456789012345678901234', nationalCode: '0012345678' }).success
).toBe(true)
})
it('accepts only positive integer withdrawal amounts', () => {

View File

@ -1,38 +1,37 @@
import { z } from 'zod'
import { convertPersianToEnglish } from '@/helpers'
import { texts } from '@/texts'
const v = texts.validation.withdrawal
const nationalId = texts.validation.nationalId
export const CreateBankAccountFormValidation = z
const toEnglishDigits = (value: string) => convertPersianToEnglish(value).trim()
export const CreateBankAccountFormValidation = (requiresNationalCode: boolean) =>
z
.object({
ownerType: z.enum(['guest', 'organizer']),
accountHolder: z.string().trim().min(3, v.accountHolderRequired).max(200, v.accountHolderMax),
iban: z
.string()
.trim()
.transform((value) => value.toUpperCase().replace(/\s+/g, ''))
.transform((value) => toEnglishDigits(value).toUpperCase().replace(/\s+/g, ''))
.refine((value) => /^IR\d{24}$/.test(value), v.ibanInvalid),
nationalCode: z.string().trim().optional().or(z.literal('')),
bankName: z.string().trim().max(100, v.bankNameMax).optional().or(z.literal('')),
nationalCode: z.string().trim().transform((value) => toEnglishDigits(value)),
})
.superRefine((value, ctx) => {
if (value.ownerType !== 'guest') return
if (!requiresNationalCode) return
const nationalCode = value.nationalCode ?? ''
if (!nationalCode) {
if (!value.nationalCode) {
ctx.addIssue({ code: 'custom', path: ['nationalCode'], message: nationalId.nationalCodeRequired })
return
}
if (!/^\d{10}$/.test(nationalCode)) {
if (!/^\d{10}$/.test(value.nationalCode)) {
ctx.addIssue({ code: 'custom', path: ['nationalCode'], message: nationalId.nationalCodeDigits })
}
})
export type CreateBankAccountFormValues = z.infer<typeof CreateBankAccountFormValidation>
export type CreateBankAccountFormValues = z.infer<ReturnType<typeof CreateBankAccountFormValidation>>
export const CreateWithdrawalRequestFormValidation = z.object({
bankAccountId: z.string().min(1, v.bankAccountRequired),
@ -40,6 +39,7 @@ export const CreateWithdrawalRequestFormValidation = z.object({
.string()
.trim()
.min(1, v.amountRequired)
.transform((value) => toEnglishDigits(value))
.refine((value) => /^\d+$/.test(value), v.amountInteger)
.refine((value) => Number(value) > 0, v.amountPositive),
})