From 1d94ff8885855cc88374cfcb36f7079a843d0446 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Fri, 3 Jul 2026 15:37:26 -0300 Subject: [PATCH] feat: implement checkout service and payment processing integration --- .../Contracts/PaymentProviderInterface.php | 15 +++ .../Services/PaymentProviderFactory.php | 18 +++ .../Services/Providers/TelepagosProvider.php | 15 +++ .../Controllers/PurchaseController.php | 87 ++----------- .../Requests/StorePurchaseRequest.php | 4 +- .../Purchase/Services/CheckoutService.php | 121 ++++++++++++++++++ 6 files changed, 186 insertions(+), 74 deletions(-) create mode 100644 app/Domains/Integration/Services/Contracts/PaymentProviderInterface.php create mode 100644 app/Domains/Integration/Services/PaymentProviderFactory.php create mode 100644 app/Domains/Integration/Services/Providers/TelepagosProvider.php create mode 100644 app/Domains/Purchase/Services/CheckoutService.php diff --git a/app/Domains/Integration/Services/Contracts/PaymentProviderInterface.php b/app/Domains/Integration/Services/Contracts/PaymentProviderInterface.php new file mode 100644 index 0000000..bc8dcd2 --- /dev/null +++ b/app/Domains/Integration/Services/Contracts/PaymentProviderInterface.php @@ -0,0 +1,15 @@ + new TelepagosProvider(), + default => throw new InvalidArgumentException("Integración de pago no soportada: {$integrationCode}"), + }; + } +} diff --git a/app/Domains/Integration/Services/Providers/TelepagosProvider.php b/app/Domains/Integration/Services/Providers/TelepagosProvider.php new file mode 100644 index 0000000..2cbd2ae --- /dev/null +++ b/app/Domains/Integration/Services/Providers/TelepagosProvider.php @@ -0,0 +1,15 @@ +response(); } - public function store(StorePurchaseRequest $request, Tenant $tenant): JsonResponse + public function store(StorePurchaseRequest $request, Tenant $tenant, CheckoutService $checkoutService): JsonResponse { $data = $request->validated(); - $items = $data['items']; + + $integrationCode = $data['integration_code']; + $paymentData = $data['payment_data']; + + unset($data['integration_code'], $data['payment_data']); - unset($data['items']); - - $variants = $this->resolveTenantVariants($tenant, $items); - $purchaseItems = $this->buildPurchaseItemsPayload($items, $variants); - - $purchase = DB::transaction(function () use ($request, $tenant, $data, $purchaseItems): Purchase { - /** @var Purchase $purchase */ - $purchase = Purchase::query()->create([ - ...$data, - 'tenant_codigo' => $tenant->codigo, - 'user_id' => $request->user()->id, - ]); - - $purchase->items()->createMany($purchaseItems); - - return $purchase->load(['items.variant.product', 'items.variant.definitions.productAttribute.attribute']); - }); + $purchase = $checkoutService->processCheckout( + $tenant, + $request->user()->id, + $data, + $integrationCode, + $paymentData + ); return PurchaseResource::make($purchase)->response()->setStatusCode(201); } @@ -63,60 +57,7 @@ class PurchaseController extends Controller ); } - /** - * @param array> $items - * @return \Illuminate\Support\Collection - */ - protected function resolveTenantVariants(Tenant $tenant, array $items) - { - $variantIds = collect($items) - ->pluck('producto_variante_id') - ->filter() - ->map(static fn (mixed $id): int => (int) $id) - ->unique() - ->values(); - $variants = ProductVariant::query() - ->with('product') - ->whereIn('id', $variantIds) - ->whereHas('product', fn ($query) => $query->where('tenant_codigo', $tenant->codigo)) - ->get() - ->keyBy('id'); - - if ($variants->count() !== $variantIds->count()) { - throw ValidationException::withMessages([ - 'items' => 'One or more product variants do not belong to the tenant.', - ]); - } - - return $variants; - } - - /** - * @param array> $items - * @param \Illuminate\Support\Collection $variants - * @return array> - */ - protected function buildPurchaseItemsPayload(array $items, $variants): array - { - return collect($items) - ->map(function (array $item) use ($variants): array { - /** @var ProductVariant $variant */ - $variant = $variants->get((int) $item['producto_variante_id']); - $quantity = (int) $item['cantidad']; - $unitPrice = (float) ($variant->product?->precio ?? 0); - - return [ - 'producto_variante_id' => $variant->getKey(), - 'cantidad' => $quantity, - 'precio_unitario' => $unitPrice, - 'discount_total' => null, - 'tax_total' => null, - 'total' => $unitPrice * $quantity, - ]; - }) - ->all(); - } protected function resolveScopedPurchase(Tenant $tenant, int $userId, Purchase $purchase): Purchase { diff --git a/app/Domains/Purchase/Requests/StorePurchaseRequest.php b/app/Domains/Purchase/Requests/StorePurchaseRequest.php index b4120e6..6164afe 100644 --- a/app/Domains/Purchase/Requests/StorePurchaseRequest.php +++ b/app/Domains/Purchase/Requests/StorePurchaseRequest.php @@ -20,7 +20,9 @@ class StorePurchaseRequest extends FormRequest return [ 'status' => ['sometimes', 'string', Rule::in(['pending', 'paid', 'cancelled'])], 'payment_status' => ['sometimes', 'string', Rule::in(['pending', 'approved', 'rejected'])], - 'payment_method' => ['nullable', 'string', 'max:255'], + 'integration_code' => ['required', 'string'], + 'payment_data' => ['required', 'array'], + 'payment_data.payment_method' => ['required', 'string'], '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/Services/CheckoutService.php b/app/Domains/Purchase/Services/CheckoutService.php new file mode 100644 index 0000000..740eb9a --- /dev/null +++ b/app/Domains/Purchase/Services/CheckoutService.php @@ -0,0 +1,121 @@ +tenantIntegrationService = $tenantIntegrationService; + $this->paymentProviderFactory = $paymentProviderFactory; + } + + public function processCheckout(Tenant $tenant, int $userId, array $purchaseData, string $integrationCode, array $paymentData): 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 + $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 { + /** @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); + + return $purchase->load(['items.variant.product', 'items.variant.definitions.productAttribute.attribute']); + }); + } + + /** + * @param array> $items + * @return \Illuminate\Support\Collection + */ + protected function resolveTenantVariants(Tenant $tenant, array $items) + { + $variantIds = collect($items) + ->pluck('producto_variante_id') + ->filter() + ->map(static fn (mixed $id): int => (int) $id) + ->unique() + ->values(); + + $variants = ProductVariant::query() + ->with('product') + ->whereIn('id', $variantIds) + ->whereHas('product', fn ($query) => $query->where('tenant_codigo', $tenant->codigo)) + ->get() + ->keyBy('id'); + + if ($variants->count() !== $variantIds->count()) { + throw ValidationException::withMessages([ + 'items' => 'One or more product variants do not belong to the tenant.', + ]); + } + + return $variants; + } + + /** + * @param array> $items + * @param \Illuminate\Support\Collection $variants + * @return array> + */ + protected function buildPurchaseItemsPayload(array $items, $variants): array + { + return collect($items) + ->map(function (array $item) use ($variants): array { + /** @var ProductVariant $variant */ + $variant = $variants->get((int) $item['producto_variante_id']); + $quantity = (int) $item['cantidad']; + $unitPrice = (float) ($variant->product?->precio ?? 0); + + return [ + 'producto_variante_id' => $variant->getKey(), + 'cantidad' => $quantity, + 'precio_unitario' => $unitPrice, + 'discount_total' => null, + 'tax_total' => null, + 'total' => $unitPrice * $quantity, + ]; + }) + ->all(); + } +}