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, ]); } }