Skip to content

Releases

Current versions

The latest release of KahunaCart and everything that runs with it, with the notes for each version. Updated every hour from the same update feed your Grav admin uses.

Checked RSS feed Atom feed

Core

The plugin every store runs.

KahunaCart 1.6.4 `kahunacart_price_state(product, variant)` 1 new · 2 improved

1.6.4

New

  • kahunacart_price_state(product, variant) answers whether a price is reduced and by how much: on_sale, kind (sale or compare), now_minor, was_minor, percent, the sale's label, the sale badge's words in badge, and the offer itself. A card's struck-through price and its sale badge now come from one answer, the same in every theme. See docs/storefront.md

Improved

  • The sale badge is worked out by KahunaCart, the same in every theme: kahunacart_badges() and partials/kahunacart-badges.html.twig badge a product when a sale with money off covers it or when its compare-at price is above its price, in the same words everywhere ("Summer Sale · 30% off", or "25% off" for a compare-at price). Until now each theme decided for itself, so a compare-at price got a badge in Kahuna's product grid and nowhere else. A theme can still pass sale to word it its own way, or sale: '' for no badge. Themes that pass nothing get the badge with no change (helpdesk #16)
  • The shop's own product card and hero section strike through a compare-at price and say how much is off, the same as a sale, and a sale's line under the price now gives the reduction after the sale's name. The product page draws the sale badge above the gallery with the merchant's badge
Older versions (9)

1.6.3

New

  • Star screens into the admin sidebar: a star beside each row of KahunaCart's nav puts that screen under KahunaCart in the Admin2 sidebar, one click away from anywhere in the admin. Every store screen and every add-on screen can be starred, and Orders, Products and Customers show until you star your own. The pencil at the foot of the Admin2 sidebar offers the same stars. Needs Admin2 2.2.0 and API 1.1.0 for the stars; older admins just don't show them

1.6.2

Fixed

  • Downloads whose file is gone aren't listed. Deleting a release, or a file, keeps the grants that pointed at it as a record of what was sold. Those grants still showed up on the account's Downloads page, the order page and the order emails as rows with the product's name and no file, one for every retired build, so a customer who had been sent twenty builds saw twenty empty rows above the one they could download. Only grants with a file to download are listed now

1.6.1

New

  • Stars on a product card. kahunacart_rating(product) answers a product's average, review count, the maximum and the lit share of the row ({average: "4.3", count: 12, max: 5, percent: 86.0}), or null when reviews are off or nothing is approved yet, so a card prints nothing rather than "0 out of 5". It reads the cached aggregate the product page uses. partials/kahunacart-rating.html.twig draws it: five stars lit to the average over five faded ones, in the reviews' star color, with "12 reviews" beside them. The shipped card is unchanged; a theme includes the partial where its card wants stars. See docs/storefront.md

1.6.0

New

  • A theme can set its card picture's shape. kahunacart.card_size: [width, height] in the active theme's config crops product card pictures (and the wishlist's) to that box instead of 600 × 450, so a theme with square cards no longer has every picture cropped twice and loses its sides. See docs/theming.md
  • Themes extend the storefront instead of copying it. The plugin's templates are now also registered under the @kahunacart Twig namespace, so a theme's kahunacart-checkout.html.twig can be {% extends '@kahunacart/kahunacart-checkout.html.twig' %} plus the blocks it changes. Everything it doesn't override stays the plugin's, so later releases reach the theme without it being touched. A copied template still works exactly as before
  • Every storefront page is split into named blocks. Checkout, cart, product, order, listing, category and search have a block per region, a _layout block for laying the regions out in columns with {{ block('…') }}, and _class blocks that add classes to the wrappers the scripts read without redrawing them. The processing page has a block per state, so a theme that wraps it keeps the declined, held and timed-out pages. See docs/theme-blocks.md
  • One card, one cart line, one receipt line, one delivery method are partials. partials/kahunacart-product-card.html.twig, kahunacart-cart-line.html.twig, kahunacart-cart-set-row.html.twig, kahunacart-order-line.html.twig and kahunacart-shipping-method.html.twig: override one small file to restyle every instance, including the filter swap
  • Attribute blocks on the forms themes wire scripts to. The search box, the estimate form and the bundle suggestion's two forms have an empty block inside the opening tag for htmx and the like
  • The country select takes an id and a class, so a theme's <label for> can point at it
  • More class and region blocks where Kahuna needed them: the currency question on the currency page, and the product grid's list class
  • Blocks on the account overview: its heading, its section's classes, and an empty block above the panels for a row of figures, with the store credit balance (credit_balance_minor) handed to the page beside the order and download counts
  • kahunacart_quick_add(product) tells a product card whether it can add straight to the cart. It answers the variant to post for a product with nothing to choose, and null for one with options, an amount to name, a date or a plan, which a card links to instead. The same rule the cart's cross-sells already used
  • kahunacart_category_image(category, width, height) draws a category's picture at any size, from a kahunacart_categories() row or a node of the listing's tree, for category tiles over the shop
  • Five more theme tokens: --kahunacart-font, --kahunacart-font-display, --kahunacart-shadow, --kahunacart-gap and --kahunacart-focus set the storefront's type, the shadow under its cards, the gap between them and the focus ring. Left unset, everything looks as it did
  • Themes can list a product's releases. kahunacart_releases(product, limit) answers a product's published releases newest first, or with null the latest across every product a shopper can see: the version, the date written in the store's language, the notes as safe HTML, whether it is the product's newest, and each file's label, name and size (never a download link). It lets a theme draw a release ticker, a changelog or a "what's new" strip. See docs/storefront.md
  • Gotify notifications. A new channel kind for store-owner messages, for a Gotify server you run yourself: the server's address and an app token, then Send test. Messages carry their priority on Gotify's scale (quiet 2, normal 5, urgent 8, emergency 10), and tapping one in the Android app opens the admin link. The token goes in the X-Gotify-Key header, never in the URL. See Gotify
  • Whole amounts. A new Whole Amounts switch on the General tab (storefront.whole_amounts, off as shipped) prints $55 instead of $55.00 on the storefront. A price with cents keeps them and is never rounded, and prices shown together (a sale's old and new price, a compare-at price, a "From" price and its top, a tier ladder) are printed alike, so $55.00 $49.50 never turns into $55 $49.50. Currencies with no minor unit are unchanged. Emails, invoices, credit notes, receipts, reports, feeds and the API keep full amounts. kahunacart_price() takes a with argument naming the prices beside it, and kahunacart_whole_amounts() answers the setting
  • A cart or order summary is printed as one set. With Whole Amounts on, every amount in the cart (the lines' prices and totals, the subtotal, the discounts and coupon rows, shipping, tax, fees, store credit, a deposit and its balance, and the total) keeps its cents if any one of them has cents, and drops them only when every one is whole, so a cart no longer reads $48 · -$5.76 · $6 · $48.24. The same goes for the checkout summary with its delivery methods and payment fees, the pay page, and the storefront receipt (the invoice still prints full amounts). The pages carry the list as price_set, each price in the plugin's partials passes it as with, and a theme that overrides a partial does the same with price_set|default([]). kahunacart_price_set(vars) builds the list for a summary a theme draws itself, such as a mini cart. The shipping and tax estimate is a table of its own, and each order in the account's order list is its own set. Messages the storefront words in PHP follow the setting too, each amount on its own: the order minimum and maximum, a pay-what-you-want refusal, the restore page's prices then and now, and the bank transfer instructions on the receipt. Emails keep full amounts
  • Cart, bag or basket. A new Name for the Cart select on the General tab (storefront.cart_word) makes every shopper-facing string that names the cart say bag or basket instead: the buy button, the cart page's title, the empty cart, the currency, coupon and wishlist messages, the restore page and the back links from payment. The language file carries one set of strings per word under CART_WORDS, a translation without them keeps its own cart wording, and add-ons and themes can ship sets of their own. The admin and emails keep saying cart. New strings for a theme's header and mini cart follow it too (CART_LINK, VIEW_CART, BACK_TO_CART, CART_COUNT_ONE, CART_COUNT, ADD_NAMED_TO_CART), and kahunacart_cart_word() answers the word
  • Pre-orders. Give a product an Out on date in the product editor (release_at, in the store's timezone) and it sells as a pre-order until then, then becomes an ordinary product on its own with nothing to switch. Checkout takes payment in full as usual and stock works as it always does, so a limited pressing still sells out. The shopper sees Pre-order on the buy button (a set's or kit's too) and the cart's cross-sells, "Out Friday, November 13" beside the button, a Pre-order badge on cards and the product page, and "Pre-order · out Nov 13" on cart and checkout lines. Each pre-order line keeps the date it was sold with, so the receipt, the confirmation email and the admin say the same thing if you move the date later, and the order list and order page show a Pre-order pill until it is out. A pre-order's download links are handed out at purchase but open on the release day (the receipt, the account's Downloads and the emails say "Soon · Nov 13" until then), and when the date passes the worker sends each order that waited one "It's out: your download is ready" email with its links, never two. The date goes through the API and MCP, the catalog file, the CSV import and export, store export and restore, Duplicate and revisions, and the feeds and structured data say preorder with the release date. See docs/products-and-catalog.md
  • kahunacart_preorder(product) for themes. It answers active, release_at, date_label ("Out Friday, November 13"), short_label ("Pre-order · out Nov 13"), date and short_date while a product is a pre-order, in the store's timezone and the shopper's language, and an empty array for anything else. The shipped buy button already says Pre-order and prints the date, so a theme that includes it gets both for free. See docs/theming.md
  • Digital variants and files on any product. A variant of a physical product can be digital: tick Digital (no shipping) on it in the variant editor (is_digital, migration 0130). A record sold as an amber LP, a black LP, a CD, a cassette and a download is now one product, and a line of its Download format is treated in every way as a digital product's line: no shipping and no weight, no address when everything in the bag is digital, digital tax and payment treatment, no fulfilling, the returns policy's download rules, and no shipping stated in the feeds and structured data for that variant. A mixed bag ships only the physical lines. Product files now work on physical products too: a file with no variant goes to every buyer of the product (each pressing comes with the download), and a file pinned to a variant with the new For select goes to that variant's buyers. Grants follow downloads.grant_on as before, Give to past buyers works the same, pre-orders hold them until their date, and a line refunded in full loses its downloads. The switch goes through the API, MCP, the catalog file, the CSV import and export, store export and restore, Duplicate and revisions; update_product_file takes variant_id. A product changed from digital to physical keeps granting its files. Code that completes orders outside checkout can freeze their pre-order lines with CheckoutService::freezePreOrderLines($orderId). See docs/digital-products.md
  • kahunacart_downloads(limit) for themes. The signed-in customer's downloads, newest first, for a theme's own library on a home page or account: each with title, product_title, product_slug, variant_title (the format it was bought in), image (the product's first picture), url, held, soon_label and order_number. A file a pre-order still holds has url null, held true and soon_label "Soon · Nov 13". It answers an empty list when nobody is signed in or anything goes wrong, and never throws. See docs/theming.md
  • A coupon can apply itself to visitors from an ad. Type the ad's utm_campaign into Ad campaign tag on the coupon (utm_campaign, migration 0129), and every visitor landing on any page with that tag holds the code just as a ?coupon= link gives it: same checks, cookie, banner and days. An ad that is already running gets a discount without editing its URL and going back through review, and the code on the order shows the sale came from the ad even when the buyer returns days later. The URL keeps its tracking, a code the visitor already has stays, and nothing is said when no code is live. In the coupon API and the MCP coupon tools. See Codes that apply from an ad campaign

Improved

  • The "Buy more, save more" ladder says what counts in a sentence. Under the rungs it used to print the admin's one-word option label, so most ladders ended in a lone "items". It now reads "Mix and match: any items in the sale count." or "Each one must be a different product in the sale." (SALE_TIER_COUNTS_UNITS / SALE_TIER_COUNTS_PRODUCTS); the admin's option labels are unchanged
  • Choosing a collection point puts the delivery methods away. While a counter is the chosen option the delivery list is hidden and disabled and an empty method is posted in its place, so a shopper collecting in store isn't shown, or charged for, a courier. Choosing delivery again brings the list back. Kahuna has done this for a while; now every theme gets it
  • Collecting in store ticks "ship to the billing address", as there is no delivery address to ask for
  • The delivery step's "still needed" line is left off printouts, like the same line under the other two steps
  • The cart's remove forms are outside every block, drawn before the cart table instead of after it, so no layout of the page can leave a remove button that does nothing
  • A test holds the block names to the docs. Every kahunacart_* block in the templates must be listed in docs/theme-blocks.md and every listed one must exist, because a block name is something themes depend on
  • The storefront holds its own under themes built on Pico, such as Quark2. Lists the plugin draws (address cards, gallery thumbnails, tags, reviews and the rest) no longer pick up the theme's square bullets, the star-rating labels stay on one line with every bar starting at the same place, the account tabs sit on their underline, and the add-address card loses a stray chevron. The search box keeps its button beside the field: its landmark is now a <search> element around the form instead of role="search" on it, which Pico draws as one pill-shaped group with the button stretched to half the row
  • Payment methods and collection points are drawn as cards at checkout, like providers and delivery methods: the radio beside its name, the price at the far edge, the description under it. The counter's address reads a line at a time and its hours as a two-column week, on the receipt as well. A theme that draws its own cards keeps them
  • Store-owner rows on the Notifications screen only show the channels you have. The greyed "Telegram — set up" chips for every kind not yet set up are gone, so a row lists only the places a message can actually go
  • Add a channel is a set of cards. On the Channels page each kind is a card with a line about it and Set up →, grouped into email and chat, push to your phone, text message and other, and a kind you already have says how many. Picking one shows its form with Choose another kind to go back. #/notifications/channels?add=<kind> still opens straight on a kind's form
  • The few storefront strings that said basket now say cart, like the rest, so a store set to cart never mixes the two: a swapped bundle, a free gift that ran out, a price that changed, the restore page and two delivery reasons
  • The demo sign-in keeps its Account tab. A protected demo login (demo.protected_customers) used to lose the Account tab and a theme's "Password and security" menu item, so a visitor trying the demo couldn't see that a customer can edit their profile. The tab and the item are back, and the page draws the profile form read-only (disabled, no buttons) under the "its details are fixed" notice. Changing anything is still refused on the server, as before
  • The account's front page is headed "Account overview". It matches its Overview tab, so it no longer reads like the Account tab beside it (the profile). The heading and the browser tab use the new ACCOUNT_OVERVIEW_TITLE string; links that say "Your account" are unchanged
  • Rating a product is a row of stars. The review form's five "N out of 5 stars" radio buttons are now five stars that light up to the one under the pointer and stay lit to the one picked, in the theme's star color for reviews. It is still a radio group underneath, so the arrow keys, the required check and a page with no script work as before, and each star still says "4 out of 5 stars" to a screen reader. New classes kahunacart-review-rating-stars, kahunacart-review-rating-input
  • A deposit is plain from the bag to the invoice. On a stay or booking paid in parts, the bag now shows "Due today" and "To pay by …" under the total (it used to start at checkout), and the screen and PDF invoices show "Paid so far" and what is left, by when. The amount paid today ("Due today" before paying, "Paid so far" after) sits in a tinted band under the total rather than reading like one more line. The invoice's date is in the store's date format
  • Account tabs a customer has nothing in are left out. A software store's customer no longer sees empty Bookings and Stock alerts tabs: with the new Hide Empty Tabs setting on (Accounts tab, on by default), Downloads, Stock alerts and any add-on tab that says how to tell ($registry->showsWhen($id, $check), see docs/customer-accounts.md) show only for a customer with something in them. Bookings, Licenses and Subscriptions do this from their next releases. Each page still opens at its address, and the tab shows while you are on it. Turn the setting off to show every tab to everyone
  • Ships its license. A LICENSE file now comes with the plugin, and the blueprint links to the KahunaCart License at kahunacart.com/license instead of the Grav Premium license

Fixed

  • The checkout's collection point no longer prints the counter's name twice. The partial's heading: false was read through default(true), which turns false into true
  • Structured custom fields no longer print raw under "More details". A public field holding a list or a map, such as a record's track list in a json field, printed as a line of JSON on the product page. The shipped partial leaves those out (row.structured), since a theme draws them itself
  • The quantity box no longer says "Limit 240 per order" when no limit is set. The hint beside it was worded after the box's maximum had been capped at the variant's stock, so a pressing with 240 on the shelf read as a per-order limit of 240. It now says only the merchant's own rule; the box still stops at what's in stock
  • Return and refund dates follow the store's language. The return deadline ("You can ask until …") and the request dates printed day first ("19 November 2026") on every store. They're written with the new kahunacart_date(at, form) helper now, which uses the store's locale and timezone: "November 19, 2026" in US English, "19 November 2026" in British English
  • A pre-order is not "In stock". kahunacart_stock() now answers preorder and in_stock: in_stock is true only for a tracked variant with some left that is not low, not on backorder and not a pre-order, so a stock line that prints "In stock" when in_stock is true says nothing under a pre-order's price. "Only 9 left" and sold out print as before. A theme that printed "In stock" for every tracked variant should test stock.in_stock instead
  • Dates a shopper reads follow the store's language everywhere. Review dates ("18 October 2026" on a US English store), the account's order dates ("2026-10-21"), a shipment's "Sent on", a download's expiry, a release's date, the credit balance's rows, the quote, pay and payment-failed pages and emails, a deposit's balance due date and the gift-card, set-password and balance-request emails now go through kahunacart_date(): "October 21, 2026" in US English, "21 October 2026" in British English. Invoices, credit notes, packing slips, the till receipt and reports keep the year-month-day form. Email templates now call kahunacart_date(), so a theme that overrides one of them needs the function too (it is registered with the rest)
  • "1 review", "3 reviews". The count under a product's stars was "3 review(s)". It is now REVIEWS_COUNT_ONE or REVIEWS_COUNT, the way the other counts are
  • The cart page's title uses the cart word in sentence case. "Your Bag" is "Your bag", "Your Basket" is "Your basket" and "Your Cart" is "Your cart", matching "Your bag is empty."
  • More shopper-facing strings follow the cart word. "Added to cart." after an add is now "Added to your bag." (ADDED_TO_CART, with a set for bag and basket), and so are the coupon refusal "This coupon does not apply to anything in your cart" (COUPON_ERROR_SCOPE, which was a fixed English sentence), "Your cart is empty" when checkout is pressed on an empty cart, and "This cart can no longer be copied" on the currency page. Emails and the JSON API still say cart
  • The account's order list shows when a pre-order is out. An order with a pre-order line that is not out yet carries a "Pre-order · out Nov 13" chip beside its status chips, on the account overview and the order history, the way the admin's order list shows its Pre-order pill
  • A pre-order can't be returned before it is out. A pre-order line whose release date is ahead leaves the returnable lines (it is listed as "Not out yet"), so the receipt and the order history no longer offer Return items on an order whose only shippable lines are pre-orders; the order offers the refund request instead. The line can be returned once the product is out

1.5.18

New

  • Disputed orders are marked in the order list. An order a customer disputed carries a red Disputed badge in the Orders list and on the order screen, and the list has a Dispute filter (open, won or lost) once any order has had one. The order's own statuses are not changed, because a dispute is not a refund. Orders whose history already holds a dispute are marked open when the store updates
  • Open disputes in Needs attention. The strip at the top of the KahunaCart admin says how many orders have an open dispute, in red because each one has a deadline, and Review the disputes opens the order list filtered to them (#/orders?dispute=open)
  • A dispute that is settled says how it ended. When the provider reports it, the badge turns into Dispute won or Dispute lost, the order's history says which, and the new Dispute won or lost message goes to the channels ticked for it. Stripe and PayPal report settlements from their next releases. For Paddle, Mollie, or a dispute reported wrong, pick the outcome from the select beside the badge on the order screen (POST /kahunacart/orders/{id}/dispute, MCP tool set_order_dispute)
  • Each payment method's card says how it stands as the Payments screen opens. Under the name, a line says Connected, Needs attention, Not connected or Not checked yet, when it was last checked, whether the webhook endpoint is registered, and when the last webhook arrived. A card whose last check is over an hour old checks again in the background, and the answer is kept, so the screen never has to be pressed to find out. Bank Transfer and Store credit, which no platform sends webhooks for, show none of the webhook parts
  • Check connection looks at the webhook too. A check that passes with something wrong in the webhook is Needs attention, with the reason written out: no endpoint at this store's address, an endpoint switched off at the platform or missing events the driver handles (a Stripe endpoint without charge.dispute.created, say), or three or more failed webhooks in a row. Stripe, Polar, PayPal, Square, Paddle and Lemon Squeezy look their endpoint up, from their next releases; Mollie, SumUp and Moneris send this store's address with each payment, so the delivery log speaks for them
  • Recent webhooks on every card. Lists the last ten webhooks from that platform: when, whether it succeeded, what it was, the answer the store gave, the order it was about and any error
  • bin/plugin kahunacart webhooks --check runs the same check from a shell, for one method with --provider or for all of them, and exits 1 when any fails or needs attention
  • "$5 shipping, free from $20" as one method. A shipping method with a free-shipping minimum has a new switch, Charge under the minimum (free_minimum_charges_below, migration 0127): under the minimum the method is offered at its own cost, with "Free from $20.00" under its price at checkout, and from the minimum the same method costs nothing. Until now a $5 flat rate and a free method gated on $20 both stayed on offer above $20, because a minimum only decides when its own method appears. The minimum follows Compare before discounts and Count only items that ship as before, a live carrier's quote can't put the postage back, and the order's shipping line records the rate it would have cost. Off on every existing method. Offers carry free_from_minor for themes. See Charging under the minimum
  • A customer group's Discount (%) now comes off prices. It was stored and shown but never applied. The cart now takes it off as a sale of that much on everything for the group's members while they're signed in (or attached to a till or admin order), and for guests when the group is the store's default. A customer in several groups gets the highest, a line already on sale gets whichever saves more, codes come off the discounted price, and the totals show one line named after the group. Product pages still show the ordinary price; use a price list with Show as a discount in the catalog for prices members see while browsing. A store that typed a percentage on a group will see it applied after updating, so check Customers → Groups first. See A group's discount
  • A badge of your own over a product's pictures. Badge text and Colour on the product ("BOGO", "Last one", in accent, red, green, amber, blue or dark; badge_text and badge_tone, migration 0128) put a badge over the product's pictures in the shop and on its page, beside the sold-out, sale and download badges. Translatable, in the product API, the MCP product tools and the catalog file. Every badge is now drawn by one partial, partials/kahunacart-badges.html.twig, with fixed classes (kahunacart-badge--{kind}, kahunacart-badge--tone-{tone}) and kahunacart_badges() behind it, so each theme only styles them. See Badges on the picture
  • Each cart line links to its product, opened on the line's variant (?variant=), so a medium red shirt in the cart opens the product page on medium red. Lines carry url, empty for a product that is gone or unpublished

Improved

  • onKahunaCartDisputeClosed for driver and add-on authors. Fired once per dispute and outcome by a driver whose platform reports a settlement, with outcome won or lost, and with manual true when a person set it on the order. Every order row carries dispute_status and disputed_at, and GET /kahunacart/orders?dispute=open filters by it. See the provider developer guide's Dispute event
  • ProviderWebhookStatusInterface for driver authors. A driver whose platform can list webhook endpoints implements webhookStatus($url), read only, and answers with WebhookStatus::forEndpoint(), so every card words the same problem the same way. See the provider developer guide
  • GET /kahunacart/providers answers each method's last check, its deliveries and how its platform finds the store (last_check, deliveries, webhook_mode), and POST /kahunacart/providers/{slug}/check answers state, warnings and webhook beside what it always did
  • GET /kahunacart/webhooks answers each delivery's order_number, so a link to the order works

1.5.17

New

  • A new account starts with the customer's addresses. When a guest's account opens (the box at checkout, the offer on the receipt, or an invitation from the Customers screen), the billing and delivery addresses from their newest orders go into the address book as the defaults, one entry when they're the same. Only an empty book is filled, and an address with no street isn't filed. An account that already has an empty book sees the addresses from its last order on Your addresses with a Save to my address book button. See Addresses from earlier orders
  • Addresses suggested as you type. With a Mapbox or Google key under Address Validation, every street box lists matching addresses after three letters, and picking one fills in the street, town, state, postcode and country and moves to the second address line. At checkout, on the account's Add an address, and in the admin's customer address book, New order form, Pickup locations and the shipping add-on's ship-from addresses. It keeps to the country already chosen, and an address from another country changes the country and its state list. The key never reaches the browser: the store asks the provider through {base}/address-lookup (nonce-checked) and GET /kahunacart/address-lookup, and the typing and the pick share one session token, so each address typed is one session on your provider's bill. Rate limited per visitor (address_lookup, 60 a minute) and for the whole store (address_lookup_store, 3,000 requests a day). Google keys need Places API (New) switched on. A form set to a country whose post is written in one Latin-script language is answered in that language, so towns read the way the post prints them (Wien, Köln, Milano). Turn it off with Suggest Addresses as You Type (checkout.address_validation.lookup). See Suggestions as you type
  • Attach a file to one order. An order's Downloads panel has Attach a file, for a file that belongs to that order alone, such as a certificate of analysis for the batch a customer was sent. The customer sees it on their receipt and in their account's downloads straight away, and with Email the customer ticked (the default) they're sent the new New file for the customer email (download-added) with the link and your message above it. It takes the store's default download limit and expiry, Revoke takes it back, and the order's history records it. Works on physical orders too. Over the API: POST /kahunacart/orders/{id}/downloads (multipart, kahunacart.orders.manage). The file is kept under orders/<id>/ in the files root and carries its order in the new order_id column (migration 0125), so nobody else is given it. A store export carries it with the orders (the new order_files set, under the orders permission) and never with the catalog's files, and anonymising the order deletes it. See Files for one order
  • Give a new product file to past buyers. Tick Give to past buyers beside Upload on a product's files (grant_past_orders=1 on the upload) and everyone who already bought the product, or the variant the file is for, gets that file too (and only that file), newest 500 orders first. Only orders that went through and kept their money are given it: a sale status, a payment not refunded or voided, and paid when downloads.grant_on is paid. See Giving a new file to past buyers

Improved

  • The admin's address forms say what's missing before you save. The customer address book, the New order form and Pickup locations mark every box the country requires (the same set checkout uses, from Settings → Countries) with a line under it, instead of refusing the save. Pickup locations now has country and state lists like the other address forms. Add-on sections get the same form through ctx.ui (countries(), addressFields(), checkAddress(), bindAddressForm()). See Admin sections → An address
  • GET /kahunacart/countries answers each country's required address fields and whether address lookup is on, and is open to kahunacart.settings and kahunacart.shipping.manage as well
  • The KahunaCart admin on a phone opens on the screen, not the nav. Under 900px wide the sidebar used to sit above every screen, so each one started with thirty nav rows to scroll past. It's now a drawer: a bar at the top names the screen you're on (Store · Orders), and tapping it slides the nav in from the right, clear of the admin's own menu on the left. Picking a row, tapping outside, the × or Escape closes it. A desktop that keeps the nav as an icon rail still gets full labels in the drawer
  • The three download upload endpoints share one set of checks. A product's files, a release's builds and an order's files refuse the same uploads (none, too big, a type that isn't stored) in the same words, through AdminController::downloadUpload() and storeDownloadUpload()

Fixed

  • The state field on a new address was a text box in a store with a default country. The account's Add an address form, and the delivery estimate on the cart and wish lists, showed the default country already chosen beside a text box for the state, and only offered the list of states after the country was changed. The state list now follows the country the form opens on

1.5.16

New

  • Cross-sells in the cart. Under the basket, Goes well with your order lists the cross-sells you picked for the products in it: the most recently added product's first, in your order, leaving out anything already in the basket, single-variant products that are sold out, and anything past the fourth. A product that needs nothing chosen (one variant, no add-on options, no pay what you want, no booking dates or plan, no minimum above one) gets an Add button that puts one in the basket and comes back to the cart; anything else gets View. On by default with the new cart.cross_sells setting, and a store that has never picked a cross-sell sees nothing. Themes get cart_cross_sells, and override partials/kahunacart-cart-cross-sells.html.twig. See Cross-sells in the cart
  • The shopper picks a part's variant in a grouped product. Each part in the product's What is in it panel now has a Variant select for a child with more than one variant: Default, a named variant, or the new Shopper picks. A picking part puts a plain select on the set's buy form (no JavaScript needed), with sold-out variants disabled and backordered ones marked the way the product page marks them, and the cart refuses a set whose pick is missing, from another product or not buyable, naming the part, with nothing added. One pick covers every unit of a part that holds more than one. A set is now its product plus the picks, so a red easel set and a blue one are two rows in the cart, each with its own quantity box and ×, and adding the red one again raises the red set; a set with no picking parts works exactly as before. The bundle price counts any variant of a picking part towards a set, at the same price whichever is picked; such a set is counted one set at a time, each taking the units worth most to it, so the same basket comes to the same total whatever order it was filled in, and a set with no picking parts is counted exactly as before. The storefront API (parts on POST /cart/items, sets by key on /api/cart/sets/{key}), an agent checkout's line items, counter carts and orders staff build take the picks too. Stored as shopper_picks on the part by migration 0124 and returned in kit_items. See Which variant a part sells

1.5.15

New

  • The cart draws a grouped product as one row with its parts under it. The lines a grouped product's buy button adds are headed by a row of the product's own, with its picture, a quantity box for how many sets, what the set adds up to, and one × that takes the whole set out. Changing the set's quantity moves every part by what one set needs and leaves any spare part where it is. By default a part keeps its own quantity box and ×, and changing one part so the cart no longer holds a full set stops a bundle price from applying. Tick the new Lock the parts in the cart (lock_parts) on the product and the parts change only through the set's row: no box and no × on a part, and a change to one part on its own is refused from the cart, the storefront API and an agent checkout. A set already in a basket keeps the rule it was added under. A kit's parts (an add-on type's) are always locked. A part of a set and the same thing bought on its own are now two lines, so a spare panel isn't folded into the easel. Pricing, stock, the bundle price, tax and refunds are unchanged, and the checkout, receipt and emails still list the parts. Themes get cart_rows, and the storefront API gives each line a set and has PATCH/DELETE /api/cart/sets/{product_id} for a whole set. See Kits and grouped products → In the cart
  • Pictures on cart lines. Each line in the cart shows a small picture: the one its variant shows on the product page (the variant's own, else its colour's, else the product's first), or none when the product has no pictures. Themes get it as item.image

Fixed

  • Removing a free gift through the storefront API answered with a server error. DELETE /api/cart/items/{id} on a promotion's gift now answers 422 with the sentence saying why it stays, like the cart page

Improved

  • The agent checkout and the cart pick a line's picture the same way. Both go through KahunaCart::lineImages()

1.5.14

New

  • A shop page puts the shop in your menu. The shop answers at its route without a page of its own, so the menu label, its place and whether it shows were the theme's to decide. Add a page at the store's address with the new Shop page template (kahunacart) and the shop is in the page tree like any other page: its menu: is the label, its folder number its place, and visible: false hides it. Every storefront page hangs off it, so the menu marks it active anywhere in the shop. The product listing takes its title, its metadata and its content as an intro, and the sitemap plugin lists it, which puts the shop's front door in the sitemap for the first time (left out while the store is coming soon). The address is still the store's route: a shop page anywhere else redirects to the store, and the new Shop page check in bin/plugin kahunacart check and on the Health screen says how to fix it. Themes read kahunacart_shop_page to leave out their own Shop link. A site without a shop page is unchanged. See The shop page

Add-ons

Bought separately, each with its own license.

Affiliates 1.1.2 Ships its license 1 improved

1.1.2

Improved

  • Ships its license. A LICENSE file now comes with the plugin, and the blueprint links to the KahunaCart License at kahunacart.com/license instead of the Grav Premium license
Older versions (5)

1.1.1

New

  • A settled dispute settles the commission. A commission held by a dispute now follows the outcome when KahunaCart reports it (onKahunaCartDisputeClosed, from Stripe and PayPal, or set on the order by hand). Won lets the hold go, as Release the dispute does, with a line on the commission's note. Lost takes the commission back as a full refund would, because the platforms report a lost dispute as its outcome and not as a refund: an unpaid commission is reversed, and a paid one gets a clawback off the partner's next payout, with the Affiliate commission taken back message saying the buyer won a dispute. Setting a dispute back to open by hand holds the commission again. A closing that names a different dispute from the one holding the commission leaves it held. Needs KahunaCart 1.5.18

1.1.0

New

  • Partners choose their code. The application form, signed in and public, has an optional Your code field that shows the link it makes. Blank still makes a code from their name. A code that's taken, kept for the store or badly formed sends the form back with every answer kept and the sentence under the field; it's never quietly changed into another one. On the public form the code waits in the unconfirmed application, and if somebody takes it before the email is confirmed, even in the very moment of confirming, confirming still works: they get a code made from their name, and the application in the admin says which code they asked for. A code made from a name leaves out Blocked Words in Codes, so Approve Automatically never hands out your brand
  • Live check as they type. Every code field asks a small public endpoint whether the code is free a moment after the typing stops, and says so under the field with the same sentence the form would give. It's only a hint, since the form checks again when it's sent, and it's rate limited in KahunaCart's limiter (affiliate_code_check, 30 a minute per visitor by default, security.rate_limits.affiliate_code_check to change it) with the visitor's address hashed before it's counted
  • Blocked Words in Codes (program.blocked_codes, on the Program tab, empty by default): words partners may not use in a code they ask for, such as your brand or a competitor's. A word blocks any code that contains it, so official also blocks theo-official. Checked on applications and change requests (again when you approve one, so a word you block later still counts), never on a code you type yourself, and it matters most with Approve Automatically on
  • Partners can ask for a new code. The Overview page in the Affiliate tab, which every partner has (coupon-only ones too), shows the partner's code and a Change code form (new code with the live check, an optional reason). It works with JavaScript off. One request waits at a time, even when Send is pressed twice at once, and while it waits the page says what they asked for and when, with a Withdraw button. Only active partners can ask. Account API routes GET, POST and DELETE /kahunacart/affiliates/account/code-request do the same
  • You approve or reject it. A waiting request shows on the partner's page and on the Applications tab with Approve… and Reject…. Approving checks the code again (if somebody took it meanwhile, or it now has a blocked word in it, it says so and you reject or edit by hand), gives the partner the new code and emails them their new link and that their old code and coupon keep working (affiliates.code_changed). Rejecting emails them your message (affiliates.code_request_rejected). Admin API routes: GET /kahunacart/affiliates/code-requests, POST …/code-requests/{id}/approve and …/reject
  • Also make a coupon for the new code. Approving a request, and changing a code yourself on the partner's page, offers a coupon with the new code in capitals at the discount of their first enabled percent coupon (on when they have one). If that coupon code already exists, even if it was made a moment earlier, the code still changes, the partner is still emailed, and you're told why no coupon was made. The old coupon is never disabled or renamed
  • The owner hears about requests. A new row on Notifications → Store owner, Affiliate asked for a new code (affiliates.owner_code_request), names the partner, the code they asked for and their reason, with a link to their page, and a dashboard alert stays up while requests are waiting
  • Old codes on the partner's page. The Details panel lists a partner's old codes, all of which still credit them
  • New events. onKahunaCartAffiliateCodeChanged fires after every code change has committed, yours or an approved request's, with affiliate, old_code, new_code, actor, request (null for your own edit) and coupon. onKahunaCartAffiliateCodeRequested (affiliate, request) and onKahunaCartAffiliateCodeRequestRejected (affiliate, request, message, actor) go with the requests
  • Every code a partner has had (table kahunacart_affiliate_codes) and their code requests travel in store exports with the partner in the Affiliates set, and reset clears them. A merge never gives a code that belongs to one partner here to another (it says so in the report), and a partner whose code is one of a partner's old codes here is matched to that partner

Fixed

  • Changing a partner's code no longer breaks their links. The old code used to stop working, so every link the partner had already printed credited nobody, and the code was free for somebody else to take, who would then get the credit for the first partner's links. Now every code a partner has had, their current one included, is kept in one table whose unique index decides who has a code, written in the same transaction as the change: links with an old code still credit the same partner, nobody else can ever take it, not even in the same moment (applications, requests, your own edits and store imports all go through it), and a partner can take their own old code back. Partners already in the store have their current code recorded when the migration runs. Erasing a partner keeps their old codes, like their code, and blanks what they wrote when asking for a new one

1.0.2

New

  • Reply-To Address (notifications.reply_to, on the Notifications tab): where a partner's reply to an affiliate email goes, such as [email protected]. It covers every email to partners and the confirmation email for a public application, and nothing else the store sends. Blank, or an address that isn't an email address, uses the Email plugin's reply_to as before, and without that replies go to the From address.
  • Spam check on the public form (program.captcha, on the Program tab, on by default): the public application form carries Cap, the Form plugin's invisible proof-of-work check, the same one Grav's contact and newsletter forms use. The visitor's browser does a moment of work before the form is sent, with no puzzle and no third party. A form sent without a valid answer, from a browser with JavaScript off for example, is sent back with the answers kept and a sentence asking to send it again. Scripts caught by the hidden field or the fill time still hear nothing. Without the Form plugin there is no check rather than a form nobody can send.
  • The owner hears about payouts, tax forms and commissions. Notifications → Store owner has five new Affiliates rows. Affiliate payouts due says how many partners have reached the payout minimum and how much is owed, once a month, on the first day anyone is over it. Affiliate payout problem (high priority) says when one payout fails, comes back or waits for the partner to claim it, or when a PayPal batch is stuck or its outcome isn't known, with the partner's name, the amount and the reason in plain words. Affiliate tax form received names the partner and the form (W-9, W-8BEN or W-8BEN-E) and never anything written on it. Affiliate commission earned ("Theo earned $14.70 on order #1042") and Affiliate commission taken back start switched off, because a busy program would send one for every sale. None of them ever includes a partner's email address
  • PayPal Payouts not switched on and tax forms needing attention reach your channels. Those two dashboard alerts now have their own rows, sending one message when they appear and a "Cleared" one when they go (needs KahunaCart 1.5.3)
  • Email on Each Commission says what it does. Its help text now says it emails the partner, and that the store-owner messages are on the Notifications screen
  • New event. onKahunaCartAffiliatePayoutProblem fires when a PayPal batch is stuck or its outcome is unknown, with payout, kind and reason. onKahunaCartAffiliateCommissionReversed now also carries why

1.0.1

New

  • Partners in a customer group: pick one of the store's customer groups as Customer Group on the Program tab and every customer with an active or paused partner is put in it, so Customers can be filtered to partners and a page can be locked to them with site.customer_group.<code>. A partner customer whose partners are all applied, rejected, closed or erased is out of it, even if they were put in by hand, so the group should be one just for partners. Customers who have never been partners are never touched, and moving a partner to another customer moves the membership with them.
  • Changing the group takes partner customers out of the old one and into the new one, and clearing it takes them out. The add-on remembers the group it last filled, so this works however the setting was changed. A code that isn't a group adds nobody and breaks nothing.
  • Approving, pausing, closing or linking a partner changes the group straight away. Saving the settings, a daily worker job and bin/plugin kahunacart-affiliates groups:sync [--dry-run] bring every partner customer into line, putting right anything an import, a restore or a customer merge changed.
  • Apply without an account: with Apply Without an Account on (program.public_apply, on by default), people with no customer account apply from a public form at /affiliate/apply under the store. They confirm their email from a link before anything is filed, and the page says "Check your email" whatever the address, so the form never tells anyone whether an address has an account. The program block's Apply button opens the form, and a visitor who is signed in is sent to their Affiliate tab.
  • The confirmation link (affiliates.application_confirm, a new row on the Notifications screen) works once, only from the newest email, until the store's account.invite_expiry_days, and only its hash is stored. Opening it shows a button, and only pressing the button files the application, so a mail scanner that opens links files nothing. Unconfirmed applications are deleted by the worker once their link expires, and they stay out of store exports and restores.
  • The account is made on approval: an address that belongs to a customer is attached to them, anything else becomes a new customer with a sign-in and no password through KahunaCart's own repositories, and the approval email carries a link to choose a password. Rejecting makes nothing. With Approve Automatically on, confirming is the approval.
  • The public form has a hidden field for scripts, a minimum fill time, and a rate limit per client and per address in KahunaCart's limiter (affiliate_apply), with the client address and the email hashed before they're counted.
  • The signed-in form says who is applying ("Applying as" with the account's name and email, and a way to sign out), and the public form invites people with an account to sign in and apply with it. Both are one shared template, apply-form.html.twig.
  • Applications are marked Existing customer, Matches a customer or No account yet, so the merchant knows whether approving makes an account, and approving one with no account says so.
  • Quick reasons for rejecting: the reject dialog has a placeholder and reasons a click puts into the message, from Quick Reasons for Rejecting (program.reject_reasons), a list on the Program tab the merchant can add to, edit, reorder or empty.
  • The rejection email says what happened, gives a general reason when the merchant didn't write one, asks the applicant to reply if something has changed, and says it changes nothing about their account (or that none was made). The confirmation, received and approved emails end with how to get help and the store's sign-off, and partner emails take the Email plugin's reply_to when one is set.

Fixed

  • A customer whose address sent a public application before they signed in sees it waiting in their Affiliate tab rather than a second form, and can't apply twice.
  • The payout line in the program's terms ends with a full stop before the store credit bonus sentence.
  • The tax disclaimer and the program's intro no longer join clauses with a semicolon or promise a commission on every order (some products may earn nothing): "on the orders you bring in".
  • The program block's line under Apply only mentions making an account when the public form is on.
  • The accept-the-terms sentence keeps "program terms." together: the checkbox labels' words are one item in their row.
  • The approval email only carries a link to choose a password when the account behind it still has the address the applicant confirmed. If that customer's address was changed, or the partner was moved to another customer, before the email was built, it goes without the link and tells them to sign in as usual.
  • The public form's minimum fill time and its hashed rate-limit keys are signed with KahunaCart's own site secret. They were using Grav's security.salt, which Grav 2 doesn't have, so the key was empty and anyone could make a valid signature. With no secret the form turns every submission away (still answering "Check your email") and logs why, once.
  • Pressing a confirmation link takes the waiting application out only if the link's token is still the current one and hasn't expired. Sending the form again, or a newer email, in the middle of a press can no longer let the old press delete the new answers and file the old ones.
  • Filing a confirmed application is one database transaction with taking it out, including the customer and sign-in an automatic approval makes. If it fails partway, nothing is lost: the application is still waiting, the same link still works, no customer is left behind and no email goes out. The applied event and the emails follow only once it has committed.
  • The confirmation email and the welcome email's password link are built on a site address the store trusts (security.site_urls or system.custom_base_url), the way KahunaCart's own sign-in and password emails are. With none, the email isn't sent and the job tries again once one is set, instead of sending a link with no host in it, and nothing is minted first.
  • A retried confirmation email keeps its identity: the job now gets its queue id, so every attempt is one entry in the email log and a retry that finds the email already sent doesn't send it again or replace the link in it.
  • An application from an address a Grav account holds no longer becomes a partner nobody can sign in as. Approving it makes and attaches no customer, the application is marked Sign in needed, and the approval email tells them to sign in with the account they have and send the form from the Affiliate tab, which connects the account to the approval they already have.
  • Approving a public application from the admin is one database transaction, including the customer and sign-in it makes. If saving the partner fails, no customer is left behind and the application is still waiting. The customer group and the status event follow only once it has committed. Approvals that are refused, like a customer who already has a partner, still say so and change nothing.
  • Every partner email (the approval, rejection, payout and statement emails and the rest) keeps its identity when it is retried: the job now gets its queue id, so every attempt is one entry in the email log and the logged mailer can tell a retry that already went out. Before, each retry got a new message id and could send the email twice.

1.0.0

New

  • First release.
  • A paid order earns its referring partner a commission once, whether it was paid at checkout or later, worked out on each line after its share of discounts with no tax in it (tax-inclusive prices are taken apart per tax class), without shipping unless the program pays on it, and without gift card purchases.
  • Rates resolve from a product override for the partner, to one for everyone, to a category override (the highest of a product's categories wins), to the partner's own rate, to the program default; flat amounts are per unit on products and categories and once per order otherwise, and never more than what they are paid on.
  • Every line's exact share is kept and the total is rounded half up once, so a partial refund takes back exactly what the refunded lines and units earned, and a refund delivered twice takes back once.
  • Commissions wait out the holding period and are approved by the worker every quarter hour, or wait for the merchant when approval is required; disputes hold an unpaid commission until a refund settles it or the merchant releases it.
  • A refund or reversal after payment leaves the paid commission alone and writes a negative clawback that nets off the next payout; balances can go negative and the merchant can add signed adjustments with a note.
  • Subscription renewals of a referred order earn renewal commissions (all, a set number, or none, with a renewal rate), and a renewal order never earns a second time as a sale.
  • Admin API for commissions (filter, sort, search, page, bulk approve and reject, release a dispute, adjustments, credit an order set by hand), rate overrides, balances and the overview numbers; account API for a partner's own commissions and totals, with orders shown by number and date only.
  • Partner links: a visit on ?ref=<code> for an active partner tracked by link is remembered in a signed first-party kc_ref cookie for the program's window, counted on the partner's daily click counter (bots skipped, nothing about the visitor stored), and sent on to the same address without the parameter.
  • Consent: the cookie is declared to the consent plugin as marketing. Without marketing consent the referral is held in the session for the visit (consent_mode: session) or not at all (strict), and a shopper who withdraws consent loses the cookie straight away.
  • Last-click or first-click attribution, the link referral stamped on the order at every checkout attempt, and at payment the partner decided between link, partner coupon (coupon wins by default) and the merchant's own choice, with self-referrals and inactive partners flagged and not credited.
  • Partner coupons: assign an existing core coupon to a partner, or make a new single code for them; every code stays an ordinary core coupon.
  • Admin API for partners, applications, coupons, the order panel's referral (set, change or remove, written to the order's history) and click charts, behind the view and manage permissions.
  • Account API for the signed-in partner: their status and code, their link, a link builder for any page of the store, their coupon links, their clicks, and the application to join the program.
  • Tax details in the partner's account: a W-9 for US persons, a W-8BEN for individuals and a W-8BEN-E (signed PDF) for entities elsewhere, with a typed signature and certify box. KahunaCart collects and totals this information; it doesn't file or give tax advice.
  • Full tax ID numbers, foreign tax IDs and dates of birth are stored only when the server has a KAHUNACART_TAX_KEY environment key, encrypted with it; without one a W-9 keeps the last four digits plus the signed PDF. Tax IDs are masked in every screen and API response.
  • A Tax ID key field on the Tax settings tab says whether the key is set, how to set it in the root .env, and generates a new key that is shown once and saved nowhere.
  • bin/plugin kahunacart-affiliates tax:rekey --from=<old key> moves every encrypted value to the current key, and tax:status reports the key and how many forms need filling in again. A form the key can't open is shown as needing re-entry, never as garbage.
  • Signed PDFs are checked by their first bytes, stored encrypted with KAHUNACART_TAX_KEY when it is set, kept under random names in user/data/kahunacart/affiliates-tax or a folder outside the web root (tax.documents_path), and served only to admins with the tax permission.
  • W-8 forms expire at the end of the third calendar year after signing; partners are told 60 days before (onKahunaCartAffiliateTaxExpiring) and the form is marked expired on the day (onKahunaCartAffiliateTaxExpired). Merchants can verify a form or ask for it again (onKahunaCartAffiliateTaxRequested).
  • The payout tax gate follows tax.require_before_payout for the payout batch builder: nobody, US partners only, or every partner.
  • Year-end report per calendar year in the store's time zone: paid totals per partner by method and currency, the form on file, and a 1099-NEC flag for W-9 partners at the threshold (other currencies listed, never converted). Exports: the IRS IRIS 1099-NEC CSV (full tax IDs only with the key), the full table CSV, and a statement PDF per partner. Every export is written to a history the admin can see.
  • Erasing a partner after the retention period also deletes their tax form and its PDF.
  • Payout batches: pick store credit, paid by hand, PayPal, or each partner's own choice (one batch per method) and a currency; the preview lists every partner with an approved balance and says why anyone is left out (below the minimum, a negative balance, no PayPal address or customer account, the method switched off, a currency PayPal cannot send, or the tax gate), and the merchant can untick anyone.
  • Creating a batch works every amount out again inside one transaction from the commissions it locks, so a refund between preview and click is counted and two people building batches at once can never pay the same commission twice.
  • Store credit posts each partner's payout to their credit account through the store credit ledger, once (a repeated run is absorbed by the item's own key, and a run cut off part way is finished by running it again or with Resume, paying only what is still pending), with an optional bonus percent for choosing credit, recorded apart on the item.
  • Paid by hand: the batch waits while the merchant pays outside the store and marks items or the whole batch paid with an optional reference, or marks an item as not paid; CSV files for a PayPal Payouts upload, a Wise batch payment (Wise emails each partner for their bank details) and a plain one with the payout details.
  • PayPal Payouts with the add-on's own client (works without PayPal at checkout), borrowing the PayPal payment plugin's keys when it is set to the same mode; one request per batch, kept before it goes, under ids made from the batch's own random reference so stores sharing a PayPal account never clash; a send that got no answer is sent again exactly as it was for up to 72 hours, a batch PayPal already has (or shows when polled) is taken only when every item matches what was sent, and when the store can no longer learn the outcome the batch says so, keeps every commission claimed, and the merchant marks each item paid or not paid.
  • PayPal outcomes are polled every ten minutes: paid, unclaimed (the partner is reminded a week before PayPal's 30 days run out), returned and failed, with PayPal's reason kept; a failed or returned payout always hands its commissions back to the partner's balance, and a failed item is retried in a new batch, never sent again under its old id.
  • PayPal connection check when the settings are saved and from a Check button on the PayPal tab, which says plainly when the PayPal account still needs Payouts switched on and links PayPal's page for it; new settings for the subject and message of PayPal's payout email.
  • Admin API for payouts behind the Pay Affiliates permission (preview, create, run, resume, cancel a draft, mark paid, retry, poll now, CSV files, connection check, a partner's payout settings), and account API for a partner's own payouts and payout settings, with manual payout details encrypted at rest, shown masked to the partner and in the merchant's editor, and opened only in the plain CSV or through a separate reveal that is written to the log with who asked.
  • Affiliates section in the KahunaCart admin with tabs for Overview, Affiliates, Applications, Commissions, Payouts, Tax, Creatives and Settings; every screen has its own address, so a reload, a pasted link or the Back button lands where it says, and tabs and buttons follow the view, manage, payouts and tax permissions.
  • Overview: a needs-attention strip (applications waiting, partners due a payout, failed or unclaimed payouts, tax forms), clicks, referred orders and earnings for 30 days, where the money stands per currency, and the top partners.
  • Partners: a searchable, sortable list with balances and tax form state, a form for a new partner tied to a customer with an optional coupon, and a partner's own record with details, rate overrides, coupons, how they're paid (stored bank details shown only on request), tax form, application, clicks chart, recent commissions and history, saved with one button and guarded against leaving with unsaved changes.
  • Applications to approve or reject with a message, commissions with filters, bulk approve and reject, dispute release and signed adjustments, the payout builder with a preview of who is paid and who is left out and why, and a batch's own page to run, resume, mark paid, ask PayPal, settle an outcome-unknown PayPal batch item by item, retry failures and download the bulk files.
  • Tax: the year-end report with the IRIS and full table files, export history, statement PDFs, and each partner's form to verify, ask for again or download.
  • Creatives: banners from the site's media, text snippets and links for partners, with ordering, editing and switching off; new admin API for them under /kahunacart/affiliates/creatives.
  • An Affiliate panel beside an order (the partner credited, how, and the commission, with change and take off) and beside a customer (their partner record and since when).
  • Dashboard alerts for applications waiting, failed payouts, PayPal Payouts switched off on the account, and tax forms that need attention, each opening the tab that deals with it.
  • A new /kahunacart/affiliates/attention endpoint gives the section its counts and the permissions held; commission and rate reads now accept the view or the manage permission, and the partner list can be filtered by customer.
  • Affiliate tab in the customer account: while the program is open, a customer who isn't a partner gets the application form (website, where they'll share the store, how they'll promote it, payout method and PayPal address, the program terms and an agree box); after applying they see a thanks note, and a turned-down applicant one short line with no form. With the program closed only partners see the tab.
  • Active partners get Overview (ready to pay, waiting, paid and earned per currency, clicks and referred orders, when the next payout comes), Links with a copy button and a builder for any page of the store, Coupons, Commissions and Payouts with paging, payout settings, Tax details and Banners and text. Paused partners keep their history, payout settings and tax form; closed partners keep their history. Nothing for sending people is shown to either.
  • Tax details asks one question (US person, someone outside the US, or a business outside the US) to pick the W-9, W-8BEN or W-8BEN-E, shows what's on file masked, and says plainly when the store has asked for the form again, when it's about to run out or has run out, and, without a tax ID key, that only the last four digits are kept with the signed PDF. A refused form keeps the partner's answers, never their tax ID or date of birth.
  • Every portal form is a plain POST with the store's nonce, so it works without JavaScript; the link builder and copy buttons add to that when it runs. The styling uses only KahunaCart's --kc-* tokens, follows light and dark, works at phone width and is skipped when builtin_css is off.
  • One account resolver (Account\AccountCustomer) finds the signed-in customer for the tracking, commission, payout and tax account APIs and the tab, refuses a customer merged into another, and only creates a customer record when someone applies.
  • Partner emails on the Notifications screen, each one switchable and re-pointable: application received, approved, rejected, new commission (off by default), payout sent, payout failed, unclaimed reminder, tax form requested, expiring and expired. They're queued as jobs, read what's true when they go, and go through the store's mailer onto its email log.
  • Owner messages through the store's channels: a new application, and a payout batch finished, with failed and unclaimed payouts called out.
  • Monthly statement (notifications.statements) for partners with no customer account who had commissions or payouts last month, booked once in the first week of the store's month.
  • Public program block with the program's terms and an Apply button, for any page: the [kahunacart-affiliate-program] shortcode, the kahunacart_affiliate_program() Twig function, or the partials/kahunacart-affiliates/program.html.twig partial.
Bookings 1.1.13 The front desk's Today is the shop's Today 1 fixed

1.1.13

Fixed

  • The front desk's Today is the shop's Today. It was worked out from the first schedule's timezone, and a schedule saved without one is stored as UTC, so a Denver shop saw tomorrow's bookings from 6 PM. The desk now uses the store's own timezone, the same one the reports use (reports.timezone, then Grav's system.timezone). The theme helpers' "today" (the availability board, the day's lanes and the counts) and the timezone handed to Bookings modes use it as well; they only read system.timezone before
Older versions (9)

1.1.12

Improved

  • No empty Bookings tab. On KahunaCart 1.6.0 with Hide Empty Tabs on (the default), a customer who has never booked doesn't get a Bookings tab in their account; the page still opens at its address. Older KahunaCart shows the tab as before
  • The booking form knows who's booking. A signed-in customer finds their name, email and phone (from their default address) already in the details form, and a guest sees "Have an account? Sign in" above it, which comes back to the page after signing in. The picker asks a new public route, GET /kahunacart/bookings/public/me, which answers only about the browser session that asked and is never cached, so the picker still works on a cached page. The words are WIDGET_HAVE_ACCOUNT and WIDGET_SIGN_IN, and the line's class is kcb-signin

1.1.11

New

  • Themes can list services and staff. kahunacart_bookings_services() returns the appointments a shopper can book, each with its name, summary, link, price, length, whether it waits for a yes, its category and the people who do it. Ask for one category (child categories come with it), a list of products in the order you give, group_by_category: true to get groups in the category tree's order, or a limit. Where a service has options that differ, the price is the lowest with from: true, and the length is a range such as "40 – 55 min". Lengths are written the way a salon says them: "45 min", "1 hr 15". kahunacart_bookings_staff() returns the people (never rooms or equipment), each with the services they take and a link that books them, and takes products or category to list only the people who can do those. Drafts, products the shopper may not see, switched-off services and classes are left out. So is any option the shopper couldn't buy, before the price and the length are worked out: one with no price in the shopper's currency, and one whose own rule is switched off (it doesn't fall back to the product's rule). A service with no option left isn't listed at all, and its people and their links come only from the options that are left. Both return an empty list on anything they cannot answer
  • A link can open a service with one person already chosen. Add ?with= and the person's slug to a service's address and the picker opens with them picked. If that person is switched off, is not a person, or cannot take that service on any option the shopper can buy, the picker opens as usual. Nothing changes when the address has no with
  • Themes can show what's free. Four more template functions, for a theme that hires things out by the day. kahunacart_bookings_lengths('trail-29') lists the lengths a product is sold in, in option order, each with its price, a length such as "4 hrs", "3 days" or "1 week", and the times it can start at ("8:00am", "1:00pm"; none for an option a day or longer). kahunacart_bookings_sizes('trail-29', { date, variant }) lists the equipment the product draws on, each with a label and note from the bookable's size and note, how many are free on that day and how many there are, a state of open, low or full, and a link that books that one. kahunacart_bookings_next_free('trail-29') gives the next day from tomorrow with something free: its date, "tomorrow" or a short day and date such as "Fri, Oct 23", and how many are free. kahunacart_bookings_board({ products, category, days, from }) gives a week at a glance: the days, and for each product how many are free on each one, with a state of open, low, full or closed and a link that opens the product on that day. Free means the most that are free at any one start that day on the product's shortest option, worked out by the same engine the picker asks, so hours, days off, outside calendars, buffers and holds all count. Low means some are taken and two or fewer are left, or a quarter of the total or fewer. Days are the shop's days, not the visitor's. The new wording (LENGTH_DAY, LENGTH_WEEK, NEXT_FREE_TOMORROW, DAY_DATE) is in the language file. Each returns an empty list (or null) on anything it cannot answer
  • A link can open the picker on a day. Add ?date=2026-10-25 to a product's address, or date to kahunacart_booking() or the shortcode, and the calendar opens on that day with its times showing. If the day has no times, is in the past or is not a date, the picker opens as usual. Nothing changes when the address has no date
  • ?with= also picks a size. The slug of a piece of equipment the customer chooses from (a bike size, say) preselects it, the same way a person's slug does. A switched-off one, one the product doesn't offer, and a room are ignored
  • A hire can only start while the shop is open. A three-day or week-long hire runs on a schedule that is open round the clock, so it used to be offered on the day the shop is shut. Add "starts_in_hours" to the rule's meta_json, with a schedule's slug or id ("golden-hour-hours") or true for the rule's location's hours, and a start is offered only inside those hours. The hire may still run on through closed time. Off unless a rule says so; see Schedules → Hires that run overnight
  • The picker draws a pool of sizes as sizes. When the customer picks from equipment (a bike's S, M, L), the picker shows a row of sizes, "Any" first, each with its size label and its note under it. Once a day and a time are picked, each size says how many are free then ("3 left", "None left"), a size with none free is switched off, and a size running short is marked low. Picking a size narrows the times to that size's and keeps the time picked. The public availability answer carries free on every start of a pool the customer picks from, and the services answer carries kind, size, note and units on each piece of equipment; nothing changes for people or for a pool the shop picks from. New class names kcb-sizes, kcb-sizelist, kcb-size, kcb-sizelabel, kcb-sizenote, kcb-sizefree, with kcb-selected, kcb-low and kcb-full. The words are WIDGET_SIZE, WIDGET_ANY_SIZE ("Any", which a store can make "Any size") and WIDGET_SIZE_NONE; a pool of people still says "Anyone"
  • A by-the-night picker can be told which stay to open on. kahunacart_booking(product, { arrive: '2027-05-25', leave: '2027-05-30', guests: 2 }) and the shortcode's arrive, leave and guests write data-arrive, data-leave and data-guests on the mount point, where Rentals' picker reads them. A date that is not a date and a number that is not a positive whole one are dropped, the picker's own link (?arrive=&leave=&guests=) still wins, and nothing is written when a page gives none of them
  • Themes can draw today's book lane by lane, and count the openings left. kahunacart_bookings_day({ people: ['jo', 'mia'], resources: ['tub-1'], date, from, to }) lays one day out with a lane for each person and each tub (or other piece of equipment or room that is not a person). A lane is a list of blocks that run end to end across the day, each with its start and end in minutes after midnight, the same times as the store prints them (start_label, end_label), a state of booked, free, closed or break, a service and a link. Booked is a booking, a hold, a class or an outside calendar on that person or tub, with the buffers around it, and service is the service's title and nothing else: never the customer's name, the pet's, the details they filled in, their notes or contact details, and no id of any kind in the block or in a link. A booking on a service the shopper couldn't open has no title. Free is time that can really be booked: after the notice the service needs, inside the booking window, on the shop's grid of starts, and long enough for the shortest service the person or tub takes, so a quarter of an hour between two bookings is closed, not free. A free block's link opens that service's page on that person or tub, that day and that time (?with=&date=&time=, and variant= when the service has several options). Break is a gap in someone's hours, such as lunch. Closed is the rest: before they start, after they finish, a day away, time already gone. The answer also carries the day (date, label, is_today), now in minutes while it is today, and open and close (the hours of the lanes asked for, or the stretch given as from and to, as HH:MM or minutes). date is today in the store's timezone unless given. Name nobody and every person is a lane. A day nothing is open has open and close empty and no blocks. kahunacart_bookings_counts({ people, resources, sessions: ['puppy-playgroup'] }) gives openings_today (the free blocks left today, the same ones the lanes draw), resources_free_now (how many of the named tubs are open and unused this minute for as long as their shortest service takes) and, for each class named, places_left, capacity, starts and a label such as "Sat, Oct 24, 10:00am" for its next date with a place left. A tub or a person is read from the same hours, days off, outside calendars, diary and holds the picker is. A service that needs more than the one lane (a groom that needs a tub too) is offered when that lane is free, and the picker the link opens checks the rest. Both return an empty answer on anything they cannot answer
  • A link can open the picker on an option and a time. Add ?variant= and an option's id to a service's address and the picker opens on that option (a size, a length), and ?time=14:30 beside a ?date= picks that start on the day when it is free: it opens ready to confirm, and nothing is held until the customer confirms. A time the day doesn't have leaves the day open on its own times; an option the service doesn't offer, a time that isn't a time, and a time with no day leave the picker as it was. Times are read the way the picker prints them, in the visitor's own timezone, as ?date= is. kahunacart_booking() and the shortcode take variant and time the same way, and a page that fills a mount point in itself can set data-variant and data-time

Fixed

  • The booking picker no longer offers an option whose own rule is switched off. An option with a rule of its own is governed by that rule, on or off; it used to fall back to the product's rule and be offered anyway. Now it is left out of the picker, asking for its times by hand is refused like any option that isn't offered, a time on it can't be held, and it can't be added to the basket as a booking (it says "That option is not available to book right now."). The store's own option list under the picker is hidden whenever the picker has an option of its own, so a switched-off option can't be picked from there either. Moving a booking that was already made is not affected
  • Long hires are said in days. The theme helpers (kahunacart_bookings_lengths() and the services helper's length and range) said a 58 hour hire was "58 hrs". A length of a day or more is now the calendar days it runs across from its start: out at 8:00, back by 6:00pm two days later is "3 days", 154 hours from 8:00 is "1 week", anything else "N days". The picker's option box says the same, through the services answer's new length. Under a day nothing changes
  • Prices follow Whole Amounts. With KahunaCart 1.6.0's Whole Amounts on, the theme helpers' price and the picker's prices said "$59.00". They now print the way core prints its own: "$59", never rounding cents away, and a set printed together (a product's lengths, the picker's options) keeps its cents together or drops them together. The mount point carries data-whole-amounts, and the services answer whole_amounts, only when it is on. On an older core nothing changes
  • A hire that ends after the range asked about is still offered. A three-day hire starting on the last day of a month view was left off the calendar, because the hours were read only to the end of the month. Every start inside the range is offered now, whatever day it ends. And a hire that starts every day at 8:00 still starts at 8:00 after the clocks change, rather than at 7:00
  • A page with service cards loads the picker. Core asks for a booking product's buy field to decide whether a card gets a quick Add button, and throws it away; that used up the page's one copy of the picker's script and stylesheet, so a picker further down the page never loaded. The buy field no longer marks them printed, and a page that prints them twice has the second copy taken out on its way out
  • No crash with KahunaCart switched off. With KahunaCart off and Bookings on, every page failed with "Class KahunaCart not found". Bookings now does nothing at all when KahunaCart is off
  • A mode's own picker prints amounts the way the store does. The price a booking mode's picker is handed ignored Whole Amounts and printed "$2,100.00" where the rest of the page said "$2,100". It follows the store for the page's own currency now, and takes the other amounts printed beside it so a list keeps its cents together

Improved

  • A day with no times says "nothing available" in the picker instead of "nothing free", so nobody reads it as a price.
  • Service lengths read in the store's language. The theme helpers' "45 min", "1 hr 15" and "40 – 55 min" are language strings now (LENGTH_*), so a German or French store can word them in Translations
  • Ships its license. A LICENSE file now comes with the plugin, and the blueprint links to the KahunaCart License at kahunacart.com/license instead of the Grav Premium license
  • With JavaScript off, a product sold by the night talks about nights. The note says to turn JavaScript on to pick your nights (WIDGET_NO_SCRIPT_NIGHTS) instead of a time, and the store's quantity box and Add to cart button, which can do nothing without the picker, stay hidden

1.1.10

New

  • Take payment at the till. With the Point of Sale add-on installed, a booking whose order still owes money has a Take payment button on the front desk. It opens the till with that order ready to pay, for anyone allowed to use the till. The desk itself still takes no money; the till does. From the till, Book next visit opens the desk's New booking sheet for the same customer (/desk#/new?customer=…)

Improved

  • A bookable service rung up at the till is refused with a sentence pointing staff to the front desk to pick a time, rather than the shop's own "Pick a time" message

1.1.9

New

  • Know when a customer moves their own booking. A customer moved their booking (normal) goes to your channels with the service, the old and the new time, the customer's name and a link to the booking, once for every move. It is sent only when the customer did the moving, from their link or account, never when you or your desk did
  • An outside calendar that stops loading reaches your phone. The dashboard alert is now also sent to your channels, once when a calendar stops loading and once when they all load again. Needs KahunaCart 1.5.3

Fixed

  • The Open People and places button on the calendar alert goes to the right screen. It pointed at an address from the old admin

1.1.8

Improved

  • A closed store takes no new booking. With KahunaCart 1.5.0, a store that is coming soon or paused refuses the public book and hold routes with the store's message (503 store_closed), so free and pay-on-the-day services stop too, as do waiting-list places. Existing bookings can still be read, cancelled and rescheduled, and signed-in staff can still book

Fixed

  • Works with a shop mounted at the site root. With KahunaCart's route: '/' (core 1.5.0) the product links in session lists, the customer's Bookings tab link in messages and the picker's cart form have one slash in them rather than two, and the picker's stylesheet loads only on the shop's own pages

1.1.7

Fixed

  • The front desk works on a site installed in a subfolder. On a store at an address like example.com/store, the desk at /store/desk asked the domain root for its diary and showed an error, and its Refresh link, its sign-out and its home-screen icon pointed at the domain root too. It now uses the store's own address throughout, and follows a store that has moved or versioned its API

Improved

  • The picker, the desk and the calendar feed links now take their addresses from KahunaCart's own SiteUrl::api() instead of each building one, so all three agree with every other add-on. Needs KahunaCart 1.4.4 or later

1.1.6

Fixed

  • The booking picker works on a site installed in a subfolder. On a store at an address like example.com/store, every booking and rental product said "That did not go through (404)" where the calendar should be, because the picker asked the domain root for its times instead of the store. It now asks the store's own address, the same way every other storefront link does. A store at the root of its domain is unchanged

1.1.5

New

  • Bookings sells in more than one currency. On a store with several currencies enabled, the booking picker, the class list and the manage page price in the visitor's own currency with that currency's decimal places (12,000 yen, 55.00 euros), the held time is quoted in it, and the basket takes it only into a basket in the same currency. A time held in one currency and carried into a basket in another is refused with a sentence asking the visitor to pick it again, and a hold made before this version is in the store's own currency. A store with one currency sends and stores exactly what it did. Needs KahunaCart 1.4.0 or later
  • The front desk and the admin take bookings in a currency. The New booking sheet shows a row of currency buttons on a store with more than one, starting on the store's own, and desk/services, POST desk/bookings and POST /kahunacart/bookings take a currency. A desk booking is collected in that currency (To collect 9,000 yen is 9,000), and one taken with payment from the admin builds its order in it
  • A booking mode says what currency it priced in. Offer has a currency, a request for one carries currency when the booking is not in the store's own, and an offer that comes back in another currency or none is refused, so a mode that has not been taught about currencies is unavailable in them instead of selling at the wrong number. Pricing\BookablePrices answers a bookable's price as an amount, free or not available
  • Every table in the Bookings section has a search box. Type a few words and the rows that do not hold all of them drop away as you type, in any order and whatever the case: Sessions by class name, a class's dates by date, who is coming to a date and who is waiting by name or reference, Services by service or rule (a service keeps its options with it), People by name, hours or place, Hours by name or timezone, Places by name or address, and the Troubleshoot answer by date, status or reason. A short table inside a record, like a class with a few dates, gets its box once it has more than eight rows. The Bookings list keeps its Reference box as it was. Needs KahunaCart 1.3.11 or later, which draws the box

Improved

  • Changing staff disables stale calendar choices while times load, and a day with no new times is cleared without losing the calendar
  • Customer cancellations show the policy's refund explanation and use a booking mode's own cancellation words
  • Moving a booking previews prices in its own currency, including after that currency is turned off, with the right decimal places
  • Taking payment for an admin booking claims the staff hold even when the browser already has a visitor cookie, and releases the hold if the order cannot be built
  • Booking and class-list shortcode pages render each visitor's currency instead of keeping the first visitor's prices in Grav's content cache
  • Ordinary appointment and class holds also refuse a basket in another currency instead of silently changing their price
  • Book and waiting-list links keep the currency shown in the class list
  • Single-currency desk and picker translations keep their previous keys
  • Existing bookings keep their own prices and desk balances after you turn their currency off
  • A price nobody set in a currency is never free. An option with no amount in the visitor's currency says "Not available in EUR" in the picker and in the class list, cannot be held, and is refused at the desk and in the admin with the same sentence. A price of 0 with nothing else set is still free in every currency
  • Everything after a booking is paid is in the order's own currency. The deposit, the balance, the "paid so far" lines on the receipt, the account tab, the manage page and the emails, the refund a cancellation offers and the price of a move are all in the currency the booking was paid in, whatever the visitor is browsing in now. A cancellation policy is told which currency the amount is in, and a fee with no amount in it does not apply
  • The widget's price helper and the manage page format an amount with the currency's own decimal places instead of dividing by 100
  • A booking with no order remembers its currency. A new column, kahunacart_booking_bookings.currency (migration bookings_0009), holds the currency of a booking that has no order behind it, and only when it is not the store's own

Fixed

  • The front desk reads payment status and currency together, avoiding an extra order lookup for every booking while retaining prices in a disabled currency

1.1.4

Improved

  • Help text in the Bookings section's forms now follows the admin's Help text setting, showing below the label or in a tooltip behind an info icon
Licenses 1.1.5 Which builds a download may have is decided by its own order's key 4 fixed · 1 improved

1.1.5

Fixed

  • Which builds a download may have is decided by its own order's key. The updates window a download was judged on came from any key matching the order's customer record or address, and a guest checkout files an order under whatever customer the typed address belongs to. Somebody holding an old grant could check out with a stranger's address and have the stranger's wider window decide which releases they could download. Only the keys the grant's own order bought count now. Every live grant has a key on its own order, so nobody loses a build they had
  • An add-on only goes on a key the buyer named or the order minted. When an add-on line named no usable key (its "license in this cart" was taken out of the basket, or its rule changed after checkout), issuance put it on the newest key matching the order's customer or address. A guest who typed somebody else's address got their add-on on that person's key, and the add-on's download (a theme's ready-to-run store, say) with that person's key written in. A storefront or agent order now gets no guessed key: the problem is recorded on the order for the merchant. Orders staff entered in the admin or at a till keep the old fallback
  • "The license in this cart" has to be a line that mints one. Checkout, the cart's "lost its license" note and the buy form's "the one in this cart" option now ask the same question issuance does: does the basket hold a line for that product with an enabled license rule? A base product sold with its rule switched off doesn't count, and owning a key already no longer lets the line through, so the buyer is asked to name a key (the buy form lists a signed-in buyer's keys)
  • A disabled variant rule no longer hides the product rule behind it. The checkout gate read a line's rule including disabled ones, while issuance uses only enabled rules, variant before product. An add-on variant with its own rule switched off skipped every check at checkout and was then sold under the product's add-on rule. Both read the rule the same way now

Improved

  • A key issued by hand with no order doesn't decide which builds a download page offers anymore, since downloads are judged only on their own order's keys. A grant whose order holds no key is left as the base plugin offers it, as for any keyless buyer
Older versions (9)

1.1.4

Fixed

  • A package is stamped only with its own order's key. A download looked for the buyer's keys by their customer record and their address as well as by the order. A guest checkout files an order under whatever customer the typed address belongs to, so somebody who bought a product with another customer's address could get that customer's key, and the add-ons it covers, written into their zip. Only the keys the download's own order bought or widened are written now. When the order has no key yet the package goes out with every gated folder removed and no key, as it did for a buyer with no key
  • An add-on's package carries the key it was bought for, and keeps the add-ons that key pays for. A product sold as an add-on (a theme whose package is a whole store, say) has no key of its own, and its download looked for one anyway. It found none, so every gated folder came out of the zip, even ones the buyer had paid for, and no licenses.yaml was written. The download now uses the key the add-on's order widened, or the key bought in the same cart while the grant is still queued. The stamp and the gates are still the add-on's own rule
  • A stamped add-on package lists the right products. The key's own product heads the {#products} list, with the add-on's catalog name and its included packages after it, so a theme rule that includes vellum puts a vellum: line in the file for Grav's package manager

Improved

  • A key can fetch the releases of an add-on granted against it. The public /release endpoint served only the key's own product. It now also serves the release files of every add-on on the key, judged by the key's updates window, and stamps them by the add-on's own rule
  • A key's card offers the packages of add-ons bought on later orders. The downloads on a license card came only from the order the key was sold on. A theme bought later is on its own order, and its package now shows on the card too. Two packages that share a file name and a title are both listed
  • The save_product_rule tool takes stamps. The REST route already accepted stamped downloads; the tool's schema now lists them, so an assistant can write a rule's stamps and gates

1.1.3

Improved

  • No empty Licenses tab. On KahunaCart 1.6.0 with Hide Empty Tabs on (the default), a customer who owns no key doesn't get a Licenses tab in their account; the page still opens at its address. Older KahunaCart shows the tab as before

1.1.2

New

  • Themes can show a product's editions, a customer's keys at a glance, and check a key from the page. kahunacart_license_rule(product) answers a licensed product's editions (one per sellable variant, with its live price and what it includes in plain words: "5 seats", "Yours to keep", "12 months of updates"), the renewal with its on-time price, the add-ons that attach to it, and the packages a key opens. kahunacart_licenses_overview() gives a signed-in customer's keys, how many are active, expiring soon and expired, and a link to the Licenses tab. kahunacart_licenses_api() gives the absolute addresses of this site's public validate and latest endpoints. See docs/storefront.md

Fixed

  • The receipt's licence panel fits a phone screen. The list of keys sized its column to its widest card, and a long path in the instructions (user/plugins/kahunacart/docs/hardening.md) had nowhere to break, so on a phone the receipt ran past the edge of the screen and scrolled sideways. The column now shrinks to the screen, and code and links in the instructions wrap. The same list on the customer's account page gets the same fix

Improved

  • Ships its license. A LICENSE file now comes with the plugin, and the blueprint links to the KahunaCart License at kahunacart.com/license instead of the Grav Premium license

1.1.1

Fixed

  • The renewal reminder sweep runs once an hour, as documented. It ran on every worker pass, about once a minute, because each sweep was booked under that hour's own key and the queue only holds a key back while a job is still waiting; it is now booked for the top of the next hour under one key. Nobody got a reminder twice either way: each reminder is recorded once when it is sent

1.1.0

New

  • Renewal reminders. A key's owner gets an email as the key's updates end or its license expires, on a schedule you set under Licenses → Settings → Renewals: a list of reminders you can add to and remove from, each a number of days before or after the date (30 before, 7 before and 1 after to start). Only keys a renewal product renews get one, never a revoked or suspended key or one a live subscription is renewing, and only the latest reminder due is sent, so switching this on doesn't send two emails at once or write to everyone whose key lapsed last year. Each reminder goes once however many workers run, and renewing starts next year's fresh. The email is Renewal reminder under Customer on the Notifications screen, appears in the email log, and its template can be overridden in a theme
  • The reminder's Renew button signs the owner in. It opens the renewal product with their key already picked, so a customer whose price needs them signed in (a group price list, like a founders' rate) gets it without knowing to sign in first. It goes through KahunaCart's own sign-in link, so the confirm button, two-factor and disabled accounts all work as they do for any sign-in link. The link dies when the key is renewed or changes hands, and expires a month after the date
  • Late renewals keep the anniversary. New setting Late Renewals Start: From the anniversary (the default) runs a late renewal from the date the old year ended, so updates that lapsed in September and were renewed in March run to next September; a key years behind gets the anniversary year it is in now. From the day they pay keeps the old behaviour. Early renewals are unchanged
  • On-time renewal discount. New On-Time Discount and Grace Days settings take a percentage off a renewal paid before the key's date or within the grace days after it, shown as its own line on the cart and receipt. Off by default (0%), and never taken off a line a price list already prices

Improved

  • The account's Renew link opens the renewal product with that key already picked in the key list

1.0.19

Fixed

  • The PHP licence client (clients/php/KahunaCartLicense.php) works on hosts that disable getmypid(). It named its cache temp file after the process id, which is a fatal error there; the name now ends in a random string

1.0.18

New

  • Hear when a paid renewal or add-on does not land. A paid renewal or add-on did not land (high) goes to your channels when a customer has paid and the line could not be applied: no key given, a key that is not yours, for another product or revoked, or no license for the add-on to attach to. Once per order line, with the order, the product and the reason, never the key
  • Know when a license runs out of seats. A license is out of seats (low, switched off until you turn it on) says when someone tries to activate a license with every seat in use, once per license per day, with the product and the customer's name

1.0.17

Fixed

  • Works with a shop mounted at the site root. With KahunaCart's route: '/' (core 1.5.0) the {account_url} placeholder in a licence file, the account links on the receipt, in emails and in the account area (https://example.com//account/licenses, or /de//account) and the admin's store links have one slash in them rather than two, and the stylesheet loads only on the shop's own pages rather than only on the home page

1.0.16

Improved

  • An add-on can't go into the cart with nothing to attach to. On an add-on's product page, when the visitor has no licence in the cart and none on an account we can see, Add to cart now reads as unavailable until a key is typed into "I already have a license key", and pressing it brings the licence box into view instead of adding a line the cart then refused with "Add-ons need a license to attach to". The box itself is drawn as a callout, with a bold question and an accent border, so it is not missed. The button is blocked rather than disabled, because KahunaCart's options script already switches disabled for stock and variants. With scripting off the form posts as before and the cart still explains
Newsletters 1.1.17 Newsletters through their own provider 2 new · 2 fixed

1.1.17

New

  • Newsletters through their own provider. With Email 5.3.3's Newsletter Mail Engine set, everything this plugin sends (campaigns, automations, confirmations, tests) goes through that provider, while the rest of the site's mail stays on the mail engine. Health names both: "Sending newsletters through Resend (API). The rest of the site's mail goes through Cloudflare." With no newsletter engine set, everything goes one way as before (newsletters kit 1.0.13)
  • No campaigns through a provider that only allows transactional mail, such as Cloudflare Email Service. While the newsletters go through one, starting, resuming, catching up, retrying or test-sending a campaign is refused with the reason, a queued campaign pauses before anybody is mailed, an automation's message fails with the reason, and Health warns (newsletters kit 1.0.13)

Fixed

  • Health no longer asks for a webhook a provider cannot have, such as Cloudflare's, which has no delivery webhooks (newsletters kit 1.0.13)
  • Skipped webhook events are no longer errors in the log. An event type the store does not act on, or another site's mail on a shared Resend account, wrote an error line every time (newsletters kit 1.0.13)
Older versions (9)

1.1.16

New

  • Proof that a provider's webhook works. Set up now sends one small test message through the site's own mail transport straight afterwards, and each provider card has a Send a test button that does the same. The card's Test line then says Waiting, Received with the seconds it took, Arrived and refused (the signing secret is wrong), or after two minutes Nothing arrived, with the likely causes. The test goes to the provider's own test address where its plugin names one (Resend's [email protected], the SES mailbox simulator, with email-resend 1.1.2 and email-amazon 1.2.1), otherwise to the store's From address, never to a subscriber. Only the provider the site sends through can be tested (newsletters kit 1.0.12)
  • Health says which provider is sending, at the top: "Sending through Resend (API)." (newsletters kit 1.0.12)

Fixed

  • The webhook health check no longer counts mail sent before the webhook existed. Straight after Set up on a store with past campaigns, Health warned that no event had ever arrived although mail had gone out, and all of that mail had gone out before the webhook. Only mail sent since Set up counts now (newsletters kit 1.0.12)

1.1.15

Improved

  • Ships its license. A LICENSE file now comes with the plugin, and the blueprint links to the KahunaCart License at kahunacart.com/license instead of the Grav Premium license

Fixed

  • A campaign bigger than one batch no longer stops after the first batch. When a batch of 50 went out inside the run's time limit and under the per-minute cap, the send job counted itself done and nothing queued the next batch, so the campaign stayed on Sending with the rest of its list waiting. A finished batch with people still waiting now goes back in the queue a second later. With the default 60 a minute and a fast mail provider this hit almost every campaign over 50 people (newsletters kit 1.0.10)

1.1.14

Fixed

  • The customer link sweep logs at info, not error. "linked 1 subscriber to a customer record" is a record of the sweep doing its job, and it was written to Grav's log as an ERROR, which set off anything that watches the log for errors. It is now an INFO line. Real failures, such as a sweep that could not be queued, still log as errors

1.1.13

New

  • You hear when a campaign has finished sending. A new Newsletter Campaign Finished row under Notifications → Store owner sends one quiet message when a campaign has gone out to everyone it was aimed at, scheduled ones included, with its name, how many were sent (and how many failed, if any) and a link to it. A campaign that pauses, is cancelled or still has work to do says nothing, and a campaign sent on to newcomers later does not send a second message
  • Five of the newsletter's alerts can reach your phone. A campaign that stopped itself and a bounce rate over your limit arrive as urgent messages; a provider that has gone quiet, a webhook with no secret behind it and a failing deliverability check arrive as normal ones, each with a "Cleared" message when it is fixed. Switch them off or choose their channels under Notifications → Store owner (KahunaCart 1.5.3 or later). The Email plugin missing, no From address, stripped unsubscribe headers and a segment that would not count stay on the dashboard only

1.1.12

New

  • Campaigns carry a postal address. US law (CAN-SPAM) and the rules in many other places ask for a physical mailing address in marketing email, and campaigns used to go out without one. There is a new Postal Address box on the Sending tab of the settings, under the From address, and whatever is in it is printed at the bottom of every campaign in small type, one line per line, in the HTML and in the plain text part. Leave it empty and a store that has filled in the invoice address in KahunaCart's settings (invoice.store_address) gets that one automatically; empty in both, campaigns go out as they did. A plain campaign carries it once, in the engine's plain layout. The double opt-in confirmation is a transactional message and has no address in its HTML. The setting is postal_address in user/config/plugins/kahunacart-newsletters.yaml

1.1.11

Fixed

  • Works with a shop mounted at the site root. With KahunaCart's route: '/' (core 1.5.0) the public newsletter pages get the same kahunacart_base the shop's own pages do, and the seeded campaigns' store links no longer end in a slash that the paths after them doubled

1.1.10

Improved

  • Subscribers follow a customer merge. When KahunaCart merges one customer into another, the merged-away customer's address keeps working as an alias of the survivor, and so does the mailing list. The subscribers already linked to the merged-away customer are moved to the survivor when the merge happens (and counted in the merge's dry-run preview as newsletters), and a new subscriber, a customers import row or an order on the old address links to the survivor instead of the alias. Only the link to the customer moves: the subscribers stay separate, so somebody on the list under two addresses is still on it twice, and no status, consent record or list membership changes. Running the same merge again moves nothing. The subscribers go to the customer's current survivor, looked up when the link is written, so a merge that finishes after its target was itself merged away links them to the final customer and not to the alias, and every run, for any pair, also moves subscribers still linked to a customer that was merged away in that chain, so a retry repairs links an overlapping merge left behind. A store on a KahunaCart without customer merging links exactly as before

1.1.9

New

  • Tables have a search box. Campaigns, a campaign's recipients, suppressions, an automation's people, automations, templates, the lists, tags and segments on the Audience screen, a campaign's links, a person's lists and what they were sent, past imports and every report get a box above the table that looks as each letter is typed. Every word has to be in the row, in any order and without regard to case. A table inside a panel only gets its box once it has more than eight rows, and the sample tables (who a draft would reach, the first rows of an import, the first ten of a segment) have none. A table that holds its whole answer hides the rows that do not match and says "3 of 12" beside the box; a table that pages asks the server and goes back to page one, because the row somebody wants is rarely on the page in hand, and a search that finds nothing keeps its box instead of saying there is nothing here. The subscribers table and the pickers keep the searches they had. GET {prefix}/campaigns (name and subject), /campaigns/{id}/sends (address, message id and error), /suppressions (address, note and provider) and /flows/{id} (address and name, for the people) take q, applied before the sort, the count and the page, so total is the search's while the status counts stay over the whole table; a % or _ is looked for as itself, and with no q each answers exactly as it did. The MCP tools list_campaigns, list_campaign_recipients, list_suppressions and get_automation take the same q. Support\ListSearch reads it, beside Support\SortOrder, and each repository names its columns in a searchColumns() next to its SORTS

Improved

  • Built on the newsletter kit 1.0.9. The search boxes above come with it. Nothing about a store's setup changes
  • "Total spent" in a segment counts orders in the store's base currency only. On a store that sells in more than one currency it added every order's total together, so 25.00 dollars, 50.00 euro and 120,000 yen made a customer who had spent 1,275.00. It now adds up the orders in the base currency, which is the figure KahunaCart's own customer screens show first, and the field is labelled "Total spent, base currency orders only" so nobody builds a segment on the wrong idea. Orders in other currencies don't count towards it. A store that sells in one currency sees no change at all
  • Needs KahunaCart 1.4.0. The segment uses the totals-per-currency work that arrives with it, so the add-on asks for 1.4.0 or later

1.1.8

Improved

  • Built on the newsletter kit 1.0.8. The change below comes with it. Nothing about a store's setup changes
  • Help under a field follows the admin's Help text setting. On the campaign, segment, automation, list and import screens the help under a field now shows below its label or in a tooltip behind an info icon, whichever the admin is set to
Point of Sale 1.0.1 Ships its license 1 improved

1.0.1

Improved

  • Ships its license. A LICENSE file now comes with the plugin, and the blueprint links to the KahunaCart License at kahunacart.com/license instead of the Grav Premium license
Older versions (1)

1.0.0

New

  • A till on your own site. /pos sells at a counter from a tablet, a laptop or a phone, behind the site's own sign-in. Scan or tap products, attach a customer, take cash with the change worked out, take cards on a card reader, split a payment, send a pay link as a QR code, print or email the receipt, and take things back by scanning the order number. Every sale is an ordinary KahunaCart order on the same stock, tax, coupons and customers as the online shop. Add it to a tablet's home screen and it opens full screen
  • Card readers through KahunaCart's card reader drivers: Stripe Terminal (Stripe 1.2.10), Square Terminal (Square 1.1.7) and SumUp Solo (SumUp 1.1.6). A payment the till can't confirm shows as checking until the platform answers, and a manager can give up on it
  • Your store's look on the till. A brand colour, from which the till works out its buttons, the text on them (kept readable to WCAG AA), the selected tint and a dark-mode version; a logo, with an optional one for dark mode, on the sign-in and Who's serving? screens and in the top bar; and light, dark or follow the device, set before the page paints. The home-screen icon and the browser's bar colour follow the colour
  • Registers and paired devices. A register sells at one of the store's pickup locations, with its own card reader and receipt setting. A manager pairs a browser to it once with a PIN, and a lost tablet is revoked from the admin
  • Staff PINs. The tablet stays signed in as one account and the person serving signs in with a PIN. Every action is checked against that person's own permissions. Wrong PINs lock the person, then the device, and the till locks itself after a few idle minutes
  • Manager overrides. A discount over the limit, a return by somebody without refunds, or pairing a browser asks for a manager's name and PIN on the spot, and the override is written on the order
  • Shifts. Open with a float, put cash in or take it out with a reason, and close by counting the drawer note by note in the store's currency. The X and Z reports give sales, tenders, refunds, discounts, tax, sales per person, payments on earlier orders and the cash expected. A drawer that is out by more than the store's limit sends the owner a message
  • Take a payment on an earlier order. Find an order by number, email or name and take what it still owes at the till: a booking's balance, or an order placed online and paid in the shop. The Bookings desk's Take payment button opens the same screen, and the Paid screen offers Book next visit
  • A Point of sale section in the KahunaCart admin with Registers, Staff, Favourites, Shifts, Reports (by register, staff, location or tender, with CSV) and the settings. A counter sale's order shows its register, who served and its shift, and the manager's routes are also MCP tools
  • No double charges. Charging and tendering carry an idempotency key, so a double tap or a retried request records one order and one payment, and two tablets opening a shift on one register leave one open
Print on Demand 1.0.3 The Review tab shows an order that arrived while the section was open 1 fixed

1.0.3

Fixed

  • The Review tab shows an order that arrived while the section was open. Review and Orders each read their list once and kept it, so an order paid and handed over for review after the section opened showed as waiting on the Orders tab while Review still said Nothing waiting. Both lists are now read again each time their tab is opened
Older versions (3)

1.0.2

New

  • Themes can say a product is printed for you. kahunacart_pod_product(product) answers printed, print_days (the store's Production days for the provider that makes it) and label, a sentence like "Printed for you in about 3 days, then shipped" that never names the provider or the cost. Anything that isn't a printed product gets an empty answer, and it never throws

Improved

  • Ships its license. A LICENSE file now comes with the plugin, and the blueprint links to the KahunaCart License at kahunacart.com/license instead of the Grav Premium license

1.0.1

New

  • You hear when a print provider's webhook is turned away. A delivery refused because the secret in the address is wrong or missing, or because its signature does not check out, now goes to KahunaCart's Webhook rejected message under the provider's name, once a day per provider and reason (KahunaCart 1.5.3 or later)
  • The review queue can reach your phone. The "Print on demand has things waiting for you" alert gets a row of its own under Notifications → Store owner, sent when the queue first has something in it and again as "Cleared" when it is empty (KahunaCart 1.5.3 or later)

1.0.0

New

  • The add-on itself: the plugin, its settings with a Printful tab and step-by-step instructions, two permissions (one for setting up, one for sending orders, because sending spends money), and the pod_0001_core migration with the links and imports tables
  • The provider contract every print-on-demand company plugs into, with the Providers card's capability matrix built from what each driver actually declares, and room for more than three providers without a migration
  • A Print on demand product type: it ships, holds no stock, and is refused at the cart door if the add-on is switched off. It is registered only once a provider is connected, so a store that connects nothing is the same as one without the add-on
  • Printful connection check: a real call that names the store the token reaches, lists its scopes, says which of orders, sync_products/read and webhooks are missing, and says "expired or revoked" when Printful refuses the token
  • Printful import: list what you designed at Printful, see each product's variants with their cost, retail price and availability, and import one as a draft product with Color and Size options, one variant per variant switched on at Printful, its mockups copied into the product's media and attached to the right variants, its own shipping profile, and a link from every variant to Printful's two references. Prices are the ones you confirm on the import sheet; nothing is priced without your word, and a second import of the same product is refused as already in your store
  • Refresh: bring Printful's cost, availability and SKUs in again, add new variants switched off for you to price, and mark variants Printful dropped as unavailable without deleting them. Title, description, prices and pictures change only when you ask
  • A variant Printful no longer makes, or whose provider is not connected, is refused at the cart door with a sentence the customer can read
  • Command line: check printful, products printful, import printful <id> [--markup=100] [--dry-run], and record printful for refreshing the test fixtures from a real store (reads only, never orders)
  • Printful's live shipping rates at checkout: a carrier driver per connected provider, quoting every Printful service for a parcel in one call, in the order's currency, with "Arrives in about N days" counting Printful's production days (a new setting, 3 by default) and a line saying import duties may be due when a parcel crosses a border
  • shipping printful --zones=1,2 and POST /kahunacart/pod/providers/{code}/shipping-methods (MCP add_pod_shipping_methods): add one method per Printful service to each zone named, bound to Printful, without touching any method that is already there
  • A parcel Printful makes is offered only Printful's methods, so your own flat rate never pays Printful's postage, and a mixed cart is two parcels charged separately and added up. With KahunaCart 1.4.6 this holds for a cart of only printed goods; KahunaCart 1.4.7 passes the parcel to onKahunaCartShippingMethodExcluded, and a mixed cart is guarded parcel by parcel. Switch it off with Printed goods ship at the printer's rates to charge your own rates instead. When Printful cannot carry a parcel, your own methods carry it
  • Address checks before payment: a state for the US, Canada and Australia, and a CPF or CNPJ for Brazil, each refusal naming the field
  • record printful also records Printful's rates for a US and a German address, through the one POST the recorder allows, which writes nothing
  • Custom products: Made by on any product sets it to be made by Printful from Printful's own blanks. Search Printful's catalogue ("Bella Canvas 3001", "hoodies", "embroidery"), match the product's own colours and sizes to a blank's (Grey and Gray, XXL and 2XL read as the same), fix the misses by pasting a blank id, and save; nothing is saved while a variant has no blank. The product becomes a Print on demand product with Printful's shipping profile, and Made by: nobody puts its own type, profile and stock counting back
  • Print files: upload a PNG, JPEG or PDF per print area, for the whole product or one variant. Checked from the bytes, not the name, and against the size limit before it is read; the pixel size is read and a file that would print under 150 dpi says so. Files are kept privately and Printful fetches them from a public address with a token of their own, which changes every time a file is replaced. A print area Printful cannot name is refused when you save, and a custom variant without a print file is refused at the cart door
  • Assign by SKU: a SKU prefix or a numeric range ("7000 to 7999") links every variant it catches to Printful, with one blank for all, blanks matched by each product's options, or each variant's own, and optionally a print file for all of them. A dry run lists what it will link, what it will replace and what it will skip and why, before anything is written
  • Links CSV: import a spreadsheet of sku, provider, mode, blank, file:<print area> rows with a dry run that names every bad line and why, then write the good rows in the background, fetching each file once however many rows share it; export the store's setup the same way. Every bulk run can be undone, and an undo is refused, naming the variants, when something has changed them since
  • Command line: links:import <file> [--commit], links:export [--provider=] [--output=], links:undo <run>
  • The pod_0002_custom migration, with the artwork and link-runs tables
  • Orders: once an order is paid, each provider's share of it is created at Printful as a draft named by your order number on the packing slip, with the cost shown when Printful has worked it out and the margin against what the customer paid. Review mode waits for Send to Printful; auto mode sends by itself once the cost is known
  • A Printful order is never made twice: the order is looked up by its reference before every attempt, and an order Printful already has is carried on with
  • Send again, Check again and Cancel at provider on the order: a draft is deleted, a pending order is cancelled with its cost going back to the Printful wallet, and one Printful has started says so on the order and in a notification
  • Tracking: each parcel Printful sends becomes a shipment on the order with its carrier, tracking number and link, and KahunaCart's shipped email goes out. Partial parcels, replacements and deliveries are recorded once, from Printful's webhooks or a poll every four hours for up to 60 days
  • Webhooks: Register webhook (and webhook printful --url=) gives Printful a secret address and keeps the signing secret it answers with. Deliveries are checked by size, address and signature, logged, and read back from Printful in the background
  • The review queue and a dashboard alert: orders waiting to be sent, orders that need attention, auto-synced drafts and cost increases. Daily reminders for orders waiting and a token about to expire
  • Shipping methods are added for the services Printful actually offers each zone, found by quoting the store's own products to the zone's first country
  • Command line: orders [order-id] [--sync] [--cancel=<id>] and webhook <provider> [--url=] [--reset]
  • The pod_0003_orders migration, with the provider orders, parcels and webhook log tables
  • Gelato: a Gelato tab with the API key, mode, production days and step-by-step instructions, and check gelato, which makes one read to prove the key works
  • Gelato products: import a Gelato template with import gelato <template id> (its sizes and colours become options, with Gelato's print areas and cost on every variant), or set any product to Made by: Gelato and paste a product UID or search Gelato's catalogue ("posters A3", "framed"). Either way it is your own print files Gelato prints, fetched from a private address only an order names
  • Gelato's shipping at checkout: Economy, Standard and Express, priced from every facility Gelato would make the parcel in and added up, offered only when every facility can do it, in the order's currency, with the duties line when the parcel crosses a border and the method does not include duties
  • Gelato orders: a paid order becomes a Gelato draft named by your order number, with its cost and margin, and is sent when you press Send, or at once in auto mode, after the draft is read back from Gelato and found to be the order as it stands. The order is looked up by its reference before every attempt, so a timeout never makes a second one. An order Gelato splits across facilities is one order here, with every parcel tracked onto it, and cancel works across all of them
  • Gelato's address rules at checkout: its name and address lengths, an email, a local-script address for China, Japan, Korea and Russia, and a product Gelato does not make for the customer's country, each named so the customer knows what to change
  • A Gelato parcel becomes a shipment on the order only once its items have left, not when Gelato first gives it a tracking code
  • Gelato stock: products that can run out (frames, say) are checked once a day and go unavailable while Gelato is out of them
  • Gelato webhooks: Gelato does not sign them and has no API to add them, so Register webhook (and webhook gelato --url=) makes a secret address and lists the steps to add it in Gelato's dashboard, with the secret hidden; webhook gelato --reveal prints the whole address to paste. Events from your other Gelato sales channels are answered and ignored
  • Printify: a Printify tab with the token, the shop, the billing currency, mode, production days and step-by-step instructions. Printify cannot be switched on until you confirm the shop is set to Manual order approval, because a Printify shop left on its default sends every order to production by itself after 24 hours and charges you; saving it on without that is refused with the reason, and check printify lists the shops the token reaches and says which scopes are missing
  • Printify products: import what you designed in Printify (its enabled variants, Color and Size options, mockups and costs), refreshed daily, and Printify's Publish button can bring products in as drafts and is always answered, so a product is never left locked in Printify
  • Printify's shipping at checkout: Standard, Priority, Express and Economy from one quote, Express and Economy only when every line can have them, in the currency Printify bills you in, with Printify's address rules (lengths, a phone outside the US, Latin letters) checked before payment
  • Printify orders: a paid order is made on hold at Printify under your order number, with its cost and margin, and sent when you press Send or at once in auto mode, after the order is read back and found to be the order as it stands. A retry never makes a second one. Cancel works while Printify still allows it and says so by name when it does not; an order Printify puts back on hold after it was sent needs attention, with the reason in plain words
  • Printify tracking: each parcel onto the order by the SKUs Printify names, from signed webhooks (or, if Printify does not sign, deliveries checked by reading the order back), and Printify disconnecting the shop switches Printify off and tells you
  • record printify for putting real Printify answers beside the documented fixtures (reads only, never an order)
  • record gelato --template=<id> records Gelato's catalogue, a template, its products and prices, shipment methods and two errors from a real account, with GET calls only
  • A changed order whose draft cannot be edited at the provider is remade: the draft is cancelled and the new lines are sent as the next attempt
  • Kahuna Print Co., a built-in demo provider for demo stores and for walking the screens before you have an account: three invented products (a tee in four colours and five sizes, a mug, a poster) with mockups drawn for it, Standard and Express postage worked out from the country and the item count, and orders that go from submitted through in production and shipped (two parcels for a mixed order) to delivered on a clock, with generated tracking numbers and a public tracking page. It never opens a socket, and it is off unless demo_provider.enabled is on
  • seed and seed --purge --yes: import, price and publish the three demo products, add Standard and Express to every shipping zone, and place two orders in different states. Idempotent and reversible
  • demo:tick [--advance=N], and the same pass on the job worker's tick, to move the demo printer's orders on
  • A demo store that is read-only shows "Demo store — this is read-only" where a Send, Check, Cancel or Refresh button would fail, and hides what a simulation cannot fake. The orders list, review list and providers payloads gain can_send and readonly for it
  • docs/demo-provider.md, with the checklist for putting it on a demo box
  • The pod_0004_order_claims migration: a claim on each provider order, so two workers, a button and a job never act on one at the same time, and what was last sent to the provider, kept for the retry that finds an order by reference
  • The documentation in docs/: getting started, a page for each of Printful, Printify and Gelato, products and import, custom products, moving from another platform, shipping, orders, tracking, configuration, the reference and testing

Improved

  • The docs say Gelato makes clothing, hats and mugs as well as wall art and cards, and that the add-on needs KahunaCart 1.4.7 or later
  • A cart of printed goods shows the printer's options and nothing else. For a parcel the printer makes all of, the store's own shipping methods were listed under the printer's Standard and Express, each turned down with a sentence ("Made by our print partner") or with another carrier's "No rate is available for this order right now". Every one of those refusals is now marked hidden, so with KahunaCart 1.4.9 or later checkout leaves them out and the shopper picks between the printer's own options. A method already refused for its own reason keeps it. On an older KahunaCart they are listed as before

Fixed

  • An order with lines from two providers no longer fails on its second provider: each provider's order reference now ends with the provider's name
  • A Gelato mug, bottle or tote asks for the right size of print file. Importing a Gelato template took the product's own dimensions as its print area for anything printed on one side, which is right for a poster or a card (the sheet is what gets printed) and wrong for a mug, a bottle or a tote bag, where Gelato's dimensions are the object itself: a 10 oz mug says 105 × 104 mm. The product panel then asked for a print file that size and could warn about a good file printing soft. Only flat_ products now take their sheet as the print area; everything else keeps the size of the area its template uses. Nothing sent to Gelato changed. Clothing was never affected, and a real Gelato tee's record is now in the tests to keep it that way
  • The demo printer and the shipping add-on's demo carrier no longer share a code. Kahuna Print Co. was registered as the carrier demo, which is also the shipping add-on's demo carrier, and core keeps one carrier driver per code. On a store running both, like demo.kahunacart.com, whichever registered second quoted nothing: the shipping add-on's Kahuna Ground and Priority methods said "No rate is available" for every parcel. The printer is now kahunaprint, and migration pod_0005 moves what an earlier version stored under demo: its links, imports, orders, parcels and webhook log, and the Standard and Express shipping methods its seeder made. The shipping add-on's own methods are left alone
Rentals 1.0.8 The gap rule is off by default 2 improved · 3 new · 3 improved · 2 fixed

1.0.8

Improved

  • The gap rule is off by default. A new property takes a stay even when it leaves a night or two too short to sell beside it, because guests often can't move their dates and a stay with a gap is better than no stay. Turn on Do not take a stay that would leave a gap too short to sell on a property that wants it. Properties already saved keep the setting they have, and the beach house recipe now leaves it off too
  • The stay request form knows who's booking. With Bookings 1.1.12 or later, a signed-in guest finds their name, email and phone already filled in, and a guest who isn't signed in sees "Have an account? Sign in" above the form. An older Bookings leaves the form as it was

New

  • Themes can show the rates and the house rules. kahunacart_rentals_rates('beach-house') lists the rate bands still to come in date order, each with its dates, nightly and weekend price as the store prints money, minimum stay and arrival days worded ("Saturday to Saturday"), and kahunacart_rentals_house('beach-house') answers the check-in and check-out times, the stay limits, the fees, the deposit, the cancellation steps as plain lines, the lowest price a night and the next night a stay could start on. Both answer in the currency the shopper is browsing in, follow Whole Amounts, and answer an empty list or nothing for anything they cannot
  • The calendar can open on the next free month and start a stay. kahunacart_availability() and the shortcode take from (2026-08, or next for the first month with a night a stay could start on) and pick, which lets a click on such a night become the arrival in the picker for the same property on the page, scrolled into view and focused. The calendar stays read-only when the page has no picker
  • A link can open the picker on a stay. ?arrive=2026-08-01&leave=2026-08-08&guests=4 on any page with the nightly picker opens it with those nights picked and the price showing, or, when they cannot be booked, on the arrival's month with the reason the picker already gives. A page can set the same three as data-arrive, data-leave and data-guests on the picker's mount point

Improved

  • The picker opens on a month you can book. With no stay in the address, a picker whose first months have no night a stay could start on (a closed winter, say) opens on the first month that has one, the same search the calendar's from: 'next' does and through the same route. A link's own arrival still opens on its month, and the page where a stay is being moved looks where it already is
  • The picker says the minimum stay. A row of small pills under the months, there from the first draw, gives the house's minimum (and maximum), its arrival days and any rate band still to come that asks for more nights or other arrival days than the house does, with its dates: "5 to 28 nights", "Arrivals on Saturdays", "High summer · 3 Jul – 27 Aug · 7 nights minimum, Saturday to Saturday". Once an arrival is picked the prompt says the earliest day to leave ("Now pick the day you leave: at least 7 nights, so Sat 24 Jul or later") using the minimum of the band the arrival is in, where it used to check the house's, and the days short of it are dimmed with the reason on them until the pick is cleared. The availability answer carries them as stay_terms and a night in a band with its own minimum says min_nights. Not on the read-only calendar
  • Ships its license. A LICENSE file now comes with the plugin, and the blueprint links to the KahunaCart License at kahunacart.com/license instead of the Grav Premium license

Fixed

  • A night is only offered as an arrival if a stay can start there. The calendar marked every night somebody could sleep in as one a stay could start on, and the picker then refused a Tuesday inside a band that takes arrivals on Saturdays only. The availability answer now honors the band's own arrival days (or the property's where the band has none), and a band's own minimum stay where the free nights after a date run out before it, so the read-only calendar's pick, the picker and the shop agree. A night that can be slept in and not started on says why in arrival_reason, with arrival_days_say or arrival_min_nights, and the picker words it ("Arrivals on Saturdays", "7 nights minimum")
  • Amounts in the picker follow Whole Amounts. With the store's Whole Amounts on, the quote read "$2,100.00", "$630.00 is due today" and "$800.00" for the damage deposit while the rest of the page said "$2,100". The quote's lines, its total, the damage deposit and the sentence about what is due today now print the way the store does, keeping their cents together when any of them has some. What is frozen on a hold, and so the receipt, keeps the full amount. Needs Bookings 1.1.11
Older versions (8)

1.0.7

Fixed

  • Works with a shop mounted at the site root. With KahunaCart's route: '/' (core 1.5.0) the listing link in a stay's messages is /product/{slug} rather than //product/{slug}

1.0.6

New

  • Rentals sells in more than one currency. On a store with several currencies enabled, a stay is quoted, held, paid and refunded in the currency the guest is browsing in. The property's own price a night comes from the product's amount in that currency, each rate band has a nightly and a weekend price in each other currency, and the cleaning fee, the extra-guest rate, the damage deposit and the monthly rate have one each too, typed in that currency's own decimal places on the booking rule and on the Rates tab. A store with one currency draws none of it, stores none of it and runs the same queries it did. Needs KahunaCart 1.4.0 and Bookings 1.1.5 or later
  • A night with no price in a currency is not available in it. The calendar and the picker say "Not available in EUR" and a stay across it is refused with those words, while the same night stays bookable in your own currency. A rate band that covers a night but has no price in the currency makes the night unavailable and is not skipped, because skipping it would lose its minimum stay and arrival days along with its price. A fee with no amount in a currency is no such charge there, and is never the figure from your own currency under another code. New reason code currency
  • --currency on bin/plugin kahunacart-rentals nights, and currency on the quote route and the quote_rental_stay tool. The rules and rates routes and tools carry and take currency_amounts in core's format on a store with more than one currency, and the quote says which currency it is in. The Rates tab says which bands are missing which currency, and its try-it box prices a stay in a chosen one
  • The beach-house showcase is priced in euros and pounds on a store that sells in them, and only in the currencies the store has turned on

Improved

  • Everything after a stay is booked is in its order's currency. The deposit, the balance and its reminder, the damage deposit in the receipt and the pre-arrival email, the refund a cancellation offers and the price of a move are all in the currency the stay was paid in, whatever the guest browses in later. A property with no damage deposit in that currency says nothing about one
  • Amounts are said with the currency's own decimal places. The command line, the quote sentences and the cancellation sentence no longer divide by 100, so yen reads 35,000 and not 350.00
  • PriceBook::nightlyMinor() takes a currency and answers a PriceAnswer (an amount, free or not available), and StayMessages::install() takes two more optional callables for tests. A store with one currency is served exactly what it was

Fixed

  • Stores with no extra currencies enabled avoid currency-table queries when removing band prices or deleting bands and rental rules, while existing foreign-currency stays still read their own prices
  • Concurrent rate-band edits clear the latest fixed prices when switching to a percentage, so an old foreign price cannot return when the band switches back
  • Customer cancellation calls a stay a stay and explains the refund policy in its own currency when the refund request is raised
  • A paid property without its own price in a currency is unavailable in the calendar and quote, even when rate bands have prices there, so the picker agrees with checkout
  • Existing stays keep reading their own currency's rate bands and damage deposit after that currency is turned off, including in the pre-arrival message

1.0.5

Improved

  • Help text on the rental rates and property screens now follows the admin's Help text setting, showing below the label or in a tooltip behind an info icon

1.0.4

Fixed

  • Seeding a rental works again with Bookings 1.1.1. Bookings 1.1.1 gave its seed recipes an optional clock of their own, under the same name the rental recipes already used for theirs, and PHP refuses to set that property twice. So bin/plugin kahunacart-rentals seed and every showcase built on a rental recipe stopped with "Cannot modify readonly property". The rental recipes now keep their clock under a name of their own and work with any Bookings version

1.0.3

Improved

  • The test suite runs against KahunaCart 1.3, which bundles brick/money and YetiSearch under its own namespace. When it borrows the base plugin's dependencies it now also loads core's vendor-prefixed/autoload.php, and Bookings is still borrowed alongside it as before. Against an older KahunaCart without that file nothing changes. Nothing in Rentals itself names brick/money or YetiSearch, so the add-on needs no other change

1.0.2

New

  • A monthly rate. A property let by the month can now say so. A new Monthly rate on the booking rule, under Who it sleeps, and what they pay, prices a stay of a month or more a whole month at a time, counted from the arrival date, and the nights after the last whole month by the rate card as before. 15 January to 20 March at $1,200 a month and $60 a night is two months and five nights, $2,700, and the receipt says "2 months at $1,200.00 a month" and then "5 nights". A month is the same date in the next month and the 31st stays the 31st where it can, so from 31 January one month is 28 February and two is 31 March. A rate band does not reach inside a month, because a monthly price is the price for the month whatever season it is in, and bands only change the leftover nights. A stay shorter than a month, and every property with no monthly rate, is priced exactly as it was. The cleaning fee, extra guests, the deposit, the balance date and cancellation work as they do on any stay, and the quote label says "2 months and 5 nights" instead of 64 nights. monthly_minor on the rule, the API and the MCP tools, and "Month 1" beside each night in bin/plugin kahunacart-rentals nights --quote; migration rentals_0003. A property's longest stay still defaults to 28 nights, so raise it, or set it to 0, before letting by the month. See Rates and seasons → A monthly rate

1.0.1

Improved

  • The "Deposits are off" note points at the right tab. KahunaCart 1.2.13 moves Deposits and part payment from Settings → Security to Settings → Commerce, so the note under a property's Paying box and the Payments and cancellation page now send you to KahunaCart → Settings → Commerce. On an older KahunaCart the switch is still under Security

1.0.0

New

  • KahunaCart Rentals, the add-on. Nightly stays and day rentals, on top of KahunaCart Bookings. A holiday let, a cabin, a B&B room, a campsite pitch, a kayak for the afternoon and a camera kit for three days are all the same thing — something booked by the day rather than by the hour — and this is where that lives. Bookings keeps everything that is true of any booking: bookables, needs, claims, holds, the double-booking guard, orders, deposits, emails with calendar entries, the manage page and the front desk. This keeps what is only true of a stay. It installs, migrates clean, and answers "which nights of August can the beach house be slept in, which days can a stay start or end on, and what do the 9th to the 16th cost for six people"
  • A night is named by the date it starts on, in the property's own timezone. The night of the 9th is the night you go to bed on the 9th and get up on the 10th, and a stay from the 9th to the 16th is seven nights with the 16th a departure rather than a night. Everything else falls out of saying it that way: the claim runs from check-in on the arrival date to check-out on the departure date, four in the afternoon to ten in the morning, and the six hours in between belong to nobody
  • The shared changeover day, which is the case home-made calendars get wrong. One party leaves the 10th at ten in the morning and another arrives the 10th at four in the afternoon. Both hold a real claim, the two claims do not touch, and the date shows as both a departure and an arrival. It is not an edge case: on a property that is doing well it is what every Saturday in August looks like
  • Five states per date, not two. A rental calendar is not a grid of free and busy squares. A date carries two facts at once — whether you could sleep there tonight and whether you could have slept there last night — and the four combinations of those are the square, the square with a diagonal one way, the square with a diagonal the other way and the filled square that every rental site in the world draws. free, arrival_only, departure_only, taken, and blocked for a property that is shut rather than booked
  • Clocks going forward and back stop being interesting. Every conversion happens through the property's own zone on the date in question, never by adding 86,400 to yesterday's answer — so the night before the clocks go forward is seventeen hours, the night before they go back is nineteen, a calendar day is 23 or 25, and a week across either is still a week. Tested in Denver, in Berlin and in Auckland, which is east of UTC and over the date line and where the transitions happen the other way round
  • What a run of nights costs, band by band. A rate card is dated bands laid over the property's own price, and they overlap on purpose: "Winter" across five months with "Christmas week" laid over the top of it is how anybody would write it down, and the higher position wins per night. A band can be a price a night or a percentage of the property's own price, can charge a different rate for Friday and Saturday nights, and can carry a minimum stay and a set of arrival days that override the property's for a stay starting inside it — which is how "seven nights, Saturday to Saturday, in August" is one band with two numbers on it. A property priced one way all year needs no bands at all: a night no band covers costs the option's own price, which is the number the merchant already typed into the shop
  • The orphan-night rule, on by default. A stay that would leave a run of free nights too short for anybody to book is refused rather than sold, because a calendar full of two-night gaps under a three-night minimum is accurate and unsellable. It looks both ways, it is fine with no gap at all, and it names the night that would have been stranded — "that would leave nights nobody could book — nobody could book the night of Wed 12 Aug" — because "no" on its own is what makes an owner turn a good rule off
  • Airbnb, VRBO and Booking.com, both ways. Give each of them one channel link and paste each of theirs back, and a week sold anywhere stops being offered everywhere. Nothing in this engine changed for it: Bookings reads their whole-date reservations as the nights those dates really cover, and the night states fall out of the arithmetic that was already here. What did have to be said is what a night runs between — four in the afternoon to ten in the morning, or whatever this property says — because "the 9th to the 16th" read literally is midnight to midnight and takes the night of the 8th off the market, which is a night that is genuinely still for sale and which no owner would ever notice they were losing
  • Both changeover days stay on the market. A channel's week from the 9th to the 16th blocks the nights of the 9th to the 15th: somebody already in the house still leaves on the 9th, and somebody else can still arrive on the 16th. Proved east and west of UTC, across the clocks going forward and back, and against a stay of our own sharing the changeover day with a channel's
  • docs/channels.md, written for an owner, with where to paste each link, how often each site looks, and the limit said plainly rather than buried: every iCal sync polls, so two sites can sell the same night in the gap between two looks. It is rare, it is the same with every plugin that works this way, the only cure is a channel manager and this is not one — and there is a numbered list of what to do on the day it happens to you
  • Seven new reasons, in the same words as Bookings' own eight. min_nights, max_nights, arrival_day, departure_day, guests, gap and past are taught to Bookings' Reason when the mode registers, so Troubleshoot, the command line and the picker all print them beside "Booked" and "Too soon" with no second lookup table anywhere. past is the one that replaces a Bookings answer rather than adding to it: a night that has already started is certainly less than the notice period away, but Too soon reads as "come back later" on a date nobody can ever book again, so a guest looking at last August is told it is last August
  • It reads nothing of Bookings' own. Which properties could take this service, how many of each there are, when they are open, the days the owner is away, what is already booked or held or blocked by an Airbnb feed — every one of those arrives through the narrow set Bookings hands a registered mode. Not one query in this add-on touches a table of Bookings', and there is a test that fails if one ever does. That is what keeps this engine and Bookings' own availability walk from drifting: they are the same three readers underneath
  • A stay goes through the same guard a haircut goes through. The mode answers with an interval, a property and one place, and Bookings claims exactly that through the write lock, the sweep, the recheck and the unique key it has always used. Nothing this add-on returns bypasses the double-booking check and there is no way to ask it to. A stay and an hour-long booking on the same property refuse each other both ways round, which is what a venue hired by the day and also by the hour needs
  • Four identical doubles are one record with four units. The calendar says "two left" rather than making a guest work out which of Room 1, Room 2, Room 3 and Room 4 is free, and the owner has one row to edit instead of four. A claim from an outside calendar takes the whole property rather than one unit, because there is no sensible reading of "Airbnb has one of your four identical doubles"
  • Two tables and nothing on anybody else's. kahunacart_rental_rules is one row per nightly property — arrival and departure times, the shortest and longest stay, which weekdays a stay may start and end on, how many guests the rate covers, the fees, the deposit terms and the cancellation policy. kahunacart_rental_rates is the rate card. Nothing is added to a Bookings table and nothing to a core table, so switching this add-on off leaves every service, booking, hold and claim in the store reading exactly as it did
  • bin/plugin kahunacart-rentals nights prints the month as a table with what each date is for, and --quote 2026-08-09..2026-08-16 prints a stay as a breakdown with the rate band beside every night. It is how this release is demonstrated with nothing to click, and it stays afterwards as the thing to run when an owner says the calendar is wrong — it answers out of the same engine the picker will, so it is not a second opinion
  • bin/plugin kahunacart-rentals seed rental writes the beach house: sleeps eight, $249 a night, $289 at weekends, a summer rate at $329 with a seven-night Saturday-to-Saturday minimum, a Christmas week, a $140 cleaning fee, $25 a night for each guest over six, a $500 damage deposit, 30% down with the rest a fortnight before arrival, a cancellation policy in three steps — and three stays already in the book, two of them sharing a changeover day. seed bnb writes four identical doubles as one record with breakfast as an option. Both are idempotent and both are written in the store's own timezone, and the rate bands are worked out from today so a store seeded in September still has a calendar worth looking at in March
  • Every time of day is written the way the rest of the shop writes it. A store that says 6:00pm on a class card says 4:00pm on the stay it sold, and a store set to 24-hour time says 16:00 in both places. It goes through Bookings' one class for that rather than a second opinion here, on the receipt, in the order email, on the account tab, on the manage page and in the command line's own banner
  • No composer install and no vendor/. This add-on has no run-time package of its own: it reads KahunaCart's database through the base plugin and Bookings' engine through the mode seam, and both are installed on any store that can run it. The autoloader is four lines of PSR-4 in the plugin file, and a GPM install is a folder of files that works as it lands
  • No permissions of its own, and no product type of its own. A stay is a Bookings booking and a property is a Bookings bookable, so kahunacart.bookings.view and kahunacart.bookings.manage already cover everything here — a separate one would mean an owner who ticked "Manage Bookings and Setup" still could not edit the rate card of the property they had just set up
  • What a stay costs, as a breakdown a guest can read. The quote a property answers with is a list of rows — 7 nights at $329.00, 2 extra guests, 4 nights, Cleaning — and those rows become the cart line, the receipt and the order email, adding up to the figure the guest was shown at every step. The cleaning fee is marked as not discountable, so a 10% coupon takes 10% off the nights and nothing off the cleaning, which is what every owner means by a discount. The damage deposit is not in the total: it is a fact printed beside the stay, because it is money held and given back rather than money taken
  • 30% now and the rest a fortnight before you arrive. The property's own deposit terms go to the store through Bookings' payment-schedule seam, as a figure and in the property's own words — Of the $2,534.00 for this stay, $760.20 is due today and the rest — $1,773.80 — by Sat 25 Jul 2026. It says what it is a share of because it is a share of the stay: a hamper bought alongside the week, and any sales tax, are due in full today, so a checkout's "Due today" can be more than the deposit on the quote. A stay booked after the balance date is due in full today and says so rather than asking for a deposit on money that was owed last week, a property with no deposit set is paid for in one go, and a stay with nothing to pay skips all of it
  • A balance nobody paid does not cost anybody their holiday. The guest is emailed a pay link three days before the date — a number an owner can change — and the owner is told the moment the date passes with the money still not in, with a sentence from the property itself naming what is owed and how much warning the guest had. The three days are there so that the owner's "not paid" message does not arrive on the same morning the guest was first asked. The dates stay held and the deposit stays where it is: a holiday cancelled automatically at one minute past midnight over a card that expired is how a rental business loses a guest it already had
  • The cancellation policy is read when somebody cancels, and it answers with a figure. The three-step policy the property was set up with — full refund far enough out, half inside that, nothing at the last minute — is applied against what the guest has actually paid for that stay, so cancelling one of two weeks in one basket offers back one of them rather than the basket, and the amount lands on the refund request a person opens. The policy is read as it stands today rather than as it stood when the stay was sold, which is written down on the settings, in the docs and in the code, because there is nowhere on a booking to freeze terms and an owner who rewrites their policy mid-season should know which stays it now covers
  • A stay with nothing to pay, and a stay paid for on arrival. A free night, a comp, a room the owner takes payment for at the door — each of them goes straight into the diary with no basket and no checkout, the same as any other booking with no money on it, and the quote still prints the breakdown so everybody can see what is being given away
  • The owner sets a property up from the Bookings section, on two screens. Setting the way of booking to Nightly stay draws this add-on's own panels straight into the service editor: when people can arrive and leave and on which weekdays, the shortest and longest stay, who it sleeps and what an extra guest costs, the cleaning fee, the damage deposit, the deposit and when the balance falls due, the cancellation policy as a repeater that reads itself back in a sentence — 100% up to 30 days before, then 50% up to 14 days before, then 0% after that — and the orphan-night rule. It saves on Bookings' own Save button, in the same write as the service
  • A Rates tab that draws the year. A month ruler, a line across the top showing what a night actually costs, and one lane per band underneath drawn to scale in the order they are laid down — so a gap in March, a band that runs past new year and two bands over the same week are all things you can see rather than things you work out. Arrows on each lane decide which band wins where two of them overlap, a band opens into an editor for its dates, its price or its percentage, its weekend rate, its own minimum stay and its own arrival days, and underneath it all is a box that prices a real stay through the same engine a guest's calendar uses — pick two dates, press Price it, and read the nights, the extra guests and the cleaning with the deposit due today under them. Nothing is held and nothing is booked
  • Two messages a stay sends of its own. A few days before somebody arrives they get the note the owner wrote for that property — directions, parking, the door code — with their dates, their check-in and check-out times and the damage deposit beside it; a day after they leave they get a thank-you with a link back to the property. Both have a switch of their own on the store's messages screen and both are ordinary Twig templates a theme can override by copying one file. Neither is sent for a stay that was cancelled, and the note is not sent to somebody whose balance fell due and was never paid. The note is the only place a door code is ever printed: it is not on the receipt, not in the confirmation, not in the calendar entry, not on the front desk and not in any calendar the shop publishes, and there is a test for every one of those clauses
  • The calendar a guest actually uses. Two months side by side where there is room and one where there is not, an arrival, a departure, and the nights between them drawn as one band. It is registered as a face on Bookings' picker rather than being a storefront of its own, so it arrives inside the store's own buy form, on whatever theme the shop already uses, in the store's own --kc-* colours, and the store's own quantity box and Add to cart button appear underneath once the dates are held — the same ones a jar of shampoo goes in with
  • Five states per date on the page, not two. The states the engine has always answered with are finally drawn: a free night tinted, a booked night filled, a night that has gone struck through, and a changeover day split on the diagonal with a line through the middle — half of it spoken for and half of it not. On a property that is doing well it is what every Saturday in August looks like, and it is the one thing a home-made calendar never has
  • A date that cannot be chosen says why, in a sentence. 3 nights minimum, Arrivals on Saturdays, Booked, That date has been and gone — on the line under the calendar when a pointer crosses the date, and on the date itself for anybody reading with a keyboard or a screen reader. The minimum stay is enforced while the range is being drawn, so a date that would make too short a stay stops being pressable rather than being refused after somebody has pressed it. A refused date is aria-disabled rather than disabled, because a date nobody can explain is worse than a date nobody can book
  • Every figure on the summary is the shop's own answer. Each rate band as its own row, the extra guests, the cleaning fee, the total, what is due today, when the balance falls due, the damage deposit as a stated amount and the cancellation policy in sentences — all of it comes back from the same quote the hold, the cart line and the receipt are built from. Nothing in the page adds two numbers together, so there is no version of this where the calendar and the checkout disagree, and a rate card edited this morning cannot be stale in a browser left open since yesterday
  • The dates are what is held, and the instant is worked out where the property is. The browser posts the 21st to the 27th and never an instant, because check-in is four in the afternoon where the cottage is and a browser in another timezone doing that arithmetic is how a calendar ends up a day out twice a year. Dates are read and written as plain YYYY-MM-DD throughout the script for the same reason
  • It works with a keyboard, and it says what it is doing. Arrow keys a day at a time, Page Up and Page Down a month at a time, Home and End to the ends of a week, Enter to choose and Escape to start again, with one tab stop into the grid and the focus kept on the same date when the calendar redraws. The chosen range, the nights, the price and every refusal are announced through a live region, so the summary is not something you have to go and look at
  • One month or two, decided by the space it has rather than by the size of the window. The picker measures its own container, so the same calendar is two months wide in a page's main column, one month in a sidebar and one month in a phone's width, and a theme that puts it in a narrow box gets a calendar that fits rather than one that scrolls sideways. It is measured again when the box changes size, and the months are added and removed rather than hidden, so nothing keyboard-reachable is ever drawn where it cannot be seen
  • Moving a stay, from a guest's own link. The manage page opens the same range picker with the guest's own booking left out of the reckoning, so the week they are moving off does not read as taken by themselves. The booking is named to the availability route by the token the guest was emailed and never by its id, so there is nothing in that request a stranger could guess
  • A read-only calendar for a listing page. [kahunacart-availability product="beach-house" months="2" /] and {{ kahunacart_availability('beach-house') }} draw a month or two of free and taken nights anywhere on the site with nothing to press. It is the same script reading the same public route the picker does, so a listing page and a booking page can never disagree about which week in August has gone
  • Every word on it is a language key with English behind it. Fifty-odd of them, sent to the browser by the server as this mode's own strings — so a store with no translation reads properly, a store with one reads in its own language with no JavaScript changed, and there is a test that fails if the script ever draws a word the server does not send or the server sends one the script has no English for
  • Eleven pages of documentation, written for somebody with a cottage to let. Two getting-started walkthroughs that end with a page a guest can book on — one for a whole property, one for four rooms sold as one record with breakfast as an option, each with a seed command that writes the same thing in one line — then what a night is and what each state on the calendar means, the rate card with the warning that a band's percentage is a multiplier and not an uplift, the deposit and the balance and what a cancellation actually gives back, Airbnb and VRBO and Booking.com with the polling limit said plainly, and a whole page of why a night is not on offer with every reason code, its sentence and what to do about it. The limits are in the prose where an owner meets them rather than in a list at the end: channels poll, an outside calendar on a property with four units blocks all four, a minimum stay that applies only at weekends cannot be written down, and a stay cannot be booked at the front desk
  • A stay can be taken at the front desk. Somebody rings and wants the second week of June. Open /desk, tap New booking, tap the property, and the sheet asks for the day they arrive, the day they leave and how many guests — then prints the same rows and the same total the guest would have read on the property's own page: the dates written out, check in and check out, every rate band as its own row, the extra guests, the cleaning and the total. Not the deposit, the due-today figure or the balance date, because none of those happens on this path. If those days cannot be had it says why in the same words the picker uses, so you can read the reason down the telephone. Nothing is taken and nothing is scheduled: a booking made at the counter has no order behind it, so there is no deposit, no balance date and no reminder, and the desk says To collect $1,714.00 until you have. Put it through the property's own page instead if you want the deposit taken now
  • A seeded stay can have somebody in it. stay() takes an optional customer — an id, or a name and an email to find or make one from — so a recipe writing a real property's book gets a diary worth looking at rather than a calendar of anonymous blocks. Neither of the two shipped recipes passes one; it is there for the recipes other people write, which had to copy the whole method to add one key
  • The read-only calendar measures itself, like the picker beside it. months on [kahunacart-availability] is the most it will draw rather than the number it must, so a phone gets one month with seven readable columns instead of two squeezed ones — and two calendars in the same column stop disagreeing about how many months fit, which reads as a broken page rather than as two different controls
  • A refusal no longer sits under a range somebody has chosen. Crossing a night the property is shut on wrote that night's reason onto the line under the calendar and left it there, because the line was only cleared while a range was half drawn. A guest who had just picked a perfectly good week read Closed under it. Once both dates are chosen the line says the range and nothing else
  • A stay with no dates on it is refused rather than priced from 1970. Both doors into the nightly mode send a start of nought where nobody named one, and reading that as an instant put the arrival on the first of January 1970 — so the quote walked fifty-six years of rate card before anybody was told they had forgotten a date
  • How wide the calendar may get is a token here too. --kc-picker-max, set on whatever wraps the picker, now lifts this face's own two caps as well as the panel's — so a page whose whole middle is the calendar says so once rather than reaching past three stylesheets with a selector long enough to beat them. The defaults are unchanged: 58rem for the picker, 44rem for the read-only calendar
  • A stay taken at the desk races a stay in somebody's basket, on all three engines. Six processes going for two overlapping weeks in one house, half of them through the storefront's hold and half of them straight off the front desk, all jumping at the same wall-clock moment. Exactly one of them gets the week. It is what found the ordering bug in Bookings' own guard that let MariaDB sell the house twice
  • The Rates tab and the stay rules panel respect an account that may only look. Somebody with kahunacart.bookings.view and not kahunacart.bookings.manage reads the whole year, opens a band to see its weekend rate and arrival days, and prices a stay with the try-it box, but is not offered Add a band, the reorder arrows, or a band's Save and Remove, all of which the server refuses. The stay rules inside Bookings' service editor draw read-only for them, with no cancellation step to add or take off. The properties list, the rate card and the rule each carry can_manage, worked out through the same check the writes go through

Fixed

  • A property only asks for a deposit when the store takes deposits. A property with a deposit percentage on a store where checkout.deposits.enabled is off still quoted "30% due today and the rest by …" in the picker, the Rates preview and the command line, while the checkout took the whole stay. Every quote now follows the store switch: with deposits off the whole stay is due when it is booked, with no deposit line, no balance date and nothing handed to Bookings to schedule. The percentage stays saved and editable, and the Paying box under the property's settings says deposits are off for this store and where to switch them on. The damage deposit is the owner's own and is still stated either way.
Shipping 1.2.4 Ships its license 1 improved

1.2.4

Improved

  • Ships its license. A LICENSE file now comes with the plugin, and the blueprint links to the KahunaCart License at kahunacart.com/license instead of the Grav Premium license
Older versions (9)

1.2.3

Improved

  • The ship-from address form works like the rest of the admin. The country and the state are lists instead of boxes to type a code into, the street suggests addresses as it's typed on a store with address lookup set up, and pressing Add this address with something missing marks every box that needs filling in, all at once, instead of refusing one at a time. Needs KahunaCart 1.5.17; on an older KahunaCart the form is the plain one it was

1.2.2

Fixed

  • Test the packer takes the order number on the receipt. The panel on the Boxes tab asked for the order's database id, which is on no receipt, email or packing slip, so typing an order number answered "That order has no lines to pack" for every order. It now takes the number as printed, with or without its # and leading zeros (1016, 01057, #01057), and still takes an id when no order has that number. A number that matches nothing says so instead. The route and the preview_packing MCP tool take it as order; order_id works as before

1.2.1

New

  • Hear when automatic labels go wrong. With Buy a label when an order is paid switched on, No label was bought for a paid order (normal) tells you when the parcels could not be planned, once per order per day. A label could not be voided (high) tells you when a label bought for an order that was then refunded or cancelled could not be refunded, whether the carrier said no straight away or later, the ninety days had passed or the request could not be made. Both name the order, give the reason in plain words rather than the carrier's own text, and say what to do
  • A webhook your store refuses is no longer silent. A tracking delivery turned away for a wrong or missing secret, a wrong source, a bad signature or a driver that cannot read tracking now sends KahunaCart's Webhook rejected message, once per reason per day, instead of only appearing in the delivery log. Needs KahunaCart 1.5.3

Fixed

  • A notification given its facts later no longer shares one dedupe key. Raising an owner message with its data as a callable made the dedupe key from nothing, so the first message of the day for any order would silence the rest. No message sent before this release was affected

1.2.0

New

  • Sendcloud is a fourth carrier aggregator. Choose Sendcloud as the aggregator, paste the public key and secret key of a Sendcloud API integration on the new Sendcloud tab, and the store quotes, buys, cancels and tracks labels with the carriers your Sendcloud account offers, such as Royal Mail, DPD, PostNL, DHL and GLS. It works on Sendcloud's free plan. Quotes come from Sendcloud's own price list, a label's file comes back with the purchase, and tracking arrives through a webhook that Sendcloud signs with your secret key and every delivery is checked against. Register webhook on the Shipping carriers screen switches it on for your integration. Services delivered to a service point or a locker are left out for now
  • Sendcloud has no test environment, so test mode buys a free label. In Test mode the quotes are the real ones, but every label is bought as Sendcloud's free "Unstamped letter", whatever service the sheet named. It is a real label that costs nothing and no carrier will carry, and the buy sheet and the label say so. Live mode buys the service that was quoted
  • A label bought twice is still one label. Each Sendcloud purchase carries the store's own reference, so a purchase whose answer never arrived is found again rather than bought a second time, and a retry after a refused address goes out under a fresh reference
  • Customs declarations for Sendcloud go out item by item with HS codes, and the Sendcloud tab has a VAT number and an EORI number for commercial exports, which Sendcloud asks for
  • Check connection warns when the store ships from the wrong country. Sendcloud offers carriers from the country of your Sendcloud sender address, so a store shipping from anywhere else would be quoted nothing. The check names the integration and the sender country, and says so when they do not match, and a quote that comes back empty for that reason says why too

Improved

  • A customs declaration is asked for only at a customs border. Parcels between EU countries, Monaco and Northern Ireland no longer need one, and the EU's own territories outside its customs area, such as the Canary Islands, Ceuta, Melilla, the Åland Islands, the French overseas departments and Mount Athos, still do. The same rule decides it in the label planner and in every driver, so ShipEngine no longer refuses a label from France to Germany for want of a declaration
  • The Buy label sheet lists services cheapest first. Services were listed in whatever order the aggregator answered in, so with Sendcloud the cheapest Royal Mail service could be eleventh of 33. They are now sorted by price, then by name, and when the order's method names no service the cheapest one is preselected
  • A webhook with nothing to act on yet is logged as ignored, not unreadable. Sendcloud sends a status for every new parcel before the carrier has given it a tracking number. The webhook log on the Shipping carriers screen now says "Read, nothing to act on yet" for that, and keeps "unreadable" for a body that could not be read at all

Fixed

  • A Sendcloud service code names its carrier correctly. Service codes were split at the first underscore, which read Sendcloud's royal_mailv2:tracked_24 as a carrier called royal. A colon is now looked for first, and Royal Mail and DPD UK on Sendcloud get core's own tracking links
  • A tracking update that arrives while its label is still being bought is no longer lost. Sendcloud posts its first status before the purchase has answered, when the store does not yet know the tracking number, so that update was dropped. While a label from the same aggregator is being bought, such an update is now answered with a request to send it again, and the retry a few minutes later is accepted. This now covers a purchase tried again after a refusal, too, not only a new one
  • Two stores on one Sendcloud account no longer take each other's labels. A copy of a store, or a test store that was reset, repeats order numbers and so the references Sendcloud knows a label by, and Sendcloud handed back the other store's label as this one. A label Sendcloud already has is now only taken when it is for the same order number, country and postcode; otherwise a new one is bought under the next reference, and the label says why
  • A Sendcloud label found again keeps the price it was quoted at. Sendcloud does not say what a label cost, so a label whose purchase answer was lost and was found later was recorded at 0 with no currency. It now keeps the quoted price and its currency, and a free test label still costs 0
  • A full-page Sendcloud label found again is stored as a full page, not as the smaller A6 label, and a PNG or ZPL label is fetched as itself
  • A Sendcloud cancellation that Sendcloud refused stays refused. When Sendcloud said a label could no longer be cancelled and the parcel was still waiting to be sent, the label was shown as cancellation requested. It now shows Sendcloud's refusal, and only a shipment that says it is cancelled or being cancelled is shown as such
  • The store stops waiting on a queued Sendcloud cancellation 14 days after it asked for it, counted from when it was asked rather than from the parcel's own dates, so a parcel with no dates on it is no longer waited on for ever
  • EasyPost and ShipEngine send a customs declaration at a customs border, not at a national one. Madrid to the Canary Islands stays in Spain and still needs one, and now gets it; France to Germany crosses a border and needs none, and no longer gets one

1.1.8

Fixed

  • The webhook address follows a changed API version. It read a setting the API plugin does not have, so a store that had changed the API's version prefix was shown a webhook address, and a demo tracking link, still ending in /v1

Improved

  • The webhook and tracking addresses now come from KahunaCart's own SiteUrl::api() instead of being built here, so they agree with every other add-on. Needs KahunaCart 1.4.4 or later

1.1.7

New

  • The Labels list, Ship-from and Boxes have search boxes. Type a few words and the rows that do not hold all of them drop away, in any order and whatever the case. Labels looks through the carrier, the service, the tracking number and the order number, and asks the server, so it finds a label on any page and the count and the pages follow the search; it works with the status chips and with the sort. Ship-from is searched by code or address and Boxes by code or name, as you type, and Ship-from only gets its box once it has more than eight addresses. GET /kahunacart/shipping/labels takes q for the same thing. Needs KahunaCart 1.3.11 or later, which draws the box
  • A carrier account has a currency. Each driver's tab has an Account currency, the currency that aggregator account is billed in, and left empty it is the store's own, which is what every store has always been quoted in. Rates are asked for in it and labels are bought in it, whatever currency an order was sold in. A store that sells in one currency with its account in that currency sees no change at all. There is a new Currencies page in the docs
  • A quote in another currency than the customer's cart is skipped, never relabelled. KahunaCart 1.4 lets one store sell in several currencies, and there are no exchange rates yet to turn a carrier's price in one into another. So when the account is billed in a currency other than the cart's, the carrier is not asked at all, every method bound to it says "This service cannot be quoted in the currency of this order", and the merchant's fallback method carries the order. The skip goes in the log once per request at debug level, however many rates it covered. The request a driver receives already carries the account's currency, the order's and a place for the rate between them, so exchange rates have somewhere to go when they arrive
  • An order can be labelled from a carrier account in another currency. Label prices used to be asked for in the order's currency and every other rate was dropped, so a euro order could not get a label from a dollar account. They are now asked for and bought in the account's currency, the label row keeps what the carrier charged and the currency it was charged in, and the Labels list, the label card, the Buy label sheet and the refund question print it in that currency with its own number of decimal places. The customs form on the same parcel stays in the order's currency
  • The demo carrier quotes in whatever currency the cart is in when its Currency it quotes in is left empty, so a demo store can show carrier rates in every currency it sells in

Improved

  • A label's cost is read at the decimals of the currency it is in. The purchase was told every currency has two decimal places, which is right for dollars and euros and would have read a yen price a hundred times too high. Needs KahunaCart 1.4.0 or later, which brings the base currency, the order builder's currency and the admin's money helper that takes a currency

Fixed

  • Account-currency changes take effect in checkout immediately. All four drivers include their account currency and selected carrier accounts in the quote cache key, so an old rate or currency refusal cannot linger for fifteen minutes after a setting changes
  • Sorting labels by Paid keeps currencies apart. Labels are grouped by currency code before their costs are sorted, so yen, dollars and dinars are never compared as one amount. Labels in one currency keep their existing order
  • Demo carrier prices stay believable across currencies. On stores selling in several currencies the demo formula uses reference USD cents and scales the whole charge with documented fixed demo factors and each currency's decimal places, rounding half up. Yen no longer reuses a cents amount as whole yen, and three-decimal currencies no longer reuse it as thousandths. These are invented demo prices, never live exchange rates; single-currency stores keep their original quotes byte for byte
  • The customs form says what currency the order is in. It said USD on every store: the declaration looked for a currency on each order line, a line has none, and so the fallback won every time. A store selling in euros or pounds declared its parcels with the right amount and the wrong code beside it, to Shippo, EasyPost and ShipEngine alike. It now reads the order's own currency and flags a line that has none instead of guessing. A store that sells in dollars sees no change

1.1.6

Improved

  • Help text on the Shipping carriers screens now follows the admin's Help text setting, showing below the label or in a tooltip behind an info icon

1.1.5

Improved

  • The download no longer carries the plugin's tests. tests/ and phpunit.xml stay in the repository and out of the release zip, 211 files fewer on every install. Nothing the plugin runs is affected

1.1.4

New

  • The tables on the Shipping screens sort by their column headings. Press a heading on the Labels, Boxes or ship-from tables to sort by it, and press it again to turn the order round. The Labels list sorts on the server, so a heading orders every label and not only the page on screen, and pressing one goes back to page one. GET /kahunacart/shipping/labels takes sort (order, carrier, service, tracking, paid, status or bought) and dir, a label with nothing in that column sorts last whichever way it runs, and any other sort is ignored, so the list is newest first as before. Needs KahunaCart 1.3.8 or later, which draws the headings
Subscriptions 1.1.10 No empty Subscriptions tab 3 improved

1.1.10

Improved

  • No empty Subscriptions tab. On KahunaCart 1.6.0 with Hide Empty Tabs on (the default), a customer with no subscription doesn't get a Subscriptions tab in their account; the page still opens at its address. Older KahunaCart shows the tab as before
  • Plan prices follow Whole Amounts. With KahunaCart 1.6.0's Whole Amounts on, the plans offered on a paywalled page said "$19.00 every month". They now print the way core prints its own prices: "$19 every month", never rounding cents away, and the offers on one page are one set, so if any of them has cents they all keep theirs. A billing option's own price in the buy box follows the setting too, the way the variant's price beside it does. Emails, invoices and the cart line's trial note keep full amounts. On an older core nothing changes
  • Ships its license. A LICENSE file now comes with the plugin, and the blueprint links to the KahunaCart License at kahunacart.com/license instead of the Grav Premium license
Older versions (9)

1.1.9

New

  • Hear about subscribers as they come and go. Under Notifications → Store owner there are four new rows. A new subscriber (low, switched off until you turn it on) says who has started, paid or on a free trial. A subscription is cancelled arrives when a cancellation is set to take effect and again when it ends. A subscription is past due (high) arrives once for each missed renewal, with what is owed. A subscription expired arrives when a past-due one runs out. Each names the plan and the customer's name, never their email, and links to the subscription. A failed renewal payment is already sent by KahunaCart as Payment failed, so there is no second row for it
  • Renewals waiting on a price reach your phone too. The alert for renewals that cannot be built for want of a price in the subscription's currency is now also sent to your channels, once when it shows and once when it clears. Needs KahunaCart 1.5.3

1.1.8

Fixed

  • Works with a shop mounted at the site root. With KahunaCart's route: '/' (core 1.5.0) a subscription's page is /subscription/{hash}, and its links, the links in subscription emails and the admin's link to the page have one slash in them rather than two. The stylesheet still loads only on the shop's own pages, and the page's Back to shop link and the account tab's Browse the shop link go to /

1.1.7

Fixed

  • A free trial is free with every payment method, not only Stripe and Polar. A plan with a free trial bought through a payment method that cannot run subscriptions itself (Mollie, PayPal, Moneris, bank transfer and others) falls back to invoice mode, and that order used to be charged the whole first period. The first invoice at the end of the trial then billed the same period again. Now the plan costs nothing on the order that starts the trial, as the docs always said: with nothing left to pay the order completes as paid and the customer goes straight to the receipt without visiting a payment page, and a fixed payment method fee and its tax, or postage on a physical plan, is still charged through the method they picked. The first invoice is for the first whole period, starting where the trial ends. The same now happens when Stripe or Polar refuses a subscription and it falls back to invoice mode. Orders placed before this version are not changed; docs/troubleshooting.md says how to put them right

1.1.6

Fixed

  • Subscription links work on a site installed in a subfolder. On a store at an address like example.com/store, the links on a subscription's page and in the account's Subscriptions tab (the subscription itself, its orders, the product) pointed at the domain root and ended on the web server's 404 page. They now carry the store's own address, and the active language like every other storefront link

Improved

  • Storefront links now come from KahunaCart's own SiteUrl::store() instead of being built here, so they agree with every other add-on. Needs KahunaCart 1.4.4 or later

1.1.5

New

  • Choose whether a plan's price includes the payment method's fee. A new setting, Payment method fees on subscriptions, is Added to the price by default: the subscriber pays the method's fee and its tax on every payment, the first and each renewal, and on a renewal invoice the fee follows the method they pick. Included in the price makes the plan's price what the subscriber pays every time, whatever the method, and the store absorbs the fee. Each plan can override the store's choice, and a subscription keeps the choice it started with. Subscriptions started before this version renew without a method fee, as they always did.
  • A plan can be sold in every currency the store sells in. On a store that sells in more than one currency (KahunaCart 1.4), the plan editor has a box for each of the other currencies beside the plan's price, in that currency's own decimal places, and the Plans table lists them under the price. A plan with a price of its own can be bought only in the currencies that have an amount: in any other the buy button draws the option greyed out as "Not available in EUR", the form opens on the first option that can be bought, and the cart refuses it with a sentence naming the plan and the currency instead of charging the dollar number or nothing. A plan with no price of its own sells at the variant's price in that currency, as it did in dollars. Amounts travel as currency_amounts on the plan endpoints and the create_plan and update_plan tools, the same as every other price in the admin, and are saved with the plan in one transaction. Clearing a plan's own price clears its other amounts, and deleting a plan deletes them. See docs/plans.md
  • A subscription renews in the currency it started in. Each renewal is built as an order in the subscription's own currency at the amount the subscription was sold at, in invoice mode and for a platform's renewed event, and its page, its emails and the admin print its money in that currency with that currency's decimals. A renewal the store cannot price there (the variant has no price in it) is never built in another currency. It fails like a failed payment: a failed attempt and a note on the timeline, "Waiting on a price in EUR" beside the status in the admin, an alert on the admin's strip, and a high priority notification to the store owner that names the plan, the currency and the subscription number and nothing about the customer. A mark on the subscription keeps the invoice sweep and a platform's retries from repeating all of it, and it clears itself when a renewal goes through. In invoice mode the next sweep builds the invoice once the price is back, and Renew now says plainly that it cannot be done right now. A platform that bills in a different currency from the subscription's has that amount left out of the record, with one note on the timeline. Needs KahunaCart 1.4.0 or later. See docs/subscriptions.md and docs/troubleshooting.md
  • The Plans table and the orders on a subscription have search boxes. Type a few words and the rows that do not hold all of them drop away as you type, in any order and whatever the case: Plans by plan, SKU or how often it bills, and a subscription's orders by order number or period. A plan's Move up and Move down still work with some rows hidden. The orders only get a box once there are more than eight of them. The subscriptions list keeps its own search. Needs KahunaCart 1.3.11 or later, which draws the box

Improved

  • Subscription checkout gives providers the first order’s payable amount separately from the recurring plan price, including its tax and payment-method fee
  • The subscribe panel on paywalled articles prices plans in the visitor's currency. A plan with no amount in that currency is left out, and a product whose other plans are left out is named as the product of the one that remains
  • The admin never prints one currency's amounts with another's decimals. A yen subscription in a dollar store shows whole yen, the plan editor takes yen without decimals, and a currency the store no longer sells in is still printed with its own decimals. Monthly recurring revenue is still counted per currency and never added together

Fixed

  • Stores with extra currencies disabled delete plans with the original single statement, while existing plans still retain their foreign-currency prices
  • Concurrent plan edits and deletion cannot recreate prices for a deleted plan, and a currency-price edit checks the plan’s latest own price before saving
  • Clearing a plan's own price clears its foreign prices in one save. Inherited plans hide the foreign-price boxes, and typing an own price shows them again
  • Guest subscription payment and invoice links work from the manage page. Receipt, invoice and renewal-payment links carry the access token required by KahunaCart
  • Immediate cancellation succeeds when the provider confirms it during the request. The admin receives the cancelled subscription without an already-cancelled error
  • Existing subscriptions keep renewing in their own currency after it is disabled. Invoice and provider renewals retain the agreed currency; a renewal can never use an amount from another currency
  • Deleting a plan also deletes its currency prices while extra currencies are enabled. Cleanup applies to command-line purges too and happens in the same transaction as the plan deletion
  • Sorting subscriptions by amount keeps each currency together. Amounts are compared only within their own currency
  • The buyer pays the payment method fee and its tax on the first subscription payment and every renewal; provider agreements retain their agreed currency amounts, and methods that cannot collect them are not offered.

1.1.4

Improved

  • Help text on the plan editor now follows the admin's Help text setting. It shows below the label or in a tooltip behind an info icon

1.1.3

Improved

  • The download no longer carries the plugin's tests. tests/ and phpunit.xml stay in the repository and out of the release zip, 91 files fewer on every install. Nothing the plugin runs is affected

1.1.2

New

  • A plan's customer group also fills the subscribe panel on paywalled articles. KahunaCart 1.3.8 can hide the rest of an article from anyone who isn't a member of a customer group and show a subscribe panel instead. The panel now lists the plans that grant that group: one button for each plan on a product that is published (listed or unlisted), open to the visitor and not sold out, with its price the way the product page states it (the first payment, then the renewal when that is different, or the trial). A product with Monthly and Yearly billing options gets a button for each, in the order the shop lists products and then the order the product page offers the plans. Nothing to set up: a plan with a Customer group while subscribed is offered on every article that asks for that group. It costs one database query however many products the shop has. Needs KahunaCart 1.3.8 or later. See docs/plans.md
  • The subscriptions list and the orders on a subscription sort by their column headings. Press a heading to sort by it, and press it again to turn the order round. The list sorts on the server, so a heading orders every subscription the filters leave and not only the page on screen, and pressing one goes back to page one. GET /kahunacart/subscriptions takes sort (customer, plan, status, billing, amount or next) and dir, a subscription with nothing in that column sorts last whichever way it runs, and any other sort is ignored, so the list is newest first as before. The Plans table has no headings to press, because its rows are in the order the buy button offers them. Needs KahunaCart 1.3.8 or later, which draws the headings

1.1.1

New

  • A plan can put its subscribers in a customer group while their subscription is live. The plan editor has a new setting, Customer group while subscribed. Pick a group and everybody whose subscription on that plan is trialing, active or past due counts as a member of it, everywhere KahunaCart asks: price lists, coupon conditions, member-only products, and the site.customer_group.<code> permission a page's access: header can ask for. So a Downloads Pro page with access: site.customer_group.members: true opens for subscribers and closes the moment a subscription stops being live. Nothing is written to the group's member list: the store works it out from the subscription on each request, so a late webhook or a status set by hand can never leave somebody in a group they have stopped paying for. A cancellation at the end of the period keeps the group until that end, because the customer paid for it; a paused subscription grants nothing. Migration subscriptions_0005_plan_customer_group adds the column on SQLite, MySQL/MariaDB and PostgreSQL. Needs KahunaCart 1.3.5 or later, the release that asks add-ons which groups a customer holds. See docs/plans.md

Payment providers

Stripe, Polar, PayPal and the test provider come with KahunaCart. Moneris, Mollie, Square, Paddle, Lemon Squeezy and SumUp are sold on their own.

Dummy 1.2.9 Simulated card readers 1 new

1.2.9

New

  • Simulated card readers. With KahunaCart 1.5.7 the test provider declares card_present and offers two simulated readers, so a counter sale can take a card on a reader with no hardware and no account. A payment approves after a short delay you can set, an amount ending in .05 is declined, and one ending in .09 times out, so every path a till has to handle can be tried. Any code starting sim- pairs another reader, a reader takes one payment at a time, and a cancel is confirmed at once while the payment is pending and answers too late once it is paid. On an older KahunaCart the readers are simply not offered. See Simulated card readers
Older versions (9)

1.2.8

New

  • The test provider's subscriptions travel in a store bundle. With KahunaCart 1.5.0's export and restore, kahunacart_dummy_subscriptions is carried as the dummy.subscriptions set and comes back on restore, linked to the restored orders and variants. It is the reference example for an add-on registering on onKahunaCartRegisterExports
  • An optional test key with a connection check, stored encrypted, so a store bundle has a provider key to carry in tests

Fixed

  • Works with a shop mounted at the site root. With KahunaCart's route: '/' (core 1.5.0) the simulated subscription webhook posts to /webhook/dummy rather than //webhook/dummy

1.2.7

New

  • Document that the simulator follows the subscription’s saved Added or Included fee choice
  • The Test Provider's agreements table has a search box. Type a few words and the agreements that do not hold all of them drop away as you type, in any order and whatever the case, looking at the agreement, the customer, the amount and the status. The whole list is on screen, so it searches in the browser. Needs KahunaCart 1.3.11 or later, which draws the box; on an older one there is no box, as before
  • Make the Test Provider refuse a currency. New setting Only Take These Currencies (only_currencies, empty by default, which takes every currency as before). With codes in it, an order in any other currency is not offered Test Payment, and a store with nothing else on offer for that currency shows checkout's "No payment method is available for orders in EUR." A payment posted by hand for such an order is refused too, with "Test Payment does not take EUR". So a test store can see what its shoppers see when a provider can't take their currency
  • Make a payment or refund report a different currency. New setting Report a Different Currency (report_currency, off by default). Every payment and refund webhook the provider receives then says its amount is in that currency, whatever the order is in, so a test store can watch KahunaCart's currency mismatch handling on a live site: the payment is recorded for the amount it was opened with, what the provider reported stays on the transaction, and the store owner gets a notice. Only for testing that case

Improved

  • The first hosted subscription payment records the order’s payable total, including tax and payment-method fees, and shows it separately from the recurring amount
  • Payment webhooks say which currency their amount is in. A posted payment_completed, payment_authorized or refund_completed that carries a currency has it passed on, in capitals, the way a real platform reports it. Without the key nothing changes. Needs KahunaCart 1.4.0, which compares it with the order's currency
  • The test pages and the agreements table show each amount in its own currency. The hosted checkout, the billing portal and bin/plugin kahunacart-dummy subscription printed every amount with two decimals, so a yen or dinar plan read wrong. They now use the currency's own decimal places, and the admin table prints each agreement in its own currency
  • Needs KahunaCart 1.4.0. The provider now uses the currency check that arrives with it, so the plugin asks for 1.4.0 or later

Fixed

  • Subscription renewals and declined renewal attempts include the payment method's fee and its tax when Subscriptions supplies the renewal order total; the hosted checkout and billing portal show that payable recurring amount
  • Sorting the Test Provider's agreements by Amount keeps currencies apart. The table sorts currency codes first and amounts within each currency, so yen, dollars and dinars are never compared as one amount

1.2.6

Improved

  • The download no longer carries the plugin's tests. tests/ and phpunit.xml stay in the repository and out of the release zip, 27 files fewer on every install. Nothing the plugin runs is affected

1.2.5

New

  • The Test Provider's agreements table sorts by its column headings. Press a heading to sort by it, and press it again to turn the order round. The whole list is on screen, so it sorts in the browser. Needs KahunaCart 1.3.8 or later, which draws the headings; on an older one the headings are plain as before

1.2.4

New

  • Act as a merchant of record, for testing a store that will sell through one. New setting Act as Merchant of Record (merchant_of_record, off by default). On, the provider declares the MerchantOfRecord capability, as Polar does, so a store rehearsing on it behaves the way it will in production: an order paid through it carries no local tax, and on a store where it's the only payment method on offer the cart and checkout say "Any tax is added when you pay." instead of quoting a tax (that part needs the base plugin at 1.3.6). It still charges nobody. Off, nothing changes, because a store testing ordinary payments has to keep computing its own tax. A local site can turn it on from its .env without touching tracked config; see Acting as a merchant of record

1.2.3

Fixed

  • The download no longer carries PHPUnit. Every release until now committed a development vendor/: PHPUnit and about thirty packages only the test suite uses, some 1,700 files in a plugin that needs no Composer package at run time. vendor/ is now a --no-dev install holding just the autoloader, and psr/http-message is declared under replace because Grav core already supplies it. A test fails if a development install is ever committed again

1.2.2

Improved

  • Composer's installed-package record carries the plugin's current package name, kahunacart/grav-plugin-kahunacart-dummy, instead of the old trilbymedia/grav-plugin-kahunacart-dummy

1.2.1

New

  • Test express pay actually pays now. The button drew itself to the store's express style and then linked to /checkout, which on the cart page was a redundant link and on the checkout page was a button that did nothing at all — a placeholder where a wallet button goes, and a broken button is worse than no button. Pressing it now opens a fake wallet sheet drawn by this plugin's own script and stylesheet, over the page, looking like a wallet sheet rather than like part of the shop: the store's name, the total, a saved contact and address, the store's real delivery offers priced for that address with the first one chosen and a price on each, and one Pay button. Choosing a different method reprices the whole order — postage, tax and all — because the sheet asks the store rather than adding numbers up itself. The purchase behind it is not faked. Pay posts to this plugin's own endpoint, which places the cart through the base plugin's two new express entry points: the sheet's address, the chosen method priced by the store's own shipping engine with carrier rates in it where the shipping add-on is installed, the taxes, the plugin veto, this provider's test payment with whatever outcome is configured, and the store's own receipt at the end. An order bought this way is indistinguishable in the admin from one bought on the checkout page. A store that cannot deliver to the wallet address gets a sentence and no Pay button, for the same reason the placeholder had to go. The saved details are settings — Plugins → KahunaCart Dummy → Express Pay — defaulting to a US address any US shipping zone will match, so a store selling elsewhere can point them somewhere it covers. Nothing about a payment passes through any of it: no card field, no token field and no wallet payload in the button, the sheet or the endpoint, and the sheet prints Test wallet · no charge where a real one would print a card. With scripting off the button is still a link to checkout. The whole flow is four short files, which is the point — this is the reference implementation, and a driver author should be able to read it start to finish. Needs the base plugin at 1.2.10, which is where quoteExpressOrder() and placeExpressOrder() come from

Fixed

  • The test express button no longer vanishes on a dark storefront. It painted itself #111111 on every theme but light and outline, so on the dark side of a theme like Kahuna it was a near-black rectangle on a near-black page — you could tab to it, and that was about the only way to find it. The base plugin's express.theme has grown an auto value and made it the default, and this button now draws that value the way the provider contract asks a driver with no SDK of its own to draw it: the fill, the label and the edge come from --kahunacart-express-auto-bg, --kahunacart-express-auto-fg and --kahunacart-express-auto-border on the region wrapper, which the storefront stylesheet resolves from the page's own ink and ground. Dark button with a light label on a light page, light button with a dark label on a dark one, and it flips with the page rather than with a setting. dark and light are unchanged and still absolute — a merchant who picked one gets exactly that — and outline now follows the page as well: the fill stays transparent, and its border and its label come from --kahunacart-express-auto-bg, the page's own foreground, because a black outline on a dark page is as gone as a black slab. This provider is also the reference implementation, so the whole change is three short match blocks anybody writing a real driver can copy. Needs the base plugin at 1.2.10, which is where auto and the wrapper's colours come from

1.2.0

New

  • The Test Provider tab has a Settings screen, and it is the plugin's own form. Every switch this provider has is a blueprint field, and until now reading one meant leaving the KahunaCart admin for the Grav admin's plugin list — the tab that drives the agreements could not tell you what capture_mode was set to, and the banner about unsigned webhooks sent you somewhere else to fix it. The tab now has two screens, Agreements and Settings, with Settings on a settings sub-route of its own so a reload and the Back button land where the address says, and KahunaCart draws the plugin's blueprint form on the second one: the manifest says settings_plugin, the screen renders the slot the base plugin forwards into, and the header's Save button saves it. Following Plugins → KahunaCart Dummy in the Grav admin lands here too, and the unsigned-webhooks banner now points at the tab beside it rather than at another page. Against an older base plugin the screen prints one line pointing at the Grav admin's page, so nothing is stranded
  • bin/plugin kahunacart reset now empties the fake platform's ledger. Every row in kahunacart_dummy_subscriptions was made by somebody testing, which makes the whole table test data by definition — and until now a pre-launch reset left it behind, so the fake platform still held live agreements for orders and customers that were gone and the next worker tick renewed them. The ref is derived from the order rather than random, so a leftover would also collide with the first real subscription that reused the number. The table is declared on the base plugin's new onKahunaCartRegisterResetTables under both the orders and the customers scope, so either flag takes it, and it appears in the plan the command prints with its row count before it asks. Deliberately not behind this plugin's enabled switch: the provider ships switched off, and a guard would hide the fake agreements on exactly the store that has finished testing
Lemon Squeezy 1.1.8 Ships its license 1 improved

1.1.8

Improved

  • Ships its license. A LICENSE file now comes with the plugin, and the blueprint links to the KahunaCart License at kahunacart.com/license instead of the Grav Premium license
Older versions (8)

1.1.7

New

  • Check connection says whether Lemon Squeezy's webhook is right. After the key checks out, the driver looks up the webhook at this store's address in the current mode, changing nothing, and the Payments card warns when there is none or when it is missing events the driver handles. Set up webhook fixes both

Improved

  • Needs KahunaCart 1.5.18

1.1.6

New

  • Lemon Squeezy sets up its own webhook. A Set up webhook button registers this store's webhook in the mode the plugin is in, with the two events it handles and a signing secret it generates and saves. Lemon Squeezy never shows a secret twice, so a webhook already at this address is adopted with a fresh secret and any missing events rather than duplicated. Reset webhook replaces it. Other webhooks on the store are never touched
  • Check connection finds the store from the API key, lists your products with their variant ids when no Carrier Variant ID is set, and checks that the carrier is published and in the same mode as the key
  • Store ID can be left empty. The plugin uses the one store the API key can see, which is nearly every account; several stores are listed by name and id. The plugin now registers as soon as the API key is saved, and is offered at checkout once the carrier variant is set

Fixed

  • The payment is recorded at the store's net. The completed payment reported Lemon Squeezy's tax-inclusive total as its amount, so the ledger counted Lemon Squeezy's tax as the store's takings. It now reports the net, with the tax and the card total beside it as core 1.5.1 reads them for receipts, and refunds report the same way
  • Partial refunds give back their share of the tax. A partial amount went out unscaled, so two partials making up the whole order left the tax with Lemon Squeezy. They are now scaled by the order's own ratio
  • The API key is encrypted on the first save. Core only knew to encrypt the key once the provider was registered, which needed the store and variant too, so a key pasted first was saved in plain text. The plugin now declares its secrets up front

Improved

  • Out of testing. Check connection, Set up webhook, checkout, the webhook, partial and full refunds, and forged webhooks were run end to end against Lemon Squeezy's test mode, so this is the first stable release
  • The Instructions tab and README describe the shorter setup

1.1.5

New

  • Lemon Squeezy is only offered for an order in the store's currency. On a store that sells in more than one currency, checkout and the pay link leave Lemon Squeezy out for an order in any other currency than the Store Currency saved in the settings, instead of letting the shopper pick it and fail at the last step. The settings, the instructions and the README now say to fill in Store Currency on a multi-currency store: while it is empty Lemon Squeezy is offered for every currency, and a mismatch is still refused when the shopper pays, as before. The payments a Lemon Squeezy store reports already carried their currency. Needs KahunaCart 1.4.0.

1.1.4

Fixed

  • The download no longer carries PHPUnit. Every release until now committed a development vendor/: PHPUnit and about thirty packages only the test suite uses, some 1,700 files in a plugin that needs no Composer package at run time. vendor/ is now a --no-dev install holding just the autoloader, and psr/http-message is declared under replace because Grav core already supplies it. A test fails if a development install is ever committed again

1.1.3

Improved

  • Composer's installed-package record carries the plugin's current package name, kahunacart/grav-plugin-kahunacart-lemonsqueezy, instead of the old trilbymedia/grav-plugin-kahunacart-lemonsqueezy

1.1.2

Improved

  • The paid order reports Lemon Squeezy's own customer id back to the store, so its record of who this shopper is on the platform is complete. Nothing new goes out: Lemon Squeezy's checkout has no parameter for an existing customer and it keys customers by email itself, so a shopper who buys twice was already one customer on its side

1.1.1

New

  • An Instructions tab in the plugin settings, after General, that walks a merchant through setup in order: the account or sandbox, which credential to copy and from where, saving it, the webhook (button or manual steps with the endpoint URL and the events), test cards or sandbox accounts, and what to check afterwards

1.1.0

New

  • Initial release: first-party Lemon Squeezy provider for KahunaCart
  • Hosted checkout flow — initiatePayment() creates a JSON:API checkout priced with custom_price equal to the order total, so any cart checks out against a single carrier variant without per-product mapping
  • MerchantOfRecord capability: Lemon Squeezy computes and remits tax at checkout, so KahunaCart's own tax estimate steps aside
  • Currency guard — a Lemon Squeezy store charges in one currency and a checkout has no currency field, so a cart priced in anything else is refused with a message naming both currencies before any request goes out; the store currency is configurable or read from GET /v1/stores/{id} once per request
  • Both KahunaCart hashes travel in checkout_data.custom and come back as meta.custom_data, which is the only link between a checkout and the order it produces
  • Webhook verification against the X-Signature HMAC-SHA256 hex digest of the raw body, with an unset secret failing closed, hash_equals comparison, and the event name read from the signed body rather than the X-Event-Name header
  • Event mapping for order_created (gated on the order's own status, so paid completes, failed and fraudulent fail, and anything else is ignored) and order_refunded (full and partial, with the running refunded_amount folded into the reference so successive partial refunds stay distinct in the ledger)
  • Full and partial refunds through POST /v1/orders/{id}/refund, with a full refund sent without an amount so Lemon Squeezy returns the tax it collected alongside the pre-tax total KahunaCart priced
  • Refunds are synchronous here, so submission is confirmation: the endpoint answers with the order, refund attributes already applied, and there is no pending state to wait on. Documented alongside the limitation it comes with — Lemon Squeezy publishes no event for a refund failing or being reversed, so a refund undone at their end cannot reach the ledger
  • Test-mode toggle alongside the mode-scoped API key, and registration only when the API key, store id, and variant id are all present
  • Exact integer and string rendering of amounts at each currency's own precision for the one line of copy the customer reads at checkout — zero-decimal (JPY, KRW…), two-decimal, and three-decimal (KWD, BHD…) currencies all render correctly, with no float arithmetic anywhere
  • Unit test suite covering signature verification and tamper rejection, JSON:API request construction, the currency guard, webhook event mapping, refund body building and reference agreement, the capability matrix, money rendering, and transport-failure degradation
  • captureMode() is declared explicitly as Automatic, and there is deliberately no setting to change it. Everything Lemon Squeezy offers for a one-time sale is create a checkout, read an order back and refund it — no authorisation resource, no capture call, no void call — and the charge happens on Lemon Squeezy's own page, so by the time an order exists to read back the money has moved and the tax has been computed and remitted against it. Authorize, Capture and Void are undeclared with it, so KahunaCart never draws a Capture or Void button on a Lemon Squeezy order
  • encryptedSettings() names the API key and the webhook signing secret, so KahunaCart stores both encrypted against a key that lives in the store's database rather than in any file. The key can move money out of the store and the secret would let anyone holding it sign a webhook this plugin cannot tell from a real one. A value written in plaintext before this reads correctly and is rewritten encrypted on the next save, with nothing for a merchant to do. The store id, the variant id and the store currency are left in the clear on purpose — none is a secret, and all three have to be read back and checked against the Lemon Squeezy dashboard
  • The README says plainly what this driver does not claim. Lemon Squeezy keeps a customer's card for the subscriptions it bills itself, but exposes no vault to store a method against and no way to charge one off-session; its checkout takes wallets, but only after the redirect, so there is no SDK to mount an express button with
Mollie 1.1.8 Ships its license 1 improved

1.1.8

Improved

  • Ships its license. A LICENSE file now comes with the plugin, and the blueprint links to the KahunaCart License at kahunacart.com/license instead of the Grav Premium license
Older versions (9)

1.1.7

New

  • A chargeback on a Mollie payment is written on the order and tells you. The order's history says "Dispute opened at Mollie" with the amount (and the bank's reason code on SEPA Direct Debit), and KahunaCart sends its Dispute opened message to the channels you have ticked. Affiliates holds the partner's commission on it. The payment is still recorded as a reversal as before, the order itself is not changed, and each chargeback is reported once however many later notifications arrive for the payment, including one whose payment is still pending
  • You are told when the rest of a hold was not released. After a partial capture the store asks Mollie to let go of the rest of the hold. When Mollie refuses, or the store cannot read the payment back to check, Part of a Mollie hold was not released goes to your notification channels with the order number, what may still be held and a link to the order, once per payment

1.1.6

New

  • Check connection. The Payments page can now check a Mollie key: whether it is a test or live key, which website profile it belongs to, and which methods Mollie will offer in each currency the store sells in, for example CHF: TWINT, Card. It fails when nothing on the profile takes the store's base currency, so a merchant finds out before a shopper does. Read-only calls only

Improved

  • Every webhook the driver refuses or ignores now says why in the webhook log (mollie_auth, mollie_unavailable, mollie_rate_limited, mismatched_payment, bad_id, unknown_payment, status_open and so on) and names the payment, instead of every Mollie failure reading "signature or payload rejected" for a provider that signs nothing
  • A refund Mollie refuses because it has less left than the store thinks (usually a refund made in the Mollie Dashboard) says how much Mollie has left and to record the outside refund, and raises the store's refund-balance notice. Mollie's one-hour rule against two refunds of the same amount on one payment is explained in a sentence instead of "A duplicate refund has been detected". No message anybody reads carries a schema field name any more
  • A shopper who let Mollie's page time out or left it is told so plainly, not that their payment was declined, and a shopper never reads Mollie's or curl's own text at checkout. The detail goes to the Grav log
  • Currency codes on webhook payloads are uppercased
  • README and Instructions: all six capabilities listed, Klarna plainly not offered in this release (Mollie needs order lines and a billing address the driver does not send yet), and TWINT and pay-by-bank on a manual-capture store come back as an ordinary paid sale rather than being refused

Fixed

  • Refunds that finish together are all recorded. Mollie settles refunds on a payment in batches and its notification names only the payment, so two line refunds made a second apart finished together and the store only ever recorded the newer one; the older one stayed pending for ever, and an older refund made in the Mollie Dashboard was never recorded at all. Every finished refund is now reported on every notification, oldest first, and the store's reference matching makes repeats harmless. Needs KahunaCart 1.4.5
  • A captured order can be refunded. A capture reported Mollie's cpt_… id, which the refund call then refused, so every refund of an order captured from the admin failed with "That transaction has no Mollie payment to refund against"
  • A partial capture releases the rest of the hold. Mollie kept the remainder of a partly captured Klarna hold on the customer's account for 28 days. The driver now asks Mollie to release it after any capture for less than the held amount, and if Mollie refuses, the capture still stands and the merchant is told the remainder is still held
  • A held payment is recorded at what Mollie captured, not at the amount that was held, when Mollie later reports it paid
  • A hold released at Mollie closes the order. A hold cancelled in the Mollie Dashboard or left to lapse was read as a shopper walking away, because Mollie drops authorizedAt on a cancel, and the order sat at authorized with Capture and Void buttons Mollie refused. Holds are now recognised from the payment's own captureMode, never from the store's current setting, so switching the store back to automatic no longer strands a hold either
  • Chargebacks reach the store as a reversal of the settled payment: a line in the order's history and a notice for the merchant, without completing the order again. A finished refund no longer hides one

1.1.5

New

  • Mollie is no longer offered for a currency it cannot take. The driver now tells KahunaCart the 28 currencies on Mollie's multicurrency page, so checkout and the pay link leave Mollie out of an order in KWD, INR, CLP or any other currency off that page instead of letting the shopper pick it and fail on Mollie's own page. The list is every currency any Mollie payment method takes. Which of them your profile can take depends on the methods you have switched on, and one that none of yours takes still fails at Mollie as before. Needs the base plugin at 1.4.0, which is where the question is asked

Improved

  • A refund the driver submits is reported with the currency Mollie says it is in, so core can compare it with the order's
  • The README and the Instructions tab say which currencies Mollie takes and which of them need PayPal switched on

1.1.4

Fixed

  • The download no longer carries PHPUnit. Every release until now committed a development vendor/: PHPUnit and about thirty packages only the test suite uses, some 1,700 files in a plugin that needs no Composer package at run time. vendor/ is now a --no-dev install holding just the autoloader, and psr/http-message is declared under replace because Grav core already supplies it. A test fails if a development install is ever committed again

1.1.3

Improved

  • Composer's installed-package record carries the plugin's current package name, kahunacart/grav-plugin-kahunacart-mollie, instead of the old trilbymedia/grav-plugin-kahunacart-mollie

1.1.2

Improved

  • A payment that names a Mollie customer reports the id back to the store, so it is on record the next time somebody needs it. Nothing new goes out and no customers are created: a one-off Mollie payment has no customer object behind it, Mollie's customers are for recurring billing, and opening one per checkout would fill the merchant's dashboard with records nothing ever charges again

1.1.1

New

  • An Instructions tab in the plugin settings, after General, that walks a merchant through setup in order: the account, which credential to copy and from where, saving it, why there is no webhook to register, test cards, and what to check afterwards

Fixed

  • A refund event named the payment on payment_id, and the base plugin only reads payment_ref. The value was always right, so this cost nothing on a refund the store submitted itself — that one is matched by its own refund id. It cost the other route entirely: a refund issued in the Mollie Dashboard arrives with no pending row to confirm, and the base plugin can only open one from the payment the refund belongs to, which it was never handed. payment_ref is now the name. The list the base reads is payment_ref, order_id, charge_id, charge, and it is closed — drivers are corrected rather than the list grown

1.1.0

New

  • Two-step capture. capture_mode: manual sends captureMode: manual on the payment, so the customer's payment stops at Mollie's authorized status and the order stands at authorized until the merchant presses Capture or Void. MollieProvider implements AuthorizeCaptureInterface: capture() posts to /v2/payments/{id}/captures with an idempotency key, and void() cancels the payment, because Mollie has no void endpoint — releasing an authorization is cancelling it
  • authorized and canceled are read against the declared mode, since Mollie reuses the payment's own status rather than sending an event. On a manual store authorized is the hold, and a canceled payment carrying an authorizedAt is the release — this store's own Void, a cancellation in the Mollie Dashboard, or an authorization Mollie let expire. A canceled payment with no authorizedAt is still a customer who walked away, and an automatic store keeps exactly the reading it always had
  • The README says plainly what manual capture costs: it has to be enabled on the Mollie account, and only some methods can honour it — cards and Klarna can, iDEAL and Bancontact and the other bank-transfer methods settle immediately and cannot be held. Mollie refuses a payment it cannot hold, and that refusal reaches the merchant in Mollie's own words rather than in a sentence this plugin invented
  • completeAuthorize() asks Mollie nothing and answers "not yet", because Mollie appends no payment id and no status to the redirect URL and says the redirect is not a signal. It is a real method rather than an omission: a driver that guessed the state from the customer's return is the failure the contract exists to prevent
  • A capture Mollie answers pending is reported as the capture. The contract has no answer between captured and failed, and telling a merchant their capture failed when it did not is the worse of the two mistakes — the payment's own webhook settles the row when Mollie finishes
  • encryptedSettings() names the API key, so KahunaCart stores it encrypted against a key that lives in the store's database and not in any file. It is the only credential this plugin holds — there is no webhook secret beside it, because Mollie does not sign its webhooks at all
  • A reference that is not a Mollie payment id is refused before any call is made, for a capture and a void as it already was for a refund — an id that does not match the format is a probe, not a payment, and it never reaches a request path

Improved

  • The README says what this driver does not claim. Mollie can keep a payment method through customers and mandates and it has Apple Pay, and neither Tokenization nor ExpressPay is declared because neither is implemented — KahunaCart draws its admin controls from these flags, so a flag claiming something that is not there would put a button in the admin that could never work

0.1.0

New

  • Initial release: first-party Mollie provider for KahunaCart
  • Hosted checkout on Payments API v2 — initiatePayment() creates a Mollie payment and redirects to _links.checkout, with the order hash and transaction hash carried in the payment's metadata
  • Verify-by-fetch webhooks, which is Mollie's documented model: the posted body is mined for a single id=tr_…, the id's format is checked before it reaches a URL, and every fact acted on comes back from an authenticated GET /v2/payments/{id}?embed=refunds. Nothing in the request body is ever trusted
  • Deliberate webhook failure paths — a body with no usable id is a 400, a well-formed id Mollie has never issued is answered 200 so the status code cannot be used to probe which ids exist, and a rejected key or unreachable API is a 400 that leaves Mollie to redeliver
  • Status mapping: paid completes the order, failed, canceled, and expired fail it with Mollie's own statusReason where there is one, and open, pending, and authorized are ignored
  • Refund reconstruction: Mollie has no refund webhook, so a refund state change re-fires the payment's own webhook with nothing to say which refund moved — the plugin embeds the refunds and reports the most recently created one that has finished, leaving queued, pending, and processing alone until the refund has actually resolved
  • Async refund lifecycle: a refund Mollie answers queued or pending is reported as pending and settled later by the payment's own webhook, keyed on the same re_… reference the refund call returned — refunded confirms it, and failed or canceled releases the held amount for a retry. Cancellation is the one transition Mollie does not announce, so a canceled refund is released the next time any notification arrives for that payment
  • Full and partial refunds through /v2/payments/{id}/refunds, including refunds issued in Mollie's dashboard, which arrive with the order hash and amount the ledger needs to record a credit it never initiated
  • Idempotency-Key on payment creation, derived from the transaction hash, so a double-submitted checkout returns the same Mollie payment rather than creating a second one — and deliberately no key on refunds, where a replayed response would read as a second refund that never happened
  • No mode toggle: a Mollie API key is prefixed test_ or live_ and the key alone decides, so the provider registers on the key and nothing can contradict it
  • Exact integer and string conversion between minor units and Mollie's decimal amounts at each currency's own precision — EUR as "10.00", JPY and ISK as "1000" — with HUF and TWD following Mollie's two-decimal handling, and unrecognised formats rejected rather than rounded
  • Unit test suite covering webhook verify-by-fetch and every reject path, status and refund mapping, payment and refund body building, decimal amounts across exponents, idempotent key derivation, and transport failure
Moneris 1.0.1 Ships its license 1 improved

1.0.1

Improved

  • Ships its license. A LICENSE file now comes with the plugin, and the blueprint links to the KahunaCart License at kahunacart.com/license instead of the Grav Premium license
Older versions (1)

1.0.0

New

  • First release. Moneris payments for KahunaCart through Moneris Checkout, on a payment page the plugin serves on your own store. The shopper is sent there from checkout, pays in Moneris's card panel, and lands on KahunaCart's normal receipt page
  • Payments are confirmed with a server-to-server receipt request to Moneris, never from the browser. The page's "done", a revisit of the page and a worker check of every open payment all ask Moneris, so a shopper who pays and closes the tab still gets their order confirmed
  • A payment is never failed just because its Moneris ticket expired. The worker keeps asking Moneris every 5 minutes for an hour after expiry and then hourly, for up to 24 hours, with the schedule kept on disk so a restart picks it up. If Moneris never says paid or declined, the payment stays pending. A checkout where the shopper never pressed Pay is then closed quietly; one where they did gets a Notices queue item, a line in the order's history and a warning in the Grav log, each with the Moneris order number to look up
  • A shopper who comes back to an expired payment page is checked with Moneris first, then told the page timed out with a link back to checkout to try again
  • The order's items go to Moneris with each payment, with SKU, quantity and unit price, plus shipping and tax, so they are kept with the transaction. They are only sent when they add up to the amount charged, with an "Other charges" line for a fee and no items at all for a discounted order or a deposit
  • The payment page loads Moneris's current chkt_v3.00.js script, and each of Moneris's double-fired panel events is acted on once
  • Full and partial refunds from the order in the KahunaCart admin, through the Moneris Gateway
  • Canadian dollars by default, or US dollars for a store ID Moneris set up for them. Moneris is only offered for orders in that currency
  • Check connection on the Payments page, which opens a real 1.00 payment with your credentials and checkout profile
  • An Instructions tab that walks through the Merchant Resource Center, the checkout profile, test cards and going live
Paddle 1.1.9 Ships its license 1 improved

1.1.9

Improved

  • Ships its license. A LICENSE file now comes with the plugin, and the blueprint links to the KahunaCart License at kahunacart.com/license instead of the Grav Premium license
Older versions (9)

1.1.8

New

  • Check connection says whether Paddle's notification destination is right. After the key checks out, the driver looks up the destination at this store's address, changing nothing, and the Payments card warns when there is none, when it is switched off, when it is missing events the driver handles, or when its secret key is not the one stored here. Set up webhook fixes each of these

Improved

  • Needs KahunaCart 1.5.18

1.1.7

New

  • The plugin serves the payment page itself. Paddle has no hosted payment page for websites, so every store had to build a page that loads Paddle.js and paste its client-side token into a template. Paste the token into the new Client-side Token setting instead, and the plugin answers {store route}/paddle/pay, sends every transaction there, opens Paddle's checkout on it, and takes the shopper to their receipt when they have paid (or back to the checkout if they close it). No default payment link is needed in Paddle. A store with its own page keeps it by leaving the token empty, and Payment Link Override still wins
  • Paddle sets up its own webhook. A Set up webhook button creates this store's notification destination in Paddle with the five events the plugin handles and saves its secret. A destination already at this address is adopted rather than duplicated, switched back on and given any missing events, so pressing twice is safe. Reset webhook rotates the secret. Other destinations on the account are never touched, and only the same path on another host is reported as stale
  • Check connection. Tests the API key with one read, catches a sandbox key or token in live mode and the other way round before any request, and says whether the webhook secret and the payment page are in place

Fixed

  • Refunds give back Paddle's tax too. Paddle adds its tax on top, and its refund amounts include it, but the store's amounts went over unscaled: refunding a $24.00 order that Paddle charged $25.24 in two parts gave the customer back $24.00 and left $1.24 with Paddle. The net amount is now scaled by the transaction's own ratio, and the whole net is sent as a full refund
  • The payment is recorded at the store's net. The completed payment reported Paddle's tax-inclusive total as its amount, so the ledger, balances and reports counted Paddle's tax as the store's takings. It now reports the net, with Paddle's tax and the card total beside it as core 1.5.1 reads them for receipts, and refunds do the same
  • A refund refused because Paddle is still reviewing an earlier one says to wait instead of quoting Paddle's error

Improved

  • Out of testing. Checkout on the plugin's own payment page, the webhook, partial and full refunds with Paddle's tax, and Check connection were all run end to end against Paddle's sandbox, so this is the first stable release
  • The Instructions tab and README describe the new setup: API key, client-side token, domain approval, Check connection, Set up webhook. They no longer claim sandbox accepts any domain; it needs the same website approval as live, which sandbox grants at once

1.1.6

New

  • A chargeback at Paddle is written on the order and tells you. When a customer's bank charges a payment back, the order's history says "Dispute opened at Paddle" with the amount, and KahunaCart sends its Dispute opened message to the channels you have ticked. Affiliates holds the partner's commission on it. The order itself is not changed, and a chargeback is reported once however often Paddle sends the created and updated events for it, including one that arrives before its payment is recorded. Nothing new to subscribe to in Paddle

1.1.5

New

  • Paddle is only offered for the currencies Paddle takes. On a store that sells in more than one currency, checkout and the pay link leave Paddle out for an order in a currency outside its 33, instead of letting the shopper pick it and fail at the last step. The list is the one the plugin already carried, checked against Paddle's supported currencies page on 2026-10-01. Needs KahunaCart 1.4.0.
  • The README and the settings screen's Instructions tab say which currencies Paddle takes.

1.1.4

Fixed

  • The download no longer carries PHPUnit. Every release until now committed a development vendor/: PHPUnit and about thirty packages only the test suite uses, some 1,700 files in a plugin that needs no Composer package at run time. vendor/ is now a --no-dev install holding just the autoloader, and psr/http-message is declared under replace because Grav core already supplies it. A test fails if a development install is ever committed again

1.1.3

Improved

  • Composer's installed-package record carries the plugin's current package name, kahunacart/grav-plugin-kahunacart-paddle, instead of the old trilbymedia/grav-plugin-kahunacart-paddle

1.1.2

Improved

  • Paddle is told which customer it is looking at. A transaction for somebody Paddle has already billed now carries its own customer id, so the checkout opens with the address and country Paddle holds instead of asking for them again, and the id Paddle names on the completed transaction goes back to the store so it is there for the next order. An id Paddle no longer recognises, after a move between the sandbox and production or after the customer was archived, is dropped and the transaction goes again without it

1.1.1

New

  • An Instructions tab in the plugin settings, after General, that walks a merchant through setup in order: the account or sandbox, which credential to copy and from where, saving it, the webhook (button or manual steps with the endpoint URL and the events), test cards or sandbox accounts, and what to check afterwards

1.1.0

New

  • Initial release: first-party Paddle Billing provider for KahunaCart, targeting the current platform rather than Paddle Classic
  • Hosted redirect flow — initiatePayment() creates a Paddle transaction and redirects to the checkout payment link, with the order hash and transaction hash on the transaction's custom_data
  • Non-catalog pricing: the whole cart is one ad-hoc item with an inline price wrapping an inline product, so any cart checks out with an empty Paddle catalog — no carrier product, no product mapping, no sync
  • MerchantOfRecord capability: Paddle computes and remits tax at checkout, so KahunaCart's own tax estimate steps aside
  • Self-contained webhook verification — HMAC-SHA256 over {ts}:{raw body} against the notification destination secret, verified on the raw bytes before any decoding, with a configurable replay window, constant-time comparison, and support for the multiple h1 signatures Paddle reserves for secret rotation
  • Event mapping for transaction.completed and transaction.paid (both complete the order, deduped by the ledger), transaction.payment_failed (with Paddle's decline code as the reason), and adjustment.created / adjustment.updated
  • Full and partial refunds through the adjustments API, allocating a partial amount across the transaction's txnitm_ line items and sending a whole-total refund as type: full
  • Refund approval handled honestly: a pending_approval adjustment is reported as pending rather than settled, and only an approved refund is ever recorded as money returned — credits and chargebacks are not
  • Async refund lifecycle: a refund left pending in Paddle's review holds its amount against the order's refundable balance until adjustment.updated settles it under the same adj_… reference the refund call returned — approved confirms it, rejected releases it for a retry, and reversed is answered too so a contradiction of an already-confirmed refund reaches the log instead of vanishing
  • Refunds issued in Paddle's own dashboard are traced back through the originating transaction, so they land on the right KahunaCart order
  • Sandbox/live toggle selecting the API root, with the provider registering only when enabled and an API key is set
  • Configurable customer-facing label, ad-hoc product name, Paddle tax category, payment link override, and signature tolerance
  • Amounts pass between KahunaCart and Paddle in minor units with no conversion, guarded by Paddle's own currency table — CLP, JPY, KRW, and VND carry no minor unit, no three-decimal currency is supported, and a currency Paddle does not settle in is refused before the request goes out
  • Unit test suite of 242 tests covering signature verification, the behavior matrix, transaction creation, webhook event mapping, refund mapping, amount and exponent handling, the contract declarations, and transport-failure degradation
  • captureMode() is declared explicitly as Automatic, and there is deliberately no setting to change it. Everything Paddle Billing does to a transaction is list it, create it, read it, preview it, update it, invoice it and revise the customer on it — no authorisation resource, no capture call, no void call — and the charge happens on Paddle's own checkout, so by the time a transaction reads completed the money has moved and the tax has been computed and remitted against it. Authorize, Capture and Void are undeclared with it, so KahunaCart never draws a Capture or Void button on a Paddle order
  • Paddle's own collection_mode: manual is explicitly not read as a capture mode, and a test asserts it is neither sent nor read. It is a different mechanism with an unfortunately similar name — bill the customer by invoice rather than collect at checkout — and it moves when the customer pays rather than when the merchant takes money already committed. Deriving CaptureMode::Manual from it would tell the base plugin to wait for an authorize() this driver has no endpoint to implement, and every checkout would stall
  • encryptedSettings() names the API key and the webhook signing secret, so KahunaCart stores both encrypted against a key that lives in the store's database rather than in any file. The key can move money out of the account and the secret would let anyone holding it sign a notification this plugin cannot tell from a real one. A value written in plaintext before this reads correctly and is rewritten encrypted on the next save, with nothing for a merchant to do. payment_link is left in the clear on purpose: it is a URL on the merchant's own approved domain, Paddle prints it into its own dashboard, every customer who checks out sees it, and a merchant has to compare it against Paddle's Checkout settings page
  • The README says plainly what this driver does not claim. Paddle keeps a customer's card for the subscriptions it bills itself, but exposes no vault to store a method against and no way to charge one off-session; its overlay checkout takes Apple Pay and Google Pay, but only once Paddle.js has opened it on the merchant's payment link after the redirect, so there is no wallet button to mount on a KahunaCart cart
PayPal 1.1.8 Ships its license 1 improved

1.1.8

Improved

  • Ships its license. A LICENSE file now comes with the plugin, and the blueprint links to the KahunaCart License at kahunacart.com/license instead of the Grav Premium license
Older versions (9)

1.1.7

New

  • A settled dispute says how it ended. PayPal's CUSTOMER.DISPUTE.RESOLVED is written on the order as Dispute won at PayPal or Dispute lost at PayPal, the order's dispute badge follows, and KahunaCart sends its Dispute won or lost message. The customer winning is lost; you winning, the customer withdrawing, or PayPal paying out of its own protection is won. Each outcome is reported once however often PayPal delivers it. The webhook now subscribes to CUSTOMER.DISPUTE.RESOLVED; press Set up webhook once on an existing store to add it (the Payments card says Needs attention until then)
  • Check connection says whether PayPal's webhook is right. After the credentials check out, the driver looks up the webhook at this store's address, changing nothing, and the Payments card warns when there is none, when it is missing events the driver handles, or when its ID is not the one in the settings, which makes every delivery fail verification. Set up webhook fixes each of these. A webhook missing only CUSTOMER.DISPUTE.CREATED is now that warning rather than a sentence in the check's summary, and the check's rows show the webhook ID on its own

Improved

  • Needs KahunaCart 1.5.18

1.1.6

New

  • A dispute at PayPal is written on the order and tells you. When a customer opens a dispute, the order's history says "Dispute opened at PayPal" with the amount, PayPal's reason and the date you must answer by, and KahunaCart sends its Dispute opened message to the channels you have ticked. Affiliates holds the partner's commission on it. The order itself is not changed, each dispute is reported once however often PayPal delivers it, and one that arrives before its payment is recorded is asked for again rather than dropped. The webhook now subscribes to CUSTOMER.DISPUTE.CREATED; on an existing store, press Set up webhook once to add it (Check connection still passes until then, and says so)

1.1.5

New

  • Tells KahunaCart what PayPal paid out. On a store that sells in more than one currency, each capture's receivable breakdown is read once on the job worker, and PayPal's exchange rate and the amount it credited to your account after its fee are recorded on the order. A capture still pending is asked about again later, and a payment held first and captured afterwards is followed from its authorization to its capture, and records nothing when the authorization was captured in several parts or the one capture is for another amount or currency than the payment, or does not say which. Needs KahunaCart 1.4.10

1.1.4

New

  • PayPal is no longer offered for a currency it cannot take. The driver now tells KahunaCart which currencies PayPal's REST API accepts, the 25 on PayPal's currency codes page, so checkout and the pay link leave PayPal out of an order in KWD, INR, ISK or any other currency off that list instead of letting the shopper pick it and fail at the end. BRL, CNY and MYR stay on the list although PayPal takes them only for in-country accounts, because the plugin cannot see where the account is and a store with a domestic account takes them today. Needs the base plugin at 1.4.0, which is where the question is asked

Improved

  • A currency PayPal refuses at pay time is named in the sentence the shopper or merchant reads, "PayPal does not take payments in BRL", where it used to say "this store's currency", which means nothing on a store that sells in several
  • The README and the Instructions tab say which currencies PayPal takes, why the three in-country ones are included, and that HUF, JPY and TWD are whole-unit currencies

1.1.3

Fixed

  • The download no longer carries PHPUnit. Every release until now committed a development vendor/: PHPUnit and about thirty packages only the test suite uses, some 1,700 files in a plugin that needs no Composer package at run time. vendor/ is now a --no-dev install holding just the autoloader, and psr/http-message is declared under replace because Grav core already supplies it. A test fails if a development install is ever committed again

1.1.2

Improved

  • Composer's installed-package record carries the plugin's current package name, kahunacart/grav-plugin-kahunacart-paypal, instead of the old trilbymedia/grav-plugin-kahunacart-paypal

1.1.1

New

  • A Checkout Label setting, like every other driver has: the customer-facing name of the payment option, empty for the default "PayPal"
  • An Instructions tab in the plugin settings, after General, that walks a merchant through setup in order: the account or sandbox, which credential to copy and from where, saving it, the webhook (button or manual steps with the endpoint URL and the events), test cards or sandbox accounts, and what to check afterwards
  • Set up webhook and Reset webhook on the PayPal card in the admin. PayPalProvider implements ProviderWebhookSetupInterface, so the button registers this store's endpoint on the REST app, subscribes it to the ten events the driver acts on, and writes the webhook ID back into the active mode's field. PayPal's equivalent of a signing secret is that ID, and unlike a Stripe signing secret it can be read back at any time, so the ID is stored whether the webhook was created or adopted — a store that lost its config gets it back by pressing the button
  • The button asks PayPal which event types the app is offered before it registers anything. PayPal refuses the whole call over one name it does not recognise, and event_types[6].name / Not a valid event name gives a merchant no way to tell which of ten names it meant, so an unavailable event is left out and named in the result instead of failing the registration
  • Pressing it twice adopts the webhook already registered for the URL rather than creating a second one that would deliver everything twice, and patches in any of the ten events it is missing. Events the merchant subscribed it to themselves are kept — the patch sends the union, not this driver's list
  • Reset webhook deletes the webhook at this exact URL and registers a fresh one. Webhooks on this store's route at a different address, which is what a site that moved off a tunnel leaves behind, are reported and never deleted: one REST app is commonly shared between a staging store and a live one
  • WEBHOOK_URL_ALREADY_EXISTS and WEBHOOK_NUMBER_LIMIT_EXCEEDED are translated into something a merchant can act on, rather than passed through as PayPal wrote them
  • Check connection on the PayPal card. PayPalProvider implements ProviderConnectionInterface: it asks PayPal for a token with the saved credentials — the same call every other request makes first — reports the app id they belong to, and names the refund or webhook-management scope when an app is missing one. There is no second "read my merchant account" call because PayPal has none for a plain REST app
  • The check reads the configured webhook ID back from PayPal, and fails on a store that has none or has one PayPal does not recognise. That is the state where checkout works, the customer's money moves and nothing settles, and a sandbox webhook ID left in the live fields is the easiest way into it. It also fails, naming them, when the webhook is not subscribed to every event the driver handles
  • Two events joined the registered set that the docs only ever suggested: PAYMENT.AUTHORIZATION.CREATED and PAYMENT.AUTHORIZATION.VOIDED are now registered whether or not the store is on manual capture, so switching capture mode later needs nothing added at PayPal

Improved

  • The webhook ID is no longer declared a secret, and the docblock that claimed an attacker could forge with it is corrected. PayPal's verification call is authenticated with the merchant's own OAuth token, so the ID alone forges nothing and gets PayPal to sign nothing — it is the equivalent of a Stripe endpoint's we_… id, which Stripe's driver prints, rather than of a signing secret, which it does not. The driver was calling it a secret and then rendering it on the connection check, in the webhook setup result and on the CLI, and of the two halves that is the one worth keeping: a merchant needs to read the ID back and compare it against their dashboard. encryptedSettings() now names the two client secrets and nothing else
  • The PayPal order carries the cart's own lines and a description, so the approval page and the buyer's own PayPal record name what was bought rather than only an amount — the record they will look at in six weeks, and the one PayPal shows in a dispute. The lines are only sent when the arithmetic is honest: PayPal validates the breakdown against the charge and refuses the whole order when it does not add up, so a cart discount goes on as discount and a cart whose lines come to less than the charge — tax or shipping added on top, and nothing says which — sends the description alone rather than labelling somebody's VAT as a handling fee
  • A refusal a shopper or a merchant reads is written for them. The refusals a real store meets — an unsupported currency, a declined funding instrument, a restricted account, a refund larger than PayPal has left — are translated into a sentence naming what to do next, and everything else falls back to PayPal's own sentence with the debug_id stripped. The debug id is kept on the connection check and the webhook buttons, which are the screens a merchant pastes into a support ticket
  • Every invalid() and ignored() carries a reason, in the contract's own vocabulary. A merchant whose orders stop settling used to open the delivery log and find a column of bare invalid with nothing to tell "our webhook ID is wrong", which is on their own settings screen, from "somebody found the endpoint and is posting junk", which needs no action. no_secret_configured earns its place on this driver in particular, because the ID is typed by hand
  • A JSON array at the top level is refused before the verification call rather than after it, so a stranger posting one cannot spend an outbound call to PayPal to find out it was ignored
  • Payment and refund events carry the payment reference under payment_ref and order_id, the names the base plugin reads, so a delivery can be joined to its order in the log. Across the September 2026 sandbox run, 228 PayPal deliveries reached a store and three of them carried an order id
  • An amount PayPal sent that this plugin will not parse is kept on the payload as amount_unparsable rather than being dropped. "PayPal named no amount" and "PayPal named one I could not read" used to be the same answer, after which the row settled at the figure it was opened with and nothing anywhere said so
  • A settled refund reports what PayPal said it refunded, and keeps PayPal's seller_payable_breakdown — gross, fee, net, and the running total refunded against the capture. Those are the figures a VAT return and a reconciliation want and they cannot be recovered later; the same refund settled by the webhook has always kept them, so a refund recorded different things depending on which leg got there first
  • The currency on a webhook payload comes back uppercased, which the contract asks for and PayPal happened to satisfy by accident
  • A capture PayPal refuses names the issue in the message rather than repeating PayPal's field vocabulary, and a refusal that repeats this store's own credentials back at us — which PayPal does not do today, and is free to start doing — is replaced rather than rendered

Fixed

  • HUF and TWD were charged a hundred times over. The exponent table was doing two jobs at once: how many decimals PayPal accepts, and how many minor units KahunaCart counts in a major unit. Those are the same number for almost every currency and they are not for these two — ISO 4217 gives both an exponent of 2, which is what the ledger uses, and PayPal transacts both in whole units. So a store meaning 9,74 Ft asked PayPal for 974 forint, PayPal held 974, and the settlement parsed it back at the same wrong exponent and agreed with itself, so every mismatch check in the base passed and the only place the real figure existed was PayPal's dashboard. Refunds repeated it exactly. There are now two tables and a conversion between them. An amount PayPal cannot represent at all — 1 000,50 Ft, which a percentage coupon on a HUF store produces — is refused with a sentence rather than silently rounded to 1 000 or 1 001
  • A refund event named the payment on capture_id, and the base plugin only reads payment_ref. The value was always right — the driver digs the capture id out of the refund's up link — so every PayPal refund event was logged refund_completed:unmatched, a refund issued in PayPal's own dashboard never reached the order, and a refund whose response was lost stranded a pending row forever. payment_ref is now the name, with capture_id beside it because it is PayPal's own word for the same id
  • A capture PayPal refused vanished. CHECKOUT.ORDER.APPROVED fires the capture call, and the result was thrown away and the delivery answered a bare ignored. An INSTRUMENT_DECLINED in the seconds between approval and capture is an ordinary PayPal outcome, and it left an order sitting in the cart table with no failed transaction, no notice, no history, no log line, and no second delivery coming — because 200 is the one answer that stops PayPal asking again. The three outcomes are now told apart: captured answers ignored:captured carrying the PayPal order id; a refusal is reported as the payment failure it is, so the pending row is marked failed and the order's timeline says what happened; and a call that never landed, or a PayPal 5xx, throws, so the base plugin answers 500 and PayPal redelivers
  • A capture PayPal refused was believed too readily, and the belief cost the sale. The fix above read any 4xx as final, and that is not what a 4xx means. Twice in the September 2026 sandbox run PayPal answered one to a capture that the very next call completed normally, on properly approved orders — and a refusal fails the purchase row, tells the shopper their payment method was turned down, and is answered 200, so PayPal never asks again and a sale that was fine is lost while the store believes it handled everything correctly. Finality is now named rather than inferred: the driver refuses only on the issues PayPal's reference documents as refusals of the payment itself — the funding instrument, the payer, this store's ability to take the money, the amount or currency, and the order's own state — and reads anything else as worth another delivery. PERMISSION_DENIED is on the retry side on purpose: it means the REST app is missing a scope, which is a mistake in the configuration rather than anything about the payment, and a merchant who fixes it still has three days of PayPal redeliveries in which the money can move
  • A refused capture left nothing behind that said what PayPal had answered. PayPal's status and issue went only into the sentence written for the shopper, which by design carries neither — so the two refusals above could not be identified afterwards, and the next occurrence would have been no more diagnosable than they were. Both outcomes that are not the money moving now put HTTP 422, INSTRUMENT_DECLINED and the debug id on the delivery-log row, which is the table a merchant already opens when an order did not settle. The shopper's sentence is unchanged and still carries no debug id
  • PayPal's raw validation text and a debug id reached a shopper. With the store currency set to one PayPal does not take, a customer read "If the currency supports decimals, only two decimal place precision is supported. (PayPal debug id: f34810357889a)" — a sentence about a JSON field, shown to somebody holding a basket, that was not even right about the cause
  • REFUND_AMOUNT_EXCEEDED and CAPTURE_FULLY_REFUNDED are classified as RefundResult::REASON_EXCEEDS_BALANCE, so the base plugin's drift line and its notice on the order both fire. Both mean PayPal holds less on the payment than this store thinks it does, which is what a store looks like after somebody refunded in PayPal's dashboard, and it was the only signal a merchant could get that the two ledgers had come apart
  • The README and the Instructions tab said a refund issued in PayPal's dashboard could not be matched to an order, and that the Webhooks Simulator does not verify. Both were tested in the September 2026 sandbox run and both were wrong: the dashboard refund was the capture_id bug above, and the simulator verifies against the real webhook ID and reaches the event handlers — its limitation is the canned payload. The "first six events are the minimum" sentence was wrong too; the minimum for a store that sells and refunds is CHECKOUT.ORDER.APPROVED, PAYMENT.CAPTURE.COMPLETED, PAYMENT.CAPTURE.REFUNDED and PAYMENT.REFUND.FAILED, two of which were outside the six. The README also claimed KahunaCart >= 1.0.0 where the blueprint says 1.1.0, and the blueprint is right
  • A refund now asks PayPal for the whole object back (Prefer: return=representation). PayPal answers a refund with an id and a status and no amount at all, and it silently clamps a refund larger than the capture has left rather than refusing it — so a $5.00 refund against $3.00 remaining came back 201 and the ledger recorded $5.00 as money the merchant never paid back. The row now says what PayPal actually returned, and the seller breakdown arrives in the same response
  • Settlements in HUF and TWD record their amount again. The read path was parsing against the wire exponent — how many decimals PayPal accepts from us, which is none for those two — but PayPal is under no such rule and echoes them back with two, so "1000.00" was refused and a settled forint order recorded no amount at all. Reading is not writing: what counts is the precision the string actually carries, and precision beyond what the store holds is accepted only when it is all zeros, so 19.99 on a yen order is still refused rather than silently rounded

1.1.0

New

  • Two-step capture. capture_mode: manual creates the PayPal order with intent: AUTHORIZE, so the customer's approval places a hold and the order stands at authorized until the merchant presses Capture or Void. PayPalProvider implements AuthorizeCaptureInterface: capture() posts to /v2/payments/authorizations/{id}/capture with final_capture so a partial capture releases the rest rather than leaving it held for another three days, void() releases the authorization, and completeAuthorize() reads the order PayPal names on the return URL and reports what its authorization actually says
  • An approval on a manual store calls /v2/checkout/orders/{id}/authorize rather than /capture, because an order created to be authorized cannot be captured directly and PayPal refuses the attempt. The call is idempotent the same two ways the capture already was — PayPal-Request-Id collapses concurrent retries, and a redelivery that arrives after the first one comes back ORDER_ALREADY_AUTHORIZED, which means the hold exists
  • PAYMENT.AUTHORIZATION.CREATED and PAYMENT.AUTHORIZATION.VOIDED are translated, so a hold and its release settle the ledger whether they happened here, in PayPal's own dashboard, or by expiry. The authorization carries what PayPal actually held, which is the figure the merchant captures against
  • A capture PayPal answers PENDING is reported as a failure rather than a sale, because a capture under review has moved no money and the completion event settles it when it does
  • A reference that names the order rather than the authorization is resolved rather than refused, which covers a merchant who captures before the authorization webhook has landed
  • encryptedSettings() names both client secrets and both webhook ids, so KahunaCart stores them encrypted against a key that lives in the store's database and not in any file. The client ids are deliberately excluded: they are not secrets, PayPal prints them into its own SDK tag, and a merchant needs to read one back to check it against their dashboard
  • The README says plainly what this driver does not claim. PayPal has Vault v3 and it has Smart Payment Buttons, and neither Tokenization nor ExpressPay is declared because neither is implemented — KahunaCart draws its buttons from these flags, so a flag claiming something that is not there would put a control in the admin that could never work

1.0.0

New

  • Initial release: first-party PayPal provider for KahunaCart
  • Hosted redirect flow on Orders v2 — initiatePayment() creates a PayPal order and redirects to the approval page, with the transaction hash on custom_id and the order hash on the purchase unit's reference_id
  • Capture runs from the webhook: CHECKOUT.ORDER.APPROVED triggers POST /v2/checkout/orders/{id}/capture, and the resulting PAYMENT.CAPTURE.COMPLETED event completes the order — no return-URL handler required
  • Webhook verification through PayPal's verify-webhook-signature endpoint, with missing transmission headers, an unconfigured webhook ID, and cert URLs off PayPal's own domains rejected before any call goes out
  • Event mapping for CHECKOUT.ORDER.APPROVED, PAYMENT.CAPTURE.COMPLETED, PAYMENT.CAPTURE.DENIED, PAYMENT.CAPTURE.DECLINED, PAYMENT.CAPTURE.REVERSED, CHECKOUT.ORDER.DECLINED, PAYMENT.CAPTURE.REFUNDED, PAYMENT.REFUND.FAILED, and PAYMENT.REFUND.PENDING
  • Full and partial refunds through /v2/payments/captures/{id}/refund
  • Async refund lifecycle: a refund PayPal answers PENDING is reported as pending and settled later by PAYMENT.CAPTURE.REFUNDED or PAYMENT.REFUND.FAILED, keyed on the same refund id the refund call returned; CANCELLED and FAILED on submission are refused rather than counted as refunded
  • PAYMENT.CAPTURE.REVERSED stays a payment failure rather than a refund failure — PayPal reversing a capture is money pulled back over the store's head, not the failure of a refund the store submitted
  • OAuth2 client-credentials tokens, cached per request lifecycle and never persisted
  • Idempotency keys on both order creation and capture, so a redelivered webhook cannot double-charge
  • Sandbox/live mode toggle with separate credential sets; the provider registers only when the active mode's client id and secret are both set
  • Exact integer and string conversion between minor units and PayPal's decimal amounts at each currency's own precision — zero-decimal (JPY, KRW…), two-decimal, and three-decimal (KWD, BHD…) currencies all convert correctly, with HUF and TWD following PayPal's whole-unit handling rather than ISO 4217, and unrecognised formats rejected rather than rounded
  • Unit test suite covering webhook verification, event mapping, capture-on-approval, order and refund body building, money conversion, and transport failure
Polar 1.1.15 Ships its license 1 improved

1.1.15

Improved

  • Ships its license. A LICENSE file now comes with the plugin, and the blueprint links to the KahunaCart License at kahunacart.com/license instead of the Grav Premium license
Older versions (9)

1.1.14

New

  • Check connection says whether Polar's webhook endpoint is right. After the key checks out, the driver looks up this store's endpoint at Polar, changing nothing, and the Payments card warns when there is none at this store's address, when Polar has switched it off, when it is missing events the driver handles, or when it is on an API version other than 2026-04. Set up webhook fixes each of these. A token without webhooks:read is reported, not warned about

Improved

  • Needs KahunaCart 1.5.18

1.1.13

New

  • The buyer's VAT number reaches KahunaCart. The driver reports the VAT number a buyer gives on Polar's checkout, and the reverse charge when Polar charged no VAT on a taxable order, so KahunaCart 1.5.8 prints both on the receipt, invoice and order emails. Only a VAT-type number counts as a reverse charge (kahunacart-issues#3)
  • Fill in older orders. bin/plugin kahunacart-polar tax-ids:backfill asks Polar for the VAT number on orders paid before this release and adds it. --dry-run shows what it would do, --order=1036 does one order, and running it twice changes nothing

Improved

  • Needs KahunaCart 1.5.8

1.1.12

Fixed

  • The Polar API version message now has a row you can tick channels on. The message sent when Polar stopped serving the pinned version was recorded under a name that was never registered, so only a catch-all channel in the config file ever received it. On KahunaCart 1.5.3 or later the existing alert is the message: Polar API version problem appears under Notifications → Store owner, goes out when the problem starts and again as "Cleared" when it is fixed, and also covers Polar not answering and a webhook endpoint that could not be moved. It can arrive up to six hours after the fallback first happens. Warn me when the API version pin stops working still switches it off. An older KahunaCart behaves as before

1.1.11

Fixed

  • Works with a shop mounted at the site root. With KahunaCart's route: '/' (core 1.5.0) the webhook endpoints Polar is checked against are https://example.com/webhook/polar rather than https://example.com//webhook/polar

1.1.10

New

  • Document that Included subscription fee choices keep Polar available and Added fee agreements are refused
  • A store that sells in more than one currency can take payments through Polar in its verified presentment currencies. Which currencies Polar takes depends on the Polar organization; the plugin offers the presentment currencies verified in Polar's source. A checkout Polar rejects over the currency now says "Polar does not take payments in EUR for this store" and suggests another way to pay, instead of repeating Polar's validation message. The README and the settings screen's instructions tell the merchant to switch each currency on in the Polar organization. A paid order or a refund Polar reports in another currency than the order's reaches KahunaCart with that currency, and a payment or refund that names none reports none. Needs KahunaCart 1.4.0.

Improved

  • A subscription plan's product name in Polar shows its price at the currency's own decimal places, so a yen plan reads "JPY 1,999 / month" and not "JPY 19.99 / month". Dollars, euros and pounds read as before.

Fixed

  • Subscription payment methods with a fee are refused. Polar cannot charge an independent first invoice adjustment, so KahunaCart no longer opens a subscription that could collect less than its order. Use a fee-free method for Polar subscriptions; one-time fees still work.
  • ISK, UGX and MGA payments and refunds use Polar’s units. Checkout and subscription prices, webhook payments, refunds and catalogue sync now convert both ways, including tax amounts. Refund ceilings compare amounts at the same scale, and a refund in a different currency from its Polar order is refused. Fractional MGA amounts and currencies without a verified Polar payment scale are refused
  • Catalog sync no longer labels every price as US dollars. A push without a currency used to send "usd" whatever the store sold in, so a euro store's prices reached Polar as dollars. The push now sends the store's base currency, which KahunaCart 1.4.0 gives it with every product, and refuses a product that arrives with none.
  • Catalog sync no longer imports a Polar price in another currency as the same number of base-currency units. A pull used to read each price's amount and ignore its currency, so a euro price on a dollar store came in as dollars. Each pulled price now says which currency Polar has it in, and KahunaCart skips one that is not in the store's base currency.

1.1.9

New

  • Other plugins hear about deliveries for orders this store never made. Polar sends every order event on an organization to every endpoint on it, so a store sharing its organization hears about orders another app sold. Those were logged as unmatched and dropped. A verified order.*, refund.* or subscription.* delivery that carries none of this store's references now also fires the Grav event onKahunaCartPolarWebhookUnmatched, with the event type, the decoded payload, the Polar order id and the delivery id, so a plugin that imported the old app's licences can revoke one when its order is refunded. The answer to Polar stays 200, and a listener that fails is logged rather than retried

Improved

  • Polar's checkout names the store, not the plugin. The private product every cart is charged against is the title a buyer reads above the amount on Polar's page, and it was created as "KahunaCart Order" on every store. A new one is now named after the store ("Grav Premium Order"; "KahunaCart Order" only for a store with no name). One that already exists is found by its metadata as before, so it keeps whatever name it has in Polar and can be renamed there, and the connection check now reports that name instead of the plugin's default

Fixed

  • The driver only touches its own webhook endpoint. An endpoint counted as this store's when its URL merely ended in /webhook/polar, so on a Polar organization shared with another store or another app, the daily API version job would move that app's endpoint to this plugin's version and Reset would delete it. An endpoint is now this store's only when its URL is exactly one of the store's own webhook URLs, one for system.custom_base_url and one for each Site Addresses entry, with scheme and host compared without regard to case and a trailing slash ignored. Set up webhook, Reset, the daily job and the dashboard alert all use that one test. Other endpoints on the organization are left alone, not reported as stale, and counted under Other endpoints in the Set up webhook result. A store that names no address of its own has the daily job move nothing, with a log line saying what to set

1.1.8

Fixed

  • The download no longer carries PHPUnit. Every release until now committed a development vendor/: PHPUnit and about thirty packages only the test suite uses, some 1,700 files in a plugin that needs no Composer package at run time. vendor/ is now a --no-dev install holding just the autoloader, and psr/http-message is declared under replace because Grav core already supplies it. A test fails if a development install is ever committed again

1.1.7

Improved

  • Composer's installed-package record carries the plugin's current package name, kahunacart/grav-plugin-kahunacart-polar, instead of the old trilbymedia/grav-plugin-kahunacart-polar

1.1.6

New

  • The webhook endpoint follows the plugin's API version on its own. A daily job moves every endpoint on this store's Polar webhook route onto the version the plugin reads, so an upgrade carries the endpoint with it without anybody pressing Set up webhook, and an endpoint left on a version Polar has since removed is rescued even after Polar has stopped delivering to it. It rides the KahunaCart worker, keeps exactly one run queued a day ahead, and logs what it moved. It also notes an endpoint still signed with Polar's original scheme, and Set up webhook shows the scheme beside the version; both schemes verify, and a secret generated fresh at Polar moves an endpoint to Standard Webhooks. A run that fails, over a token without the webhooks scope or a Polar outage, logs once and tries again tomorrow
  • You are told when the pin stops working. A line at the top of the store admin says Polar no longer serves the pinned version and the store is selling on Polar's current one, or that Polar is not answering at all, or that an endpoint could not be moved; it rechecks every six hours and names what to do. The first time the fallback fires, one message goes to the store's notification channels, once per version. Both are behind a new Warn me when the API version pin stops working setting on the Polar screen, on by default; off, the Grav log still records it

Improved

  • A pin Polar has stopped serving no longer stops the store. Polar removes an API version nine months after it opens, and from that day every pinned call answers a bare 404. The driver now reads the first one as the pin having expired: it logs that, retries the call with no version, which Polar answers in whichever version is current, and floats that way for the rest of the request. That is the behaviour the driver had before it pinned anything, so a store on an old plugin keeps selling while the log says to update it. A bare 404 with no version on it is still thrown, and the message names both things it can be, a wrong path or a removed version, rather than asserting one
Square 1.1.9 Ships its license 1 improved

1.1.9

Improved

  • Ships its license. A LICENSE file now comes with the plugin, and the blueprint links to the KahunaCart License at kahunacart.com/license instead of the Grav Premium license
Older versions (9)

1.1.8

New

  • Check connection says whether Square's webhook subscription is right. After the token checks out, the driver looks up the subscription at this store's address, changing nothing, and the Payments card warns when there is none, when Square has switched it off, when it is missing events the driver handles, when no signature key is stored, or when the Notification URL setting differs from the address the subscription calls. Set up webhook fixes each of these

Improved

  • Needs KahunaCart 1.5.18

1.1.7

New

  • Card readers with Square Terminal. With KahunaCart 1.5.7 and the Point of Sale add-on, a counter sale can be paid on a Square Terminal. Pairing works the way Square does it: the till shows a code, you type it into the Terminal, and the Terminal appears as a reader. A payment goes to the Terminal under the store's own idempotency key and settles from Square's terminal.checkout.updated webhook or the till's status check, whichever comes first, exactly once, and refunds go to the Square payment as usual. A cancel from the customer or the till, and a timeout, are told apart. A timeout is only treated as final once the Terminal has moved on to a later checkout, because Square documents that a timed-out checkout can still complete; until then the till shows it as checking and staff can give up on it. A payment sent again after a lost answer finds the checkout the first attempt made, even days later. In the sandbox, Square's test devices are listed as readers ready to use. Press Set up webhook once on an existing store to add the Terminal event. Interac, tipping on the Terminal and delayed capture are not supported yet. Needs KahunaCart 1.5.7

1.1.6

New

  • Square sets up its own webhook. A Set up webhook button on the Square card registers this store's webhook subscription with Square, subscribed to the four events the plugin handles, and saves the signature key for you. A subscription already at this store's address, including one added by hand in the Developer Dashboard, is adopted rather than duplicated: switched back on if it was disabled, given any missing events while keeping the ones it had, and its key read back, so pressing the button twice is always safe. Reset webhook deletes this store's subscription and registers a fresh one with a new key. Subscriptions left at an old address by a tunnel or a staging copy are reported and never touched. A token Square will not let manage subscriptions gets a sentence pointing at the manual steps

Improved

  • Out of testing. Checkout, webhooks, partial and full refunds, and a refund issued in the Square Dashboard were all run end to end against Square's sandbox, so this is the first stable release
  • With Notification URL blank, signatures are checked against the same address Set up webhook registers, built from the site's base URL, instead of the address the request appears to arrive on, which a proxy or tunnel can change
  • Check connection notes when no webhook signature key is stored yet
  • The Instructions tab and README describe the button first and the manual subscription as the fallback, and the sandbox steps describe Square's testing panel (Next, then Test Payment) instead of a test card

1.1.5

New

  • Square is offered only for the currency its location takes. A Square location has one currency and Square charges in it, and which currency that is belongs to your account, so the plugin carries no list of its own. A new Location Currency setting on the API Credentials tab takes it, and from KahunaCart 1.4.0 checkout and the pay link leave Square out of an order in any other currency instead of sending the shopper to a Square page that fails. Leave it blank and Square is offered for every currency, as before. Needs the base plugin at 1.4.0, which is where the question is asked
  • Square has a Check connection button. It reads the location with one GET /v2/locations/{id}, shows its name, country and currency, tells you what to enter as Location Currency, and fails when the currency you entered disagrees with Square's or the location is not active. A token that is not allowed to read locations still passes, with a note, because it can take payments all the same

Improved

  • A refund the driver submits is reported with the currency Square says it is in, so core can compare it with the order's
  • The README and the Instructions tab explain Location Currency and the check
  • An order hand-posted to Square in another currency than the location's is refused with a sentence before any request goes out

1.1.4

Fixed

  • The download no longer carries PHPUnit. Every release until now committed a development vendor/: PHPUnit and about thirty packages only the test suite uses, some 1,700 files in a plugin that needs no Composer package at run time. vendor/ is now a --no-dev install holding just the autoloader, and psr/http-message is declared under replace because Grav core already supplies it. A test fails if a development install is ever committed again

1.1.3

Improved

  • Composer's installed-package record carries the plugin's current package name, kahunacart/grav-plugin-kahunacart-square, instead of the old trilbymedia/grav-plugin-kahunacart-square

1.1.2

Improved

  • The same buyer is one Square customer now, instead of a new profile on every order. A quick_pay payment link has nowhere to name a customer, so Square opened a fresh profile for every payment and a shopper who bought three times arrived in the seller's Customer Directory three times with their history split across all three. KahunaCart 1.2.3 keeps Square's own id per customer and hands it back on PaymentContext::$providerCustomerRef, so when the store holds one the link is built on the order branch of CreatePaymentLink carrying customer_id. That is the only branch Square allows it on, and Square says so in its own words: "At this time adding a known customer to a quick pay link isn't currently available. You'll need to add the customer in an order." A store that has never been told an id stays on quick_pay exactly as before, one line item for the exact total, and pre_populated_data.buyer_email is sent either way because it prefills the form rather than identifying anybody
  • The id is reported back wherever Square names it: payment.customer_id on the completed payment, which is the first and only place a quick_pay checkout ever names one; the order Square echoes alongside a link created on the order branch; and the payment a completed refund reads back for its hashes, which was being fetched anyway
  • A customer id Square no longer knows falls back instead of stranding the shopper. A stored id stops being real when a merchant moves a store from the sandbox to production or connects a different Square account, and Square answers NOT_FOUND. The link is created once more without the id, under an idempotency key of its own because the retry is a different request, and the new profile's id replaces the old one from the payment webhook. One line goes into the Grav log. Once, deliberately: a second refusal is Square saying something else and is returned as the failure it is, and a NOT_FOUND that names anything other than the customer, a wrong location id say, is never retried at all
  • The base plugin dependency requires KahunaCart 1.2.3, where PaymentContext::$providerCustomerRef and WebhookResult's providerCustomerRef were added

1.1.1

New

  • An Instructions tab in the plugin settings, after General, that walks a merchant through setup in order: the account or sandbox, which credential to copy and from where, saving it, the webhook (button or manual steps with the endpoint URL and the events), test cards or sandbox accounts, and what to check afterwards

Fixed

  • A refund event named the payment on payment_id, and the base plugin only reads payment_ref. The value was always right — a completed refund spends a GET /v2/payments/{payment_id} precisely to recover it — so this cost nothing on a refund the store submitted itself, and everything on the route that lookup exists for: a refund issued in the Square Dashboard arrives with no pending row to confirm, and the base plugin can only open one from the payment it belongs to, which it was never handed. payment_ref is now the name. payment_id stays beside it, because it is Square's own word for the same id — on the refund object and in the refund request — and a merchant reconciling by hand reads the ledger row rather than the provider contract

1.1.0

New

  • encryptedSettings() names the access token and the webhook signature key, so KahunaCart stores both encrypted against a key that lives in the store's database and not in any file. The access token is the whole Square application — payments, catalog, customers, refunds — and the signature key is what lets a forged webhook mark an order paid. The location ID and the notification URL are deliberately excluded: neither is a secret, and a merchant has to be able to read both back to check them against the Developer Dashboard, which is the first thing to do when a signature will not verify
  • captureMode() is declared explicitly as automatic. It is what the base plugin would assume anyway, and saying it means the next person reading this driver does not have to work out whether the silence was a decision

Improved

  • The README says why two-step capture is not here, with Square's own words rather than a hand-wave. Square has delayed capture — CreatePayment takes autocomplete: false and CompletePayment and CancelPayment finish it — but it lives on the Payments API and needs a card token minted in the buyer's browser by the Web Payments SDK. Payment Links cannot use it: "At this time delaying the capture of the payment isn't currently possible with the payment links today." So Authorize, Capture and Void are not declared, because KahunaCart draws the Capture and Void buttons from those flags and a button here could never work
  • Same for Tokenization and ExpressPay. Square can store a card on file and it has Apple Pay and Google Pay, and all three want the same on-site flow, so they arrive together or not at all
  • The base plugin dependency requires KahunaCart 1.1.0 rather than 1.0.0, because captureMode() and encryptedSettings() do not exist in 1.0.x
Stripe 1.2.12 Ships its license 1 improved

1.2.12

Improved

  • Ships its license. A LICENSE file now comes with the plugin, and the blueprint links to the KahunaCart License at kahunacart.com/license instead of the Grav Premium license
Older versions (9)

1.2.11

New

  • A settled dispute says how it ended. Stripe's charge.dispute.closed is written on the order as Dispute won at Stripe or Dispute lost at Stripe, the order's dispute badge follows, and KahunaCart sends its Dispute won or lost message. An inquiry that closed without becoming a chargeback counts as won. Each outcome is reported once however often Stripe delivers it. The endpoint now subscribes to charge.dispute.closed; press Set up webhook once on an existing store to add it (the Payments card says Needs attention until then)
  • Check connection says whether Stripe's webhook endpoint is right. After the key checks out, the driver looks up this store's endpoint at Stripe, changing nothing, and the Payments card warns when there is none at this store's address, when Stripe has disabled it, or when it is missing events the driver handles, such as charge.dispute.created on an endpoint set up before disputes were reported. Set up webhook fixes each of these. Old endpoints at another address on this store's route are listed

Improved

  • Needs KahunaCart 1.5.18

1.2.10

New

  • Card readers with Stripe Terminal. With KahunaCart 1.5.7 and the Point of Sale add-on, a counter sale can be paid on a Stripe reader (WisePOS E or S700). Pair a reader with the registration code it shows; the first pairing creates a Stripe Terminal location from your store address. A payment goes to the reader and settles from Stripe's webhook or from the till's status check, whichever comes first, exactly once. A decline, a cancel on the reader and a timeout are told apart, Cancel at the till clears the reader, and a refund goes back through the ordinary refund route. The driver only reports what Stripe has confirmed: a command that times out is checked against the reader and the payment before anything is retried, so a card is never charged twice. Test mode works with Stripe's simulated readers. Press Set up webhook once on an existing store to add the two reader events. Interac in Canada is not supported yet. Needs KahunaCart 1.5.7

1.2.9

New

  • A dispute at Stripe is written on the order and tells you. When a customer disputes a charge, the order's history says "Dispute opened at Stripe" with the amount, Stripe's reason and the date your evidence is due, and KahunaCart sends its Dispute opened message to the channels you have ticked. Affiliates holds the partner's commission on it. The order itself is not changed. Each dispute is reported once however often Stripe delivers it, a dispute that arrives before its payment is recorded is asked for again rather than dropped, and a dispute on a charge that is not one of this store's orders is left alone. The endpoint now subscribes to charge.dispute.created; press Set up webhook once on an existing store to add it

1.2.8

New

  • Tells KahunaCart what Stripe paid out. On a store that sells in more than one currency, each payment's balance transaction is read once on the job worker, and Stripe's exchange rate and the net that reached your balance after Stripe's fee are recorded on the order ("Paid out as $100.90 at 1.1202" under a €94.00 charge). Works for one-off payments, captures, a subscription's first period and its renewals (an invoice paid in several payments records nothing, rather than one payment's payout as the whole invoice's), with the key of the account the payment was taken on. Needs KahunaCart 1.4.10

Fixed

  • A held payment captured in Stripe before the customer came back is recorded at what Stripe took. When part of a held payment had been captured in Stripe's own dashboard by the time the customer returned to the store, the order was marked paid at the full amount that was asked. The driver now reports the intent's amount_received (also on a capture, a saved-card charge and an agent payment), and KahunaCart 1.4.10 no longer marks such an order paid in full on the return: it leaves it for Stripe's webhook, which settles it at what was taken

1.2.7

New

  • A Stripe account for each payment method. On a KahunaCart store with more than one selling company, each payment method can carry its own Stripe account: account ID, keys, signing secret, Managed Payments settings, tax code and API version. Hosted checkout and every later action on the order (capture, void, refund, webhook) use the account the order was paid through. Needs KahunaCart 1.4.2
  • A webhook per account. Each account gets its own endpoint and signing secret from its Set up webhook button. The default account's endpoint and requests are unchanged
  • Customer references are kept apart per account, and an account stays on file for its old orders after its method is edited or deleted
  • A fresh installation can set up its first payment method account without default credentials. A default account that isn't configured stays unavailable at checkout

1.2.6

Fixed

  • Discounted Managed Payments invoices record and refund the amount paid. The first-invoice coupon is subtracted from Stripe's subtotal before calculating the pre-tax ledger amount, tax breakdown and partial refund ratio
  • Subscription payments include the payment method fee. Recurring prices use the renewal order total, including the fee and its tax; any difference on the first order is charged or discounted on its initial invoice only. Managed Payments receives the net price including the fee and lets Stripe add tax. Methods without fees keep their existing requests
  • Orders in Icelandic krónur and Ugandan shillings are charged the right amount. Stripe's API counts ISK and UGX in hundredths, the decimal part always 00, although they have no minor unit: 5 ISK is an amount of 500. The driver sent the store's whole-unit figure as it was, so a 5,000 ISK order was a checkout for 50 ISK. Amounts now go to Stripe at its own scale for these two currencies in checkouts, subscription prices, captures, saved-card charges and refunds, and come back to the store's scale in settlements, holds, captures, refunds and subscription invoices, so a payment is recorded for the amount the order was for. Other currencies keep their existing conversion except MGA, corrected below
  • Malagasy ariary orders are charged and recorded at the right scale. Stripe counts MGA in whole ariary while KahunaCart stores hundredths. Checkouts, subscription prices, captures, saved-card charges and refunds now convert both ways; fractional ariary amounts are refused rather than rounded
  • Authorization webhooks keep the currency Stripe reports. A hold reported in a different currency from its order now reaches KahunaCart with that currency, so the ledger keeps the requested order amount and the mismatch is reported

Improved

  • The README has a Currencies section and the Instructions tab a step on selling in more than one currency: the plugin keeps no list of its own, because which currencies a Stripe account can take depends on its country and payment method, so a store that takes a currency today does not lose it after an update

1.2.5

Fixed

  • The download no longer carries PHPUnit. Every release until now committed a development vendor/: PHPUnit and about thirty packages only the test suite uses, some 1,700 files in a plugin that needs no Composer package at run time. vendor/ is now a --no-dev install holding just the autoloader, and psr/http-message is declared under replace because Grav core already supplies it. A test fails if a development install is ever committed again

1.2.4

New

  • Included subscription fee choices keep fee-free request bodies, while Added keeps the agreed fee and tax
  • Agent payments: Google Pay through UCP. StripeProvider implements KahunaCart's new AgentPaymentInterface, so an agent platform such as Google AI Mode or Gemini can complete a checkout with a card the buyer picked in Google Pay, without a redirect. The driver declares agent_payments when a publishable key is set and Managed Payments is off, advertises com.google.pay with Stripe's Google Pay gateway parameters (stripe:version 2018-10-31), turns the Stripe token Google mints into a PaymentMethod with card[token], and confirms a PaymentIntent with error_on_requires_action, so a card that wants a 3-D Secure challenge fails with a sentence the agent can relay rather than waiting for a browser. Manual capture holds the money as a two-step checkout does, both calls carry an idempotency key made from the order and the platform's own key so a retried completion never charges twice, and the intent carries the usual order and transaction metadata plus kahunacart_source: agent. The token goes to Stripe in one call and never into a log, a ledger row or a result, and the payment_intent.succeeded delivery that follows settles nothing a second time. Needs the base plugin at 1.3.0
  • The test suite can load KahunaCart core from another checkout: set KAHUNACART_TEST_CORE_ROOT to its folder
  • 3-D Secure on a Google Pay agent charge becomes a handoff to the buyer. The PaymentIntent is confirmed with return_url (the store's payment return page) instead of error_on_requires_action, and a requires_action intent answers KahunaCart with Stripe's hosted authentication page and the intent id, which KahunaCart 1.2.15 turns into UCP's requires_escalation with the page behind the checkout's continue_url. A use_stripe_sdk action is followed only to a page on Stripe's own host; anything else still fails with a sentence the agent can repeat.
  • An agent's payment_intent.succeeded is reported by its transaction hash. It settles a challenged payment and a charge whose answer the store never recorded; after an ordinary charge core's ledger gate makes it a no-op. An agent's payment_intent.payment_failed no longer looks for a Checkout Session that was never there.

Improved

  • Composer's installed-package record carries the plugin's current package name, kahunacart/grav-plugin-kahunacart-stripe, instead of the old trilbymedia/grav-plugin-commerce-stripe

1.2.3

Fixed

  • The wallet button no longer disappears on a dark storefront. It painted itself #111111 on every theme but light and outline, so on the dark side of a theme like Kahuna the Apple Pay / Google Pay / Link button was a near-black rectangle on a near-black page, and outline was no better — a black edge and a black label with nothing behind them. The base plugin's express.theme has grown an auto value and made it the default, and this button now draws it the way the provider contract asks: the fill, the label and the edge come from --kahunacart-express-auto-bg, --kahunacart-express-auto-fg and --kahunacart-express-auto-border on the region wrapper, which the storefront stylesheet resolves from the page's own ink and ground. Dark button with a light label on a light page, light button with a dark label on a dark one, and it flips with the page rather than with a setting. outline follows the page as well now — the fill stays transparent and the border and the text take the page's own foreground, because picking an outlined button is a choice about the button's edge and never a choice to have it vanish. dark and light are unchanged and still absolute, so a merchant who picked one gets exactly that. Needs the base plugin at 1.2.10, which is where auto and the wrapper's colours come from
SumUp 1.1.7 Ships its license 1 improved

1.1.7

Improved

  • Ships its license. A LICENSE file now comes with the plugin, and the blueprint links to the KahunaCart License at kahunacart.com/license instead of the Grav Premium license
Older versions (8)

1.1.6

New

  • Card readers with SumUp Solo. With KahunaCart 1.5.7 and the Point of Sale add-on, a counter sale can be paid on a SumUp Solo. Pair a Solo with the code it shows, and send the payment to it from the till. A payment settles from SumUp's callback or the till's status check, whichever comes first, exactly once, and refunds go to the SumUp transaction as usual. A decline, a cancel from the till or the Solo, and a payment the reader never took are told apart. SumUp has no idempotency key for reader payments, so a payment is sent to the Solo once; if its answer is lost, the till finds the payment in SumUp's history rather than sending it again. When SumUp can't confirm how a payment ended, the till shows it as checking and staff can check the SumUp app and give up on it. Live SumUp accounts need an Affiliate Key for Solo payments (Settings, then Instructions step 8). Tips, instalments and SumUp Go terminals aren't supported yet. Needs KahunaCart 1.5.7

Fixed

  • Removed a curl_close() call that PHP 8.5 reports as deprecated

1.1.5

New

  • Out of testing. Tested end to end against a SumUp sandbox merchant: Hosted Checkout payments with the success and 3D Secure test cards, the webhook and the return to the store, partial and full refunds, and forged and replayed webhooks
  • Check connection on the Payments card reads the merchant the API key belongs to and shows its name, merchant code, country, currency and whether it is a sandbox, and says so when the Merchant Code field names a different merchant
  • The Merchant Code can be left empty. The plugin uses the merchant the API key belongs to, so a key is all SumUp needs

Fixed

  • Refunds work. They were sent to SumUp's /v1.0/merchants/…/payments/…/refunds, which refuses a decimal amount with "Invalid amount". They now go to /v0.1/me/refund/{id}, the endpoint SumUp documents
  • The API key is encrypted from the first save. It used to be saved in plain text until the merchant code was filled in as well, because KahunaCart only learned which settings to encrypt from a provider that was already set up

1.1.4

New

  • SumUp is no longer offered for an order in a currency it cannot take. The plugin now tells KahunaCart which sixteen currencies SumUp's Checkouts API accepts, so on a store that sells in more than one currency checkout and the pay link leave SumUp out of an order in yen or dinars instead of letting the shopper pick it and fail. The list is the one on SumUp's Create a checkout page, checked on 10/01/2026, and it does not depend on the account. The refusal when a payment is started stays as a second line of defence. A store that sells in one currency sees no change. Requires KahunaCart 1.4.0

Improved

  • A refund reports the currency of the SumUp transaction it was made against, so KahunaCart can tell a refund in another currency from the order's own

1.1.3

Fixed

  • The download no longer carries PHPUnit. Every release until now committed a development vendor/: PHPUnit and about thirty packages only the test suite uses, some 1,700 files in a plugin that needs no Composer package at run time. vendor/ is now a --no-dev install holding just the autoloader, and psr/http-message is declared under replace because Grav core already supplies it. A test fails if a development install is ever committed again

1.1.2

Improved

  • Composer's installed-package record carries the plugin's current package name, kahunacart/grav-plugin-kahunacart-sumup, instead of the old trilbymedia/grav-plugin-kahunacart-sumup

1.1.1

New

  • An Instructions tab in the plugin settings, after General, that walks a merchant through setup in order: the account, which credential to copy and from where, saving it, why there is no webhook to register, test cards or a sandbox merchant, and what to check afterwards

1.1.0

New

  • encryptedSettings() names the API key, so KahunaCart stores it encrypted against a key that lives in the store's database and not in any file — a config file copied to staging no longer carries a bearer token for the whole SumUp account. Nothing about how a merchant enters it changes, and a key already saved in plaintext is rewritten encrypted on the next save. The merchant code is deliberately excluded: it is an identifier rather than a credential, it travels in the URL of most of these calls, and it is the field a merchant checks against their dashboard when one is refused

Improved

  • captureMode() is declared explicitly as automatic with the reason written out, rather than inherited silently. SumUp's Checkouts API cannot hold money without taking it: the create-checkout body carries no intent, a checkout's status enum is PENDING, FAILED, PAID and EXPIRED with no member for a hold, and there is no capture or void endpoint to move or release one with. purpose, the only field that changes what a payment means, chooses between a payment and collecting a card for later. A provider that cannot authorize declares automatic and says why — it is never special-cased in the base plugin — and Authorize, Capture and Void stay undeclared so KahunaCart draws no button with nothing behind it
  • The README says plainly what this driver does not claim and why, for both two-step capture and saved cards. SumUp does tokenize cards, but its own guide documents that flow for the Payment Widget rather than for the Hosted Checkout this plugin redirects to, and a Tokenization flag with no way to store a method would give a customer a saved-methods page that never fills up

0.1.0

New

  • Initial release: first-party SumUp provider for KahunaCart
  • Hosted checkout on the Checkouts API — initiatePayment() creates a checkout with hosted_checkout.enabled and redirects to the hosted_checkout_url SumUp answers with, so card details never touch the Grav server
  • Correct handling of SumUp's two URL fields, which is the detail that silently breaks integrations: return_url is SumUp's server-to-server callback and carries the webhook URL, while redirect_url is the browser redirect and carries the return URL
  • Verify-by-fetch webhooks, which is SumUp's documented model rather than a workaround: SumUp signs nothing and its entire webhook body is {"event_type": "CHECKOUT_STATUS_CHANGED", "id": "…"}, so the body is mined for one id, the id's format is checked before it reaches a URL, and every fact acted on comes back from an authenticated GET /v0.1/checkouts/{id}. Nothing in the request body is ever trusted — SumUp's own documentation is explicit that an application "must always verify if the event really took place, by calling a relevant SumUp's API"
  • Deliberate webhook failure paths — a body with no usable id is a 400, an event type SumUp adds later is answered 200 before any API call is spent on it, a well-formed id this merchant has never issued is answered 200 so the status code cannot be used to probe which ids exist, and a rejected key or unreachable API is a 400 that leaves SumUp to redeliver
  • A checkout belonging to a different merchant code is ignored rather than acted on, so a key covering more than one merchant cannot land another merchant's payment on this store's ledger
  • Status mapping across the whole of SumUp's checkout enum: PAID completes the order, FAILED and EXPIRED fail it, and PENDING is ignored
  • The ledger reference becomes the transaction id once a checkout is paid, because the transaction id is the only identifier SumUp's refund endpoint accepts — a completed payment that reported the checkout id would be unrefundable
  • Both hashes ride in checkout_reference, which is the only merchant-controlled field on a SumUp checkout that survives to the webhook; a reference this plugin did not write reads as "no hashes" rather than as two wrong ones
  • That same reference is the idempotency key SumUp does not otherwise offer: SumUp has no Idempotency-Key header anywhere in its API but refuses a duplicate checkout_reference with 409 DUPLICATED_CHECKOUT, so a double-submitted checkout recovers the existing checkout by listing the reference back instead of billing a second one against one order
  • Full and partial refunds through POST /v1.0/merchants/{code}/payments/{txn}/refunds, with the refund read back out of the transaction's REFUND events — SumUp answers an accepted refund 201 with a body of {}, so there is no refund id in the response to key a ledger row on, and the event list is the only place one exists
  • Refund events are matched by amount rather than by being newest, so a concurrent partial refund against the same transaction cannot be mistaken for this one
  • Honest refund semantics: a refund SumUp reports as REFUNDED or SUCCESSFUL is confirmed, a FAILED one is failed, and one still PENDING — or one that could not be read back at all — is reported pending with the ledger row held rather than recorded as money that has moved
  • SumUp emits no refund webhook and this plugin does not pretend otherwise. CHECKOUT_STATUS_CHANGED is its only event and a refund does not change a checkout's status, so refund() reading the transaction back at submission time is the entire settlement mechanism; the README says plainly that a refund left pending is the merchant's to check
  • Exact conversion between minor units and SumUp's decimal major-unit amounts with no float ever constructed on the way out — the amount is built as an exact decimal string with integer arithmetic and unquoted into the JSON so it reaches SumUp as a number, and the noise json_decode introduces on the way back is rounded off at the currency's own precision
  • Currency validation against SumUp's own sixteen-member enum, refused locally with a message naming the currency rather than sent and rejected as an unreadable API error, and CLP handled as the zero-decimal currency ISO 4217 says it is
  • All three of SumUp's error shapes are read — the Checkouts API's error_code/message/param, the differently spelled error_message on a refused checkout, and RFC 9457 problem details from the gateway and the newer endpoints — with a refund's nested errors[] preferred over the outer detail, so "Amount exceeds the refundable amount" reaches the merchant rather than "Refund failed."
  • No mode toggle: a SumUp sandbox is a separate merchant account with its own code and key on the same host, so the credentials carry the mode and a switch could only contradict them
  • No SumUp SDK is vendored — four JSON endpoints are cheaper to write than to depend on
  • Unit test suite covering webhook verify-by-fetch and every reject path, checkout status mapping, checkout and refund body building, the refund read-back including its async and unreadable cases, decimal amount handling in both directions, duplicate-checkout recovery, transport failure, and packaging consistency

Theme

The storefront theme that comes with KahunaCart.

Aegean 1.1.3 Sale badges on compare-at prices, in the same words as every other theme 1 improved

1.1.3

Improved

  • Sale badges on compare-at prices, in the same words as every other theme: a product priced below its compare-at price now carries a sale badge on its card, in the hero and over the product's pictures ("25% off"), just like a product in a running sale ("Summer Sale · 30% off"). KahunaCart works out whether a price is reduced and what the badge says, so every theme agrees on it. Needs KahunaCart 1.6.4
Older versions (4)

1.1.2

Improved

  • A plainer description: the description on getgrav.org and in the admin's theme list leads with the look and what the theme does, and names the demo as an example instead of as the kind of business the theme is for

1.1.1

Improved

  • Icons are picked from the theme's own set: the icon fields (facts, rates, products and the contact page's blocks, wherever this theme has them) use Admin2's icon picker listing the icons the theme draws, instead of a list of names. The icons now live in one file, icons.yaml, which both the site and the picker read, so the two can't drift apart. Pages keep the same icon names, so nothing needs re-saving. Needs Admin2 2.1.40 and API 1.0.57 for the picker; the theme now requires API 1.0.57, which KahunaCart already installs

1.1.0

Fixed

  • The sign-in card fits its form: the login form inside the card no longer takes the page's own margins, which left a tall empty gap at the foot of the card

New

  • Pages built from modules: the home page and the about page are Grav modular pages, one module folder per part (hero, facts, rooms, rates, the stay picker, the island, extras, products, journal, reviews, owners, band, text, intro, team, steps, gallery and the appointment modules), each with its own form in Admin2, its own pictures picked from its own folder, icons picked from the theme's own set, and every field optional. Add a module from Pages › Add › Module, drag to reorder, and switch Published off to take one off the page. The page's Sections tab and the sections template are gone
  • A module whose plugin is missing says so: the stay picker without Rentals, the rates and the calendar of a house without it, the shop's modules without KahunaCart, and the appointment modules without Bookings (when they read from it with nothing typed in), show a notice in their place naming the plugin and the module, until the plugin is installed or the module is unpublished

Improved

  • The header's button and the hero's date pill go to the house's page while the stay picker can't draw (Rentals off), instead of to a #book that isn't there
  • A home page written for the Kahuna theme keeps working, with its Home page tab in the admin and Kahuna's shop link wording (featured_more)
  • The product page's spec cells and Who with portraits come from the home page's fleet and team modules
  • Fields for what was drawn but had no setting: a link beside the heading on the rooms, extras, fleet, availability, steps, pricing, sessions and trails, the extras' product slugs, a line per season on the rates, a band picture's description, the journal's line, the services' heading and From prices, the categories' heading and the text's second button
  • Journal cards look for a card.webp before a card.jpg, since the demo's pages now keep only the WebP of each photo

1.0.0

New

  • Aegean, a calm, light storefront for KahunaCart, made for a house or a few rooms let by the night that sells a few things too. White ground, one deep blue, Prata headings over Work Sans, full-width photographs and soft rounded cards. Every KahunaCart page draws, built on KahunaCart 1.6.0's theme blocks, so the plugin's forms and scripts work unchanged
  • A home page that sells the stay: a hero with a date pill and the lowest nightly rate, facts, the house room by room, rates by season with the terms, the live calendar, the stay picker on the page, the island with a map and pins, extras, the shop, notes, reviews and a word from the owners, arranged on the page's Sections tab in the admin
  • Rentals and Bookings, when they are there: the seasons, the rates, the shortest stays and arrival days, the terms, the next free night and the "from" price read the store through the theme's own wrappers, and draw nothing (or only your own words) when the add-ons are off
  • The stay picker on the page: the store's own add-to-cart form with the Rentals picker in it, under three step headings, with the house's notes beside it. A night pressed on the calendar starts the pick there, the hero's date pill opens it with the dates picked, and every Reserve button on the page goes to it
  • A booking page template and the house's own product page drawn the same way, with the extras under the picker as cross-sells
  • One blue: navy by default, or any blue of your own, with text made from it clamped so it stays readable (WCAG AA) in light and dark, and words on blue buttons turning white or dark to suit
  • Light and dark, following the visitor's system or fixed by the theme, with a moon button in the header. Light is the theme's own look
  • Every shop page and card state: listing, product page, cart, checkout, receipt, account with the Bookings tab and the manage page, and cards for sale, merchant badges, no picture, energy label, price ladders, low stock, sold out, pay what you want and a house with its next free night
  • A real map picture on the island section: an SVG named as the map's image is drawn in the page, so its land takes the theme's blue in light and dark, the box keeps its proportions so pins set in percent stay on the right spot, a pin's label can sit beside its dot, and a credit line (© OpenStreetMap contributors unless you write your own) sits under it. Without one the section still draws its placeholder island
  • The picker's stay terms and too-soon days: the minimum stay, arrival days and seasons with terms of their own show as soft blue chips in the picker's footer band, and the days between a picked arrival and the earliest day to leave are dimmed like the other days that can't be pressed. Needs Rentals 1.0.8
  • A contact page with blocks from the Business settings, a form in a card, numbered steps for getting there and a map or photo band
  • Prata and Work Sans bundled, or the system's own fonts
  • Demo mode for demo sites: a panel where a visitor tries four blues, a color of their own and light, dark or auto, live and only for themselves
  • Every word translatable from Admin2: page wording in each page's front matter with fields on its tabs, site-wide wording as language strings with plain defaults
  • The appointment sections from KahunaCart's sister themes are kept (fleet, pricing, trails, availability board, sessions, request, booking panel) for a business that books times rather than nights
Boulder 1.1.3 Sale badges on compare-at prices, in the same words as every other theme 1 improved

1.1.3

Improved

  • Sale badges on compare-at prices, in the same words as every other theme: a product priced below its compare-at price now carries a sale badge on its card, in the hero and over the product's pictures ("25% off"), just like a product in a running sale ("Summer Sale · 30% off"). KahunaCart works out whether a price is reduced and what the badge says, so every theme agrees on it. Needs KahunaCart 1.6.4
Older versions (4)

1.1.2

Improved

  • A plainer description: the description on getgrav.org and in the admin's theme list leads with the look and what the theme does, and names the demo as an example instead of as the kind of business the theme is for

1.1.1

Improved

  • Icons are picked from the theme's own set: the icon fields (facts, rates, products and the contact page's blocks, wherever this theme has them) use Admin2's icon picker listing the icons the theme draws, instead of a list of names. The icons now live in one file, icons.yaml, which both the site and the picker read, so the two can't drift apart. Pages keep the same icon names, so nothing needs re-saving. Needs Admin2 2.1.40 and API 1.0.57 for the picker; the theme now requires API 1.0.57, which KahunaCart already installs

1.1.0

Fixed

  • The sign-in card fits its form: the login form inside the card no longer takes the page's own margins, which left a tall empty gap at the foot of the card

New

  • Pages built from modules: the home page and the about page are Grav modular pages, one module folder per part (hero, facts, marquee, fleet, availability, steps, pricing, sessions, request, trails, team, products, journal, reviews, visit, band, intro, text, gallery, services, categories), each with its own form in Admin2, its own pictures picked from its own folder, icons picked from the theme's own set, and every field optional. Add a module from Pages › Add › Module, drag to reorder, and switch Published off to take one off the page. The page's Sections tab and the sections template are gone
  • A module whose plugin is missing says so: the shop's modules without KahunaCart, the availability board and the sessions without Bookings, and the fleet, prices, services, team and request without Bookings (when they read from it with nothing typed in), show a notice in their place naming the plugin and the module, until the plugin is installed or the module is unpublished

Improved

  • A home page written for the Kahuna theme keeps working, with its Home page tab in the admin and Kahuna's shop link wording (featured_more)
  • The product page's spec cells and Who with portraits come from the home page's fleet and team modules
  • Fields for what was drawn but had no setting: a link beside the heading on the fleet, availability, steps, pricing, sessions and trails, a band picture's description, the journal's line, the services' heading and From prices, the categories' heading and the text's second button
  • Journal cards look for a card.webp before a card.jpg, since the demo's pages now keep only the WebP of each photo

1.0.0

New

  • Boulder, a bold, dark-first storefront for KahunaCart, made for a business that rents things out and sells a few things too: a bike shop, a ski or kayak rental, a guiding outfit. Dusty dark ground, tall condensed headlines, one loud accent, full-bleed photographs and a running marquee. Every KahunaCart page draws, built on KahunaCart 1.6.0's theme blocks, so the plugin's forms and scripts work unchanged
  • A home page built from sections: hero (with a second button and an optional quick-book form), facts, marquee, introduction, fleet, steps, pricing, trails, availability, sessions, request, photo band, services, team, products, journal, reviews, visit, text, gallery, categories and the page's own text, arranged on the page's Sections tab in the admin, with rows you can add, drag, hide and remove
  • Bookings, when it is there: the fleet cards, the price table, the availability board, the dated sessions and the request section read the store, so prices, lengths, sizes and free counts stay current. Without Bookings the same sections take a typed list, and the ones that only make sense with it draw nothing
  • A booking panel on the page: every Book button opens the product's own buy form with the Bookings picker in a panel over the page, holds the time, then adds it to the cart or takes the booking, and hands the time back on close. With JavaScript off every Book button is a plain link to the product's page. On a phone a booking bar follows the page down
  • The booking page and picker restyled: numbered steps (1, 2, 3 whether or not the product has sizes or a who-with), length cards, Bookings' own row of sizes drawn as big size letters with the note and a live free count under them (amber when few are left, struck and switched off when none), start times as pills, the hold note under the held time, and the price, hold, where and who with beside it. The account's Bookings tab and the manage page match
  • One accent: orange (the default), gold, lime, sky, red or your own color, with text made from it clamped so it stays readable (WCAG AA) in light and dark, and words on accent buttons turning black or white to suit
  • Business details typed once: address, hours, phone, email, a directions link and a map, used on the home page, the contact page, the booking page and the footer
  • Light and dark, following the visitor's system or fixed by the theme, with a moon button in the header to pick. Dark is the theme's own look
  • Every shop page and card state: listing with boxed tabs and filters, product page with option pills, cart, checkout, receipt, account and the Login plugin's forms, and cards for sale, compare-at, set price, pay what you want, low stock, backorder, sold out, request-only, bookable and no picture
  • Journal pages with category tabs, a lead piece, cards, Members marks and a membership block from Subscriptions
  • Bebas Neue and Barlow bundled, or the system's own fonts
  • A tab icon: the uploaded logo when there is one, otherwise the theme's own dial mark, so the browser stops asking for /favicon.ico
  • A price note on the request section: the small line under the price (such as per group) is a field of the section, and nothing is drawn when it is blank
  • Demo mode for demo sites: a Change the colors panel where a visitor tries the five accents, a color of their own and light, dark or auto, live and only for themselves
  • Every word translatable from Admin2: wording on one page (the sections, the contact page's lines) is in that page's front matter, so each language's copy of the page has its own, and wording used on every page (the header button, the tagline, the footer line, the headings, Closed, the booking panel's and quick-book form's labels) is a language string with a plain default, edited under Translations
Encore 1.1.3 Sale badges on compare-at prices, in the same words as every other theme 1 improved

1.1.3

Improved

  • Sale badges on compare-at prices, in the same words as every other theme: a product priced below its compare-at price now carries a sale badge on its card, in the hero and over the product's pictures ("25% off"), just like a product in a running sale ("Summer Sale · 30% off"). KahunaCart works out whether a price is reduced and what the badge says, so every theme agrees on it. Needs KahunaCart 1.6.4
Older versions (4)

1.1.2

Improved

  • A plainer description: the description on getgrav.org and in the admin's theme list leads with the look and what the theme does, and names the demo as an example instead of as the kind of business the theme is for

1.1.1

Improved

  • Icons are picked from the theme's own set: the icon fields (facts, rates, products and the contact page's blocks, wherever this theme has them) use Admin2's icon picker listing the icons the theme draws, instead of a list of names. The icons now live in one file, icons.yaml, which both the site and the picker read, so the two can't drift apart. Pages keep the same icon names, so nothing needs re-saving. Needs Admin2 2.1.40 and API 1.0.57 for the picker; the theme now requires API 1.0.57, which KahunaCart already installs

1.1.0

Improved

  • Journal cards look for a card.webp before a card.jpg, since the demo's pages now keep only the WebP of each photo
  • Pages are built from modules: the home page and the about page are Grav modular pages, each part of the page a module in its own folder (01._hero, 02._road …) with its own pictures, instead of one page holding a list of sections. Every module type has its own form in the admin with every field optional, and the module's prose goes in its own body for the types that take some (text, intro, owners). Add, reorder, edit and unpublish modules in Admin2 like any page
  • A module whose plugin is missing or off says so: in place of a Bookings calendar without Bookings, the mailing list without Newsletters or a shop module without KahunaCart, a notice names the plugin and the module and asks for the plugin to be installed or the module unpublished. Each module type lists what it needs in its own template
  • A module with content of its own doesn't need its plugin: the services, team, fleet, price list, request, seasons, extras, downloads library and track list read from KahunaCart, Bookings or Rentals by default, but a module that has its own content typed in to fall back on (groups, people with names, cards, rows, a link, seasons, tiles, example rows, songs) still draws it with the plugin missing or off, and shows the missing-plugin notice only when it has nothing typed in. Before, those modules went quiet without their plugin. The availability module now asks for Bookings, and for Rentals as well when it shows a house, rather than for either one
  • Every visible part of a module has a field: the press photo, its side and focus beside the steps, the steps' example lines, the downloads library's example rows, the gallery's head, the track list's compact rows and more. Icons are picked from the theme's own icons, and pictures from the module's folder
  • A home page written for Kahuna still draws and keeps its Home page tab, now with the shop link's words too

Fixed

  • The sign-in card fits its form: the login form inside the card no longer takes the page's own margins, which left a tall empty gap at the foot of the card
  • An empty hero or dates module draws nothing, and a hero never prints the module's admin name as its title
  • Module settings typed as a list where words belong are left out instead of breaking the page

1.0.0

New

  • Encore, a bright, joyful storefront for KahunaCart, made for a shop that sells music and the things around it, such as a band with a record in several formats, downloads, shirts printed to order and tour dates. A white page with soft butter, mint and lilac bands, one violet you can change, Gabarito headings, Instrument Sans text, pill buttons and big rounded photos. Every KahunaCart page draws, built on KahunaCart 1.6.0's theme blocks, so the plugin's forms and scripts work unchanged
  • The record sold on the home page: its formats as radio rows with each one's line, live stock and price, driving KahunaCart's own picker; the button says Pre-order · $32 until release day and Add to bag · $32 after it, and adds to the bag without leaving the page. With JavaScript off it is KahunaCart's own form
  • A player for a product's previews: one audio element for the page, so one song plays at a time, a waveform that fills as it plays, a play button on each song in the track list that has a preview, and the browser's own controls with JavaScript off
  • New sections: the player, one product sold on the page, the track list, tour dates with past ones left out by the store's date, a downloads library, the mailing list on a photo, printed-for-you steps with Print on Demand's being-made panel, the people with a big photo, cards on tinted bands. Every section and key has its fields on the page's Sections tab in Admin2
  • A tour page from its own front matter, with a button that saves every date as one calendar file, the dates with doors, ages and prices, a box for anybody not near a date and a pick-up note
  • The product page for a record and a printed shirt: the release date in the kind line and a pre-order chip, format rows, what's included, the printed-for-you box, the player and the track list, folded details, reviews and Goes with it
  • The account's downloads as a library, a pre-order's download held as "Soon · Nov 13", and Print on Demand's being-made panel on the orders and the receipt
  • One color: Violet by default, Coral, Teal, Cobalt and Magenta, or any color of your own, worked out in OKLCH so text in it keeps WCAG AA on white, on the tints and on the dark page, and the words on a filled button turn near-black or white to suit
  • Light and dark, following the visitor's system or fixed by the theme, with a moon button in the header. Light is the theme's own look
  • Gabarito and Instrument Sans bundled, or the system's own fonts
  • Demo mode for demo sites: a panel where a visitor tries the five presets, a color of their own and light, dark or auto, live and only for themselves
  • Every word translatable from Admin2: page wording in each page's front matter with fields on its tabs, site-wide wording as language strings with plain defaults, day and month names from Grav. The cart's name comes from KahunaCart's storefront setting
  • The bookings, stay, rental and software sections from KahunaCart's sister themes are kept and restyled, for a shop that also books sessions or sells a license
Graphene 1.1.3 Sale badges on compare-at prices, in the same words as every other theme 1 improved

1.1.3

Improved

  • Sale badges on compare-at prices, in the same words as every other theme: a product priced below its compare-at price now carries a sale badge on its card, in the hero and over the product's pictures ("25% off"), just like a product in a running sale ("Summer Sale · 30% off"). KahunaCart works out whether a price is reduced and what the badge says, so every theme agrees on it. Needs KahunaCart 1.6.4
Older versions (4)

1.1.2

Improved

  • A plainer description: the description on getgrav.org and in the admin's theme list leads with the look and what the theme does, and names the demo as an example instead of as the kind of business the theme is for

1.1.1

Improved

  • Icons are picked from the theme's own set: the icon fields (facts, rates, products and the contact page's blocks, wherever this theme has them) use Admin2's icon picker listing the icons the theme draws, instead of a list of names. The icons now live in one file, icons.yaml, which both the site and the picker read, so the two can't drift apart. Pages keep the same icon names, so nothing needs re-saving. Needs Admin2 2.1.40 and API 1.0.57 for the picker; the theme now requires API 1.0.57, which KahunaCart already installs

1.1.0

Improved

  • Journal cards look for a card.webp before a card.jpg, since the demo's pages now keep only the WebP of each photo
  • Pages are built from modules: the home page and the about page are Grav modular pages, each part of the page a module in its own folder (01._hero, 02._ticker …) with its own pictures, instead of one page holding a list of sections. Every module type has its own form in the admin with every field optional, and the module's prose goes in its own body for the types that take some (text, intro, owners). Add, reorder, edit and unpublish modules in Admin2 like any page
  • A module whose plugin is missing or off says so: in place of the key check without Licenses, a Bookings calendar without Bookings or a shop module without KahunaCart, a notice names the plugin and the module and asks for the plugin to be installed or the module unpublished. Each module type lists what it needs in its own template
  • A module with content of its own doesn't need its plugin: the services, team, fleet, price list, request, seasons and extras read from KahunaCart, Bookings or Rentals by default, but a module that has its own content typed in to fall back on (groups, people with names, cards, rows, a link, seasons, tiles) still draws it with the plugin missing or off, and shows the missing-plugin notice only when it has nothing typed in. Before, those modules went quiet without their plugin. The availability module now asks for Bookings, and for Rentals as well when it shows a house, rather than for either one
  • Every visible part of a module has a field: the journal's line, the key check's placeholder, the services' heading, the text's second button, the features' link, the categories' heading and more. Icons are picked from the theme's own icons, and pictures from the module's folder
  • A home page written for Kahuna still draws and keeps its Home page tab, now with the shop link's words too

Fixed

  • The sign-in card fits its form: the login form inside the card no longer takes the page's own margins, which left a tall empty gap at the foot of the card
  • An empty hero module draws nothing, and a hero never prints the module's admin name as its title
  • Module settings typed as a list where words belong are left out instead of breaking the page

1.0.0

New

  • Graphene, a dark, precise storefront for KahunaCart, made for a small company that sells software. Near-black ground with film grain and soft glows, one accent color, Schibsted Grotesk headings with JetBrains Mono for versions, keys and prices. Every KahunaCart page draws, built on KahunaCart 1.6.0's theme blocks, so the plugin's forms and scripts work unchanged
  • A home page that sells the apps: the newest release in the hero beside a drawn app window, a ticker of releases, the apps one to a row, a bento of features, the editions with live prices and a Buy button each, the changelog as a terminal, a live key check, reviews, the studio band, the shop and the journal, arranged on the page's Sections tab in the admin
  • Releases and Licenses, read live: release labels, the ticker, the changelog, the editions, what each key gives, the renewal and add-on prices and the customer's own keys come from the store through the theme's own wrappers, and draw nothing (or only your own words) when the add-ons are off
  • File sizes on the changelog chips are optional: the releases section's sizes setting (a toggle in Admin2, on by default) turns them off so a chip shows the platform only, and the "Bought after" note prints its date in the store's long date
  • A live key check: paste a key and see its status, updates window, seats and newest covered release, from the store's public Licenses API, with a face for active, updates ended, expired, suspended, revoked, not found and rate limited. The key never goes into the address, the console or the browser's storage
  • Editions as tiles on the product page, picking the buy form's own option, with what the chosen key gives, its releases and add-ons, reviews with a histogram, and "Goes with" cross-sells
  • One accent: mint by default, eight more presets or any color of your own, worked out in OKLCH so text in it keeps WCAG AA on dark and light, words on accent buttons turn black or white to suit, and white makes a monochrome store
  • Dark and light, following the visitor's system or fixed by the theme, with a moon button in the header. Dark is the theme's own look
  • Licenses pages restyled: the account's Licenses tab, a key's page and the keys on the receipt
  • A support page with cards for the ways in, the form beside common questions, the studio's address and hours, a trial band and a map
  • Schibsted Grotesk and JetBrains Mono bundled, or the system's own fonts
  • Demo mode for demo sites: a panel where a visitor tries the nine presets, a color of their own and light, dark or auto, live and only for themselves
  • Every word translatable from Admin2: page wording in each page's front matter with fields on its tabs, site-wide wording as language strings with plain defaults
  • The stay and appointment sections from KahunaCart's sister themes are kept, for a studio that also sells office hours or workshops through Bookings
Kahuna 1.2.3 Sale badges on compare-at prices everywhere, and the same words as every other theme 1 improved

1.2.3

Improved

  • Sale badges on compare-at prices everywhere, and the same words as every other theme: a product priced below its compare-at price now carries a sale badge in the hero, not only in the product grid, and the badge says how much is off ("Summer Sale · 30% off", "25% off") the way every KahunaCart theme does. KahunaCart works out whether a price is reduced, so the hero no longer strikes a price through for a "Buy 2+ and save" offer that takes nothing off one item. Needs KahunaCart 1.6.4
Older versions (9)

1.2.2

Fixed

  • The hero no longer goes blank when its pinned products are gone: when none of the products picked for the hero is in the shop any more (the starter's sample products once you delete them, say), the shop's first three show instead, featured first, and the products module below starts after them. The help text says so too

1.2.1

Improved

  • The description says this theme is free with your KahunaCart license, so the theme's listing in GPM and on getgrav.org no longer reads as something sold on its own

1.2.0

New

  • A home page built from modules. Make the home page a modular page (modular.md with content: items: '@self.modular') and build it from module folders, each with its own form in admin: hero, categories, products, intro and facts. They draw the same parts as the flat home page, and the same types and keys are in Slate and Pebble, so one home page draws in all three themes. A module with nothing set draws nothing, except categories and products, which show the store's own; to hide one for a while, unpublish it
  • Flat home pages keep working. A home page written with the hero_*, featured_*, intro_title and facts keys draws exactly as before, through the same markup as the modules, and keeps its Home page tab. Nothing on a site is converted; a site moves to modules when its owner makes the home page modular
  • A module that needs KahunaCart says so. With KahunaCart switched off or not installed, the categories and products modules, and a hero that is only the product stack, draw a notice in their place naming the plugin and the module, so the owner switches it on or unpublishes the module
  • Two parts that had no setting do now, in a module: where the link beside the products heading goes (more.link), and the label of the categories' first link (all)

Improved

  • The home page's parts are partials (templates/partials/sections/): home.html.twig turns its flat keys into what each partial takes, and the module templates hand over their front matter, so there is one copy of the markup
  • modular.html.twig draws modules the way the home page does: a first hero module full width above the page column, the others in the column, without the old .k-modular wrapper; a modular page with no modules is drawn as an ordinary page

1.1.0

New

  • Kahuna is built on KahunaCart's storefront blocks. The shop pages (product, listing, category, search, cart, checkout, order, payment return, store closed, download refused, currency question and paywall) now extend KahunaCart's own templates and change only the parts that look different, instead of each being a full copy. Fixes and features in KahunaCart's pages reach Kahuna without a theme update. Needs KahunaCart 1.6.0, which this release now requires
  • Parts the copies had lost are back: "Save for later" under the buy box, the payment reference, order history and Download PDF on the order page, the quantity limits under a cart line ("Limit 7 per order"), the reason a payment method can't be used on physical goods, and the release version beside a download
  • Follows KahunaCart's Whole Amounts and Name for the Cart settings: a sale's two prices on cards, the product page, the home hero and the picks rows are printed alike when the store shows whole amounts, and the header and footer cart links say bag or basket when the store does; the header's hidden count reads "1 item in your cart" rather than "1 items in cart", and the cart table, the checkout summary and the receipt print every amount of an order alike (all with cents when any has them, none when every one is whole)
  • The home page is edited in admin. A Home page tab on the home page holds every setting the theme reads (the small line, headline, paragraph, picture and its description, the button and second link, the products beside the headline, the category list, the products heading and count, the shop link label, the text heading and the facts), so nothing about it is typed into a template. The same fields, in the same order, are in Kahuna, Slate and Pebble, and saving keeps any front matter key the form doesn't list
  • A label for the home page's shop link (featured_more), beside the products heading, in place of the fixed "View all"
  • The 404 page takes its words from the store's error page: the page's own title and text, and Home button and Shop button labels on a Buttons tab, with the theme's wording as the fallback when the store hasn't written an error page
  • A switch for the credit line in the footer (Settings, Credit Line), so a store can drop it
  • Store name, tagline, announcement and footer text fall back to a language string when the theme setting is empty, so a store in more than one language can leave the settings empty and write each language under Translations; the help on each field says so

Improved

  • The delivery step, a delivery method, a cart line, a set's row, a product card and a receipt line are small overrides of KahunaCart's partials, so the markup scripts depend on stays the plugin's
  • The product gallery is KahunaCart's, styled by Kahuna, with its lightbox and the link for an embed that won't play
  • The theme's description says it needs KahunaCart (licensed), and its dependencies name KahunaCart 1.6.0, so GPM installs the plugin with it
  • The delivery, payment and collection choices keep their spacing now that KahunaCart 1.6.0 draws those rows as cards by default: Kahuna's own cards set their margin, so the plugin's doesn't add to the gap between them
  • Kahuna has a screenshot and thumbnail for the theme list in the admin and on GPM, taken from the demo shop
  • The check scripts in tests/ run against KahunaCart 1.6.0: the plugin's templates are loaded as @kahunacart, the badges and quantity-limit functions are stubbed, and the pages that now extend the plugin's are no longer compared with theme 1.0.11

Fixed

  • "You have been logged out" shows on the page you land on. Grav's own messages on a page with no messages area of its own (the home page after signing out, a sent contact form) were only drawn on a store with a currency switcher, so they waited and turned up on the next shop page. They show at the top of the page now on every store

1.0.21

New

  • Your own badge over a product's pictures. A product's Badge text ("BOGO", "Last one") shows over its pictures in the shop and on its page, in the colour picked for it, beside the sold-out, sale and download badges. The badges are now KahunaCart's own partial, which Kahuna styles, so they read the same in every theme. Needs KahunaCart 1.5.18
  • Cart lines link to their product, opened on the variant in the cart. Needs KahunaCart 1.5.18; with an older plugin the titles stay plain text
  • "Free from $20.00" under a shipping method that charges under its free-shipping minimum, at checkout. Needs KahunaCart 1.5.18

Improved

  • The product page and product grid are stamped for KahunaCart 1.5.18, so the template drift check stops listing them. Both already draw the plugin's badges through the product picture, so nothing else in them changed

Fixed

  • The theme's homepage and bug links pointed at a private repository, so they were a 404 in the admin and GPM. They now go to the theming docs on kahunacart.com and the KahunaCart issue tracker

1.0.20

New

  • Related products, cross-sells and up-sells on the product page. The lists you pick under Merchandising → Related products now show in Kahuna: Consider this instead above the buy box, Goes well with this under it, both as compact rows with a picture, the name and the price, and You might also like as product cards at the foot of the page. Until now Kahuna's product page left all three out
  • Goes well with your order under the cart. The cross-sells of what is in the basket, as the same compact rows, with an Add button on each product that needs nothing chosen and View on the rest. Needs KahunaCart 1.5.16; with an older plugin nothing shows
  • A set whose parts the shopper picks shows each pick in the cart. Two sets of one grouped product with different picks, a red easel and a blue one, are two rows, each with its own quantity box and ×, and the set's row names what was picked under its title. Needs KahunaCart 1.5.16; with an older plugin the cart works as before

1.0.19

New

  • The cart shows a grouped product as one row with its parts under it. The set's row has the product's picture, a quantity box for how many sets, what the set adds up to and one × for the whole set, and the parts sit under it, indented, with their prices greyed. Needs KahunaCart 1.5.15; with an older plugin every line draws on its own as before
  • Pictures on cart lines. Each line shows a small square picture beside its name. Needs KahunaCart 1.5.15

1.0.18

New

  • The Shop menu link follows the site's shop page. With a page using KahunaCart's Shop page template at the store's address, the menu draws that page with its own label, place and visibility instead of the theme's fixed Shop link, marks it active on every shop page, and the shop's front door prints the page's content as an intro under the heading. Needs KahunaCart 1.5.14; without a shop page, or on an older KahunaCart, the menu shows the Shop link as before

1.0.17

Improved

  • Shop pages print cleanly. The receipt, cart, checkout, product and catalog pages print without the header, footer, action rows, tinted panels or the cart's sidebar, in one column, with the product picture on the first sheet beside the price. Needs KahunaCart 1.5.8 for the marks on the plugin's own parts (kahunacart-issues#3)
Pebble 1.1.3 Sale badges on compare-at prices, in the same words as every other theme 1 improved

1.1.3

Improved

  • Sale badges on compare-at prices, in the same words as every other theme: a product priced below its compare-at price now carries a sale badge on its card, in the hero and over the product's pictures ("25% off"), just like a product in a running sale ("Summer Sale · 30% off"). KahunaCart works out whether a price is reduced and what the badge says, so every theme agrees on it. Needs KahunaCart 1.6.4
Older versions (5)

1.1.2

Fixed

  • The hero no longer goes blank when its pinned products are gone: when none of the products picked for the hero is in the shop any more (the starter's sample products once you delete them, say), the shop's first three show instead, featured first, and the products module below starts after them. The help text says so too
  • A hero with no picture and no products uses the full width: with Show Products off, the headline and its text now run across the page instead of leaving an empty column beside them, as Kahuna already did

1.1.1

Improved

  • The description says this theme is free with your KahunaCart license, so the theme's listing in GPM and on getgrav.org no longer reads as something sold on its own

1.1.0

New

  • A home page built from modules. Make the home page a modular page (modular.md with content: items: '@self.modular') and build it from module folders, each with its own form in admin: hero, categories, products, intro and facts. They draw the same parts as the flat home page, and the same types and keys are in Kahuna and Slate, so one home page draws in all three themes. A module with nothing set draws nothing, except categories and products, which show the store's own; to hide one for a while, unpublish it
  • Flat home pages keep working. A home page written with the hero_*, featured_*, intro_title and facts keys draws exactly as before, through the same markup as the modules, and keeps its Home page tab. Nothing on a site is converted; a site moves to modules when its owner makes the home page modular
  • A module that needs KahunaCart says so. With KahunaCart switched off or not installed, the categories and products modules, and a hero that is only the product stack, draw a notice in their place naming the plugin and the module, so the owner switches it on or unpublishes the module
  • Two parts that had no setting do now, in a module: where the link beside the products heading goes (more.link), and the label of the categories' first link (all)

Improved

  • The home page's parts are partials (templates/partials/sections/): home.html.twig turns its flat keys into what each partial takes, and the module templates hand over their front matter, so there is one copy of the markup
  • modular.html.twig draws modules the way the home page does: a first hero module full width above the page column, the others in the column; a modular page with no modules is drawn as an ordinary page

1.0.1

Fixed

  • The sign-in page in the dark scheme: the Login plugin's white box is now a card in the theme's own colors, with the Login button in the accent, so its heading, labels and fields read in dark as well as light

1.0.0

New

  • Pebble, a soft storefront for KahunaCart. A cool blue-grey ground, white rounded cards under diffused shadows and a header that floats as one white bar. Every KahunaCart page draws, built on KahunaCart 1.6.0's theme blocks, so the plugin's forms and scripts work unchanged
  • Category tiles and a Refine panel: round pictures for the categories over the listing and the filters in a card down the side, with one Filters pill on a phone
  • A round Add button on every card that puts a product with nothing to choose straight in the cart, and a quiet arrow or calendar on the rest
  • Load more under the listing, a plain link to the next page that adds the next cards in place when the script runs
  • Follows KahunaCart's Whole Amounts and Name for the Cart settings: a sale's two prices on cards and the product page are printed alike when the store shows whole amounts, and the header and footer cart links, the cart page's title, the Add button's label and the header's hidden count say cart, bag or basket as the store does
  • Light and dark, following the visitor's system, with a moon button in the header to pick
  • Restyle with tokens: KahunaCart's own custom properties, so the theme and the plugin's pieces change together
  • The home page is edited in admin. A Home page tab on the home page holds every setting the theme reads (the small line, headline, paragraph, picture and its description, the button and second link, the products beside the headline, the category list, the products heading and count, the shop link label, the text heading and the facts), so nothing about it is typed into a template. The same fields, in the same order, are in Kahuna, Slate and Pebble, and saving keeps any front matter key the form doesn't list
  • A label for the home page's shop link (featured_more), beside the products heading, in place of the fixed "View all"
  • The 404 page takes its words from the store's error page: the page's own title and text, and Home button and Shop button labels on a Buttons tab, with the theme's wording as the fallback when the store hasn't written an error page
  • A switch for the credit line in the footer (Settings, Credit Line), so a store can drop it
  • A switch for the cart note under Checkout (Cart note), whose words are a language string a store edits under Translations
  • Store name, tagline, announcement and footer text fall back to a language string when the theme setting is empty, so a store in more than one language can leave the settings empty and write each language under Translations; the help on each field says so
Slate 1.1.3 Sale badges on compare-at prices 1 improved

1.1.3

Improved

  • Sale badges on compare-at prices: a product priced below its compare-at price now carries the "−25%" badge on its card, in the hero and over the product's pictures, just like a product in a running sale. KahunaCart works out whether a price is reduced, so every theme agrees on it. Needs KahunaCart 1.6.4
Older versions (5)

1.1.2

Fixed

  • The hero no longer goes blank when its pinned products are gone: when none of the products picked for the hero is in the shop any more (the starter's sample products once you delete them, say), the shop's first three show instead, featured first, and the products module below starts after them. The help text says so too
  • A hero with no picture and no products uses the full width: with Show Products off, the headline and its text now run across the page instead of leaving an empty column beside them, as Kahuna already did

1.1.1

Improved

  • The description says this theme is free with your KahunaCart license, so the theme's listing in GPM and on getgrav.org no longer reads as something sold on its own

1.1.0

New

  • A home page built from modules. Make the home page a modular page (modular.md with content: items: '@self.modular') and build it from module folders, each with its own form in admin: hero, categories, products, intro and facts. They draw the same parts as the flat home page, and the same types and keys are in Kahuna and Pebble, so one home page draws in all three themes. A module with nothing set draws nothing, except categories and products, which show the store's own; to hide one for a while, unpublish it
  • Flat home pages keep working. A home page written with the hero_*, featured_*, intro_title and facts keys draws exactly as before, through the same markup as the modules, and keeps its Home page tab. Nothing on a site is converted; a site moves to modules when its owner makes the home page modular
  • A module that needs KahunaCart says so. With KahunaCart switched off or not installed, the categories and products modules, and a hero that is only the product stack, draw a notice in their place naming the plugin and the module, so the owner switches it on or unpublishes the module
  • Two parts that had no setting do now, in a module: where the link beside the products heading goes (more.link), and the label of the categories' first link (all)

Improved

  • The home page's parts are partials (templates/partials/sections/): home.html.twig turns its flat keys into what each partial takes, and the module templates hand over their front matter, so there is one copy of the markup
  • modular.html.twig draws modules the way the home page does: a first hero module full width above the page column, the others in the column; a modular page with no modules is drawn as an ordinary page

1.0.1

Fixed

  • The sign-in page in the dark scheme: the Login plugin's white box is now a card in the theme's own colors, with the Login button in the accent, so its heading, labels and fields read in dark as well as light

1.0.0

New

  • Slate, a clean grid-first storefront for KahunaCart. A white page, a cool grey ramp, ink buttons, bordered cards and a sticky header with the shop's search in the middle. Every KahunaCart page draws, built on KahunaCart 1.6.0's theme blocks, so the plugin's forms and scripts work unchanged
  • Category tabs and a filter panel over and beside the listing, with each filter group folding away and one Filters button on a phone
  • An Add button on every card that puts a product with nothing to choose straight in the cart, and Choose or Book on the rest
  • Follows KahunaCart's Whole Amounts and Name for the Cart settings: a sale's two prices on cards and the product page are printed alike when the store shows whole amounts, and the header and footer cart links, the cart page's title, the Add button's label and the header's hidden count say cart, bag or basket as the store does
  • Light and dark, following the visitor's system, with a moon button in the header to pick
  • Restyle with tokens: KahunaCart's own custom properties, so the theme and the plugin's pieces change together
  • The home page is edited in admin. A Home page tab on the home page holds every setting the theme reads (the small line, headline, paragraph, picture and its description, the button and second link, the products beside the headline, the category list, the products heading and count, the shop link label, the text heading and the facts), so nothing about it is typed into a template. The same fields, in the same order, are in Kahuna, Slate and Pebble, and saving keeps any front matter key the form doesn't list
  • A label for the home page's shop link (featured_more), beside the products heading, in place of the fixed "View all"
  • The 404 page takes its words from the store's error page: the page's own title and text, and Home button and Shop button labels on a Buttons tab, with the theme's wording as the fallback when the store hasn't written an error page
  • A switch for the credit line in the footer (Settings, Credit Line), so a store can drop it
  • A switch for the cart note under Checkout (Cart note), whose words are a language string a store edits under Translations
  • Store name, tagline, announcement and footer text fall back to a language string when the theme setting is empty, so a store in more than one language can leave the settings empty and write each language under Translations; the help on each field says so
Vellum 1.1.2 Sale badges on compare-at prices, in the same words as every other theme 1 improved

1.1.2

Improved

  • Sale badges on compare-at prices, in the same words as every other theme: a product priced below its compare-at price now carries a sale badge on its card, in the hero and over the product's pictures ("25% off"), just like a product in a running sale ("Summer Sale · 30% off"). KahunaCart works out whether a price is reduced and what the badge says, so every theme agrees on it. Needs KahunaCart 1.6.4
Older versions (3)

1.1.1

Improved

  • A plainer description: the description on getgrav.org and in the admin's theme list leads with the look and what the theme does, and names the demo as an example instead of as the kind of business the theme is for

1.1.0

Fixed

  • The sign-in page in the dark scheme: the Login plugin's white box is now a card in the page's own colors, so its heading, labels and fields read in dark as well as light, and the form inside it no longer leaves a tall empty gap

New

  • Pages built from modules: the home page and the about page are Grav modular pages, one module folder per part (hero, introduction, photo band, services, team, products, journal, reviews, visit, text, gallery, categories), each with its own form in Admin2, its own pictures picked from its own folder, and every field optional. Add a module from Pages › Add › Module, drag to reorder, and switch Published off to take one off the page. The page's Sections tab and the sections template are gone
  • A module whose plugin is missing says so: the shelf and the category row without KahunaCart, and the price list and the team without Bookings (when they read from it with nothing typed in), show a notice in their place naming the plugin and the module, until the plugin is installed or the module is unpublished

Improved

  • A home page written for the Kahuna theme keeps working, with its Home page tab in the admin and Kahuna's shop button wording (featured_more)
  • The booking page's Who with portraits and its stand-in picture come from the home page's team and services band modules
  • Fields for what was drawn but had no setting: the services' heading and From prices, the categories' heading and the text's second button
  • Journal cards look for a card.webp before a card.jpg, since the demo's pages now keep only the WebP of each photo

1.0.0

New

  • Vellum, a photo-led storefront for KahunaCart, made for a business with a shop and a diary: white paper in one tint, a wide-tracked serif wordmark, black and white photo bands, a black team band and a charcoal footer. Every KahunaCart page draws, built on KahunaCart 1.6.0's theme blocks, so the plugin's forms and scripts work unchanged
  • A home page built from sections: hero, introduction, photo band, services, team, products, journal, reviews, visit, text, gallery, categories and the page's own text, arranged on the page's Sections tab in the admin, with rows you can add, drag, hide and remove
  • Bookings, when it is there: the price list and the team come from the store, each service gets a booking page with a Who with row of portraits and a What to expect panel, and the picker, the account's Bookings tab and the manage page are restyled to match. Without Bookings the same sections take a typed list
  • One tint: gold (the default), ink, sage, rose or your own color, with the paper and the text worked out from it so text and buttons stay readable (WCAG AA) in light and dark whatever the color
  • Business details typed once: address, hours, phone, email, a directions link and a map, used on the home page, the contact page, the booking page and the footer
  • Follows the store's Whole Amounts and Name for the Cart settings (KahunaCart 1.6.0): prices on cards, the product page and the services list print as $55 where the store asks for it, a sale's two prices always alike, and the header, the cart page and the card buttons say cart, bag or basket as the store chooses
  • Light and dark, following the visitor's system or fixed by the theme, with a moon button in the header to pick
  • Journal pages with a lead piece, a three-column grid, Members marks and a membership block from Subscriptions
  • Playfair Display and Montserrat bundled, or the system's own fonts, and black and white or color editorial photographs
  • Demo mode for demo sites: a Change the colors panel where a visitor tries the tints, a color of their own and light, dark or auto, live and only for themselves
  • Every word translatable from Admin2: wording on one page (the sections, the contact page's lines) is in that page's front matter, so each language's copy of the page has its own, and wording used on every page (the header button, the tagline, the footer line, the note under the address, the headings, Closed, the booking page's labels) is a language string with a plain default, edited under Translations. The theme's settings hold only the address, links, colors, pictures and switches, and the opening hours are times per day (mon to sun), with day names from Grav's own translations
Zoom 1.1.3 Sale badges on compare-at prices, in the same words as every other theme 1 improved

1.1.3

Improved

  • Sale badges on compare-at prices, in the same words as every other theme: a product priced below its compare-at price now carries a sale badge on its card, in the hero and over the product's pictures ("25% off"), just like a product in a running sale ("Summer Sale · 30% off"). KahunaCart works out whether a price is reduced and what the badge says, so every theme agrees on it. Needs KahunaCart 1.6.4
Older versions (5)

1.1.2

Improved

  • A plainer description: the description on getgrav.org and in the admin's theme list leads with the look and what the theme does, and names the demo as an example instead of as the kind of business the theme is for

1.1.1

Improved

  • Icons are picked from the theme's own set: the icon fields (facts, rates, products and the contact page's blocks, wherever this theme has them) use Admin2's icon picker listing the icons the theme draws, instead of a list of names. The icons now live in one file, icons.yaml, which both the site and the picker read, so the two can't drift apart. Pages keep the same icon names, so nothing needs re-saving. Needs Admin2 2.1.40 and API 1.0.57 for the picker; the theme now requires API 1.0.57, which KahunaCart already installs

1.1.0

Improved

  • Journal cards look for a card.webp before a card.jpg, since the demo's pages now keep only the WebP of each photo
  • Pages are built from modules: the home page and the about page are Grav modular pages, each part of the page a module in its own folder (01._hero, 02._counts …) with its own pictures, instead of one page holding a list of sections. Every module type has its own form in the admin with every field optional, and the module's prose goes in its own body for the types that take some (text, intro, owners). Two modules in a row with width: half still sit side by side. Add, reorder, edit and unpublish modules in Admin2 like any page
  • A module whose plugin is missing or off says so: in place of the lanes, the playgroup or the find-a-time bar without Bookings, or a shop module without KahunaCart, a notice names the plugin and the module and asks for the plugin to be installed or the module unpublished. Each module type lists what it needs in its own template
  • A module with content of its own doesn't need its plugin: the services, team, fleet, price list, request, seasons and extras read from KahunaCart, Bookings or Rentals by default, but a module that has its own content typed in to fall back on (groups, people with names, cards, rows, a link, seasons, tiles) still draws it with the plugin missing or off, and shows the missing-plugin notice only when it has nothing typed in. Before, those modules went quiet without their plugin. The availability module now asks for Bookings, and for Rentals as well when it shows a house, rather than for either one
  • Every visible part of a module has a field: the band's small line, the key check's placeholder, the price list's heading and more. Icons are picked from the theme's own icons, and pictures from the module's folder
  • A home page written for Kahuna still draws and keeps its Home page tab, now with the shop link's words too

Fixed

  • The sign-in card fits its form: the login form inside the card no longer takes the page's own margins, which left a tall empty gap at the foot of the card
  • An empty hero module draws nothing, and a hero never prints the module's admin name as its title
  • Module settings typed as a list where words belong are left out instead of breaking the page

1.0.1

New

  • Stars on product cards: a product with approved reviews shows its stars and review count under its name, from KahunaCart 1.6.1's rating partial, in the theme's color. Needs KahunaCart 1.6.1

1.0.0

New

  • Zoom, a bold, sporty storefront for KahunaCart, made for a shop that sells goods and books appointments, such as a pet supply store with a grooming salon. A dark navy page with one electric color, Sora headings, prices and big numbers, Archivo text, rectangular buttons and every photo in color. Every KahunaCart page draws, built on KahunaCart 1.6.0's theme blocks, so the plugin's forms and scripts work unchanged
  • The day, lane by lane: a lane per groomer and per tub with the booked stretches named by service only, the free stretches in the color, lunch and closed time hatched and a line at now. After closing it moves on to the next day with something free and says which day; on phones each lane is a row of free times
  • One booking panel for the whole home page: a free stretch, a Book button on the price list or the team, the class meter, the request and the find-a-time bar all open the Bookings picker in place, with the store's add-ons under it, and Add to cart keeps the visitor on the page. With JavaScript off each is a plain link or form to the booking page
  • New sections: big numbers (openings, resources free now, a class's places left, or typed), the lanes, the find-a-time bar, the price list as rows with sizes as columns and the add-ons as chips, numbered portraits with each person's next free time, the class meter and the request as half-width panels, the visit board with the hours by kind, journal rows, and a closing band. Every section and key has its fields on the page's Sections tab in Admin2
  • A booking page with the services as tiles, who with as tiles with portraits, the sizes as tiles with their price and weight range, the day and time, and a column of notes from the page
  • One color: Electric by default, Tangerine, Lime, Magenta and Butter, or any color of your own, worked out in OKLCH so text in it keeps WCAG AA on the navy page and on white (Butter turns amber on white), and the words on a filled button turn navy-black or white to suit
  • Dark and light, following the visitor's system or fixed by the theme, with a moon button in the header. Dark is the theme's own look
  • Cover photos keep their subject: a focus on the hero, bands, team portraits, text and journal posts, the top kept in view by default, and the hero's photo stops growing on very wide screens
  • Sora and Archivo bundled, or the system's own fonts
  • Demo mode for demo sites: a panel where a visitor tries the five presets, a color of their own and light, dark or auto, live and only for themselves
  • Every word translatable from Admin2: page wording in each page's front matter with fields on its tabs, site-wide wording as language strings with plain defaults, day names from Grav. The cart's name comes from KahunaCart's storefront setting
  • The stay, rental and software sections from KahunaCart's sister themes are kept and restyled, for a shop that also lets a room or sells a license