diff --git a/backend/app/Enums/PaymentStatusEnum.php b/backend/app/Enums/PaymentStatusEnum.php index 78be8b94..24e0e9a6 100644 --- a/backend/app/Enums/PaymentStatusEnum.php +++ b/backend/app/Enums/PaymentStatusEnum.php @@ -2,7 +2,7 @@ namespace App\Enums; -enum PaymentStatusEnum:int +enum PaymentStatusEnum: int { case Unpaid = 0; case Cancelled = 1; @@ -10,6 +10,8 @@ enum PaymentStatusEnum:int case Paid = 3; case Refunded = 4; + case PendingVerification = 5; + public function label(): string { return match ($this) { @@ -18,6 +20,7 @@ enum PaymentStatusEnum:int self::Expired => 'expired', self::Paid => 'paid', self::Refunded => 'refunded', + self::PendingVerification => 'pending_verification', }; } } diff --git a/backend/app/Models/User.php b/backend/app/Models/User.php index 20e0ad4a..fc04ec71 100755 --- a/backend/app/Models/User.php +++ b/backend/app/Models/User.php @@ -24,7 +24,7 @@ class User extends Authenticatable public function documents(): MorphToMany { - return $this->morphToMany(Document::class, 'documentable'); + return $this->morphToMany(Document::class, 'documents'); } public function validateForPassportPasswordGrant(string $password): bool diff --git a/backend/app/Services/FIB/DTO/FIBPaymentCreateDTO.php b/backend/app/Services/FIB/DTO/FIBPaymentCreateDTO.php index 13ab44c2..a119f71d 100644 --- a/backend/app/Services/FIB/DTO/FIBPaymentCreateDTO.php +++ b/backend/app/Services/FIB/DTO/FIBPaymentCreateDTO.php @@ -26,13 +26,13 @@ class FIBPaymentCreateDTO public static function fromResponse($data): FIBPaymentCreateDTO { return new self([ - 'paymentId' => $data->paymentId, - 'readableCode' => $data->readableCode, - 'qrCode' => $data->qrCode, - 'validUntil' => $data->validUntil, - 'personalAppLink' => $data->personalAppLink, - 'businessAppLink' => $data->businessAppLink, - 'corporateAppLink' => $data->corporateAppLink, + 'paymentId' => $data['paymentId'], + 'readableCode' => $data['readableCode'], + 'qrCode' => $data['qrCode'], + 'validUntil' => $data['validUntil'], + 'personalAppLink' => $data['personalAppLink'], + 'businessAppLink' => $data['businessAppLink'], + 'corporateAppLink' => $data['corporateAppLink'], ]); } } diff --git a/backend/app/Services/FIB/FIBClient.php b/backend/app/Services/FIB/FIBClient.php deleted file mode 100755 index be22b64c..00000000 --- a/backend/app/Services/FIB/FIBClient.php +++ /dev/null @@ -1,86 +0,0 @@ -contentType('application/json') - ->withToken($this->getAccessToken()) - ->timeout(30); - } - - public function getAccessToken() - { - try { - if ($this->isNotProduction()) { - return json_decode( - file_get_contents(storage_path('app/mock/fib/token.json')) - ); - } - $response = Http::withOptions([ - 'proxy' => 'http://host.docker.internal:10808', - ])->asForm()->post('https://fib-stage.fib.iq/auth/realms/fib-online-shop/protocol/openid-connect/token', [ - 'grant_type' => 'client_credentials', - 'client_id' => 'ipay-test-payment', - 'client_secret' => '8dae3180-f39c-448e-8b45-a01c8f4c8c15', - ]); - - if ($response->failed()) { - throw new \Exception( - 'Unable to authenticate with FIB: ' . - $response->body() - ); - } - - $data = $response->json(); - - if (!isset($data['access_token'])) { - throw new \Exception('FIB token missing in response'); - } - - return $data['access_token']; - } catch (\Exception $exception) { - return $exception->getMessage(); - } - } - - public function createPaymentService() - { - if ($this->isNotProduction()) { - return json_decode( - file_get_contents(storage_path('app/mock/fib/payment_create_success.json')) - ); - } - $response = $this->client()->post("https://fib-stage.fib.iq/protected/v1/payments"); - if ($response->failed()) { - throw new \Exception( - 'Unable to authenticate with FIB: ' . - $response->body() - ); - } - - $data = $response->json(); - if (!isset($data['paymentId'])) { - throw new \Exception('FIB paymentId missing in response'); - } - return $data; - } - - public function isNotProduction() - { - return env('APP_ENV') === 'local'; - } -} diff --git a/backend/app/Services/Payments/DTO/PaymentCreateDTO.php b/backend/app/Services/Payments/DTO/PaymentCreateDTO.php index a3129a3a..6c986e19 100644 --- a/backend/app/Services/Payments/DTO/PaymentCreateDTO.php +++ b/backend/app/Services/Payments/DTO/PaymentCreateDTO.php @@ -9,7 +9,6 @@ class PaymentCreateDTO public string $username; public int $user_id; - public string $track_id; public string $amount; public string $currency; public string $callback_url; @@ -21,7 +20,6 @@ class PaymentCreateDTO { $this->username = $data['username']; $this->user_id = $data['user_id']; - $this->track_id = $data['track_id']; $this->amount = $data['amount']; $this->currency = $data['currency']; $this->callback_url = $data['callback_url']; @@ -35,7 +33,6 @@ class PaymentCreateDTO return new self([ 'username' => $request->username, 'user_id' => '2', - 'track_id' => uuid_create(), 'amount' => $request->amount, 'currency' => $request->currency, 'callback_url' => $request->callback_url, diff --git a/backend/app/Services/Payments/PaymentService.php b/backend/app/Services/Payments/PaymentService.php index ff4d0a21..8116ab6c 100644 --- a/backend/app/Services/Payments/PaymentService.php +++ b/backend/app/Services/Payments/PaymentService.php @@ -6,6 +6,8 @@ use App\Enums\PaymentStatusEnum; use App\Models\FibTransaction; use App\Models\Payment; use App\Models\Transaction; +use App\Models\UserGateway; +use App\Services\FIB\CheckPaymentStatusService; use App\Services\FIB\CreatePaymentService; use App\Services\FIB\DTO\FIBPaymentCreateDTO; use App\Services\Payments\DTO\PaymentCreateDTO; @@ -16,10 +18,16 @@ use Throwable; class PaymentService { protected CreatePaymentService $create_payment_service; + protected CheckPaymentStatusService $check_payment_status_service; - public function __construct(CreatePaymentService $create_payment_service) + + public function __construct( + CreatePaymentService $create_payment_service, + CheckPaymentStatusService $check_payment_status_service + ) { $this->create_payment_service = $create_payment_service; + $this->check_payment_status_service = $check_payment_status_service; } /** @@ -28,19 +36,33 @@ class PaymentService */ public function createPayment(PaymentCreateDTO $dto) { - return DB::transaction(function () use ($dto) { + $gateway = UserGateway::query() + ->where('id', $dto->user_gateway_id) + ->where('user_id', $dto->user_id) + ->firstOrFail(); + + return DB::transaction(function () use ($dto, $gateway) { $payment = Payment::query()->create([ 'user_id' => $dto->user_id, 'username' => $dto->username, - 'track_id' => $dto->track_id, + 'track_id' => $this->generateTrackId(), 'amount' => $dto->amount, 'currency' => $dto->currency, - 'callback_url' => $dto->callback_url, + 'callback_url' => 'https://user_callback', 'status_id' => PaymentStatusEnum::Unpaid->value, 'status_name' => PaymentStatusEnum::Unpaid->name, 'ip' => $dto->ip, - 'user_gateway_id' => $dto->user_gateway_id, + 'user_gateway_id' => $gateway->id, + ]); + + $this->create_payment_service->setInputParameters([ + 'monetaryValue' => [ + 'amount' => $dto->amount, + 'currency' => $dto->currency, + ], + 'statusCallbackUrl' => 'https://ipay/api/fib_callback', + 'description' => $dto->description ?? "Payment #{$payment->track_id}", ]); $response = $this->create_payment_service->run(); @@ -51,12 +73,12 @@ class PaymentService 'amount' => $payment->amount, 'currency' => $payment->currency, 'callback_url' => $payment->callback_url, - // 'description' => '', - // 'expires_in' => '', + // 'description' => '', + // 'expires_in' => '', 'payment_id' => $payment->id, 'payment_track_id' => $payment->track_id, 'user_gateway_id' => $payment->user_gateway_id, - // 'status' => '', + // 'status' => '', ]); FibTransaction::query()->create([ @@ -73,7 +95,82 @@ class PaymentService 'status_name' => PaymentStatusEnum::Unpaid->name, ]); - return $payment; + return [ + 'payment_id' => $payment->id, + 'track_id' => $payment->track_id, + 'qr_code' => $fib_response_dto->qrCode, + 'readable_code' => $fib_response_dto->readableCode, + 'personal_app_link' => $fib_response_dto->personalAppLink, + 'business_app_link' => $fib_response_dto->businessAppLink, + 'corporate_app_link' => $fib_response_dto->corporateAppLink, + 'valid_until' => $fib_response_dto->validUntil, + 'status' => PaymentStatusEnum::Unpaid->name, + ]; }); } + + private function generateTrackId(): string + { + do { + $id = uuid_create(); + } while (Payment::query()->where('track_id', $id)->exists()); + + return $id; + } + + public function checkPaymentStatus($track_id) + { + $user_id = auth()->id(); + return Payment::query()->where('track_id', $track_id) + ->where('user_id', 2) + ->firstOrFail(); + } + + public function verifyPayment($track_id) + { + $user_id = auth()->id(); + return DB::transaction(function () use ($track_id, $user_id) { + + $payment = Payment::query() + ->where('track_id', $track_id) + ->where('user_id', $user_id) + ->lockForUpdate() + ->firstOrFail(); + + if ($payment->status_id === PaymentStatusEnum::Paid->value) { + return [ + 'status' => 'already_verified', + 'paid' => true, + ]; + } + + if ($payment->status_id !== PaymentStatusEnum::PendingVerification->value) { + return [ + 'status' => $payment->status_name, + 'paid' => false, + ]; + } + + $payment->update([ + 'status_id' => PaymentStatusEnum::Paid->value, + 'status_name' => PaymentStatusEnum::Paid->name, + ]); + + Transaction::query() + ->where('payment_id', $payment->id) + ->update([ + 'status_id' => PaymentStatusEnum::Paid->value, + 'status_name' => PaymentStatusEnum::Paid->name, + ]); + + return [ + 'status' => 'verified', + 'paid' => true, + 'track_id' => $payment->track_id, + 'amount' => $payment->amount, + 'currency' => $payment->currency, + ]; + }); + + } } diff --git a/backend/app/User/Controllers/Payment/PaymentController.php b/backend/app/User/Controllers/Payment/PaymentController.php index 69e7cd97..6611ec51 100644 --- a/backend/app/User/Controllers/Payment/PaymentController.php +++ b/backend/app/User/Controllers/Payment/PaymentController.php @@ -6,9 +6,12 @@ use App\Http\Controllers\Controller; use App\Services\Payments\DTO\PaymentCreateDTO; use App\Services\Payments\PaymentService; use App\Traits\ApiResponse; +use App\User\Requests\Payment\FIB\InquiryRequest; use App\User\Requests\Payment\FIB\StoreRequest; +use App\User\Resources\Payment\InquiryResource; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Support\Js; class PaymentController extends Controller @@ -32,4 +35,20 @@ class PaymentController extends Controller throw $exception; } } + + public function checkPaymentStatus(InquiryRequest $request): JsonResponse + { + try { + $result = $this->payment_service->checkPaymentStatus($request->track_id); + return $this->successResponse(new InquiryResource($result)); + } catch (\Exception $exception) { + throw $exception; + } + } + + public function verify() + { + + } + } diff --git a/backend/app/User/Requests/Payment/FIB/InquiryRequest.php b/backend/app/User/Requests/Payment/FIB/InquiryRequest.php new file mode 100644 index 00000000..c05ac90c --- /dev/null +++ b/backend/app/User/Requests/Payment/FIB/InquiryRequest.php @@ -0,0 +1,30 @@ + + */ + public function rules(): array + { + return [ + 'track_id' => ['required', 'uuid'], + ]; + } +} diff --git a/backend/app/User/Requests/Payment/FIB/StoreRequest.php b/backend/app/User/Requests/Payment/FIB/StoreRequest.php index 7ef61312..84c83c86 100644 --- a/backend/app/User/Requests/Payment/FIB/StoreRequest.php +++ b/backend/app/User/Requests/Payment/FIB/StoreRequest.php @@ -25,12 +25,12 @@ class StoreRequest extends FormRequest public function rules(): array { return [ - 'username' => ['required'], - 'track_id' => ['required'], + 'username' => ['required', 'string'], 'amount' => ['required'], - 'currency' => ['required'], + 'currency' => ['required', 'string'], 'callback_url' => ['required'], - 'user_gateway_id' => ['required'], + 'user_gateway_id' => ['required', 'integer', 'exists:user_gateways,id'], + 'description' => ['nullable', 'string', 'max:500'], ]; } } diff --git a/backend/app/User/Resources/Payment/InquiryResource.php b/backend/app/User/Resources/Payment/InquiryResource.php new file mode 100755 index 00000000..05e4036c --- /dev/null +++ b/backend/app/User/Resources/Payment/InquiryResource.php @@ -0,0 +1,30 @@ + + */ + public function toArray(Request $request): array + { + return [ + "track_id" => $this->track_id, + "amount" => $this->amount, + "currency" => $this->currency, + "status_id" => $this->status_id, + "status_name" => $this->status_name, + 'payment_method' => $this->payment_method, + 'paid_currency' => $this->paid_currency, + 'paid_amount' => $this->paid_amount, + 'paid_at' => $this->paid_at, + 'verified_at' => $this->verified_at, + ]; + } +} diff --git a/backend/database/migrations/2026_06_05_142646_add_columns_to_payments_table.php b/backend/database/migrations/2026_06_05_142646_add_columns_to_payments_table.php new file mode 100755 index 00000000..0a427575 --- /dev/null +++ b/backend/database/migrations/2026_06_05_142646_add_columns_to_payments_table.php @@ -0,0 +1,34 @@ +string('payment_method')->nullable(); + $table->string('paid_currency')->nullable(); + $table->string('paid_amount')->nullable(); + $table->timestamp('paid_at')->nullable(); + $table->timestamp('verified_at')->nullable(); + $table->timestamp('refunded_at')->nullable(); + $table->string('refund_reason')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('payments', function (Blueprint $table) { + $table->dropColumn(['payment_method', 'paid_currency', 'paid_amount', + 'paid_at', 'verified_at', 'refunded_at', 'refund_reason',]); + }); + } +}; diff --git a/backend/routes/api.php b/backend/routes/api.php index 5eaa6efa..594f6249 100755 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -14,6 +14,8 @@ Route::prefix('v1')->group(function () { // ->middleware('auth:api') ->group(function () { Route::post('/create_payment', [PaymentController::class, 'createPayment'])->name('create_payment'); + Route::post('/inquiry', [PaymentController::class, 'checkPaymentStatus'])->name('inquiry'); + Route::post('/verify', [PaymentController::class, 'checkPaymentStatus'])->name('inquiry'); }); });