whenLoaded('pricings', function () { return $this->pricings->min('price'); }); $osIds = $this->whenLoaded('operatingSystems', function () { return $this->operatingSystems->pluck('id')->toArray(); }); return [ 'id' => $this->id, 'uuid' => $this->uuid, 'name' => $this->name, 'slug' => $this->slug, 'thumbnail' => $this->thumbnail, 'product_type' => $this->product_type, 'page_type' => $this->resolvePageType($this->product_type), 'product_categories_id' => $this->whenLoaded('category', fn() => $this->category?->id), 'category' => $this->whenLoaded('category', fn() => $this->category?->name), // Which billing_cycle values this product's category expects (drives cycle // pickers in the UI) — e.g. domain is annual-only, IT service can be one_time. 'category_billing_cycles' => $this->whenLoaded('category', fn() => $this->category?->billing_cycles), 'service_plan_id' => $this->service_plan_id, 'cpu' => $this->cpu, 'ram' => $this->ram, 'disk' => $this->disk, 'bandwidth' => $this->bandwidth, 'domain_count' => $this->domain_count, 'features' => $this->features, 'setup_fee' => $this->setup_fee, 'min_price' => $minPrice, 'billing_type' => $this->whenLoaded('pricings', function () { // 'one_time' (with underscore) is the convention used everywhere else in the // app (OrderHelper::cycleToMonths, ServiceExpiry, renewService, product_pricings // migration comment) — this used to check 'onetime' and never matched real rows. $hasOnetime = $this->pricings->where('billing_cycle', 'one_time')->isNotEmpty(); $hasRecurring = $this->pricings->where('billing_cycle', '!=', 'one_time')->isNotEmpty(); return ($hasOnetime && !$hasRecurring) ? 'one_time' : 'recurring'; }), 'currency' => $this->whenLoaded('pricings', fn () => $this->pricings->first()?->currency ?? 'VND'), 'price_monthly' => $this->whenLoaded('pricings', fn () => optional($this->pricings->firstWhere('billing_cycle', 'monthly'))->price), 'price_quarterly' => $this->whenLoaded('pricings', fn () => optional($this->pricings->firstWhere('billing_cycle', 'quarterly'))->price), 'price_semiannually' => $this->whenLoaded('pricings', fn () => optional($this->pricings->firstWhere('billing_cycle', 'semi_annually'))->price), 'price_annually' => $this->whenLoaded('pricings', fn () => optional($this->pricings->firstWhere('billing_cycle', 'annually'))->price), 'price_biennially' => $this->whenLoaded('pricings', fn () => optional($this->pricings->firstWhere('billing_cycle', 'biennially'))->price), 'price_triennially' => $this->whenLoaded('pricings', fn () => optional($this->pricings->firstWhere('billing_cycle', 'triennially'))->price), 'price_onetime' => $this->whenLoaded('pricings', fn () => optional($this->pricings->firstWhere('billing_cycle', 'one_time'))->price), 'is_active' => $this->is_active, 'created_at' => $this->created_at, 'os_ids' => $osIds, 'module_host_id' => $this->module_host_id, 'module_datastore_id' => $this->module_datastore_id, ]; } private function resolvePageType(?string $productType): string { return match ($productType) { 'vps', 'cloud' => 'service_vps', 'hosting', 'email', 'ssl' => 'service_hosting', 'domain' => 'service_domain', 'blog' => 'blog', default => 'service_vps', }; } }