feat: implement Telepagos webhook handling with request validation and service integration

This commit is contained in:
ncoronel 2026-07-06 09:57:45 -03:00
parent 403caf77f5
commit 3731cd67fe
9 changed files with 329 additions and 0 deletions

View File

@ -0,0 +1,33 @@
<?php
namespace App\Domains\Integration\Controllers;
use App\Domains\Integration\Requests\TelepagosWebhookRequest;
use App\Domains\Integration\Services\TelepagosWebhookService;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class TelepagosWebhookController extends Controller
{
/**
* Handle the incoming Telepagos webhook.
*
* @param TelepagosWebhookRequest $request
* @param string $tenantCodigo
* @param TelepagosWebhookService $service
* @return JsonResponse
*/
public function handle(TelepagosWebhookRequest $request, string $tenantCodigo, TelepagosWebhookService $service): JsonResponse
{
try {
$cashinId = $request->validated('id');
$service->handleWebhook($tenantCodigo, $cashinId);
return response()->json(['status' => 'success']);
} catch (\Exception $e) {
return response()->json(['status' => 'error', 'message' => $e->getMessage()], 500);
}
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Domains\Integration\Requests;
use Illuminate\Foundation\Http\FormRequest;
class TelepagosWebhookRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
*/
public function rules(): array
{
return [
'id' => ['required', 'string'],
];
}
}

View File

@ -0,0 +1,92 @@
<?php
namespace App\Domains\Integration\Services;
use App\Domains\Purchase\Models\TelepagosPayment;
use App\Domains\Purchase\Models\TelepagosQr;
use App\Domains\Tenant\Models\Tenant;
use Exception;
use Illuminate\Support\Facades\Log;
class TelepagosWebhookService
{
/**
* Handle the Telepagos webhook notification.
*
* @param string $tenantCodigo
* @param string $cashinId
* @return void
* @throws Exception
*/
public function handleWebhook(string $tenantCodigo, string $cashinId): void
{
$tenant = Tenant::where('codigo', $tenantCodigo)->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;
}
}
}

View File

@ -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']);

View File

@ -57,4 +57,33 @@ class Purchase extends Model
{
return $this->hasMany(PurchaseItem::class, 'compra_id');
}
/**
* @return \Illuminate\Database\Eloquent\Relations\HasOne<TelepagosQr, $this>
*/
public function telepagosQr()
{
return $this->hasOne(TelepagosQr::class, 'compra_id');
}
/**
* @return HasMany<TelepagosPayment, $this>
*/
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',
]);
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Domains\Purchase\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
#[Fillable([
'compra_id',
'cuit_buyer',
'cvu_buyer',
'amount',
'concept',
'operation',
'operation_id',
'transaction_id',
'qr_order_id',
'link_id',
])]
class TelepagosPayment extends Model
{
use HasFactory;
protected $table = 'telepagos_payments';
protected function casts(): array
{
return [
'compra_id' => 'integer',
'amount' => 'decimal:2',
];
}
/**
* @return BelongsTo<Purchase, $this>
*/
public function compra(): BelongsTo
{
return $this->belongsTo(Purchase::class, 'compra_id');
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Domains\Purchase\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
#[Fillable([
'compra_id',
'qr_order_id',
'qr_code',
])]
class TelepagosQr extends Model
{
use HasFactory;
protected $table = 'telepagos_qr';
protected function casts(): array
{
return [
'compra_id' => 'integer',
];
}
/**
* @return BelongsTo<Purchase, $this>
*/
public function compra(): BelongsTo
{
return $this->belongsTo(Purchase::class, 'compra_id');
}
}

View File

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('telepagos_payments', function (Blueprint $table) {
$table->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');
}
};

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('telepagos_qr', function (Blueprint $table) {
$table->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');
}
};