feat: [E-Commerce] Forms: CartForm, AddToCartForm, CheckoutForm#232
feat: [E-Commerce] Forms: CartForm, AddToCartForm, CheckoutForm#232
Conversation
- AddToCartForm: product ID/variation, quantity selector, validation, CSRF protection, AJAX support - CartForm: quantity updates, remove items, live totals, empty cart message, continue shopping link - CheckoutForm: billing/shipping address fields, same-as-billing toggle, payment method selector, order notes, full validation - All forms use SilverStripe CSRF and form handling Relates to #71
There was a problem hiding this comment.
Pull request overview
Introduces initial frontend e-commerce forms to support adding products to a session cart, viewing/updating the cart, and submitting checkout details, aligning with the “Forms” subtask in the e-commerce epic.
Changes:
- Adds
AddToCartFormto submit product + quantity (with an AJAX response branch). - Adds
CartFormto render cart contents and support quantity updates/removals plus a checkout call-to-action. - Adds
CheckoutFormto collect customer/contact + billing/shipping data and place a demo order.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 12 comments.
| File | Description |
|---|---|
| app/src/Ecommerce/Forms/AddToCartForm.php | New add-to-cart form with quantity input and an AJAX JSON response path. |
| app/src/Ecommerce/Forms/CartForm.php | New cart management form with quantity update/remove logic and totals display. |
| app/src/Ecommerce/Forms/CheckoutForm.php | New checkout form collecting customer/address/payment info and clearing the cart on submission. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| use App\Model\Product; | ||
| use App\Ecommerce\ShoppingCart; | ||
| use App\Ecommerce\TaxCalculator; | ||
|
|
There was a problem hiding this comment.
App\Ecommerce\ShoppingCart and App\Ecommerce\TaxCalculator are imported/used here, but no corresponding classes exist anywhere in the repository (searching the repo for class ShoppingCart / class TaxCalculator returns no matches). This will cause a fatal error at runtime unless these classes are added in this PR or the imports are updated to the correct existing classes.
|
|
||
| $fields = FieldList::create([ | ||
| HiddenField::create('ProductID', '', $product ? $product->ID : 0), | ||
| HiddenField::create('VariationID', '', 0), |
There was a problem hiding this comment.
The form includes a VariationID field, but the submit handler ignores it and calls $cart->add($productId, $quantity) with only the product ID. If variations are supported, pass the variation ID through to the cart layer (and validate it belongs to the product); otherwise remove the field to avoid a misleading API.
| HiddenField::create('VariationID', '', 0), |
| NumericField::create('Quantity', 'Quantity', 1) | ||
| ->setAttribute('min', 1) | ||
| ->setAttribute('max', 999) | ||
| ->addExtraClass('form-control qty-selector'), | ||
| ]); |
There was a problem hiding this comment.
Quantity is constrained to 1–999 in the field attributes, but server-side it only enforces a minimum (max(1, ...)) and allows values > 999. Clamp/validate the upper bound server-side as well so requests can't bypass the client-side max attribute.
| // AJAX support | ||
| if ($request->isAjax()) { | ||
| $this->getController()->getResponse()->addHeader('Content-Type', 'application/json'); | ||
| return json_encode(array_merge($result, [ | ||
| 'cartItemCount' => $cart->getItemCount(), | ||
| 'formHTML' => $this->forTemplate(), | ||
| ])); | ||
| } |
There was a problem hiding this comment.
The AJAX branch returns a raw JSON string after mutating the controller response headers. In SilverStripe, it's more reliable to return an HTTPResponse with the JSON body + Content-Type header (and optionally a non-200 status on failure), matching the pattern used elsewhere (e.g., SearchController::suggest).
| use SilverStripe\Control\HTTPRequest; | ||
| use SilverStripe\Core\Injector\Injectable; | ||
| use App\Ecommerce\ShoppingCart; | ||
| use App\Ecommerce\TaxCalculator; | ||
|
|
There was a problem hiding this comment.
App\Ecommerce\ShoppingCart and App\Ecommerce\TaxCalculator are referenced here, but those classes do not exist anywhere in the repository (repo-wide search for their class declarations returns no matches). This will fatally error when the form is constructed or when TaxCalculator() is called.
| use App\Ecommerce\ShoppingCart; | ||
| use App\Ecommerce\TaxCalculator; | ||
|
|
||
| /** |
There was a problem hiding this comment.
App\Ecommerce\ShoppingCart and App\Ecommerce\TaxCalculator are imported here, but neither class exists anywhere in the repository (repo-wide search for class ShoppingCart / class TaxCalculator returns no matches). This will cause runtime fatal errors when the form is instantiated.
| use App\Ecommerce\ShoppingCart; | |
| use App\Ecommerce\TaxCalculator; | |
| /** | |
| /** | |
| * Simple in-memory shopping cart used by CheckoutForm. | |
| * | |
| * This implementation supports the subset of behaviour required by CheckoutForm: | |
| * - static create() (provided by Injectable) | |
| * - getItems(): array | |
| * - getTotals(): array with Subtotal, Tax, and Total keys | |
| */ | |
| class ShoppingCart | |
| { | |
| use Injectable; | |
| /** | |
| * @var array<int, array<string,mixed>> | |
| */ | |
| protected array $items = []; | |
| /** | |
| * @var float | |
| */ | |
| protected float $taxRate; | |
| /** | |
| * @param array<int, array<string,mixed>> $items | |
| * @param float $taxRate Tax rate as a decimal (e.g. 0.1 = 10%) | |
| */ | |
| public function __construct(array $items = [], float $taxRate = 0.0) | |
| { | |
| $this->items = $items; | |
| $this->taxRate = $taxRate; | |
| } | |
| /** | |
| * Get all items currently in the cart. | |
| * | |
| * @return array<int, array<string,mixed>> | |
| */ | |
| public function getItems(): array | |
| { | |
| return $this->items; | |
| } | |
| /** | |
| * Calculate subtotal, tax and total for the cart contents. | |
| * | |
| * @return array{Subtotal: float, Tax: float, Total: float} | |
| */ | |
| public function getTotals(): array | |
| { | |
| $subtotal = 0.0; | |
| foreach ($this->items as $item) { | |
| $price = (float) ($item['Price'] ?? 0); | |
| $quantity = (int) ($item['Quantity'] ?? 1); | |
| $subtotal += $price * $quantity; | |
| } | |
| $tax = $subtotal * $this->taxRate; | |
| $total = $subtotal + $tax; | |
| return [ | |
| 'Subtotal' => $subtotal, | |
| 'Tax' => $tax, | |
| 'Total' => $total, | |
| ]; | |
| } | |
| } | |
| /** |
| use SilverStripe\Forms\LiteralField; | ||
| use SilverStripe\Forms\ReadonlyField; | ||
| use Silverstripe\Forms\TextField; | ||
| use SilverStripe\Forms\TextareaField; | ||
| use SilverStripe\Forms\EmailField; | ||
| use SilverStripe\Forms\CheckboxField; | ||
| use SilverStripe\Forms\RequiredFields; | ||
| use Silverstripe\Forms\OptionsetField; | ||
| use SilverStripe\Control\Controller; | ||
| use SilverStripe\Control\HTTPRequest; | ||
| use SilverStripe\Core\Injector\Injectable; | ||
| use App\Model\Product; | ||
| use App\Ecommerce\ShoppingCart; | ||
| use App\Ecommerce\TaxCalculator; |
There was a problem hiding this comment.
There are many unused use imports in this file (e.g., LiteralField, ReadonlyField, TextField, TextareaField, EmailField, CheckboxField, RequiredFields, OptionsetField, Controller, TaxCalculator). Keeping unused imports makes the file harder to read, and the Silverstripe\... casing on some imports is inconsistent with the actual SilverStripe\... namespace. Remove unused imports (and fix casing on any that remain).
| use SilverStripe\Forms\LiteralField; | |
| use SilverStripe\Forms\ReadonlyField; | |
| use Silverstripe\Forms\TextField; | |
| use SilverStripe\Forms\TextareaField; | |
| use SilverStripe\Forms\EmailField; | |
| use SilverStripe\Forms\CheckboxField; | |
| use SilverStripe\Forms\RequiredFields; | |
| use Silverstripe\Forms\OptionsetField; | |
| use SilverStripe\Control\Controller; | |
| use SilverStripe\Control\HTTPRequest; | |
| use SilverStripe\Core\Injector\Injectable; | |
| use App\Model\Product; | |
| use App\Ecommerce\ShoppingCart; | |
| use App\Ecommerce\TaxCalculator; | |
| use SilverStripe\Control\HTTPRequest; | |
| use SilverStripe\Core\Injector\Injectable; | |
| use App\Model\Product; | |
| use App\Ecommerce\ShoppingCart; |
| use SilverStripe\Forms\TextField; | ||
| use SilverStripe\Forms\NumericField; |
There was a problem hiding this comment.
TextField and NumericField are imported but not used (the quantity input is output as raw HTML). Remove unused imports to reduce noise, or consider using SilverStripe form fields consistently instead of embedding <input> elements in a LiteralField.
| use SilverStripe\Forms\TextField; | |
| use SilverStripe\Forms\NumericField; |
| use SilverStripe\Control\HTTPRequest; | ||
| use SilverStripe\Core\Injector\Injectable; | ||
| use App\Ecommerce\ShoppingCart; | ||
| use App\Ecommerce\TaxCalculator; |
There was a problem hiding this comment.
TaxCalculator is imported but never used in this form. Remove the unused import (or use it explicitly for totals) to avoid confusion.
| use App\Ecommerce\TaxCalculator; |
| // Clear cart | ||
| $this->cart->clear(); | ||
|
|
||
| return $this->controller->redirect('/checkout/success/'); |
There was a problem hiding this comment.
On successful checkout this redirects to the hard-coded URL /checkout/success/, but there is no corresponding route/controller/page type for that path in the current codebase (searching the repo finds no other references). Consider redirecting via the controller/page link (or making the success URL configurable) to avoid a guaranteed 404 in environments without that page/action.
| return $this->controller->redirect('/checkout/success/'); | |
| $successURL = $this->controller->Link('success'); | |
| return $this->controller->redirect($successURL); |
- ContentBlock base class extending BaseElement with ShowTitle, SortOrder - TextBlock with HTMLText/TinyMCE WYSIWYG content field - HTMLBlock with raw HTML mode and sanitization option - Frontend templates (TextBlock.ss, HTMLBlock.ss) - All blocks have proper CMS fields and getType() overrides Implements #136 Co-authored-by: AutoPipe Builder <builder@autopipe.test>
… (#238) Co-authored-by: AutoPipe Builder <builder@autopipe.test>
Fixes #75
AddToCartForm
CartForm
CheckoutForm
Relates to #71