*/ public function toArray(Request $request): array { $items = $this->resource->relationLoaded('items') ? $this->resource->getRelation('items') : collect(); $ticketsCount = array_key_exists('tickets_count', $this->resource->getAttributes()) ? (int) $this->resource->getAttribute('tickets_count') : null; $paymentVerification = $this->resolvePaymentVerification(); $subtotal = $items->isNotEmpty() ? $items->reduce( fn (float $carry, PurchaseItem $item): float => $carry + $this->resolveItemSubtotal($item), 0.0, ) : (float) ($this->total ?? 0); $total = $this->status === Purchase::STATUS_PAID && $this->total !== null ? (float) $this->total : ($items->isNotEmpty() ? $items->reduce( fn (float $carry, PurchaseItem $item): float => $carry + $this->resolveItemTotal($item), 0.0, ) : (float) ($this->total ?? 0)); return [ 'id' => $this->id, 'cart_id' => $this->cart_id, 'tenant_codigo' => $this->tenant_codigo, 'user_id' => $this->user_id, 'created_at' => $this->created_at, 'status' => $this->status, 'payment_method' => $this->payment_method, 'expires_at' => $this->expires_at, 'dni' => $this->dni, 'transfer_payer_dni' => $this->transfer_payer_dni, 'telefono' => $this->telefono, 'nombre_apellido' => $this->nombre_apellido, 'email' => $this->email, 'items_source' => $items->isNotEmpty() ? 'purchase' : null, 'items' => PurchaseItemResource::collection($items), 'tickets_count' => $this->when($ticketsCount !== null, $ticketsCount), 'has_generated_tickets' => $this->when($ticketsCount !== null, $ticketsCount > 0), 'payment_verification' => $this->when($paymentVerification !== null, $paymentVerification), 'subtotal' => $this->formatMoney($subtotal), 'total' => $this->formatMoney($total), ]; } protected function resolveItemSubtotal(PurchaseItem $item): float { return (float) $item->precio_unitario * $item->cantidad; } protected function resolveItemTotal(PurchaseItem $item): float { return (float) ($item->total ?? 0); } protected function formatMoney(float|int|string|null $amount): string { return number_format((float) ($amount ?? 0), 2, '.', ''); } /** @return array|null */ private function resolvePaymentVerification(): ?array { if ( $this->status !== Purchase::STATUS_IN_REVIEW || $this->payment_method !== 'transfer' || ! $this->resource->relationLoaded('telepagosPaymentCandidates') ) { return null; } $candidates = $this->resource ->getRelation('telepagosPaymentCandidates') ->sort(fn (TelepagosPaymentCandidate $left, TelepagosPaymentCandidate $right): int => $this->comparePaymentCandidates($left, $right)) ->values(); /** @var TelepagosPaymentCandidate|null $primary */ $primary = $candidates->first(); return [ 'status' => $primary === null ? 'pending' : 'candidate', 'candidate_count' => $candidates->count(), 'primary' => $primary === null ? null : [ 'reason' => $primary->match_reason, 'dni_distance' => $primary->dni_distance, 'payment_amount' => $this->formatMoney($primary->payment_amount), 'purchase_amount' => $this->formatMoney($primary->purchase_amount), 'amount_difference' => $this->formatMoney($primary->amount_difference), 'confidence' => $primary->confidence, 'detected_at' => $primary->payment?->created_at?->toIso8601String(), ], 'reasons' => $candidates ->pluck('match_reason') ->unique() ->values() ->all(), ]; } private function comparePaymentCandidates( TelepagosPaymentCandidate $left, TelepagosPaymentCandidate $right, ): int { $reasonComparison = $this->paymentCandidateRank($left->match_reason) <=> $this->paymentCandidateRank($right->match_reason); if ($reasonComparison !== 0) { return $reasonComparison; } $differenceComparison = (float) $left->amount_difference <=> (float) $right->amount_difference; if ($differenceComparison !== 0) { return $differenceComparison; } $leftTimestamp = $left->payment?->created_at?->getTimestamp() ?? 0; $rightTimestamp = $right->payment?->created_at?->getTimestamp() ?? 0; return ($rightTimestamp <=> $leftTimestamp) ?: ($right->id <=> $left->id); } private function paymentCandidateRank(string $reason): int { return match ($reason) { 'ambiguous_exact_match' => 0, 'exact_dni_near_amount' => 1, 'exact_amount_different_dni' => 2, default => 3, }; } }