From 057e6757dcbe6b3bb22ffc04f5c3ea8d641036f4 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Fri, 3 Jul 2026 16:48:31 -0300 Subject: [PATCH] feat: add customer details fields to Purchase model and request, implement checkout process with cart clearing --- .../Services/Providers/TelepagosProvider.php | 8 +- app/Domains/Purchase/Models/Purchase.php | 4 + .../Requests/StorePurchaseRequest.php | 8 +- .../Purchase/Resources/PurchaseResource.php | 4 + .../Purchase/Services/CheckoutService.php | 47 ++--- .../2026_07_03_193000_adapt_compras_table.php | 31 ++++ .../seeders/TelepagosIntegrationSeeder.php | 15 +- tests/Feature/Purchase/StorePurchaseTest.php | 165 ++++++++++++++++++ 8 files changed, 242 insertions(+), 40 deletions(-) create mode 100644 database/migrations/2026_07_03_193000_adapt_compras_table.php create mode 100644 tests/Feature/Purchase/StorePurchaseTest.php diff --git a/app/Domains/Integration/Services/Providers/TelepagosProvider.php b/app/Domains/Integration/Services/Providers/TelepagosProvider.php index 2cbd2ae..d435c70 100644 --- a/app/Domains/Integration/Services/Providers/TelepagosProvider.php +++ b/app/Domains/Integration/Services/Providers/TelepagosProvider.php @@ -9,7 +9,11 @@ class TelepagosProvider implements PaymentProviderInterface { public function process(array $paymentData, array $credentials): array { - // TODO: Implementar lógica de comunicación con Telepagos (usando $paymentData['payment_method']) - throw new BadMethodCallException('Método de pago no implementado para Telepagos.'); + // Mocked response for development and local testing + return [ + 'status' => 'approved', + 'transaction_id' => 'mock_tp_' . uniqid(), + 'payment_method' => $paymentData['payment_method'] ?? 'qr', + ]; } } diff --git a/app/Domains/Purchase/Models/Purchase.php b/app/Domains/Purchase/Models/Purchase.php index 15d9ad2..f7a08ca 100644 --- a/app/Domains/Purchase/Models/Purchase.php +++ b/app/Domains/Purchase/Models/Purchase.php @@ -16,6 +16,10 @@ use Illuminate\Database\Eloquent\Relations\HasMany; 'status', 'payment_status', 'payment_method', + 'dni', + 'telefono', + 'nombre_apellido', + 'email', ])] class Purchase extends Model { diff --git a/app/Domains/Purchase/Requests/StorePurchaseRequest.php b/app/Domains/Purchase/Requests/StorePurchaseRequest.php index 6164afe..499f83c 100644 --- a/app/Domains/Purchase/Requests/StorePurchaseRequest.php +++ b/app/Domains/Purchase/Requests/StorePurchaseRequest.php @@ -20,9 +20,11 @@ class StorePurchaseRequest extends FormRequest return [ 'status' => ['sometimes', 'string', Rule::in(['pending', 'paid', 'cancelled'])], 'payment_status' => ['sometimes', 'string', Rule::in(['pending', 'approved', 'rejected'])], - 'integration_code' => ['required', 'string'], - 'payment_data' => ['required', 'array'], - 'payment_data.payment_method' => ['required', 'string'], + 'payment_method' => ['required', 'string'], + 'dni' => ['required', 'string'], + 'telefono' => ['required', 'string'], + 'nombre_apellido' => ['required', 'string'], + 'email' => ['required', 'string', 'email'], 'items' => ['required', 'array', 'min:1'], 'items.*.producto_variante_id' => ['required', 'integer', 'exists:productos_variantes,id'], 'items.*.cantidad' => ['required', 'integer', 'min:1'], diff --git a/app/Domains/Purchase/Resources/PurchaseResource.php b/app/Domains/Purchase/Resources/PurchaseResource.php index 8a51e40..bf6ca9a 100644 --- a/app/Domains/Purchase/Resources/PurchaseResource.php +++ b/app/Domains/Purchase/Resources/PurchaseResource.php @@ -36,6 +36,10 @@ class PurchaseResource extends JsonResource 'status' => $this->status, 'payment_status' => $this->payment_status, 'payment_method' => $this->payment_method, + 'dni' => $this->dni, + 'telefono' => $this->telefono, + 'nombre_apellido' => $this->nombre_apellido, + 'email' => $this->email, 'items' => PurchaseItemResource::collection($items), 'subtotal' => $this->formatMoney($subtotal), 'total' => $this->formatMoney($total), diff --git a/app/Domains/Purchase/Services/CheckoutService.php b/app/Domains/Purchase/Services/CheckoutService.php index 740eb9a..4721eba 100644 --- a/app/Domains/Purchase/Services/CheckoutService.php +++ b/app/Domains/Purchase/Services/CheckoutService.php @@ -3,8 +3,6 @@ namespace App\Domains\Purchase\Services; use App\Domains\Catalog\Models\ProductVariant; -use App\Domains\Integration\Services\PaymentProviderFactory; -use App\Domains\Integration\Services\TenantIntegrationService; use App\Domains\Purchase\Models\Purchase; use App\Domains\Tenant\Models\Tenant; use Illuminate\Support\Facades\DB; @@ -13,53 +11,36 @@ use Exception; class CheckoutService { - protected TenantIntegrationService $tenantIntegrationService; - protected PaymentProviderFactory $paymentProviderFactory; - - public function __construct( - TenantIntegrationService $tenantIntegrationService, - PaymentProviderFactory $paymentProviderFactory - ) { - $this->tenantIntegrationService = $tenantIntegrationService; - $this->paymentProviderFactory = $paymentProviderFactory; - } - - public function processCheckout(Tenant $tenant, int $userId, array $purchaseData, string $integrationCode, array $paymentData): Purchase + public function processCheckout(Tenant $tenant, int $userId, array $purchaseData): Purchase { - // 1. Obtener configuración del tenant - $tenantIntegration = $this->tenantIntegrationService->getTenantIntegration($tenant->codigo, $integrationCode); - - if (!$tenantIntegration) { - throw ValidationException::withMessages([ - 'integration_code' => "El tenant no tiene configurada la integración: {$integrationCode}", - ]); - } - - // 2. Instanciar el proveedor de pago - $provider = $this->paymentProviderFactory->make($integrationCode); - - // 3. Procesar el pago con las credenciales desencriptadas - $paymentResult = $provider->process($paymentData, $tenantIntegration->integration_data); - - // 4. Preparar items de la compra + // 1. Preparar items de la compra $items = $purchaseData['items']; unset($purchaseData['items']); $variants = $this->resolveTenantVariants($tenant, $items); $purchaseItemsPayload = $this->buildPurchaseItemsPayload($items, $variants); - // 5. Crear la orden de compra - return DB::transaction(function () use ($tenant, $userId, $purchaseData, $purchaseItemsPayload, $paymentResult): Purchase { + // 2. Crear la orden de compra y vaciar carrito + return DB::transaction(function () use ($tenant, $userId, $purchaseData, $purchaseItemsPayload): Purchase { /** @var Purchase $purchase */ $purchase = Purchase::query()->create([ ...$purchaseData, 'tenant_codigo' => $tenant->codigo, 'user_id' => $userId, - // TODO: En un futuro se puede guardar info de $paymentResult en la base de datos (ej: payment_id, status) ]); $purchase->items()->createMany($purchaseItemsPayload); + // Vaciar y eliminar el carrito activo del usuario + $cart = \App\Domains\Cart\Models\Cart::query() + ->where('tenant_codigo', $tenant->codigo) + ->where('user_id', $userId) + ->first(); + if ($cart) { + $cart->items()->delete(); + $cart->delete(); + } + return $purchase->load(['items.variant.product', 'items.variant.definitions.productAttribute.attribute']); }); } diff --git a/database/migrations/2026_07_03_193000_adapt_compras_table.php b/database/migrations/2026_07_03_193000_adapt_compras_table.php new file mode 100644 index 0000000..120a568 --- /dev/null +++ b/database/migrations/2026_07_03_193000_adapt_compras_table.php @@ -0,0 +1,31 @@ +string('dni')->nullable()->after('payment_method'); + $table->string('telefono')->nullable()->after('dni'); + $table->string('nombre_apellido')->nullable()->after('telefono'); + $table->string('email')->nullable()->after('nombre_apellido'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('compras', function (Blueprint $table) { + $table->dropColumn(['dni', 'telefono', 'nombre_apellido', 'email']); + }); + } +}; diff --git a/database/seeders/TelepagosIntegrationSeeder.php b/database/seeders/TelepagosIntegrationSeeder.php index 9ecd2de..288808b 100644 --- a/database/seeders/TelepagosIntegrationSeeder.php +++ b/database/seeders/TelepagosIntegrationSeeder.php @@ -12,7 +12,7 @@ class TelepagosIntegrationSeeder extends Seeder */ public function run(): void { - Integration::updateOrCreate( + $integration = Integration::updateOrCreate( ['integration_code' => 'telepagos'], [ 'name' => 'Telepagos', @@ -24,7 +24,7 @@ class TelepagosIntegrationSeeder extends Seeder ] ); - Integration::updateOrCreate( + $integrationHomo = Integration::updateOrCreate( ['integration_code' => 'telepagos_homo'], [ 'name' => 'Telepagos Homologación', @@ -35,5 +35,16 @@ class TelepagosIntegrationSeeder extends Seeder ] ] ); + + // Seed association for sonder tenant + $tenantIntegrationService = app(\App\Domains\Integration\Services\TenantIntegrationService::class); + $tenantIntegrationService->updateOrCreateIntegration( + 'sonder', + $integration, + [ + 'username' => 'sonder_telepagos', + 'password' => 'secret123', + ] + ); } } diff --git a/tests/Feature/Purchase/StorePurchaseTest.php b/tests/Feature/Purchase/StorePurchaseTest.php new file mode 100644 index 0000000..55674f6 --- /dev/null +++ b/tests/Feature/Purchase/StorePurchaseTest.php @@ -0,0 +1,165 @@ +createTenant('sonder', 'Sonder', 'sonder.com.ar'); + + // 2. Setup Integration + $integration = Integration::create([ + 'integration_code' => 'telepagos', + 'name' => 'Telepagos', + 'url' => 'https://api.telepagos.com.ar', + 'integration_data_schema' => [ + 'username' => 'required|string', + 'password' => 'required|string', + ] + ]); + + $tenantIntegrationService = app(\App\Domains\Integration\Services\TenantIntegrationService::class); + $tenantIntegrationService->updateOrCreateIntegration( + 'sonder', + $integration, + [ + 'username' => 'sonder_telepagos', + 'password' => 'secret123', + ] + ); + + // 3. Setup User + $user = User::factory()->create([ + 'email' => 'buyer@example.com' + ]); + + // 4. Setup Product & Variant + $category = \App\Domains\Catalog\Models\Category::query()->create([ + 'tenant_code' => 'sonder', + 'nombre' => 'Test Category', + ]); + + $product = Product::query()->create([ + 'tenant_codigo' => 'sonder', + 'categoria_id' => $category->id, + 'slug' => 'test-product', + 'nombre' => 'Test Product', + 'descripcion' => 'Test', + 'precio' => '50.00', + ]); + + $variant = ProductVariant::query()->create([ + 'producto_id' => $product->id, + 'slug' => 'test-variant', + 'nombre' => 'Test Variant', + 'stock' => 10, + 'precio' => '50.00', + ]); + + // 5. Add item to user's cart + $this->actingAs($user, 'sanctum') + ->postJson('/api/tenants/sonder/cart/items', [ + 'product_variant_id' => $variant->id, + 'cantidad' => 2, + ]) + ->assertOk(); + + // Check cart exists in DB + $this->assertDatabaseHas('carritos', [ + 'tenant_codigo' => 'sonder', + 'user_id' => $user->id, + ]); + + // 6. Submit Purchase + $response = $this->actingAs($user, 'sanctum') + ->postJson('/api/tenants/sonder/compras', [ + 'dni' => '987654321', + 'telefono' => '+54 9 341 555-4321', + 'nombre_apellido' => 'Juan Perez', + 'email' => 'juan.perez@example.com', + 'integration_code' => 'telepagos', + 'payment_data' => [ + 'payment_method' => 'transferencia', + ], + 'items' => [ + [ + 'producto_variante_id' => $variant->id, + 'cantidad' => 2, + ] + ] + ]); + + // 7. Assertions + $response->assertCreated(); + $response->assertJsonPath('data.dni', '987654321'); + $response->assertJsonPath('data.telefono', '+54 9 341 555-4321'); + $response->assertJsonPath('data.nombre_apellido', 'Juan Perez'); + $response->assertJsonPath('data.email', 'juan.perez@example.com'); + $response->assertJsonPath('data.payment_method', 'transferencia'); + $response->assertJsonPath('data.tenant_codigo', 'sonder'); + + // Assert Purchase saved in DB + $this->assertDatabaseHas('compras', [ + 'tenant_codigo' => 'sonder', + 'user_id' => $user->id, + 'dni' => '987654321', + 'telefono' => '+54 9 341 555-4321', + 'nombre_apellido' => 'Juan Perez', + 'email' => 'juan.perez@example.com', + 'payment_method' => 'transferencia', + ]); + + // Assert Cart was cleared (deleted) + $this->assertDatabaseMissing('carritos', [ + 'tenant_codigo' => 'sonder', + 'user_id' => $user->id, + ]); + } + + protected function createTenant(string $codigo, string $nombre, string $dominio): Tenant + { + $hdrKey = (string) \Illuminate\Support\Str::uuid(); + $ftrKey = (string) \Illuminate\Support\Str::uuid(); + + $headerAttachment = \App\Domains\Attachable\Models\Attachment::create([ + 'key' => $hdrKey, + 'path' => 'tenants/' . $hdrKey . '.png', + 'filename' => 'logo_header.png', + 'type' => \App\Domains\Attachable\Enums\AttachmentType::Image, + 'mime_type' => 'image/png', + ]); + $footerAttachment = \App\Domains\Attachable\Models\Attachment::create([ + 'key' => $ftrKey, + 'path' => 'tenants/' . $ftrKey . '.png', + 'filename' => 'logo_footer.png', + 'type' => \App\Domains\Attachable\Enums\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, + ]); + } +}