Skip to content

Store ​

Products with variants and prices in tables of their own, a cart, a checkout with taxes and delivery zones, orders with receipts — a store for a new site.

kitloom/store · admins: kitloom/store-filament, kitloom/store-nova

Video lessons ​

Recorded in the Filament admin; the Nova admin works the same way.

A store for a new Laravel site: products with variants and prices in tables of their own, categories and tags, the catalogue and product pages, a cart, a checkout with taxes and delivery zones, orders paid through kitloom/payments, receipts, the customer's account. Not the WooCommerce shop (kitloom/shop) — that one carries a site over from WordPress; this one starts clean. The admin is kitloom/store-filament or kitloom/store-nova.

What a product is ​

A product is a post of the store_product type — its title, description, address, status, author, categories, tags and pictures are the post's, so SEO, search, revisions, translations and custom fields work as on any page — with its sale half in two tables of the store:

TableRow
store_productsone per product: the kind (simple, variable), whether it is sellable, the tax class, the unit, the flags (featured, sold individually), the minimum quantity and step, the minimum age, the sale window, the options a variable product is picked by
store_variantsone per thing sold: the SKU, the values of the options it was made for, the price and sale price, the cost, the stock, the measures, a picture, whether it is active

A simple product has one variant without values; a variable one a variant per combination it sells. Prices are kept in minor units (cents) with the store's currency; the forms, the sheets and the agent tools speak in decimals.

Screens ​

Deliveries: the board from ready to delivered
Orders: from pending to ready
Products: prices, stock and variants
Store settings: currency, checkout, orders
Delivery zones, tax rates and coupons
  • Products — the list with the SKU, the price and the stock; the form with the post's fields and the "Store data" box: the kind, a simple product's price, SKU and stock, a variable product's options and variants, the tax class, the unit, the flags, the sale window.
  • Categories — a tree, each with a picture and an order on the storefront.
  • Tags — flat labels.
  • Orders — the list by status with a search for the number, the customer or a product; an order's page with its items, totals, addresses, payment and delivery, the notes and the history. From it: Change status (with a reason; a settled order needs Force), Add note (the staff's own, or sent to the customer), Edit details (the email, the phone, the addresses, the note).
  • Deliveries — the board: every order on its way, by tab (open, today's windows, on the way, pickups, all), with the window, the courier's status and tracking link, and the hands that move it: Move (a status with a reason), Call the courier (again, after a refusal), Call off.
  • Customers — who ordered, how often, for how much, when last.
  • Coupons — a code, percent off, an amount off or free delivery, from a subtotal, between dates, so many uses in all and per customer.
  • Delivery zones — where the store delivers (countries, regions as CA:MB, postcode patterns as R3*) and how: each method a type — a flat rate, free over an amount, pickup, local delivery — with its own settings. The first zone that fits the address serves it; a zone with nothing named fits everywhere, so it goes last.
  • Tax rates — a percentage by name (GST, PST, VAT) for a country, a region and a postcode pattern, on a tax class, by priority; a compound rate is taken on the price plus the rates before it. Prices are entered without tax unless Prices include tax is on.
  • Settings — the currency (set it before the first product: prices are not converted), products per page, the default sorting, extra tax classes; the store's own address (the checkout starts from its country, a pickup is taxed there); the cart and checkout addresses; guest checkout; the order number prefix; where a new order is announced; the receipt's header; how long a guest's cart is kept.

How an order goes ​

Pending until the money is in — or, with an offline method such as a bank transfer, until the store starts on it — then paid, preparing, ready (for the counter or the courier), completed; cancelled, refunded or failed on the side. The stock is held from the moment the order is placed, given back when it is cancelled or fails, taken for good when it is completed. A declined payment fails the order, an abandoned one cancels it; an order still owed can be paid again or cancelled by the customer from its page. The customer hears of each step by email (and by text, with kitloom/text-messages): received, paid (with the receipt as a PDF), ready, completed, cancelled, refunded, and a note written for them; the store hears of an order to work on at the address in the settings.

How a delivery goes ​

Every order with a delivery method gets one delivery: the way chosen, the fee, the address or the pickup place, the window the customer picked at the checkout (delivery_window_from, delivery_window_to, optional). It is pending until the order is ready — the Ready status; or paid, as the setting Call the courier when the order is says — then a method carried by a courier (a Courier named by the shipping method) is handed over from the queue and becomes scheduled with the courier's id and tracking link, while a pickup becomes ready to collect. From there the courier's webhooks, store:sync-deliveries (every fifteen minutes, for a courier without webhooks) or the board move it: picked up, out for delivery, delivered — which completes the order — or failed (the owner is told, the order waits for another try), returned, cancelled. A cancelled order calls its delivery off with the courier. The customer hears of it: out for delivery (with the tracking link), delivered, could not be delivered.

On the site ​

  • /store/ is the catalogue, /store/{slug}/ a product, /store/category/{path}/ a category with its subcategories' products, /store/tag/{slug}/ a tag.
  • The catalogue is narrowed and sorted from the query string: ?q= a phrase (title, short description or SKU), ?min_price= and ?max_price= in the store's currency, ?in_stock=1, ?orderby= one of newest, price, price-desc, name, featured, and ?attr_{option}=value,value for a product's options.
  • A theme reads two page parts: storeCatalog (one page of product cards, the filters, the price bounds, the category tree, the options with their counts) and storeProduct (the card, the short description, the gallery, the options and the active variants, the spec rows, related products). php artisan kitloom:schema writes their shape.
  • A product the store no longer sells (Sellable off) keeps its page and stays out of the catalogue and the site's search.
  • /cart/ and /checkout/ are pages of the store's own (the slugs are settings), with no WordPress page behind them; /checkout/received/{number}/?key=… is where an order lands, by the key its customer holds. A theme draws them from the store-cart, store-checkout and store-order-received templates and the storeCart, storeCheckout and storeOrderReceived parts; storeMiniCart is on every page, storeNotices says what the last action did.
  • The forms post to /wp-json/store/v1/: cart/lines (add), cart/lines/{id} (a quantity), cart/lines/{id}/remove, cart/coupon, cart/coupon/remove, cart/delivery (the method and address for the fees), checkout, orders/{number}/pay, orders/{number}/cancel, orders/{number}/receipt, account/addresses. A form gets a redirect with a notice, a script (Accept: application/json) the priced cart or the order. The urls of each part name them.
  • The customer's account page gets Orders (?section=store-orders) and Addresses (?section=store-addresses).
  • Shopping carts live in a table: a customer's by their account, a guest's by a token in the session, joined on sign-in. The page cache leaves out the store's pages and every page of a visitor with a cart.

Console ​

bash
php artisan store:demo           # three categories and a dozen products with pictures, GST and PST, a zone for Canada, a coupon, orders in every status
php artisan store:expire-carts     # guest carts past their day, scheduled daily
php artisan store:sync-deliveries  # ask the couriers how their active deliveries go, every fifteen minutes

For developers ​

  • Kitloom\Store\Products\ProductWriter writes a product the one way for every caller: create($attributes, $actor, $terms, $featuredImage), update($product, $attributes, $actor, $terms, $featuredImage), updateStore($product, $fields, $actor). The attributes are the post's (title, content, excerpt, slug, status) and the store's (kind, sellable, tax_class, unit, sold_individually, min_quantity, step, min_age, featured, sale_from, sale_until, attributes, variants); a simple product may give price, sale_price, sku, stock, manage_stock at the top. Prices are decimals. The writer refuses a taken SKU, a sale price above the price, a value outside the options.
  • Kitloom\Store\Events\ProductSaved after every write, with the actor.
  • A vertical's fields on a product (a beer's strength, a plant's potency): a Kitloom\Store\Contracts\ProductExtension tagged ProductExtension::TAG — fields() by key (text, number, select, boolean, textarea, with a label, a unit, options), taxonomies() of the product type the module registered, defaults($attributes) for a new product of its own (['min_age' => 18]), specs($product) for the rows on the page. The values live in the product's data, the writer checks them, both admins draw them under Details, the sheet exports them, the storeProduct part carries the rows and the taxonomies, and a taxonomy's page is a catalogue page (see kitloom/beer-store).
  • An amount beside a line's price (a container deposit, a fee): a Kitloom\Store\Contracts\LineExtras tagged LineExtras::TAG — extras($line) answering Cart\Extras (a key, a label, the amount per unit, a tax class). Summed by key on the cart (extras, extrasTotal), taxed by their own class, never discounted, a line of their own on the order (data.extra), the receipt and the emails; Order::extras() sums them back.
  • Kitloom\Store\Catalog\ProductCatalog lists products by CatalogFilters; ProductCards draws a card.
  • The cart: Cart\ShoppingCart (the request's — add, setQuantity, remove, applyCoupon, setDelivery, priced()), Cart\CartPricing (a cart priced for an address: today's prices, the coupon, the delivery quotes, the taxes, the total — a PricedCart).
  • The checkout: Checkout\Checkout::place($data, $pricedCart, $user) writes the order (Orders\OrderWriter), holds the stock, counts the coupon and starts the payment; Checkout\CheckoutForm has the rules. Orders\OrderStatusChanger::change($order, $status, $actor, $force, $reason) moves an order; Orders\OrderNotes writes notes; Orders\Orders::locked() changes one under its row lock. Events: OrderPlaced, OrderStatusChanged (from, to, the actor, the reason), OrderNoteAdded.
  • Deliveries: Deliveries\Deliveries — of($order), change($delivery, $status, $actor, $reason), apply($delivery, CourierUpdate) for what a courier's webhook learnt, dispatch, cancel, sync, require($delivery, [...]) for what a vertical needs at the door; event DeliveryStatusChanged.
  • Contracts: Contracts\ShippingMethod tagged ShippingMethod::TAG — a delivery type of your own (key, label, settingsFields, courier, quote); Contracts\Courier tagged Courier::TAG — a courier called by API (create, cancel, status, each answering a CourierUpdate); kitloom/inventory binds its LedgerForStore over the store's StockLedger when installed (the variants become its items on first contact, their stock is what the warehouse has available); Contracts\StockLedger — the shelf (available, reserve, release, fulfil), the store's Stock\SimpleStock by bindIf, a warehouse module binds its own; Contracts\CheckoutStep tagged CheckoutStep::TAG — fields a vertical adds to the checkout and what they write on the order's data; Contracts\PdfRenderer — the receipt's engine, dompdf by default.
  • Offered to other modules: NotificationTopics store_order.new, store_order.cancelled and store_delivery.failed; webhook topics store.order.placed, store.order.paid, store.order.status, store.delivery.status (with kitloom/webhooks); PersonalDataExporter and PersonalDataEraser over the orders and the saved addresses (with kitloom/privacy); the sheets store:product and store:order; the agent tools store_products_*, store_orders_list, store_orders_get, store_orders_update_status, store_orders_add_note, store_deliveries_update; the public agent tools store_search, store_product, store_cart_link; the trackers' add_to_cart, begin_checkout and purchase events.
  • Tests: Kitloom\Store\Testing\StoreTestCase boots the skeleton with the store's tables (product(), category(), zone(), rate(), addToCart(), placeOrder() with the fake gateway); Testing\MakesOrders writes an order without the checkout; Testing\FakeCourier and Testing\CourierMethod stand for a courier called by API.
  • Money: Kitloom\Store\Support\Money (minor units ↔ decimals), PriceFormat (the store's currency in the request's locale).
  • With kitloom/ai: store_products_list, store_products_get, store_products_save; with kitloom/spreadsheets: the store:product sheet, a row per variant.
  • Tests: php vendor/bin/phpunit --testsuite Store from the application.

Read more ​

kitloom — modules and themes for Laravel.
Contact · Pricing · Services · License · Refunds · Privacy · Community · YouTube · [email protected]