feat(telepagos): enhance webhook handling

This commit is contained in:
ncoronel 2026-08-19 15:26:44 -03:00
parent 32d2b182c6
commit 161693509f
5 changed files with 145 additions and 13 deletions

View File

@ -39,6 +39,20 @@ class TelepagosWebhookService
$amount = $this->normalizeAmount($details['data']['amount'] ?? $details['amount'] ?? 0);
$operationId = $details['data']['operation_id'] ?? $details['operation_id'] ?? null;
$paymentData = [
'compra_id' => null,
'matched_purchase_ids' => null,
'cuit_buyer' => $details['data']['buyer']['cuit'] ?? $details['buyer']['cuit'] ?? null,
'cvu_buyer' => $details['data']['buyer']['cvu'] ?? $details['buyer']['cvu'] ?? 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,
];
$transferenciaOperationIds = [1, 3, 11];
$qrOperationIds = [31, 37, 47];
@ -68,12 +82,16 @@ class TelepagosWebhookService
->where('payment_method', 'transfer')
->where('total', $amount)
->latest()
->limit(2)
->get();
$paymentData['matched_purchase_ids'] = $purchases->pluck('id')->all();
$compra = $purchases->count() === 1 ? $purchases->first() : null;
if (! $compra) {
if ($purchases->count() > 1) {
TelepagosPayment::create($paymentData);
}
Log::channel('telepagos')->warning('Telepagos webhook: Expected exactly one matching purchase.', [
'client_code' => $client->code,
'cashin_id' => $cashinId,
@ -163,18 +181,7 @@ class TelepagosWebhookService
return;
}
$paymentData = [
'compra_id' => $compra->id,
'cuit_buyer' => $details['data']['buyer']['cuit'] ?? $details['buyer']['cuit'] ?? null,
'cvu_buyer' => $details['data']['buyer']['cvu'] ?? $details['buyer']['cvu'] ?? 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,
];
$paymentData['compra_id'] = $compra->id;
DB::transaction(function () use ($compra, $paymentData) {
TelepagosPayment::create($paymentData);

View File

@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
#[Fillable([
'compra_id',
'matched_purchase_ids',
'cuit_buyer',
'cvu_buyer',
'amount',
@ -29,6 +30,7 @@ class TelepagosPayment extends Model
{
return [
'compra_id' => 'integer',
'matched_purchase_ids' => 'array',
'amount' => 'decimal:2',
];
}

View File

@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('telepagos_payments', function (Blueprint $table) {
$table->foreignId('compra_id')->nullable()->change();
});
}
public function down(): void
{
Schema::table('telepagos_payments', function (Blueprint $table) {
$table->foreignId('compra_id')->nullable(false)->change();
});
}
};

View File

@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('telepagos_payments', function (Blueprint $table) {
$table->json('matched_purchase_ids')->nullable()->after('compra_id');
});
}
public function down(): void
{
Schema::table('telepagos_payments', function (Blueprint $table) {
$table->dropColumn('matched_purchase_ids');
});
}
};

View File

@ -14,6 +14,7 @@ use App\Domains\Catalog\Models\Variant;
use App\Domains\Integration\Models\ClientIntegration;
use App\Domains\Integration\Models\Integration;
use App\Domains\Purchase\Models\Purchase;
use App\Domains\Purchase\Models\TelepagosPayment;
use App\Domains\Purchase\Services\CheckoutService;
use App\Domains\Tenant\Models\Tenant;
use Illuminate\Foundation\Testing\RefreshDatabase;
@ -261,6 +262,84 @@ class TelepagosWebhookTest extends TestCase
]);
}
public function test_transfer_webhook_records_unmatched_payment_when_multiple_purchases_match(): void
{
$tenant = $this->createTenant('ambiguous', 'Ambiguous', 'ambiguous.com.ar');
$this->configureTelepagosIntegration($tenant);
$variant = $this->createVariantForTenant('ambiguous', 10, '50.00');
$firstPurchase = $this->createPendingTransferPurchase(
$tenant,
User::factory()->create()->id,
$variant->id,
1,
'12345678',
);
$secondPurchase = $this->createPendingTransferPurchase(
$tenant,
User::factory()->create()->id,
$variant->id,
1,
'12345678',
);
$thirdPurchase = $this->createPendingTransferPurchase(
$tenant,
User::factory()->create()->id,
$variant->id,
1,
'12345678',
);
Http::fake([
'https://api.telepagos.com.ar/v2/auth/token' => Http::response([
'status' => 'ok',
'token' => 'test-token',
'expires_at' => now()->addHour()->toIso8601String(),
]),
'https://api.telepagos.com.ar/v2/payment/cashin/ambiguous' => Http::response([
'status' => 'ok',
'data' => [
'amount' => 50,
'operation_id' => 1,
'transaction_id' => 'tx-ambiguous',
'buyer' => [
'cuit' => '20123456789',
],
],
]),
]);
$this->postJson('/api/webhooks/telepagos/ambiguous', [
'id' => 'ambiguous',
])->assertOk()->assertJsonPath('status', 'success');
$this->assertDatabaseHas('telepagos_payments', [
'compra_id' => null,
'amount' => 50,
'operation_id' => 1,
'transaction_id' => 'tx-ambiguous',
]);
$payment = TelepagosPayment::query()
->where('transaction_id', 'tx-ambiguous')
->firstOrFail();
$this->assertEqualsCanonicalizing(
[$firstPurchase->id, $secondPurchase->id, $thirdPurchase->id],
$payment->matched_purchase_ids,
);
$this->assertDatabaseHas('compras', [
'id' => $firstPurchase->id,
'status' => Purchase::STATUS_PENDING_PAYMENT,
]);
$this->assertDatabaseHas('compras', [
'id' => $secondPurchase->id,
'status' => Purchase::STATUS_PENDING_PAYMENT,
]);
$this->assertDatabaseHas('compras', [
'id' => $thirdPurchase->id,
'status' => Purchase::STATUS_PENDING_PAYMENT,
]);
}
public function test_webhook_confirms_a_purchase_with_tickets_enabled(): void
{
$tenant = $this->createTenant('expired-ticket', 'Expired Ticket', 'expired-ticket.com.ar');