diff --git a/app/Domains/Integration/Services/TelepagosWebhookService.php b/app/Domains/Integration/Services/TelepagosWebhookService.php index e74e4bf..73327a3 100644 --- a/app/Domains/Integration/Services/TelepagosWebhookService.php +++ b/app/Domains/Integration/Services/TelepagosWebhookService.php @@ -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); diff --git a/app/Domains/Purchase/Models/TelepagosPayment.php b/app/Domains/Purchase/Models/TelepagosPayment.php index 20f4729..7bfb028 100644 --- a/app/Domains/Purchase/Models/TelepagosPayment.php +++ b/app/Domains/Purchase/Models/TelepagosPayment.php @@ -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', ]; } diff --git a/database/migrations/2026_08_19_040000_make_compra_id_nullable_on_telepagos_payments_table.php b/database/migrations/2026_08_19_040000_make_compra_id_nullable_on_telepagos_payments_table.php new file mode 100644 index 0000000..d2d5667 --- /dev/null +++ b/database/migrations/2026_08_19_040000_make_compra_id_nullable_on_telepagos_payments_table.php @@ -0,0 +1,22 @@ +foreignId('compra_id')->nullable()->change(); + }); + } + + public function down(): void + { + Schema::table('telepagos_payments', function (Blueprint $table) { + $table->foreignId('compra_id')->nullable(false)->change(); + }); + } +}; diff --git a/database/migrations/2026_08_19_050000_add_matched_purchase_ids_to_telepagos_payments_table.php b/database/migrations/2026_08_19_050000_add_matched_purchase_ids_to_telepagos_payments_table.php new file mode 100644 index 0000000..9a05157 --- /dev/null +++ b/database/migrations/2026_08_19_050000_add_matched_purchase_ids_to_telepagos_payments_table.php @@ -0,0 +1,22 @@ +json('matched_purchase_ids')->nullable()->after('compra_id'); + }); + } + + public function down(): void + { + Schema::table('telepagos_payments', function (Blueprint $table) { + $table->dropColumn('matched_purchase_ids'); + }); + } +}; diff --git a/tests/Feature/Integration/TelepagosWebhookTest.php b/tests/Feature/Integration/TelepagosWebhookTest.php index c17fdf5..a28a448 100644 --- a/tests/Feature/Integration/TelepagosWebhookTest.php +++ b/tests/Feature/Integration/TelepagosWebhookTest.php @@ -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');