model ->where('customer_id', $customerId) ->orderByDesc('created_at') ->paginate($limit); } // ------------------------------------------------------------------------- // createFromOrder // ------------------------------------------------------------------------- public function createFromOrder(Order $order): Invoice { $maxMonths = $order->orderItems->max(fn($item) => $this->cycleToMonths($item->billing_cycle)) ?? 1; $invoice = $this->model->create([ 'invoice_number' => OrderHelper::generateOrderNumber('INV'), 'customer_id' => $order->customer_id, 'order_id' => $order->id, 'invoice_type' => 1, // new service 'sub_total' => $order->total, 'discount_amount' => $order->discount_amount ?? 0, 'tax' => 0, 'total' => $order->total, 'status' => InvoiceStatus::UNPAID->value, 'due_date' => now()->addMonths($maxMonths)->toDateString(), ]); // Create one invoice_item per order_item line + addon sub-lines foreach ($order->orderItems as $item) { InvoiceItem::create([ 'invoice_id' => $invoice->id, 'product_id' => $item->product_id, 'type' => 'service', 'description' => $item->product?->name ?? 'Dịch vụ', 'months' => $this->cycleToMonths($item->billing_cycle), 'quantity' => $item->quantity ?? 1, 'unit_price' => $item->price, 'total_price' => $item->price * ($item->quantity ?? 1), ]); $addonsMeta = is_array($item->addons_meta) ? $item->addons_meta : []; foreach ($addonsMeta as $meta) { $qty = (int) ($meta['qty'] ?? 1); $price = (int) ($meta['price'] ?? 0); $subtotal = (int) ($meta['subtotal'] ?? $price * $qty); $name = $meta['name'] ?? ('Addon #' . ($meta['addon_id'] ?? '?')); $code = strtoupper($meta['code'] ?? ''); $label = $code ? "[{$code}] {$name}" : $name; InvoiceItem::create([ 'invoice_id' => $invoice->id, 'product_id' => $item->product_id, 'type' => 'addon', 'description' => $label, 'months' => $this->cycleToMonths($item->billing_cycle), 'quantity' => $qty, 'unit_price' => $price, 'total_price' => $subtotal, ]); } } return $invoice; } // ------------------------------------------------------------------------- // Helpers // ------------------------------------------------------------------------- private function cycleToMonths(?string $cycle): int { return match ($cycle) { 'monthly' => 1, 'quarterly' => 3, 'semi_annually' => 6, 'annually' => 12, 'biennially' => 24, 'triennially' => 36, default => 1, }; } }