nullableDate($row['duedate'] ?? null); $status = $this->mapStatus($row['status'], $dueDate); $total = (int) round((float) $row['total']); $whmcsOrderId = $this->extractor->orderIdFor($legacyId); $rawItems = $this->extractor->items($legacyId); // WHMCS records discounts as negative-amount line items; invoice_items.unit_price // is unsigned, so fold them into the header's discount_amount instead. $discountAmount = (int) round(-$rawItems->sum(fn ($item) => min(0, (float) $item->amount))); $items = $rawItems ->map(fn ($item) => $this->itemTransformer->transform($item)) ->filter() ->all(); return new InvoiceData( legacyId: $legacyId, invoiceNumber: $row['invoicenum'] ?: "WHMCS-{$legacyId}", customerId: $this->customers->resolveFor('invoice', $row['invoicenum'] ?: $row['id'], (int) $row['userid']), orderId: $whmcsOrderId ? $this->orders->resolve($whmcsOrderId) : null, date: $this->nullableDate($row['date'] ?? null), subTotal: (int) round((float) $row['subtotal']), discountAmount: $discountAmount, tax: (int) round((float) $row['tax'] + (float) $row['tax2']), total: $total, // Không có amount_paid thì mọi hoá đơn WHMCS đã thanh toán sẽ hiện "chưa thu" // và outstanding = total trên toàn bộ dữ liệu import. amountPaid: $status === 2 ? $total : (int) round((float) ($row['credit'] ?? 0)), status: $status, paymentMethod: $row['paymentmethod'] ?: null, dueDate: $dueDate, paidAt: $this->nullableDate($row['datepaid'] ?? null), note: $row['notes'] ?: null, createdAt: $this->nullableDate($row['created_at'] ?? null), items: $items, ); } /** * invoices.status: 1=unpaid, 2=paid, 3=cancelled, 4=overdue, 5=refunded. */ private function mapStatus(string $status, ?string $dueDate): int { return match ($status) { 'Paid' => 2, 'Cancelled' => 3, 'Refunded' => 5, 'Collections' => 4, default => ($dueDate && Carbon::parse($dueDate)->isPast()) ? 4 : 1, }; } /** * WHMCS has zero-dates ('0000-00-00...') and, in this data, at least one bogus * year (e.g. '0004-04-22') — both invalid for MySQL strict mode, so nullify them. */ private function nullableDate(?string $value): ?string { if (! $value || (int) substr($value, 0, 4) < 1970) { return null; } return $value; } }