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
+68 -3
View File
@@ -250,14 +250,18 @@ Get yearly earnings dashboard for a representation.
"mobile_number": "09120671756",
"city_id": 132,
"commission_percent": "10.00",
"bank_account": null,
"bank_account": [
{ "id": "iban-uuid-1", "iban": "IR000000000000000000000000", "bank_name": "بانک ملت", "owner_name": "حامد حسینی", "verified": true, "created_at": 1718000000 }
],
"active": true,
"created_at": 1718000000
"created_at": 1718000000,
"national_code": "0012345678",
"national_code_verified": true
}
}
}
```
> double-nested: مقدار با `data.data` استخراج می‌شود.
> double-nested: مقدار با `data.data` استخراج می‌شود. `bank_account` آرایه‌ای از ۰ تا ۲ شبا است (`null` اگر هیچ شبایی ثبت نشده). `national_code`/`national_code_verified` از کاربرِ نماینده می‌آید.
#### Errors
| Code | HTTP | Description |
@@ -266,6 +270,67 @@ Get yearly earnings dashboard for a representation.
---
### POST `/api/v1/representation/verify-national-code`
تأیید کد ملی نماینده با استعلام **شاهکار** (`s.api.ir` → ShahkarLite): تطبیق کد ملی با موبایلِ کاربر جاری. در صورت موفقیت، `national_code` ذخیره و `national_code_verified=true` می‌شود.
#### Request Body
```json
{ "national_code": "0012345678" }
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `national_code` | string | ✅ | کد ملی ۱۰ رقمی |
#### Response `200`
آبجکت پروفایل نماینده (مثل `me`، با `national_code_verified: true`).
#### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_VALIDATION_001` | 422 | کد ملی ۱۰ رقم نیست (`field: national_code`) |
| `ERR_IDENTITY_001` | 422 | کد ملی متعلق به این موبایل نیست (`field: national_code`) |
| `ERR_EXTERNAL_001` | 502 | خطا در استعلام |
| `ERR_EXTERNAL_002` | 503 | سرویس استعلام پیکربندی نشده |
---
### POST `/api/v1/representation/iban`
افزودن یک شماره شبا. ابتدا با **IbanMatch** (`s.api.ir`) بررسی می‌شود شبا متعلق به کد ملیِ تأییدشده‌ی نماینده باشد. حداکثر ۲ شبا.
#### Request Body
```json
{ "iban": "IR000000000000000000000000" }
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `iban` | string | ✅ | شماره شبا (با/بدون `IR` و فاصله؛ نرمال‌سازی می‌شود) |
#### Response `200`
آبجکت پروفایل نماینده با `bank_account` به‌روزشده.
#### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_IDENTITY_004` | 409 | کد ملی هنوز تأیید نشده |
| `ERR_IDENTITY_003` | 409 | سقف ۲ شبا پر است |
| `ERR_VALIDATION_001` | 422 | شبا نامعتبر (`field: iban`) |
| `ERR_IDENTITY_002` | 422 | شبا متعلق به نماینده نیست (`field: iban`) |
| `ERR_EXTERNAL_001` | 502 | خطا در استعلام |
| `ERR_EXTERNAL_002` | 503 | سرویس استعلام پیکربندی نشده |
---
### DELETE `/api/v1/representation/iban/{id}`
حذف یک شماره شبا با `id` آن (از `bank_account[].id`).
#### Response `200`
آبجکت پروفایل نماینده با `bank_account` به‌روزشده.
---
### POST `/api/v1/representation/doctor`
افزودن پزشک توسط نماینده. `representation_id` پزشک به‌صورت خودکار روی نماینده‌ی کاربر جاری ست می‌شود. پس از ثبت موفق، یک پیامک خوش‌آمد (تگ `welcome`) به‌صورت async به موبایل پزشک ارسال می‌شود.
+29 -13
View File
@@ -91,23 +91,16 @@ Request a settlement (withdrawal from wallet to bank account).
```json
{
"amount_rials": 1000000,
"bank_account": {
"iban": "IR...",
"account_number": "1234567890",
"bank_name": "بانک ملت",
"owner_name": "علی احمدی"
}
"iban_id": "iban-uuid-..."
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `amount_rials` | integer | ✅ | Amount to withdraw (must be ≤ wallet balance) |
| `bank_account` | object | | Bank account details (saved if not previously set) |
| `bank_account.iban` | string | ❌ | IBAN (شبا) |
| `bank_account.account_number` | string | ❌ | Account number |
| `bank_account.bank_name` | string | ❌ | Bank name |
| `bank_account.owner_name` | string | ❌ | Account owner name |
| `iban_id` | string | | شناسه‌ی یکی از شباهای **تأییدشده‌ی** نماینده (از `GET /api/v1/representation/me``bank_account[].id`) |
> شبای انتخابی به‌صورت snapshot (`iban`, `bank_name`, `owner_name`) داخل خود رکورد تسویه ذخیره می‌شود؛ حذف بعدی شبا در پروفایل، این رکورد را تغییر نمی‌دهد. مبلغ همان لحظه‌ی ثبت از کیف‌پول کسر (debit) می‌شود.
### Response `201`
```json
@@ -131,7 +124,9 @@ Request a settlement (withdrawal from wallet to bank account).
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_VALIDATION_001` | 422 | Amount exceeds balance or invalid amount |
| `ERR_VALIDATION_001` | 422 | مبلغ نامعتبر/بیش از موجودی، یا شبا تأییدنشده/نامعتبر (`field: iban_id`) |
| `ERR_VALIDATION_002` | 422 | `iban_id` ارسال نشده (`field: iban_id`) |
| `ERR_CONFLICT_001` | 422 | موجودی کافی نیست |
---
@@ -274,9 +269,30 @@ Updated settlement object with `status: "rejected"`.
{ "success": true, "data": { "uuid": "...", "url": "/uploads/settlements/receipts/2026-06/...", "filename": "...", "filemime": "image/jpeg" } }
```
### POST `/api/v1/settlement/{uuid}/receipt`
ذخیره‌ی رسید پرداخت **بدون** نهایی‌سازی. فقط روی تسویه‌ی **approved**؛ وضعیت تغییر نمی‌کند (همچنان `approved`). پس از این مرحله دکمه‌ی «تکمیل» در پنل فعال می‌شود. **Permission:** `ROLE_ADMIN`
**Request Body:**
```json
{ "receipt": "/uploads/settlements/receipts/2026-06/..." }
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `receipt` | string | ✅ | URL رسیدِ آپلودشده |
**Response `200`:** آبجکت تسویه (با `receipt` ذخیره‌شده، `status: "approved"`).
**Errors:**
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_NOT_FOUND_001` | 404 | درخواست یافت نشد |
| `ERR_VALIDATION_001` | 422 | تسویه approved نیست |
| `ERR_VALIDATION_002` | 422 | `receipt` خالی (`field: receipt`) |
### POST `/api/v1/settlement/{uuid}/paid`
ثبت پرداخت نهایی با رسید. فقط روی تسویه‌ی **approved**. وضعیت → `paid` و `receipt` ذخیره می‌شود. کیف‌پول تغییر نمی‌کند. **Permission:** `ROLE_ADMIN`
**«تکمیل»** — ثبت پرداخت نهایی. فقط روی تسویه‌ی **approved**. وضعیت → `paid` و `receipt` ذخیره می‌شود. کیف‌پول تغییر نمی‌کند (مبلغ هنگام ثبت درخواست کسر شده). پس از تکمیل، آن مبلغ دیگر قابل درخواست تسویه نیست. **Permission:** `ROLE_ADMIN`
**Request Body:**
```json