diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 0c4e4c8..93ab4dc 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -830,33 +830,60 @@ class Controller extends BaseController Storage::put('refreshTokenGibit', 'توکن رفرش اولیه'); } - $apiData['refresh'] = Http::withoutVerifying()->withToken(Storage::get('refreshTokenGibit')) - ->post('https://napi.jibit.ir/ide/v1/tokens/generate', [ - 'apiKey' => 'J5WxrPxPOg', - 'secretKey' => '7dgeE70xQXsQL7r36j3wif6X9', - ])->json(); + $apiData['refresh'] = $this->jibitHttpJson( + Http::withoutVerifying()->withToken(Storage::get('refreshTokenGibit')) + ->post('https://napi.jibit.ir/ide/v1/tokens/generate', [ + 'apiKey' => 'J5WxrPxPOg', + 'secretKey' => '7dgeE70xQXsQL7r36j3wif6X9', + ]) + ); Storage::put('accessTokenGibit', $apiData['refresh']['accessToken'] ?? ''); if (!empty($apiData['refresh']['refreshToken'])) { Storage::put('refreshTokenGibit', $apiData['refresh']['refreshToken']); } - $response = Http::withoutVerifying()->withToken(Storage::get('accessTokenGibit')) - ->get("https://napi.jibit.ir/ide/v1/services/matching?mobileNumber=$mobile&nationalCode=$nationalCode") - ->json(); + if (empty($apiData['refresh']['accessToken'])) { + return [ + 'code' => 'token_failed', + 'http_status' => $apiData['refresh']['http_status'] ?? null, + 'message' => $apiData['refresh']['message'] ?? '', + ]; + } - if ((isset($response['code'])) && $response['code'] == 'forbidden') { - $refreshToken = Http::withoutVerifying()->withToken(Storage::get('refreshTokenGibit')) - ->post('https://napi.jibit.ir/ide/v1/tokens/generate', [ - 'apiKey' => 'J5WxrPxPOg', - 'secretKey' => '7dgeE70xQXsQL7r36j3wif6X9', - ])->json(); + $response = $this->jibitHttpJson( + Http::withoutVerifying()->withToken(Storage::get('accessTokenGibit')) + ->get('https://napi.jibit.ir/ide/v1/services/matching', [ + 'mobileNumber' => $mobile, + 'nationalCode' => $nationalCode, + ]) + ); + + if (($response['code'] ?? null) == 'forbidden') { + $refreshToken = $this->jibitHttpJson( + Http::withoutVerifying()->withToken(Storage::get('refreshTokenGibit')) + ->post('https://napi.jibit.ir/ide/v1/tokens/generate', [ + 'apiKey' => 'J5WxrPxPOg', + 'secretKey' => '7dgeE70xQXsQL7r36j3wif6X9', + ]) + ); Storage::put('accessTokenGibit', $refreshToken['accessToken'] ?? ''); if (!empty($refreshToken['refreshToken'])) { Storage::put('refreshTokenGibit', $refreshToken['refreshToken']); } - $response = Http::withoutVerifying()->withToken(Storage::get('accessTokenGibit')) - ->get("https://napi.jibit.ir/ide/v1/services/matching?mobileNumber=$mobile&nationalCode=$nationalCode") - ->json(); + if (empty($refreshToken['accessToken'])) { + return [ + 'code' => 'token_failed', + 'http_status' => $refreshToken['http_status'] ?? null, + 'message' => $refreshToken['message'] ?? '', + ]; + } + $response = $this->jibitHttpJson( + Http::withoutVerifying()->withToken(Storage::get('accessTokenGibit')) + ->get('https://napi.jibit.ir/ide/v1/services/matching', [ + 'mobileNumber' => $mobile, + 'nationalCode' => $nationalCode, + ]) + ); } $counter->value = (int) $counter->value + 1; @@ -865,6 +892,25 @@ class Controller extends BaseController return $response; } + /** + * @param \Illuminate\Http\Client\Response $pending + * @return array + */ + protected function jibitHttpJson($pending): array + { + $decoded = $pending->json(); + if (! is_array($decoded)) { + return [ + 'code' => 'empty_response', + 'http_status' => $pending->status(), + 'message' => '', + ]; + } + $decoded['http_status'] = $pending->status(); + + return $decoded; + } + public static function jibit($method, $p1 = null, $p2 = null, $p3 = null) { if (!app(ConfigService::class)->isJibitEnabled()) { diff --git a/app/Http/Controllers/Customer/CustomerController.php b/app/Http/Controllers/Customer/CustomerController.php index a2dfe99..9858ed3 100644 --- a/app/Http/Controllers/Customer/CustomerController.php +++ b/app/Http/Controllers/Customer/CustomerController.php @@ -18,9 +18,11 @@ use Rooyesh\Wallet\models\Transaction; use App\Models\Setting; use App\Http\Controllers\Admin\TahesabController; use App\Services\ConfigService; +use App\Services\JibitIdentityService; use App\Services\ReferralGiftService; use App\Services\WalletBalanceService; use Carbon\Carbon; +use Illuminate\Database\QueryException; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Schema; @@ -207,9 +209,30 @@ class CustomerController extends Controller // $tah->store(["customer_id" => $customer->id]); $cfg = app(ConfigService::class); if (!$wasLegal && $cfg->isRegisterJibitVerifyEnabled()) { - $response = $this->jibitNcode($customer->n_code, $customer->mobile); + $nCodeError = JibitIdentityService::nationalCodeError($customer->n_code); + if ($nCodeError !== null) { + return response()->json([ + 'message' => $nCodeError, + 'code' => 'invalid_national_code', + ], 422); + } - if (isset($response['matched']) and $response['matched'] == true) { + try { + $response = $this->jibitNcode($customer->n_code, $customer->mobile); + } catch (\Throwable $e) { + Log::error('customer updateMe jibit unreachable', [ + 'customer_id' => $customer->id, + 'message' => $e->getMessage(), + ]); + + return response()->json([ + 'message' => 'ارتباط با سرویس احراز هویت جیبیت برقرار نشد. اطلاعات پروفایل ذخیره شد اما هویت تأیید نشد.', + 'code' => 'jibit_unreachable', + ], 422); + } + + $jibit = JibitIdentityService::interpretNationalMobileMatch($response); + if ($jibit['ok']) { $customer->is_legal = 1; $customer->save(); // واریز پاداش معرف مستقل از has_page_earn_money (آن فقط منوی اپ است) @@ -222,18 +245,18 @@ class CustomerController extends Controller true ); } else { - if (isset($response['matched']) && $response['matched'] === false) { - return response()->json([ - 'message' => 'کد ملی و شماره موبایل با یکدیگر مطابقت ندارند. لطفاً هر دو را با دقت بررسی کرده و دوباره ثبت کنید.', - ], 422); - } + Log::warning('customer updateMe jibit not verified', [ + 'customer_id' => $customer->id, + 'code' => $jibit['code'], + 'jibit_code' => is_array($response) ? ($response['code'] ?? null) : null, + 'http_status' => is_array($response) ? ($response['http_status'] ?? null) : null, + ]); return response()->json([ - 'message' => 'کد ملی ثبت شد، اما در حال حاضر سیستم نتوانست آن را تأیید کند. لطفاً منتظر بررسی و تأیید مدیر بمانید.', + 'message' => $jibit['message'], + 'code' => $jibit['code'], ], 422); } - - } elseif (!$customer->is_legal && !$cfg->isRegisterJibitVerifyEnabled()) { return response()->json([ 'ok' => true, @@ -242,13 +265,47 @@ class CustomerController extends Controller ]); } return response()->json(true); + } catch (QueryException $e) { + Log::error('customer updateMe failed', [ + 'customer_id' => auth('customer')->id(), + 'message' => $e->getMessage(), + ]); + $driverCode = (int) ($e->errorInfo[1] ?? 0); + $sqlMessage = (string) ($e->errorInfo[2] ?? $e->getMessage()); + if ($driverCode === 1062) { + if (str_contains($sqlMessage, 'n_code')) { + return response()->json([ + 'message' => 'این کد ملی قبلاً برای حساب دیگری ثبت شده است.', + 'code' => 'duplicate_national_code', + ], 422); + } + if (str_contains($sqlMessage, 'mobile')) { + return response()->json([ + 'message' => 'این شماره موبایل قبلاً برای حساب دیگری ثبت شده است.', + 'code' => 'duplicate_mobile', + ], 422); + } + + return response()->json([ + 'message' => 'این مقدار قبلاً در سیستم ثبت شده و تکراری است.', + 'code' => 'duplicate_value', + ], 422); + } + + return response()->json([ + 'message' => 'ذخیره اطلاعات در دیتابیس انجام نشد. جزئیات در لاگ سرور ثبت شد.', + 'code' => 'db_error', + ], 422); } catch (\Exception $e) { Log::error('customer updateMe failed', [ 'customer_id' => auth('customer')->id(), 'message' => $e->getMessage(), ]); - return response()->json(['message' => 'خطا در ذخیره اطلاعات. لطفاً دوباره تلاش کنید.'], 422); + return response()->json([ + 'message' => 'خطا در ذخیره اطلاعات. لطفاً دوباره تلاش کنید.', + 'code' => 'update_failed', + ], 422); } } diff --git a/app/Services/JibitIdentityService.php b/app/Services/JibitIdentityService.php new file mode 100644 index 0000000..16a8410 --- /dev/null +++ b/app/Services/JibitIdentityService.php @@ -0,0 +1,153 @@ + false, + 'code' => 'jibit_no_response', + 'message' => 'پاسخ سرویس احراز هویت دریافت نشد. اتصال سرور به جیبیت یا توکن را بررسی کنید.', + ]; + } + + if (array_key_exists('matched', $response) && self::isTruthyMatch($response['matched'])) { + return [ + 'ok' => true, + 'code' => 'jibit_matched', + 'message' => '', + ]; + } + + if (array_key_exists('matched', $response) && self::isFalseyMatch($response['matched'])) { + return [ + 'ok' => false, + 'code' => 'jibit_mismatch', + 'message' => 'کد ملی و شماره موبایل در سامانه شاهکار مطابقت ندارند. سیم‌کارت باید به نام صاحب همین کد ملی باشد.', + ]; + } + + $apiCode = strtolower(trim((string) ($response['code'] ?? $response['errorCode'] ?? ''))); + $mapped = self::mapApiCode($apiCode); + if ($mapped !== null) { + return [ + 'ok' => false, + 'code' => $apiCode !== '' ? 'jibit_'.$apiCode : 'jibit_error', + 'message' => $mapped, + ]; + } + + $detail = trim(implode(' — ', array_filter([ + $apiCode !== '' ? 'کد جیبیت: '.$apiCode : '', + self::stringifyApiMessage($response), + isset($response['http_status']) ? 'وضعیت HTTP: '.$response['http_status'] : '', + ], fn ($part) => $part !== '' && $part !== null))); + + return [ + 'ok' => false, + 'code' => $apiCode !== '' ? 'jibit_'.$apiCode : 'jibit_unknown', + 'message' => 'احراز هویت از طرف جیبیت تأیید نشد' + .($detail !== '' ? ' ('.$detail.')' : '.') + .' اطلاعات پروفایل ذخیره شد؛ تا تأیید مدیر امکان معامله نیست.', + ]; + } + + public static function nationalCodeError(?string $nCode): ?string + { + $digits = preg_replace('/\D+/', '', (string) $nCode) ?? ''; + if ($digits === '') { + return 'کد ملی وارد نشده است.'; + } + if (strlen($digits) !== 10) { + return 'کد ملی باید ۱۰ رقم باشد.'; + } + if (! self::isValidIranianNationalCode($digits)) { + return 'کد ملی از نظر رقم کنترل نامعتبر است.'; + } + + return null; + } + + public static function isValidIranianNationalCode(string $digits): bool + { + if (! preg_match('/^\d{10}$/', $digits)) { + return false; + } + if (preg_match('/^(\d)\1{9}$/', $digits)) { + return false; + } + + $sum = 0; + for ($i = 0; $i < 9; $i++) { + $sum += (int) $digits[$i] * (10 - $i); + } + $remainder = $sum % 11; + $check = (int) $digits[9]; + + return $remainder < 2 ? $check === $remainder : $check === (11 - $remainder); + } + + private static function isTruthyMatch($value): bool + { + return $value === true || $value === 1 || $value === '1' || $value === 'true'; + } + + private static function isFalseyMatch($value): bool + { + return $value === false || $value === 0 || $value === '0' || $value === 'false'; + } + + private static function mapApiCode(string $apiCode): ?string + { + return match ($apiCode) { + 'forbidden', 'unauthorized', 'unauthenticated' => 'اتصال به جیبیت مجاز نیست (توکن یا کلید API). با پشتیبانی فنی بررسی کنید.', + 'empty_response', 'token_failed' => 'سرویس احراز هویت پاسخ معتبر نداد. چند دقیقه دیگر دوباره تلاش کنید.', + 'too_many_requests', 'rate_limit', 'rate.limit' => 'تعداد استعلام احراز هویت بیش از حد مجاز است. کمی بعد دوباره تلاش کنید.', + 'invalid_national_code', 'nationalcode.invalid', 'national_code.invalid' => 'کد ملی از نظر سرویس احراز هویت نامعتبر است.', + 'invalid_mobile_number', 'mobilenumber.invalid', 'mobile.invalid' => 'شماره موبایل از نظر سرویس احراز هویت نامعتبر است.', + default => null, + }; + } + + /** + * @param array $response + */ + private static function stringifyApiMessage(array $response): string + { + $message = $response['message'] ?? null; + if (is_string($message) && trim($message) !== '') { + return self::sanitizeDetail($message); + } + + $errors = $response['errors'] ?? null; + if (is_array($errors)) { + foreach ($errors as $value) { + if (is_string($value) && trim($value) !== '') { + return self::sanitizeDetail($value); + } + if (is_array($value) && isset($value[0]) && is_string($value[0]) && trim($value[0]) !== '') { + return self::sanitizeDetail($value[0]); + } + } + } + + return ''; + } + + private static function sanitizeDetail(string $text): string + { + $plain = trim(html_entity_decode(strip_tags($text), ENT_QUOTES | ENT_HTML5, 'UTF-8')); + if (mb_strlen($plain) > 180) { + $plain = mb_substr($plain, 0, 180).'…'; + } + + return $plain; + } +} diff --git a/tests/Unit/JibitIdentityServiceTest.php b/tests/Unit/JibitIdentityServiceTest.php new file mode 100644 index 0000000..b2543e5 --- /dev/null +++ b/tests/Unit/JibitIdentityServiceTest.php @@ -0,0 +1,71 @@ + true]); + + $this->assertTrue($result['ok']); + $this->assertSame('jibit_matched', $result['code']); + } + + public function test_matched_false_explains_shahkar_mismatch(): void + { + $result = JibitIdentityService::interpretNationalMobileMatch(['matched' => false]); + + $this->assertFalse($result['ok']); + $this->assertSame('jibit_mismatch', $result['code']); + $this->assertStringContainsString('شاهکار', $result['message']); + $this->assertStringContainsString('سیم‌کارت', $result['message']); + } + + public function test_forbidden_mentions_token(): void + { + $result = JibitIdentityService::interpretNationalMobileMatch([ + 'code' => 'forbidden', + 'message' => 'no access', + ]); + + $this->assertFalse($result['ok']); + $this->assertSame('jibit_forbidden', $result['code']); + $this->assertStringContainsString('توکن', $result['message']); + } + + public function test_unknown_payload_includes_jibit_code_and_message(): void + { + $result = JibitIdentityService::interpretNationalMobileMatch([ + 'code' => 'some.service.error', + 'message' => 'provider down', + 'http_status' => 503, + ]); + + $this->assertFalse($result['ok']); + $this->assertSame('jibit_some.service.error', $result['code']); + $this->assertStringContainsString('some.service.error', $result['message']); + $this->assertStringContainsString('provider down', $result['message']); + $this->assertStringContainsString('503', $result['message']); + } + + public function test_empty_response_is_not_generic_wait_for_admin(): void + { + $result = JibitIdentityService::interpretNationalMobileMatch(null); + + $this->assertFalse($result['ok']); + $this->assertSame('jibit_no_response', $result['code']); + $this->assertStringNotContainsString('منتظر بررسی', $result['message']); + } + + public function test_national_code_checksum(): void + { + $this->assertNull(JibitIdentityService::nationalCodeError('0452262615')); + $this->assertSame('کد ملی وارد نشده است.', JibitIdentityService::nationalCodeError('')); + $this->assertSame('کد ملی باید ۱۰ رقم باشد.', JibitIdentityService::nationalCodeError('123')); + $this->assertSame('کد ملی از نظر رقم کنترل نامعتبر است.', JibitIdentityService::nationalCodeError('0000000000')); + } +}