From 4a166e3cbff6e0b24b56fba54b161368f868b19e Mon Sep 17 00:00:00 2001 From: ncoronel Date: Thu, 27 Aug 2026 12:10:33 -0300 Subject: [PATCH] refactor(telepagos): remove unused webhook endpoint --- .../TelepagosWebhookController.php | 32 -- .../Requests/TelepagosWebhookRequest.php | 28 - .../Services/TelepagosWebhookService.php | 212 ------- .../Integration/TelepagosWebhookTest.php | 531 ------------------ 4 files changed, 803 deletions(-) delete mode 100644 app/Domains/Integration/Controllers/TelepagosWebhookController.php delete mode 100644 app/Domains/Integration/Requests/TelepagosWebhookRequest.php delete mode 100644 app/Domains/Integration/Services/TelepagosWebhookService.php delete mode 100644 tests/Feature/Integration/TelepagosWebhookTest.php diff --git a/app/Domains/Integration/Controllers/TelepagosWebhookController.php b/app/Domains/Integration/Controllers/TelepagosWebhookController.php deleted file mode 100644 index b0e90a6..0000000 --- a/app/Domains/Integration/Controllers/TelepagosWebhookController.php +++ /dev/null @@ -1,32 +0,0 @@ -validated('id'); - - $service->handleWebhook($client, $cashinId); - - return response()->json(['status' => 'success']); - } catch (\Exception $e) { - return response()->json([ - 'status' => 'error', - 'code' => 'integration.webhook_failed', - 'message' => __('api.integration.webhook_failed'), - ], 500); - } - } -} diff --git a/app/Domains/Integration/Requests/TelepagosWebhookRequest.php b/app/Domains/Integration/Requests/TelepagosWebhookRequest.php deleted file mode 100644 index 3c97f42..0000000 --- a/app/Domains/Integration/Requests/TelepagosWebhookRequest.php +++ /dev/null @@ -1,28 +0,0 @@ - - */ - public function rules(): array - { - return [ - 'id' => ['required', 'string'], - ]; - } -} diff --git a/app/Domains/Integration/Services/TelepagosWebhookService.php b/app/Domains/Integration/Services/TelepagosWebhookService.php deleted file mode 100644 index 05c4c3e..0000000 --- a/app/Domains/Integration/Services/TelepagosWebhookService.php +++ /dev/null @@ -1,212 +0,0 @@ -info('Telepagos webhook received.', [ - 'client_code' => $client->code, - 'cashin_id' => $cashinId, - ]); - - $telepagosService = new TelepagosIntegrationService; - $telepagosService->forClient($client); - - try { - $details = $telepagosService->getCashinDetails($cashinId); - - $qrOrderId = $details['data']['qr_order_id'] ?? $details['qr_order_id'] ?? null; - $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]; - - $compra = null; - - if (in_array((int) $operationId, $transferenciaOperationIds, true)) { - $cuit = $details['data']['buyer']['cuit'] ?? $details['buyer']['cuit'] ?? null; - - if (! $cuit) { - Log::channel('telepagos')->warning('Telepagos webhook: CUIT not found for transfer.', [ - 'client_code' => $client->code, - 'cashin_id' => $cashinId, - ]); - - return; - } - - $dni = substr($cuit, 2, -1); - - $tenantCodes = $client->tenants()->pluck('codigo'); - $purchases = Purchase::whereIn('tenant_codigo', $tenantCodes) - ->where('transfer_payer_dni', $dni) - ->whereIn('status', [ - Purchase::STATUS_CREATED, - Purchase::STATUS_PENDING_PAYMENT, - Purchase::STATUS_IN_REVIEW, - ]) - ->where('payment_method', 'transfer') - ->where('total', $amount) - ->latest() - ->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, - 'amount' => $amount, - 'matches' => $purchases->count(), - ]); - - return; - } - } elseif (in_array((int) $operationId, $qrOperationIds, true)) { - if (! $qrOrderId) { - Log::channel('telepagos')->warning('Telepagos webhook: qr_order_id not present in provider response.', [ - 'client_code' => $client->code, - 'cashin_id' => $cashinId, - ]); - - return; - } - - $telepagosQr = TelepagosQr::where('qr_order_id', $qrOrderId)->first(); - - if (! $telepagosQr) { - Log::channel('telepagos')->warning('Telepagos webhook: QR not found in database.', [ - 'client_code' => $client->code, - 'cashin_id' => $cashinId, - 'qr_order_id' => $qrOrderId, - ]); - - return; - } - - $compra = $telepagosQr->compra; - - if (! $compra) { - Log::channel('telepagos')->warning('Telepagos webhook: Purchase not found for QR.', [ - 'client_code' => $client->code, - 'cashin_id' => $cashinId, - 'qr_order_id' => $qrOrderId, - ]); - - return; - } - - if (! $client->tenants()->where('codigo', $compra->tenant_codigo)->exists()) { - Log::channel('telepagos')->warning('Telepagos webhook: Purchase does not belong to client.', [ - 'client_code' => $client->code, - 'cashin_id' => $cashinId, - 'purchase_id' => $compra->id, - ]); - - return; - } - - if (! in_array($compra->status, [ - Purchase::STATUS_PENDING_PAYMENT, - ], true)) { - Log::channel('telepagos')->warning('Telepagos webhook: Purchase is not awaiting payment confirmation.', [ - 'client_code' => $client->code, - 'cashin_id' => $cashinId, - 'purchase_id' => $compra->id, - 'purchase_status' => $compra->status, - ]); - - return; - } - - $totalAmount = $this->normalizeAmount($compra->getTotalAmount()); - - if ($amount !== $totalAmount) { - Log::channel('telepagos')->warning('Telepagos webhook: Amount mismatch.', [ - 'client_code' => $client->code, - 'cashin_id' => $cashinId, - 'purchase_id' => $compra->id, - 'cashin_amount' => $amount, - 'purchase_amount' => $totalAmount, - ]); - - return; - } - } else { - Log::channel('telepagos')->warning('Telepagos webhook: Unknown operation_id.', [ - 'client_code' => $client->code, - 'cashin_id' => $cashinId, - 'operation_id' => $operationId, - ]); - - return; - } - - $paymentData['compra_id'] = $compra->id; - - DB::transaction(function () use ($compra, $paymentData) { - TelepagosPayment::create($paymentData); - $this->checkoutService->confirmPaidPurchase($compra); - }); - - Log::channel('telepagos')->info('Telepagos webhook processed successfully.', [ - 'client_code' => $client->code, - 'cashin_id' => $cashinId, - 'purchase_id' => $compra->id, - 'transaction_id' => $paymentData['transaction_id'], - ]); - } catch (Exception $e) { - Log::channel('telepagos')->error('Telepagos webhook processing failed.', [ - 'client_code' => $client->code, - 'cashin_id' => $cashinId, - 'error' => $e->getMessage(), - ]); - throw $e; - } - } - - protected function normalizeAmount(mixed $amount): string - { - return number_format((float) $amount, 2, '.', ''); - } -} diff --git a/tests/Feature/Integration/TelepagosWebhookTest.php b/tests/Feature/Integration/TelepagosWebhookTest.php deleted file mode 100644 index 38b607c..0000000 --- a/tests/Feature/Integration/TelepagosWebhookTest.php +++ /dev/null @@ -1,531 +0,0 @@ - 'base64:'.base64_encode(random_bytes(32))]); - Cache::flush(); - Queue::fake(); - } - - public function test_transfer_payment_intent_requires_a_valid_transfer_payer_dni(): void - { - $tenant = $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); - $user = User::factory()->create(); - $variant = $this->createVariantForTenant('sonder', 10, '50.00'); - $purchase = $this->createPendingTransferPurchase($tenant, $user->id, $variant->id, 1, '12345678'); - - $this->actingAs($user, 'sanctum') - ->postJson("/api/tenants/sonder/compras/{$purchase->id}/payment-intent", [ - 'method' => 'transfer', - ]) - ->assertUnprocessable() - ->assertJsonValidationErrors(['transfer_payer_dni']); - - $this->actingAs($user, 'sanctum') - ->postJson("/api/tenants/sonder/compras/{$purchase->id}/payment-intent", [ - 'method' => 'transfer', - 'transfer_payer_dni' => '12.345.678', - ]) - ->assertUnprocessable() - ->assertJsonValidationErrors(['transfer_payer_dni']); - } - - public function test_transfer_payment_intent_persists_data_without_extending_checkout_expiration(): void - { - config()->set('purchase.checkout_expiration_minutes', 30); - $now = now()->startOfSecond(); - $this->travelTo($now); - - $tenant = $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); - $this->configureTelepagosIntegration($tenant); - $user = User::factory()->create(); - $variant = $this->createVariantForTenant('sonder', 10, '50.00'); - $purchase = $this->createPendingTransferPurchase($tenant, $user->id, $variant->id, 1, '12345678'); - $purchase->update(['status' => Purchase::STATUS_CREATED]); - - 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/account/info' => Http::response([ - 'status' => 'ok', - 'holder' => 'Telepagos Test', - 'cvu' => '0000003100000000000001', - 'alias' => 'telepagos.test', - 'entity' => 'Telepagos S.A.', - ]), - ]); - - $this->actingAs($user, 'sanctum') - ->postJson("/api/tenants/sonder/compras/{$purchase->id}/payment-intent", [ - 'method' => 'transfer', - 'transfer_payer_dni' => '23456789', - ]) - ->assertOk() - ->assertJsonPath('transfer_data.alias', 'telepagos.test'); - - $this->assertDatabaseHas('compras', [ - 'id' => $purchase->id, - 'dni' => '87654321', - 'transfer_payer_dni' => '23456789', - 'payment_method' => 'transfer', - 'status' => Purchase::STATUS_PENDING_PAYMENT, - ]); - $this->assertDatabaseHas('stock_reservations', [ - 'id' => $purchase->stock_reservation_id, - 'status' => 'active', - 'expires_at' => $now->copy()->addMinutes(30)->toDateTimeString(), - ]); - - $this->travelBack(); - } - - public function test_transfer_webhook_matches_pending_purchase_by_dni_and_total_amount(): void - { - $tenant = $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); - $this->configureTelepagosIntegration($tenant); - - $matchingUser = User::factory()->create([ - 'email' => 'buyer@example.com', - ]); - $newerUser = User::factory()->create([ - 'email' => 'buyer-2@example.com', - ]); - - $variant = $this->createVariantForTenant('sonder', 10, '50.00'); - - $matchingPurchase = $this->createPendingTransferPurchase( - $tenant, - $matchingUser->id, - $variant->id, - 1, - '12345678' - ); - - $newerPurchase = $this->createPendingTransferPurchase( - $tenant, - $newerUser->id, - $variant->id, - 2, - '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/6351' => Http::response([ - 'status' => 'ok', - 'data' => [ - 'amount' => 50, - 'operation_id' => 1, - 'transaction_id' => 'tx-123', - 'buyer' => [ - 'cuit' => '20123456789', - ], - ], - ]), - ]); - - $this->postJson('/api/webhooks/telepagos/sonder', [ - 'id' => '6351', - ])->assertOk()->assertJsonPath('status', 'success'); - - $this->assertDatabaseHas('compras', [ - 'id' => $matchingPurchase->id, - 'status' => Purchase::STATUS_PAID, - 'payment_method' => 'transfer', - 'total' => 50, - ]); - - $this->assertDatabaseHas('compras', [ - 'id' => $newerPurchase->id, - 'status' => Purchase::STATUS_PENDING_PAYMENT, - 'payment_method' => 'transfer', - 'total' => 100, - ]); - - $this->assertDatabaseHas('telepagos_payments', [ - 'compra_id' => $matchingPurchase->id, - 'amount' => 50, - 'operation_id' => 1, - 'transaction_id' => 'tx-123', - ]); - - $this->assertDatabaseMissing('telepagos_payments', [ - 'compra_id' => $newerPurchase->id, - ]); - - $this->assertDatabaseHas('compra_items', [ - 'compra_id' => $matchingPurchase->id, - 'source_catalog_item_id' => $variant->catalog_item_id, - 'source_variant_id' => $variant->id, - 'cantidad' => 1, - 'total' => 50, - ]); - - $this->assertDatabaseHas('inventories', [ - 'id' => $variant->inventory_id, - 'real_stock' => 9, - 'reserved_stock' => 2, - 'sold_units' => 1, - ]); - - $this->assertDatabaseHas('compra_items', [ - 'compra_id' => $newerPurchase->id, - 'cantidad' => 2, - ]); - $newerReservationId = $newerPurchase->fresh()->stock_reservation_id; - $this->assertNotNull($newerReservationId); - $this->assertDatabaseHas('stock_reservations', [ - 'id' => $newerReservationId, - 'status' => 'active', - ]); - $this->assertDatabaseHas('stock_reservation_lines', [ - 'stock_reservation_id' => $newerReservationId, - 'inventory_id' => $variant->inventory_id, - 'quantity' => 2, - ]); - - $this->assertSoftDeleted('carritos', [ - 'id' => $matchingPurchase->cart_id, - ]); - } - - public function test_transfer_webhook_buys_unlimited_inventory_without_reducing_real_stock(): void - { - $tenant = $this->createTenant('unlimited', 'Unlimited', 'unlimited.com.ar'); - $this->configureTelepagosIntegration($tenant); - $user = User::factory()->create(); - $variant = $this->createVariantForTenant( - 'unlimited', - 0, - '50.00', - 'service', - InventoryPolicy::Unlimited, - ); - $purchase = $this->createPendingTransferPurchase( - $tenant, - $user->id, - $variant->id, - 3, - '87654321', - ); - - 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/7000' => Http::response([ - 'status' => 'ok', - 'data' => [ - 'amount' => 150, - 'operation_id' => 1, - 'transaction_id' => 'tx-unlimited', - 'buyer' => ['cuit' => '20876543219'], - ], - ]), - ]); - - $this->postJson('/api/webhooks/telepagos/unlimited', ['id' => '7000']) - ->assertOk() - ->assertJsonPath('status', 'success'); - - $this->assertDatabaseHas('compras', [ - 'id' => $purchase->id, - 'status' => Purchase::STATUS_PAID, - ]); - $this->assertDatabaseHas('inventories', [ - 'id' => $variant->inventory_id, - 'real_stock' => 0, - 'reserved_stock' => 0, - 'sold_units' => 3, - ]); - } - - 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'); - $this->configureTelepagosIntegration($tenant); - $user = User::factory()->create(); - $variant = $this->createVariantForTenant('expired-ticket', 1, '50.00'); - $variant->catalogItem->update([ - 'has_tickets' => true, - ]); - $purchase = $this->createPendingTransferPurchase( - $tenant, - $user->id, - $variant->id, - 1, - '87654321', - ); - - 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/7001' => Http::response([ - 'status' => 'ok', - 'data' => [ - 'amount' => 50, - 'operation_id' => 1, - 'transaction_id' => 'tx-expired-ticket', - 'buyer' => ['cuit' => '20876543219'], - ], - ]), - ]); - - $this->postJson('/api/webhooks/telepagos/expired-ticket', ['id' => '7001']) - ->assertOk() - ->assertJsonPath('status', 'success'); - - $this->assertDatabaseHas('compras', [ - 'id' => $purchase->id, - 'status' => Purchase::STATUS_PAID, - ]); - $this->assertDatabaseHas('tickets', [ - 'source_catalog_item_id' => $variant->catalog_item_id, - 'source_variant_id' => $variant->id, - 'user_id' => $user->id, - ]); - } - - private function createPendingTransferPurchase( - Tenant $tenant, - int $userId, - int $variantId, - int $quantity, - string $dni, - ): Purchase { - $cart = Cart::query()->create([ - 'tenant_codigo' => $tenant->codigo, - 'user_id' => $userId, - 'status' => 'active', - ]); - - $variant = Variant::query()->findOrFail($variantId); - $cart->addItem($variant->catalog_item_id, $variant->id, $quantity); - - /** @var CheckoutService $checkoutService */ - $checkoutService = app(CheckoutService::class); - - $purchase = $checkoutService->startCheckout($tenant, $userId, [ - 'cart_id' => $cart->id, - 'dni' => '87654321', - 'telefono' => '+54 9 341 555-4321', - 'nombre_apellido' => 'Juan Perez', - 'email' => 'juan.perez@example.com', - ]); - - $purchase->update([ - 'payment_method' => 'transfer', - 'transfer_payer_dni' => $dni, - ]); - - $checkoutService->completePurchase($purchase); - - return $purchase->fresh(); - } - - private function configureTelepagosIntegration(Tenant $tenant): void - { - Integration::create([ - 'integration_code' => 'telepagos_homo', - 'name' => 'Telepagos', - 'url' => 'https://api.telepagos.com.ar', - 'integration_data_schema' => [ - 'username' => 'required|string', - 'password' => 'required|string', - ], - ]); - - ClientIntegration::create([ - 'client_id' => $tenant->client_id, - 'integration_code' => 'telepagos_homo', - 'integration_data' => [ - 'username' => 'user123', - 'password' => 'pass123', - ], - ]); - } - - private function createVariantForTenant( - string $tenantCode, - int $stock, - string $price, - string $slugPrefix = 'shirt', - InventoryPolicy $inventoryPolicy = InventoryPolicy::Tracked, - ): Variant { - $category = Category::query()->create([ - 'tenant_code' => $tenantCode, - 'nombre' => "{$slugPrefix} category {$tenantCode}", - ]); - - $inventory = Inventory::query()->create(['real_stock' => $stock]); - $catalogItem = CatalogItem::query()->create([ - 'tenant_code' => $tenantCode, - 'category_id' => $category->id, - 'slug' => "{$slugPrefix}-{$tenantCode}-".CatalogItem::query()->count(), - 'nombre' => ucfirst($slugPrefix)." {$tenantCode}", - 'descripcion' => 'Test product', - 'precio' => $price, - 'inventory_policy' => $inventoryPolicy, - ]); - - return Variant::query()->create([ - 'catalog_item_id' => $catalogItem->id, - 'inventory_id' => $inventory->id, - ])->load(['catalogItem', 'inventory']); - } - - private function createTenant(string $codigo, string $nombre, string $dominio): Tenant - { - $hdrKey = (string) Str::uuid(); - $ftrKey = (string) Str::uuid(); - - $headerAttachment = Attachment::create([ - 'key' => $hdrKey, - 'path' => 'tenants/'.$hdrKey.'.png', - 'filename' => 'logo_header.png', - 'type' => AttachmentType::Image, - 'mime_type' => 'image/png', - ]); - $footerAttachment = Attachment::create([ - 'key' => $ftrKey, - 'path' => 'tenants/'.$ftrKey.'.png', - 'filename' => 'logo_footer.png', - 'type' => AttachmentType::Image, - 'mime_type' => 'image/png', - ]); - - return Tenant::create([ - 'codigo' => $codigo, - 'nombre' => $nombre, - 'dominio' => $dominio, - 'primary_color' => '#111111', - 'secondary_color' => '#222222', - 'danger_color' => '#333333', - 'success_color' => '#28a745', - 'header_bg_color' => '#444444', - 'footer_bg_color' => '#444444', - 'header_logo_id' => $headerAttachment->id, - 'footer_logo_id' => $footerAttachment->id, - ]); - } -}