From 3731cd67fe733cbafdde5514445a65702c9f2a8e Mon Sep 17 00:00:00 2001 From: ncoronel Date: Mon, 6 Jul 2026 09:57:45 -0300 Subject: [PATCH] feat: implement Telepagos webhook handling with request validation and service integration --- .../TelepagosWebhookController.php | 33 +++++++ .../Requests/TelepagosWebhookRequest.php | 28 ++++++ .../Services/TelepagosWebhookService.php | 92 +++++++++++++++++++ app/Domains/Integration/routes/api.php | 2 + app/Domains/Purchase/Models/Purchase.php | 29 ++++++ .../Purchase/Models/TelepagosPayment.php | 43 +++++++++ app/Domains/Purchase/Models/TelepagosQr.php | 35 +++++++ ...124304_create_telepagos_payments_table.php | 37 ++++++++ ...07_06_124305_create_telepagos_qr_table.php | 30 ++++++ 9 files changed, 329 insertions(+) create mode 100644 app/Domains/Integration/Controllers/TelepagosWebhookController.php create mode 100644 app/Domains/Integration/Requests/TelepagosWebhookRequest.php create mode 100644 app/Domains/Integration/Services/TelepagosWebhookService.php create mode 100644 app/Domains/Purchase/Models/TelepagosPayment.php create mode 100644 app/Domains/Purchase/Models/TelepagosQr.php create mode 100644 database/migrations/2026_07_06_124304_create_telepagos_payments_table.php create mode 100644 database/migrations/2026_07_06_124305_create_telepagos_qr_table.php diff --git a/app/Domains/Integration/Controllers/TelepagosWebhookController.php b/app/Domains/Integration/Controllers/TelepagosWebhookController.php new file mode 100644 index 0000000..cdb6347 --- /dev/null +++ b/app/Domains/Integration/Controllers/TelepagosWebhookController.php @@ -0,0 +1,33 @@ +validated('id'); + + $service->handleWebhook($tenantCodigo, $cashinId); + + return response()->json(['status' => 'success']); + } catch (\Exception $e) { + return response()->json(['status' => 'error', 'message' => $e->getMessage()], 500); + } + } +} diff --git a/app/Domains/Integration/Requests/TelepagosWebhookRequest.php b/app/Domains/Integration/Requests/TelepagosWebhookRequest.php new file mode 100644 index 0000000..3c97f42 --- /dev/null +++ b/app/Domains/Integration/Requests/TelepagosWebhookRequest.php @@ -0,0 +1,28 @@ + + */ + public function rules(): array + { + return [ + 'id' => ['required', 'string'], + ]; + } +} diff --git a/app/Domains/Integration/Services/TelepagosWebhookService.php b/app/Domains/Integration/Services/TelepagosWebhookService.php new file mode 100644 index 0000000..19ae609 --- /dev/null +++ b/app/Domains/Integration/Services/TelepagosWebhookService.php @@ -0,0 +1,92 @@ +firstOrFail(); + + $telepagosService = new TelepagosIntegrationService(); + $telepagosService->forTenant($tenant); + + try { + $details = $telepagosService->getCashinDetails($cashinId); + + // Extract the qr_order_id from the response + // The exact path depends on Telepagos response structure, assuming it's inside 'data' + // Need to handle potential nested structures based on actual API + $qrOrderId = $details['data']['qr_order_id'] ?? $details['qr_order_id'] ?? null; + $amount = $details['data']['amount'] ?? $details['amount'] ?? 0; + + if (!$qrOrderId) { + Log::warning("Telepagos webhook: qr_order_id not found for cashin {$cashinId}"); + return; + } + + $telepagosQr = TelepagosQr::where('qr_order_id', $qrOrderId)->first(); + + if (!$telepagosQr) { + Log::warning("Telepagos webhook: QR {$qrOrderId} not found in database for cashin {$cashinId}"); + return; + } + + $compra = $telepagosQr->compra; + + if (!$compra) { + Log::warning("Telepagos webhook: Purchase not found for QR {$qrOrderId}"); + return; + } + + $totalAmount = $compra->getTotalAmount(); + + // Depending on precision, we might need a looser comparison, but == should work for floats here + if ($amount != $totalAmount) { + Log::warning("Telepagos webhook: Amount mismatch. Cashin amount: {$amount}, Purchase amount: {$totalAmount}"); + // In a real scenario, you might want to record a partial payment or flag it, + // but the instructions say to finalize ONLY if the amount is correct. + return; + } + + // Create the TelepagosPayment and finalize the purchase in a transaction + $paymentData = [ + 'compra_id' => $compra->id, + 'cuit_buyer' => $details['data']['cuit_buyer'] ?? $details['cuit_buyer'] ?? null, + 'cvu_buyer' => $details['data']['cvu_buyer'] ?? $details['cvu_buyer'] ?? null, + 'amount' => $amount, + 'concept' => $details['data']['concept'] ?? $details['concept'] ?? null, + 'operation' => $details['data']['operation'] ?? $details['operation'] ?? null, + 'operation_id' => $details['data']['operation_id'] ?? $details['operation_id'] ?? null, + 'transaction_id' => $details['data']['transaction_id'] ?? $details['transaction_id'] ?? null, + 'qr_order_id' => $qrOrderId, + 'link_id' => $details['data']['link_id'] ?? $details['link_id'] ?? null, + ]; + + \Illuminate\Support\Facades\DB::transaction(function () use ($compra, $paymentData) { + TelepagosPayment::create($paymentData); + $compra->finalize(); + }); + + Log::info("Telepagos webhook: Successfully processed cashin {$cashinId} for purchase {$compra->id}"); + + } catch (Exception $e) { + Log::error("Telepagos webhook error: " . $e->getMessage()); + throw $e; + } + } +} diff --git a/app/Domains/Integration/routes/api.php b/app/Domains/Integration/routes/api.php index f63a3cd..910cbda 100644 --- a/app/Domains/Integration/routes/api.php +++ b/app/Domains/Integration/routes/api.php @@ -17,3 +17,5 @@ Route::group(['prefix' => '{tenant_code}/integrations'], function () { Route::get('/{integration_code}', [TenantIntegrationController::class, 'show']); Route::post('/{integration_code}', [TenantIntegrationController::class, 'store']); }); + +Route::post('webhooks/telepagos/{tenant_codigo}', [\App\Domains\Integration\Controllers\TelepagosWebhookController::class, 'handle']); diff --git a/app/Domains/Purchase/Models/Purchase.php b/app/Domains/Purchase/Models/Purchase.php index f7a08ca..2b8df34 100644 --- a/app/Domains/Purchase/Models/Purchase.php +++ b/app/Domains/Purchase/Models/Purchase.php @@ -57,4 +57,33 @@ class Purchase extends Model { return $this->hasMany(PurchaseItem::class, 'compra_id'); } + + /** + * @return \Illuminate\Database\Eloquent\Relations\HasOne + */ + public function telepagosQr() + { + return $this->hasOne(TelepagosQr::class, 'compra_id'); + } + + /** + * @return HasMany + */ + public function telepagosPayments(): HasMany + { + return $this->hasMany(TelepagosPayment::class, 'compra_id'); + } + + public function getTotalAmount(): float + { + return (float) $this->items()->sum('total'); + } + + public function finalize(): void + { + $this->update([ + 'status' => 'pagado', // Or the equivalent final status in your system + 'payment_status' => 'paid', + ]); + } } diff --git a/app/Domains/Purchase/Models/TelepagosPayment.php b/app/Domains/Purchase/Models/TelepagosPayment.php new file mode 100644 index 0000000..20f4729 --- /dev/null +++ b/app/Domains/Purchase/Models/TelepagosPayment.php @@ -0,0 +1,43 @@ + 'integer', + 'amount' => 'decimal:2', + ]; + } + + /** + * @return BelongsTo + */ + public function compra(): BelongsTo + { + return $this->belongsTo(Purchase::class, 'compra_id'); + } +} diff --git a/app/Domains/Purchase/Models/TelepagosQr.php b/app/Domains/Purchase/Models/TelepagosQr.php new file mode 100644 index 0000000..8968b67 --- /dev/null +++ b/app/Domains/Purchase/Models/TelepagosQr.php @@ -0,0 +1,35 @@ + 'integer', + ]; + } + + /** + * @return BelongsTo + */ + public function compra(): BelongsTo + { + return $this->belongsTo(Purchase::class, 'compra_id'); + } +} diff --git a/database/migrations/2026_07_06_124304_create_telepagos_payments_table.php b/database/migrations/2026_07_06_124304_create_telepagos_payments_table.php new file mode 100644 index 0000000..7fe2dca --- /dev/null +++ b/database/migrations/2026_07_06_124304_create_telepagos_payments_table.php @@ -0,0 +1,37 @@ +id(); + $table->foreignId('compra_id')->constrained('compras')->onDelete('cascade'); + $table->string('cuit_buyer')->nullable(); + $table->string('cvu_buyer')->nullable(); + $table->decimal('amount', 10, 2); + $table->string('concept')->nullable(); + $table->string('operation')->nullable(); + $table->string('operation_id')->nullable(); + $table->string('transaction_id')->nullable(); + $table->string('qr_order_id')->nullable(); + $table->string('link_id')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('telepagos_payments'); + } +}; diff --git a/database/migrations/2026_07_06_124305_create_telepagos_qr_table.php b/database/migrations/2026_07_06_124305_create_telepagos_qr_table.php new file mode 100644 index 0000000..67580a0 --- /dev/null +++ b/database/migrations/2026_07_06_124305_create_telepagos_qr_table.php @@ -0,0 +1,30 @@ +id(); + $table->foreignId('compra_id')->constrained('compras')->onDelete('cascade'); + $table->string('qr_order_id'); + $table->text('qr_code'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('telepagos_qr'); + } +};