diff --git a/backend/app/Models/FibTransaction.php b/backend/app/Models/FibTransaction.php index 23c37102..c208c0a7 100644 --- a/backend/app/Models/FibTransaction.php +++ b/backend/app/Models/FibTransaction.php @@ -9,4 +9,6 @@ class FibTransaction extends Model { /** @use HasFactory<\Database\Factories\FibTransactionFactory> */ use HasFactory; + + protected $guarded = ['id']; } diff --git a/backend/app/Services/FIB/FIBClient.php b/backend/app/Services/FIB/FIBClient.php new file mode 100755 index 00000000..be22b64c --- /dev/null +++ b/backend/app/Services/FIB/FIBClient.php @@ -0,0 +1,86 @@ +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/FIBPaymentCreateDTO.php b/backend/app/Services/Payments/DTO/FIBPaymentCreateDTO.php new file mode 100644 index 00000000..e09cab6b --- /dev/null +++ b/backend/app/Services/Payments/DTO/FIBPaymentCreateDTO.php @@ -0,0 +1,38 @@ +paymentId = $data['paymentId']; + $this->readableCode = $data['readableCode']; + $this->qrCode = $data['qrCode']; + $this->validUntil = $data['validUntil']; + $this->personalAppLink = $data['personalAppLink']; + $this->businessAppLink = $data['businessAppLink']; + $this->corporateAppLink = $data['corporateAppLink']; + } + + 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, + ]); + } +} diff --git a/backend/app/Services/Payments/DTO/PaymentCreateDTO.php b/backend/app/Services/Payments/DTO/PaymentCreateDTO.php new file mode 100644 index 00000000..a3129a3a --- /dev/null +++ b/backend/app/Services/Payments/DTO/PaymentCreateDTO.php @@ -0,0 +1,46 @@ +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']; + $this->ip = $data['ip']; + $this->user_gateway_id = $data['user_gateway_id']; + + } + + public static function fromRequest(Request $request): 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, + 'ip' => $request->getClientIp(), + 'user_gateway_id' => $request->user_gateway_id, + ]); + } +} diff --git a/backend/app/Services/Payments/FibAdapter.php b/backend/app/Services/Payments/FibAdapter.php new file mode 100644 index 00000000..0e6adb2b --- /dev/null +++ b/backend/app/Services/Payments/FibAdapter.php @@ -0,0 +1,37 @@ +create([ + 'user_id' => auth()->id(), + 'username' => $data['username'], + 'track_id' => $data['track_id'], + 'amount' => $data['amount'], + 'currency' => $data['currency'], + 'callback_url' => $data['callback_url'], + 'expires_at' => $data['expires_at'], + 'status_id' => $data['status_id'], + 'status_name' => $data['status_name'], + 'ip' => $data['ip'], + 'user_gateway_id' => $data['user_gateway_id'], + 'created_at' => $data['created_at'], + 'updated_at' => $data['updated_at'], + ]); + $this->fib_client->client(); + } +} diff --git a/backend/app/Services/Payments/PaymentService.php b/backend/app/Services/Payments/PaymentService.php new file mode 100644 index 00000000..d50d8b7f --- /dev/null +++ b/backend/app/Services/Payments/PaymentService.php @@ -0,0 +1,72 @@ +fib_client = new FibClient(); + } + + public function createPayment(PaymentCreateDTO $dto) + { + try { + $payment = Payment::query()->create([ + 'user_id' => $dto->user_id, + 'username' => $dto->username, + 'track_id' => $dto->track_id, + 'amount' => $dto->amount, + 'currency' => $dto->currency, + 'callback_url' => $dto->callback_url, + 'status_id' => PaymentStatusEnum::Unpaid->value, + 'status_name' => PaymentStatusEnum::Unpaid->name, + 'ip' => $dto->ip, + 'user_gateway_id' => $dto->user_gateway_id, + ]); + + $response = $this->fib_client->createPaymentService(); + $fib_response_dto = FIBPaymentCreateDTO::fromResponse($response); + + $transaction = Transaction::query()->create([ + 'user_id' => $payment->user_id, + 'amount' => $payment->amount, + 'currency' => $payment->currency, + 'callback_url' => $payment->callback_url, +// 'description' => '', +// 'expires_in' => '', + 'payment_id' => $payment->id, + 'payment_track_id' => $payment->track_id, + 'user_gateway_id' => $payment->user_gateway_id, +// 'status' => '', + ]); + + $fib_transactions = FibTransaction::query()->create([ + 'transaction_id' => $transaction->id, + 'qrcode' => $fib_response_dto->qrCode, + 'readable_code' => $fib_response_dto->readableCode, + 'personalAppLink' => $fib_response_dto->personalAppLink, + 'businessAppLink' => $fib_response_dto->businessAppLink, + 'corporateAppLink' => $fib_response_dto->corporateAppLink, + 'paymentId' => $fib_response_dto->paymentId, + 'paymentMethod' => 'fib', + 'validUntil' => $fib_response_dto->validUntil, + 'status_id' => PaymentStatusEnum::Unpaid->value, + 'status_name' => PaymentStatusEnum::Unpaid->name, + ]); + return $payment; + } catch (\Exception $exception) { + throw new \Exception($exception->getMessage()); + } + } +} diff --git a/backend/app/User/Controllers/Payment/PaymentController.php b/backend/app/User/Controllers/Payment/PaymentController.php new file mode 100644 index 00000000..69e7cd97 --- /dev/null +++ b/backend/app/User/Controllers/Payment/PaymentController.php @@ -0,0 +1,35 @@ +payment_service = $payment_service; + } + + public function createPayment(StoreRequest $request): JsonResponse + { + try { + $dto = PaymentCreateDTO::fromRequest($request); + $result = $this->payment_service->createPayment($dto); + return $this->successResponse($result); + } catch (\Exception $exception) { + throw $exception; + } + } +} diff --git a/backend/app/User/Requests/Payment/FIB/StoreRequest.php b/backend/app/User/Requests/Payment/FIB/StoreRequest.php new file mode 100644 index 00000000..7ef61312 --- /dev/null +++ b/backend/app/User/Requests/Payment/FIB/StoreRequest.php @@ -0,0 +1,36 @@ +user()->kyc_status; + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules(): array + { + return [ + 'username' => ['required'], + 'track_id' => ['required'], + 'amount' => ['required'], + 'currency' => ['required'], + 'callback_url' => ['required'], + 'user_gateway_id' => ['required'], + ]; + } +} diff --git a/backend/database/migrations/2026_05_18_095610_create_transactions_table.php b/backend/database/migrations/2026_05_18_095610_create_transactions_table.php index 208f6f8e..0ffccd66 100644 --- a/backend/database/migrations/2026_05_18_095610_create_transactions_table.php +++ b/backend/database/migrations/2026_05_18_095610_create_transactions_table.php @@ -4,8 +4,7 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ +return new class extends Migration { /** * Run the migrations. */ @@ -14,15 +13,15 @@ return new class extends Migration Schema::create('transactions', function (Blueprint $table) { $table->id(); $table->foreignId('user_id')->constrained('users'); - $table->string('amount'); - $table->string('currency'); - $table->string('callback_url'); - $table->string('description'); - $table->dateTime('expires_in'); + $table->string('amount')->nullable(); + $table->string('currency')->nullable(); + $table->string('callback_url')->nullable(); + $table->string('description')->nullable(); + $table->dateTime('expires_in')->nullable(); $table->foreignId('payment_id')->constrained('payments'); - $table->string('payment_track_id'); + $table->string('payment_track_id')->nullable(); $table->foreignId('user_gateway_id')->constrained('user_gateways'); - $table->integer('status'); + $table->integer('status')->nullable(); $table->timestamps(); }); } diff --git a/backend/database/migrations/2026_05_18_104524_create_fib_transactions_table.php b/backend/database/migrations/2026_05_18_104524_create_fib_transactions_table.php index 3e914953..2b346264 100644 --- a/backend/database/migrations/2026_05_18_104524_create_fib_transactions_table.php +++ b/backend/database/migrations/2026_05_18_104524_create_fib_transactions_table.php @@ -4,8 +4,7 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ +return new class extends Migration { /** * Run the migrations. */ @@ -14,23 +13,23 @@ return new class extends Migration Schema::create('fib_transactions', function (Blueprint $table) { $table->id(); $table->foreignId('transaction_id')->constrained('transactions'); - $table->text('qrcode'); - $table->string('readable_code'); - $table->string('personalAppLink'); - $table->string('businessAppLink'); - $table->string('corporateAppLink'); - $table->string('paymentId'); - $table->string('paymentMethod'); - $table->timestamp('validUntil'); + $table->text('qrcode')->nullable(); + $table->string('readable_code')->nullable(); + $table->string('personalAppLink')->nullable(); + $table->string('businessAppLink')->nullable(); + $table->string('corporateAppLink')->nullable(); + $table->string('paymentId')->nullable(); + $table->string('paymentMethod')->nullable(); + $table->timestamp('validUntil')->nullable(); $table->foreignId('status_id')->constrained('fib_transaction_statuses'); - $table->string('status_name'); - $table->timestamp('paidAt'); - $table->string('paidAmount'); - $table->string('paidCurrency'); - $table->string('decliningReason'); - $table->timestamp('declinedAt'); - $table->string('paidByName'); - $table->string('paidByIban'); + $table->string('status_name')->nullable(); + $table->timestamp('paidAt')->nullable(); + $table->string('paidAmount')->nullable(); + $table->string('paidCurrency')->nullable(); + $table->string('decliningReason')->nullable(); + $table->timestamp('declinedAt')->nullable(); + $table->string('paidByName')->nullable(); + $table->string('paidByIban')->nullable(); $table->timestamps(); }); } diff --git a/backend/database/seeders/FibTransactionStatusSeeder.php b/backend/database/seeders/FibTransactionStatusSeeder.php index 0949ff45..f634b061 100644 --- a/backend/database/seeders/FibTransactionStatusSeeder.php +++ b/backend/database/seeders/FibTransactionStatusSeeder.php @@ -4,6 +4,7 @@ namespace Database\Seeders; use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; +use Illuminate\Support\Facades\DB; class FibTransactionStatusSeeder extends Seeder { @@ -12,6 +13,12 @@ class FibTransactionStatusSeeder extends Seeder */ public function run(): void { - // + DB::table('fib_transaction_statuses')->insert([ + ['id' => 0, 'name' => 'unpaid', 'created_at' => now(), 'updated_at' => now(),], + ['id' => 1, 'name' => 'cancelled', 'created_at' => now(), 'updated_at' => now(),], + ['id' => 2, 'name' => 'expired', 'created_at' => now(), 'updated_at' => now(),], + ['id' => 3, 'name' => 'paid', 'created_at' => now(), 'updated_at' => now(),], + ['id' => 4, 'name' => 'refunded', 'created_at' => now(), 'updated_at' => now(),], + ]); } } diff --git a/backend/routes/api.php b/backend/routes/api.php index f35f6f84..5eaa6efa 100755 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -1,8 +1,19 @@ user(); })->middleware('auth:api'); + +Route::prefix('v1')->group(function () { + Route::prefix('payment') +// ->middleware('auth:api') + ->group(function () { + Route::post('/create_payment', [PaymentController::class, 'createPayment'])->name('create_payment'); + }); + +}); diff --git a/docker-compose.yml b/docker-compose.yml index d21e5ef4..a93a5001 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -40,9 +40,7 @@ services: - payment backend: - build: - context: ${BACKEND_PATH} - dockerfile: Dockerfile + image: payment-backend:latest container_name: laravel tty: true restart: unless-stopped @@ -54,6 +52,8 @@ services: - postgres networks: - payment + command: ["php", "artisan", "serve", "--host=0.0.0.0", "--port=9000"] + # Prints "Overridden command!" # logging: # driver: "syslog" # options: