From 065411d7bb29221b26a570e5cb53b54d8307c9d8 Mon Sep 17 00:00:00 2001 From: ste87 Date: Mon, 9 Mar 2026 12:39:42 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20Checkout.php?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Checkout.php | 271 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 271 insertions(+) create mode 100644 Checkout.php diff --git a/Checkout.php b/Checkout.php new file mode 100644 index 0000000..cd0ea8d --- /dev/null +++ b/Checkout.php @@ -0,0 +1,271 @@ +product = $this->category->products()->where('slug', $product)->firstOrFail(); + if ($this->product->stock === 0) { + return $this->redirect(route('products.show', ['category' => $this->category, 'product' => $this->product]), true); + } + + // Is there a existing item in the cart? + if (Cart::get()->items->where('id', $this->cartProductKey)->isNotEmpty() && Cart::get()->items->where('id', $this->cartProductKey)->first()->product->id === $this->product->id) { + $item = Cart::get()->items->where('id', $this->cartProductKey)->first(); + // Get the item from the cart + $this->plan = $item->plan; + $this->plan_id = $this->plan->id; + $this->configOptions = array_column($item->config_options, 'value', 'option_id'); + // Update config options for checkbox types + foreach ($this->product->configOptions->where('type', 'checkbox') as $option) { + $this->configOptions[$option->id] = isset($this->configOptions[$option->id]) ? true : false; + } + $this->checkoutConfig = (array) $item->checkout_config; + } else { + // Set the first plan as default + $this->plan = $this->plan_id ? $this->product->plans->findOrFail($this->plan_id) : $this->product->plans->first(); + $this->plan_id = $this->plan->id; + + // Prepare the config options + $this->configOptions = $this->product->configOptions->mapWithKeys(function ($option) { + if (in_array($option->type, ['text', 'number'])) { + return [$option->id => $this->configOptions[$option->id] ?? null]; + } + if ($option->type === 'checkbox') { + return [$option->id => isset($this->configOptions[$option->id]) && in_array($this->configOptions[$option->id], [true, 'true'], true) ? true : false]; + } + + return [$option->id => $this->configOptions[$option->id] ?? $option->children->first()->id]; + })->toArray(); + foreach ($this->getCheckoutConfig() as $config) { + if (in_array($config['type'], ['select', 'radio'])) { + $this->checkoutConfig[$config['name']] = $this->checkoutConfig[$config['name']] ?? $config['default'] ?? array_key_first($config['options']); + } else { + $this->checkoutConfig[$config['name']] = $this->checkoutConfig[$config['name']] ?? $config['default'] ?? null; + } + } + } + // Update the pricing + $this->updatePricing(); + + // As there is only one plan, config options and checkout config, we can directly call the checkout method to avoid confusion + // This is only done when the user is not editing the cart item + if ($this->product->plans->count() === 1 && empty($this->configOptions) && empty($this->checkoutConfig)) { + $this->checkout(); + } + } + + public function updatePricing() + { + $total = $this->plan->price()->price; + $setup_fee = $this->plan->price()->setup_fee; + + $this->product->configOptions->each(function ($option) use (&$total, &$setup_fee) { + // Check if checkbox is set, if so, add price if checked + if ($option->type === 'checkbox' && (isset($this->configOptions[$option->id]) && $this->configOptions[$option->id])) { + $total += $option->children->first()?->price(billing_period: $this->plan->billing_period, billing_unit: $this->plan->billing_unit)->price; + $setup_fee += $option->children->first()?->price(billing_period: $this->plan->billing_period, billing_unit: $this->plan->billing_unit)->setup_fee; + + return; + } + // Skip text, number and checkbox types as they have no price + if (in_array($option->type, ['text', 'number', 'checkbox'])) { + $total += 0; + $setup_fee += 0; + + return; + } + + // Add price of selected option + $total += $option->children->where('id', $this->configOptions[$option->id])->first()?->price(billing_period: $this->plan->billing_period, billing_unit: $this->plan->billing_unit)->price; + $setup_fee += $option->children->where('id', $this->configOptions[$option->id])->first()?->price(billing_period: $this->plan->billing_period, billing_unit: $this->plan->billing_unit)->setup_fee; + }); + + $this->total = new Price([ + 'price' => $total, + 'currency' => $this->plan->price()->currency, + 'setup_fee' => $setup_fee, + ], apply_exclusive_tax: true); + } + + // On change of the plan, update the config options + public function updatedPlanId($value) + { + $this->plan = Plan::findOrFail($value); + $this->updatePricing(); + } + + // On change of the config options, update the pricing + public function updatedConfigOptions() + { + $this->updatePricing(); + } + + public function getCheckoutConfig() + { + return once(fn () => ExtensionHelper::getCheckoutConfig($this->product, $this->checkoutConfig)); + } + + public function rules() + { + $rules = [ + 'plan_id' => [ + 'required', + Rule::exists('plans', 'id')->where(function ($query) { + $query->where('priceable_id', $this->product->id)->where('priceable_type', get_class($this->product)); + }), + ], + ]; + foreach ($this->product->configOptions as $option) { + if (in_array($option->type, ['text', 'number'])) { + $rules["configOptions.{$option->id}"] = ['required']; + } elseif ($option->type === 'checkbox') { + // No validation needed for checkbox + } else { + $rules["configOptions.{$option->id}"] = [ + 'required', + Rule::in($option->children->pluck('id')->toArray()), + ]; + } + } + foreach ($this->getCheckoutConfig() as $key => $config) { + $validationRules = []; + if ($config['required'] ?? false) { + $validationRules[] = 'required'; + } + if (isset($config['type'])) { + switch ($config['type']) { + case 'text': + case 'number': + $validationRules[] = 'string'; + break; + case 'select': + case 'radio': + $validationRules[] = 'in:' . implode(',', array_keys($config['options'])); + break; + case 'checkbox': + $validationRules[] = 'nullable'; + $validationRules[] = 'boolean'; + break; + } + } + if (isset($config['validation'])) { + if (is_array($config['validation'])) { + $validationRules = array_merge($validationRules, $config['validation']); + } else { + // Is validation seperated by |? + $validationRules = array_merge($validationRules, explode('|', $config['validation'])); + } + } + if (count($validationRules) > 0) { + $rules["checkoutConfig.{$config['name']}"] = $validationRules; + } + } + + return $rules; + } + + public function attributes() + { + $messages = []; + foreach ($this->product->configOptions as $option) { + $messages["configOptions.{$option->id}"] = $option->name; + } + foreach ($this->getCheckoutConfig() as $key => $config) { + $messages["checkoutConfig.{$config['name']}"] = $config['label'] ?? $config['name']; + } + + return $messages; + } + + public function checkout() + { + // Do the checkout + // First we validate the plans + $this->validate(attributes: $this->attributes()); + + // Change configOptions so they also contain the name of the option (resulting in less database calls = faster speeds) + $configOptions = $this->product->configOptions->map(function ($option) { + if ($option->type == 'checkbox') { + return (object) [ + 'option_id' => $option->id, + 'option_name' => $option->name, + 'option_type' => $option->type, + 'option_env_variable' => $option->env_variable, + 'value' => isset($this->configOptions[$option->id]) && in_array($this->configOptions[$option->id], [true, 'true'], true) ? $option->children->first()->id : null, + 'value_name' => isset($this->configOptions[$option->id]) && in_array($this->configOptions[$option->id], [true, 'true'], true) ? 'Yes' : 'No', + ]; + } + if (in_array($option->type, ['text', 'number'])) { + return (object) [ + 'option_id' => $option->id, + 'option_name' => $option->name, + 'option_type' => $option->type, + 'option_env_variable' => $option->env_variable, + 'value' => $this->configOptions[$option->id], + 'value_name' => $this->configOptions[$option->id], + ]; + } + + return (object) [ + 'option_id' => $option->id, + 'option_name' => $option->name, + 'option_type' => $option->type, + 'option_env_variable' => $option->env_variable, + 'value' => $this->configOptions[$option->id], + 'value_name' => $option->children->where('id', $this->configOptions[$option->id])->first()->name, + ]; + }); + + Cart::add($this->product, $this->plan, $configOptions, $this->checkoutConfig, key: $this->cartProductKey); + + $this->dispatch('cartUpdated'); + + return $this->redirect(route('cart'), true); + } + + public function render() + { + return view('products.checkout')->layoutData([ + 'title' => $this->product->name, + 'image' => $this->product->image ? Storage::url($this->product->image) : null, + ]); + } +}