where('tenant_codigo', $tenant->codigo) ->where('status', Purchase::STATUS_PAID) ->sum('total'); return number_format((float) $total, 2, '.', ''); } /** * @param array{ * q?: string|null, * id?: int|null, * sale_date?: string|null, * status?: string|null, * sort_by?: string, * sort_direction?: string * } $filters * @return LengthAwarePaginator */ public function sales(Tenant $tenant, array $filters = []): LengthAwarePaginator { return $this->salesQuery($tenant, $filters) ->paginateFromRequest() ->withQueryString(); } public function detail(Tenant $tenant, int $saleId): Purchase { return Purchase::query() ->where('tenant_codigo', $tenant->codigo) ->with([ 'items', 'cart' => fn ($query) => $query->withTrashed(), 'cart.items.catalogItem', 'cart.items.variant.catalogItem', 'cart.items.variant.eventDates', 'cart.items.variant.eventDate', ]) ->findOrFail($saleId); } /** @return Collection */ public function tickets(Tenant $tenant, int $saleId): Collection { return $this->findForTenant($tenant, $saleId) ->tickets() ->with([...TicketValidityResolver::RELATIONS, ...TicketPresentationResolver::RELATIONS]) ->orderBy('id') ->get(); } public function confirm(Tenant $tenant, int $saleId): Purchase { $sale = $this->findForTenant($tenant, $saleId); return $this->saleForResponse( $this->checkoutService->confirmPaidPurchase($sale) ); } public function cancel(Tenant $tenant, int $saleId): Purchase { $sale = $this->findForTenant($tenant, $saleId); return $this->saleForResponse( $this->checkoutService->cancelPurchaseWithoutRestoringCart($sale) ); } /** * @param array $filters * @return Collection */ public function salesForExport(Tenant $tenant, array $filters = []): Collection { return $this->salesQuery($tenant, $filters)->get(); } /** @return LengthAwarePaginator */ public function modifications(Tenant $tenant): LengthAwarePaginator { return $this->modificationsQuery($tenant) ->paginateFromRequest() ->withQueryString(); } /** @return Collection */ public function modificationsForExport(Tenant $tenant): Collection { return $this->modificationsQuery($tenant)->get(); } /** @param array $filters */ protected function salesQuery(Tenant $tenant, array $filters): Builder { $sortColumns = [ 'id' => 'id', 'date' => 'created_at', 'customer_name' => 'nombre_apellido', 'quantity' => 'quantity', 'status' => 'status', 'total' => 'total', ]; $requestedSort = $filters['sort_by'] ?? 'date'; $sortBy = array_key_exists($requestedSort, $sortColumns) ? $requestedSort : 'date'; $requestedDirection = $filters['sort_direction'] ?? 'desc'; $sortDirection = in_array($requestedDirection, ['asc', 'desc'], true) ? $requestedDirection : 'desc'; return Purchase::query() ->where('tenant_codigo', $tenant->codigo) ->when($filters['q'] ?? null, function (Builder $query, string $search): void { $term = trim($search); $query->where(function (Builder $query) use ($term): void { $query ->where('id', 'like', "%{$term}%") ->orWhere('nombre_apellido', 'like', "%{$term}%") ->orWhere('created_at', 'like', "%{$term}%"); }); }) ->when($filters['id'] ?? null, fn (Builder $query, int $id): Builder => $query->whereKey($id)) ->when( $filters['sale_date'] ?? null, fn (Builder $query, string $date): Builder => $query->whereDate('created_at', $date) ) ->when( $filters['status'] ?? null, fn (Builder $query, string $status): Builder => $query->where('status', $status) ) ->select('compras.*') ->selectRaw( 'CASE WHEN compras.status IN (?, ?) ' .'THEN (SELECT COALESCE(SUM(cart_items.cantidad), 0) FROM carrito_items AS cart_items ' .'WHERE cart_items.cart_id = compras.cart_id) ' .'ELSE (SELECT COALESCE(SUM(purchase_items.cantidad), 0) FROM compra_items AS purchase_items ' .'WHERE purchase_items.compra_id = compras.id) END AS quantity', [ Purchase::STATUS_CREATED, Purchase::STATUS_PENDING_PAYMENT, ], ) ->withCount('tickets') ->orderBy($sortColumns[$sortBy], $sortDirection) ->when($sortBy !== 'id', fn (Builder $query): Builder => $query->orderByDesc('id')); } /** @return Builder */ protected function modificationsQuery(Tenant $tenant): Builder { return ValueChange::query() ->where('tenant_code', $tenant->codigo) ->where('trackable_type', (new Purchase)->getMorphClass()) ->with(['trackable', 'user']) ->orderByDesc('changed_at') ->orderByDesc('id'); } protected function findForTenant(Tenant $tenant, int $saleId): Purchase { return Purchase::query() ->where('tenant_codigo', $tenant->codigo) ->findOrFail($saleId); } protected function saleForResponse(Purchase $sale): Purchase { /** @var Purchase */ return Purchase::query() ->select('compras.*') ->selectRaw($this->quantityExpression()) ->withCount('tickets') ->findOrFail($sale->getKey()); } protected function quantityExpression(): string { // El fallback se resuelve en SQL para poder ordenar por cantidad antes de paginar; // los ítems consolidados de la compra tienen prioridad sobre los del carrito de origen. return <<<'SQL' COALESCE( (SELECT SUM(compra_items.cantidad) FROM compra_items WHERE compra_items.compra_id = compras.id), (SELECT SUM(carrito_items.cantidad) FROM carrito_items WHERE carrito_items.cart_id = compras.cart_id), 0 ) AS quantity SQL; } }