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:
@@ -75,6 +75,14 @@ class ErrorCodes
|
||||
// Rating
|
||||
public const ERR_RATING_NOT_ELIGIBLE = 'ERR_RATING_NOT_ELIGIBLE';
|
||||
|
||||
// External Inquiry (s.api.ir)
|
||||
public const ERR_EXTERNAL_001 = 'ERR_EXTERNAL_001';
|
||||
public const ERR_EXTERNAL_NOT_CONFIGURED = 'ERR_EXTERNAL_002';
|
||||
public const ERR_NATIONAL_CODE_MISMATCH = 'ERR_IDENTITY_001';
|
||||
public const ERR_IBAN_MISMATCH = 'ERR_IDENTITY_002';
|
||||
public const ERR_IBAN_LIMIT = 'ERR_IDENTITY_003';
|
||||
public const ERR_NATIONAL_CODE_UNVERIFIED = 'ERR_IDENTITY_004';
|
||||
|
||||
public static function message(string $code): string
|
||||
{
|
||||
return match ($code) {
|
||||
@@ -113,6 +121,12 @@ class ErrorCodes
|
||||
self::ERR_SESSION_NOT_FOUND => 'مراجعه یافت نشد',
|
||||
self::ERR_SMS_WALLET_INSUFFICIENT => 'موجودی کیف پیامک کافی نیست',
|
||||
self::ERR_RATING_NOT_ELIGIBLE => 'برای ثبت نظر یا امتیاز باید در یک ماه گذشته نوبت تاییدشده نزد این پزشک داشته باشید',
|
||||
self::ERR_EXTERNAL_001 => 'خطا در استعلام. لطفاً بعداً تلاش کنید',
|
||||
self::ERR_EXTERNAL_NOT_CONFIGURED => 'سرویس استعلام پیکربندی نشده است',
|
||||
self::ERR_NATIONAL_CODE_MISMATCH => 'کد ملی متعلق به این شماره موبایل نیست',
|
||||
self::ERR_IBAN_MISMATCH => 'شماره شبا متعلق به شما نیست',
|
||||
self::ERR_IBAN_LIMIT => 'حداکثر دو شماره شبا میتوانید ثبت کنید',
|
||||
self::ERR_NATIONAL_CODE_UNVERIFIED => 'ابتدا کد ملی خود را تأیید کنید',
|
||||
default => 'خطای ناشناخته',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace App\Shared\Service;
|
||||
|
||||
use App\Shared\Constant\ErrorCodes;
|
||||
use App\Shared\Exception\AppException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
/**
|
||||
* کلاینت استعلام هویت api.ir (s.api.ir).
|
||||
*
|
||||
* دو سرویس: شاهکار (تطبیق کد ملی با موبایل) و IbanMatch (تطبیق شبا با کد ملی).
|
||||
*
|
||||
* نگاشت فیلدها بر اساس قرارداد متداول api.ir پیاده شده است. اگر پاسخ واقعی
|
||||
* سرویس نام فیلد متفاوتی داشت، فقط همین کلاس (متدهای parse*) باید اصلاح شود.
|
||||
*
|
||||
* رفتار «fail-closed»: اگر توکن پیکربندی نشده باشد، استعلام انجام نمیشود و
|
||||
* خطا برمیگردد — هرگز بهصورت پیشفرض «تأییدشده» برنمیگرداند تا مسیر پولی
|
||||
* (مالکیت شبا) بهاشتباه تأیید نشود.
|
||||
*/
|
||||
class ApiIrService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly HttpClientInterface $httpClient,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly string $baseUrl, // https://s.api.ir
|
||||
private readonly string $token, // توکن api.ir — از .env
|
||||
) {}
|
||||
|
||||
public function isConfigured(): bool
|
||||
{
|
||||
return $this->token !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* تطبیق کد ملی با موبایل (شاهکار). true یعنی هر دو متعلق به یک نفر است.
|
||||
*/
|
||||
public function shahkarMatch(string $nationalCode, string $mobile): bool
|
||||
{
|
||||
// پاسخ: {"data": true|false, "success": true, "code": 0, "message": "..."}
|
||||
$data = $this->post('/api/sw1/ShahkarLite', [
|
||||
'nationalCode' => $nationalCode,
|
||||
'mobile' => $mobile,
|
||||
]);
|
||||
|
||||
return $this->extractMatched($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* تطبیق شبا با کد ملی و تاریخ تولد.
|
||||
*
|
||||
* پاسخ سرویس فقط نتیجهی boolean (`data`) میدهد و نام بانک/صاحب حساب را برنمیگرداند.
|
||||
*
|
||||
* @param string $birthDate تاریخ تولد شمسی به فرمت Y/m/d (مثلاً 1370/01/01)
|
||||
* @return array{matched: bool, bank_name: ?string, owner_name: ?string}
|
||||
*/
|
||||
public function ibanMatch(string $iban, string $nationalCode, string $birthDate): array
|
||||
{
|
||||
$data = $this->post('/api/sw1/IbanMatch', [
|
||||
'iban' => $iban,
|
||||
'nationalCode' => $nationalCode,
|
||||
'birthDate' => $birthDate,
|
||||
]);
|
||||
|
||||
return [
|
||||
'matched' => $this->extractMatched($data),
|
||||
'bank_name' => null,
|
||||
'owner_name' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $payload
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function post(string $path, array $payload): array
|
||||
{
|
||||
if (!$this->isConfigured()) {
|
||||
throw new AppException(ErrorCodes::ERR_EXTERNAL_NOT_CONFIGURED, null, 503);
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this->httpClient->request('POST', rtrim($this->baseUrl, '/') . $path, [
|
||||
'json' => $payload,
|
||||
'headers' => ['Authorization' => 'Bearer ' . $this->token],
|
||||
'timeout' => 10,
|
||||
]);
|
||||
|
||||
$status = $response->getStatusCode();
|
||||
if ($status >= 500) {
|
||||
throw new AppException(ErrorCodes::ERR_EXTERNAL_001, null, 502);
|
||||
}
|
||||
|
||||
return $response->toArray(false);
|
||||
} catch (AppException $e) {
|
||||
throw $e;
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->error('api.ir inquiry failed', ['path' => $path, 'error' => $e->getMessage()]);
|
||||
throw new AppException(ErrorCodes::ERR_EXTERNAL_001, null, 502);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* استخراج نتیجهی boolean از پاسخ api.ir.
|
||||
*
|
||||
* قالب پاسخ: {"data": true|false, "success": true, "code": 0, "message": "..."}
|
||||
* - success=false یعنی ورودی نامعتبر/خطای درخواست → نتیجه «عدم تطبیق».
|
||||
* - data همان نتیجهی boolean تطبیق است.
|
||||
*
|
||||
* @param array<string,mixed> $data
|
||||
*/
|
||||
private function extractMatched(array $data): bool
|
||||
{
|
||||
if (($data['success'] ?? false) !== true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return filter_var($data['data'] ?? false, FILTER_VALIDATE_BOOLEAN);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user