Customers, Wishlists, Reviews & Retention
This page covers what KahunaCart stores about a person: their address book, their group, wishlists and reviews, the cookies the storefront sets, and how long it is all kept. It is for store administrators and whoever answers a data-protection request.
Before you begin
- The Customers, Customer groups and Reviews screens are in the KahunaCart admin sidebar.
- Group settings need
kahunacart.settings. Customer actions needkahunacart.customers.vieworkahunacart.customers.manage. Review moderation needskahunacart.products.manage. - The retention sweep runs from the worker tick, so Grav's scheduler must be running. See CLI.
A customer's address book
A customer's saved addresses are where they are now; an order's address is where they were at checkout. Customers keep their own book from their account page (see Customer accounts → The address book), and you can keep it for them from the admin, for the trade customer who orders by phone and never opens their account page at all.
An address you save is checked exactly as one the customer saves, by the same code. It needs at least a street, a town, a postcode or a country. The country has to be one from the list, and for countries that have states, the state has to be one of that country's. Both are stored as their codes, because those are what your tax and shipping zones match on, so an address saved here is taxed and shipped just like one typed at checkout.
Changing the book needs kahunacart.customers.manage and reading it needs kahunacart.customers.view, the same two permissions as the rest of the customer record.
Add or edit a customer's address
- Open KahunaCart → Customers and open the customer.
- Under Address book, click Add address, or Edit on the address you want to change.
- Fill in the address. Country is a list, and so is State for countries that have states.
- Tick Default billing address or Default shipping address to make it the one checkout and the admin's New order form start with.
- Click Save address.
The ⋯ menu on each address has Make default billing, Make default shipping and Delete. Making an address the default takes the default away from whichever address had it. Deleting a default leaves the customer without one instead of promoting another address, and orders already sent to a deleted address keep their own copy of it.
What a customer has spent
The customer list and the customer record show how many orders someone has placed and what they spent. The order count is every order. The money follows the Currency picker, which remembers your choice (the base currency until you change it), because 120,000 yen and 40.00 euro added together is not an amount of money.
A customer who also ordered in other currencies has those beside it, as separate amounts. On the list there is a muted line under the total for each currency (hover it for the number of orders). On the record there is a "Lifetime in EUR" and an "Average order in EUR" beside the base currency's. A customer who only ever ordered in euro shows nothing spent in the base currency and their euro total beside it, so they don't read as having spent nothing, and their average order in the base currency is a dash.
The list sorts by Spent on the total you are looking at. Through the API, GET /kahunacart/customers and GET /kahunacart/customers/{id} take ?currency=EUR to put the figures in euro instead, and each row or customer and metrics block carries other_currencies, a list of { "currency", "orders", "total_minor", "average_order_minor" }. A store whose customers only ever ordered in one currency gets no other_currencies and nothing else different. See Reports and totals.
Lifetime value across currencies
Pick All, in USD (with your own base code) and the list and the record show each customer's lifetime value in the base currency. Each qualifying order is multiplied by the rate it was placed at, the exact amounts are added and the total is rounded once. Orders with no recorded rate are left out and counted beside the total, old base-currency orders included. Sorting by value uses the converted total before the list is split into pages. The order count still counts every qualifying order.
Through the API, currency=all adds total_spent_base, base_currency, converted_orders, excluded_orders and excluded_currencies.
For add-on segments, CustomerSpend::FIELD is total_spent_base. CustomerRepository::statsInBase($customerIds) and CustomerSpend::forMany($customerIds) return its minor-unit values and its coverage counts without changing the report choice. An add-on such as Newsletters has to register this money field itself, label it with the base currency and fill it through that helper. Its existing total_spent still means base-currency orders only. Core offers no customer money export.
Customer groups
A group is a name you can point a rule at: trade, wholesale, staff, members. Each is one row with a code, a label, an optional flat discount and an optional "everybody else" flag.
| Field | Means |
|---|---|
code |
The stable name a rule points at, such as trade. Normalised on write and unique across the store. |
label |
What you call the group, such as "Trade Customers". Shown in the admin, never in a rule. |
description |
A note about who belongs in it. |
position |
Sort order in the admin list; lower comes first. |
is_default |
Treat customers who are in no group as being in this one. At most one group carries it. |
discount_percent |
A flat percentage off everything for this group, as a decimal string such as 10 or 8.375. It comes off everything in the cart like a sale; see A group's discount. Null for a group that is an identity rather than a discount. |
The code is the identity
A rule names the code, never the id and never the label, so you can rename a label without rewriting every rule. Codes are normalised on write: lower-cased, non-word characters folded to hyphens, trimmed, capped at 64 characters. "Trade Customers" becomes trade-customers. Send no code and one is derived from the label the same way.
Warning
Rules pointing at an old code stop matching anybody rather than erroring, so change a code only when nothing points at it.
The default group covers guests too
One group may be the default. It applies to anybody in no group of their own, including a shopper who is not signed in, so the shop window and a logged-out basket price the same.
A store with no groups pays one COUNT per request and nothing else.
Create a customer group
- Open KahunaCart → Customers and choose the Groups tab.
- Click New group.
- Enter a Label. Leave Code empty to derive it from the label, or type one to control it.
- Set Discount percent for a group that is a flat percentage off everything. Leave it empty for a group that is only an identity.
- Tick Default group to have it cover customers who are in no group, guests included.
- Click Save.
The group is available to rules immediately, under its code.
Put a customer in groups
- Open KahunaCart → Customers and open the customer.
- On the Customer groups panel, tick every group they belong to, or none. Each tick saves as you make it.
A customer can be in any number of groups. Over the API, POST /kahunacart/customers/{id}/group with group_ids sets the whole list, and an empty list takes them out of every group.
Groups an add-on puts a customer in
A customer can also be in a group because an add-on says so, while something of theirs is live. The Subscriptions add-on does it for a plan with Customer group while subscribed set: while the subscription is live, the subscriber is in that group. Nobody ticks a box, and nothing is written to the customer's record. See Subscriptions → Access for subscribers.
A group an add-on puts a customer in counts everywhere a ticked one does: price lists, coupon and sale conditions, member-only products, the badges on their account page, and pages only a group can see. It's worked out on every request, so when the subscription ends the customer is out of the group on their next click, with nothing left behind to clean up.
On the customer's record, the Customer groups panel shows it as a line with the add-on's reason, such as "In Members through Subscription to Surf Club Membership", and not as a ticked box, because ticking would store it and the membership would outlive the subscription. When a group is both ticked and granted, the panel adds "Also granted" with the reason, and unticking it leaves the grant in place. The customer list's Groups column and its group filter show only the groups you ticked.
Delete a group
You can delete a group that still has members. Nothing about those members or their orders changes, and the answer names how many customers were freed. Rules naming the deleted code stop matching anybody, so check what points at a code before removing it.
What a group is for
A group fills customer_group, the attribute the condition vocabulary publishes.
- A group-scoped price list is an ordinary price list with an ordinary rule on it. See Price lists and quantity tiers.
- A group-scoped sale is configuration, not a separate feature: the group is named in the sale's conditions.
- A group-only coupon is the same condition on a code:
customer_group is trade. The code works for Trade and is refused for everybody else, with a sentence naming the group. See Coupons.
A group counts only for a customer who is signed in, or whose order staff built and attached them to (a till sale, or an order made in the admin). A Trade customer signs in to get Trade prices. A guest who types a member's email at checkout is judged as a guest, with the store's default group. See Whose customer data a rule reads.
A group's discount
Give a group a Discount (%) and its members get that much off everything in the cart, like a sale made for that group. Nothing else has to be set up.
- It applies while the member is signed in, or when staff attach them to a till or admin order. A group marked as the default gives it to guests too.
- A customer in several groups gets the highest percentage among them, never the sum.
- On something that's already on sale, the member gets whichever of the two takes more off, never both.
- Discount codes come off the discounted price. A code set to leave sale items alone leaves these lines alone too.
- The cart and the receipt show one discount line named after the group.
- It's worked out in the cart, so product pages show the ordinary price. If members should see their prices while they browse, use a price list with Show as a discount in the catalog instead.
A group also decides who may buy a product at all. See Member-only products.
Member-only products
A product can name one or more customer groups, and then only their members may see or buy it.
The case is a concert. A Member ticket at one price that only the Members group may buy, and a General admission ticket at another that anybody may. They are two ordinary products, each with its own price and its own quantity rules; the member one names one or more groups, and that is the whole of the feature.
To a shopper outside those groups a restricted product is absent: from the shop page, from every category and tag page, from the counts beside them, from search, from the related-products blocks and from the sitemap. Not greyed out and not badged. A store that listed it and refused the sale at checkout would have advertised a price it will not honour.
Its URL still resolves, and that is deliberate. Somebody sent the link to a member who has not signed in yet, and a 404 gives them a dead end where one sentence and a sign-in link fixes it. The page renders with no buy button, a "This is for members only" notice and a link to sign in. Add-to-cart refuses the same product, and so does checkout, because a group can change between the two and a guest basket adopted at sign-in can arrive holding a line the customer may no longer buy.
Visibility and restriction are two different axes. Unlisted takes a product out of every listing for everybody while leaving it buyable by URL. A group restriction takes it out of the listings of the people who may not buy it. A member ticket is listed and restricted.
The admin lists every product whatever its restriction, for the reason it lists unlisted products too: a product you cannot see in the admin is a product you cannot fix.
Deleting a group takes its restrictions with it in the same transaction, so a product is never left naming a group that is not there. Deleting a product does the same. A store that has never restricted a product pays one COUNT for the whole feature.
Restrict a product to a group
- Open KahunaCart → Catalog → Products and open the product.
- In the aside, tick one or more groups in the Who can buy picker.
- Click Save.
Pick none — which is what every product does until you say otherwise — and anybody may buy it. Over the API, send customer_group_ids on the product. See Products and catalog.
Map a Grav user group to a customer group
A store whose memberships already live in Grav — a members user group, a subscription plugin that maintains one, a committee that is a Grav group — should not have to keep a second list in the Customers screen. customers.group_map says the two names mean the same people.
customers:
group_map:
- grav_group: members
customer_group: members
- grav_group: staff
customer_group: staff
grav_group is a Grav user group name as Grav spells it. customer_group is a KahunaCart group code. At sign-in, the customer is added to the group of every entry whose Grav group the user belongs to. A code this store does not have is logged and ignored rather than created.
A sign-in only ever adds: a user who matches nothing keeps whatever groups they have, and a user who matches keeps them too, with the mapped groups added beside them. Taking somebody out of a group is done on their record.
The mapping needs a customer record to write to. A Grav account that has never ordered gets its group on the first sign-in after one exists. When account.claim_by_email is on, that is also the sign-in that claims a guest record with the same email. It is off by default; see Customer accounts.
The map is empty by default, and a store that leaves it empty pays one config read per sign-in. See Configuration.
Wishlists
A customer keeps many lists, and they are named, so a list can be a gift registry, a Christmas list or a re-order sheet.
| Field | Means |
|---|---|
name |
What the customer calls it. Defaults to "Wishlist". |
visibility |
private or shared. |
token |
The unguessable string a shared list is reached by. Minted once and kept for the life of the list. |
Each row on a list carries a product, an optional variant, a quantity and a note.
An anonymous list survives a login
A visitor with no account gets a list too, keyed to a cookie named kahunacart_wishlist — HttpOnly, SameSite=Lax, one year. It survives closing the browser and logging out and back in on the same machine.
Signing in merges that list into the account without duplicating anything. A product on both lists stays one row, keeping the account's quantity and note. Merging twice does nothing. A list the account did not already have is claimed instead: it keeps its id and its token, so a link the visitor already shared keeps working.
The worker sweeps anonymous lists nobody has touched for wishlists.anonymous_days. The default is 90 days, and 0 keeps them forever.
Sharing a list
Set a list's visibility to shared and anybody holding the link can read it at {base}/wishlist/{token}. The page carries Cache-Control: no-store.
A private list is invisible even to somebody holding the token, so switching back to private stops the link working.
Moving items to the cart
Move-to-cart goes through the same CartService the buy button uses, so every stock check, product-type rule and plugin veto applies. A product you cannot buy from its own page cannot be bought from a list either. Moving all of them reports how many landed and how many were skipped.
An unpublished product stays on the list and says so, so its owner can still take it off.
Estimating delivery for a list
A customer can price delivery for a whole list before buying any of it. It runs the calculator checkout runs, over a basket that lasts only for the call. Nothing is written.
Limits
| Limit | Value |
|---|---|
| Items on one list | 500 |
| Lists per customer | 20 |
Both refuse with a message the shopper can read rather than truncating silently. A merge into a nearly full list fills the room left.
Set wishlists.enabled to false to remove the routes, the buttons and the account page entirely.
Reviews and ratings
A review is a whole-number rating from 1 to 5, an optional title, a body of up to 5,000 characters, and an author name. It attaches to a product, optionally to a variant, and to a customer if the writer is signed in.
Verified is computed, never set
A review is marked verified when its writer has a placed, paid order carrying this product. The lookup runs when the review is submitted and again when it is approved, keyed off status flags rather than status names.
There is no API field for it and no admin control. A review written before an offline payment cleared earns the badge on approval, when the lookup runs again.
Moderation
reviews.moderated |
reviews.allow_anonymous |
What happens |
|---|---|---|
true |
either | Every review waits for a merchant. |
false |
either | A verified purchaser's review goes live immediately; a stranger's still waits. |
| either | false |
Somebody without an account cannot write one at all. |
A review that has to wait sends a Review waiting for approval message to the channels ticked on that row under Notifications → Store owner, naming the product and the stars and linking to this queue. A review that goes live on its own sends nothing. See the messages that come to you.
Reject, or delete
Rejecting keeps the row, stamped with the time and the moderator's display name frozen at that moment, so you can say later why a review is not on the site.
Deleting removes it outright, leaving no record. Use it for spam only; reject anything you merely disagree with, which keeps the audit trail.
Important
There is no endpoint to write or edit a review from the admin, and there will not be one.
Star ratings and their cache
A product's aggregate is a GROUP BY rating over its approved reviews. A listing page draws one per card, so it is cached with a lifetime and a version stamp.
The stamp moves on every approval, rejection and deletion, and entries expire on their own after five minutes. POST /kahunacart/reviews/rebuild moves the stamp by hand. Reach for it after a database was edited outside the plugin.
Reviews on the product page
Reviews render on the product page by default, below the description and above the related products. The approved reviews, the aggregate and whether this visitor may write one arrive as Twig variables. The markup is partials/kahunacart-reviews.html.twig, overridable in your theme. See Products and catalog.
The page's JSON-LD gains an AggregateRating once there is at least one approved review.
Ask for a review after an order
Set reviews.request_after_days to a number of days and the worker emails each customer that long after their order completes. It is 0 — off — by default. One request per order, ever, and only about products the customer has not already reviewed. See Emails.
Moderate a review
- Open KahunaCart → Reviews. Reviews waiting are listed oldest first.
- Read the review. The verified-purchase badge is already computed and is not editable.
- Click Approve to publish it, or Reject to turn it down.
- Use Delete only for spam.
An approved review appears on its product page and counts in that product's stars on the next request.
Cookies and consent
KahunaCart sets two cookies on a shopper's device and nothing else.
| Cookie | Category | Lasts | What it is for |
|---|---|---|---|
Grav's session cookie (grav-site unless your site renamed it) |
Strictly necessary | The browser session | Keeps the basket and the sign-in together across a page load. |
kahunacart_wishlist |
Functional | 1 year | Addresses an anonymous list so it is still there next visit. HttpOnly, so nothing in the page can read it. |
With a consent banner installed
Install the Consent plugin and both cookies appear in its banner with the descriptions above, translated like any other KahunaCart string, and the session cookie under the name your site uses. There is nothing to configure. KahunaCart declares the cookies when the banner asks, so turning wishlists off stops the wishlist cookie being declared.
The wishlist cookie is then gated on consent. Until a shopper allows the functional category it is not written at all. Signing in still gets them a wishlist, because a list attached to an account needs no cookie. Withdrawing consent later expires the cookie on the server.
Without one, nothing changes
The gate answers yes when there is no consent plugin installed, when it is switched off, and if it fails.
The retention schedule
Four windows decide how long the store keeps what. Every one is off by default, so upgrading a live store deletes nothing until you say what you want kept and for how long.
| Rule | Config key | Covers |
|---|---|---|
| Trash pending orders | retention.trash_pending_after |
Placed orders whose payment never arrived. |
| Trash failed orders | retention.trash_failed_after |
Orders whose payment failed. |
| Anonymise completed orders | retention.anonymise_completed_after |
Finished sales. The customer details go, every amount stays. |
| Delete inactive customers | retention.delete_inactive_accounts_after |
Inactive customers, erased through the blockers below. |
Each window is a number plus a unit of days, weeks, months or years. A month is 30 days and a year is 365. A zero window, a negative one, one that is not a number, and an unrecognised unit all read as off.
A log row is written per rule per run, dry runs included.
Completed orders are anonymised, not deleted
An order is a financial record: its total is in last year's VAT return. Anonymising replaces the customer's identifying fields and leaves every amount as it was, so revenue, tax and product reports come back the same numbers.
| On an anonymised order | What happens |
|---|---|
| Email, name, company, street, phone, postcode, town | Removed. The name reads [removed] rather than a plausible fake. |
| Country and region | Kept. They are what the order's tax was computed from. |
| Merchant notes | Removed. |
| Order history | Kept, and the anonymisation joins it. |
| Every amount, every line, every adjustment | Untouched. |
Trashing means archiving. Neither trash rule ever deletes anything.
The rules read what a status means, not what it is called
Every rule asks which values of a column carry a given flag and builds its query from the answer. No status name appears in the sweep, so you can rename, translate or add a status and it keeps doing what you asked. The flags come from your order status registry.
| The rule wants | It asks the registry for | Which is, on a store that has not changed its statuses |
|---|---|---|
| A placed sale | Order statuses flagged complete | completed, archived |
| Somewhere to file an order away | The first enabled order status flagged complete and marked final | archived |
| Money that has not arrived | Payment statuses flagged new, other than failed |
pending |
| Money that arrived | Payment statuses marked counts as paid | paid, partially_refunded |
| A payment that was refused | The failed payment status |
failed |
| Goods still owed | Fulfillment statuses flagged new | unfulfilled, partial |
Two consequences. Add a status flagged complete and the anonymise rule reaches those orders. Add a payment status flagged new and the trash pending rule sweeps it: you cannot mark an added status as "the payment was refused", so a second refused-payment status joins the pending sweep rather than the failed one. failed is the one status name the retention code knows, and it is a built-in row you cannot delete.
Erasing an inactive customer runs the same blockers
The fourth rule runs through the same deletion blockers an operator hits. A blocked customer is skipped and looked at again on the next run.
Chunked, idempotent and logged
Each rule touches at most retention.chunk rows per run, 200 by default, so a first sweep over years of orders catches up over successive worker ticks instead of holding the queue.
Every rule is idempotent: a second run finds nothing left to do. One rule failing does not take the other three with it. Run rows older than a year are purged.
Switch a retention window on
- Run
bin/plugin kahunacart retention:run --dry-run. On an unconfigured store it says so and exits. - Set the window you want in
user/config/plugins/kahunacart.yaml, for exampleretention.anonymise_completed_after: 24withretention.anonymise_completed_unit: months. - Run
bin/plugin kahunacart retention:run --dry-runagain and read the table. It names the cutoff date and the orders it would touch, rule by rule, and writes nothing but log rows. - When the numbers are what you expected, run
bin/plugin kahunacart retention:run, or leave it to the worker.
Caution
There is no undo. Dry-run first, every time, on every store.
Reassign orders to another customer
Move every order from one customer to another: a company account split in two, or a duplicate record two checkouts created.
The orders take their history, notes, refunds and download grants with them, because those hang off the order rather than the customer. Each order gains a history entry naming the move.
Reassign orders
- Open KahunaCart → Customers and open the customer whose orders are moving.
- Click Reassign orders and choose the customer to move them to.
- Tick Dry run and confirm, to see exactly what would move.
- Untick it and confirm to move them.
From a terminal, bin/plugin kahunacart orders:reassign [email protected] [email protected] --dry-run does the same and takes an id or an email address on either side.
Erase a customer
Erasure is what you do when somebody exercises their right to be forgotten. It anonymises everything pointing at a person and deletes no order.
Deletion blockers run first, and they name themselves
Every blocker is collected before a row is written, and one blocker stops the lot. The refusal names them all.
| Blocker | Raised by | Means |
|---|---|---|
open_order |
Core | An order that is placed and either still owed money or still owing goods, named by its number. |
download_grant |
Core | A download the customer paid for and can still fetch. |
| anything else | An add-on | Whatever that add-on holds on this customer's behalf. |
Add-ons report through onKahunaCartCollectErasureBlockers: an active subscription, an issued licence, an unspent credit balance. A listener that throws is recorded as a blocker itself. See Events.
GET /kahunacart/customers/{id}/erasure-blockers answers the same question and writes nothing. It is what the confirm dialog renders.
What erasure changes
| Touched | What happens |
|---|---|
| Every order | Anonymised exactly as the retention sweep does it. No order is deleted, no amount changes. |
| The address book | Removed. |
| The sign-in account | Removed. |
| Wishlists | Removed. |
| Reviews | The identity goes; the text stays, and so does the rating. The verified badge goes with the identity. |
| The customer row | Survives, as a number. |
Run an erasure
- Open KahunaCart → Customers and open the customer.
- Click Erase customer. The dialog lists every blocker, or says there are none.
- Clear each blocker: refund or complete the open order, revoke the download grant, cancel whatever an add-on named.
- Confirm.
Caution
There is no undo, and no export is taken on your behalf. Take one first if the customer asked for their data as well as its removal.
Export the customer list
The Export… menu on Customers downloads every customer as a CSV, in the importer's own column names, so the file imports back with nothing to map. It needs kahunacart.export as well as kahunacart.customers.view.
To move customers along with their addresses, groups, orders and sign-in passwords, use a store bundle instead. See Export and restore.
Configuration
Every key in this chapter is in Configuration with its default: wishlists, reviews, the retention schedule and the two rate-limit buckets for wishlist writes and review submissions.
Storefront routes
| Method | Route | What it does |
|---|---|---|
GET |
{base}/account/wishlist |
The signed-in customer's lists, with the share links and the delivery estimate. |
POST |
{base}/account/wishlist |
Every list action: create, rename, set visibility, delete, remove an item, change a quantity, move one or all to the cart, estimate delivery. |
GET |
{base}/wishlist/{token} |
A list its owner has shared. Read-only, and Cache-Control: no-store. |
Reviews are written by posting to the product page, limited by security.rate_limits.review_submit. Wishlist writes are limited by security.rate_limits.wishlist_write. See Hardening.
The API
| Route | Permission | What it does |
|---|---|---|
GET /kahunacart/customer-groups |
kahunacart.settings |
Every group with its code, label, discount and member count. |
POST /kahunacart/customer-groups |
kahunacart.settings |
Create a group. |
PATCH /kahunacart/customer-groups/{id} |
kahunacart.settings |
Change a group. Only the fields you send are written. |
DELETE /kahunacart/customer-groups/{id} |
kahunacart.settings |
Delete a group. Answers with how many customers were freed. |
GET /kahunacart/customers/{id}/addresses |
kahunacart.customers.view |
A customer's saved addresses, defaults first. |
POST /kahunacart/customers/{id}/addresses |
kahunacart.customers.manage |
Add an address. The fields are the ones checkout uses (name, company, address1, address2, city, region, postcode, country, phone), plus label, is_default_billing and is_default_shipping. A 422 says what is wrong with it. Answers with the new row and the whole book. |
PATCH /kahunacart/customers/{id}/addresses/{addressId} |
kahunacart.customers.manage |
Change an address. Only the keys you send change; false on a default flag stops it being that default. |
DELETE /kahunacart/customers/{id}/addresses/{addressId} |
kahunacart.customers.manage |
Delete an address. No other address is promoted in its place. |
POST /kahunacart/customers/{id}/addresses/{addressId}/default |
kahunacart.customers.manage |
Make an address the default for kind, billing or shipping. |
GET /kahunacart/countries |
kahunacart.orders.manage or kahunacart.customers.view |
Every country an address may name, and the states of the countries that list them. |
POST /kahunacart/customers/{id}/group |
kahunacart.customers.manage |
Put one customer in a group, or a null group_id to take them out of every group. |
GET /kahunacart/customers/{id}/wishlists |
kahunacart.customers.view |
One customer's lists and what is on them. Read-only. |
GET /kahunacart/customers/{id}/erasure-blockers |
kahunacart.customers.manage |
What is stopping this customer being erased. Writes nothing. |
POST /kahunacart/customers/{id}/erase |
kahunacart.customers.manage |
Erase a customer. Refused, with the blockers named, when anything holds data on their behalf. |
POST /kahunacart/customers/{id}/reassign-orders |
kahunacart.customers.manage |
Move every order to another customer. Takes dry_run. |
GET /kahunacart/retention |
kahunacart.customers.view |
The four windows and what each rule last did. |
POST /kahunacart/retention/run |
kahunacart.customers.manage |
Run the sweep now. Takes dry_run. |
GET /kahunacart/reviews |
kahunacart.products.manage |
Reviews for moderation, oldest first, narrowable by state or product. |
POST /kahunacart/reviews/rebuild |
kahunacart.products.manage |
Move the aggregate version stamp so every star rating recomputes. |
POST /kahunacart/reviews/{id}/approve |
kahunacart.products.manage |
Publish a review and recompute its verified badge. |
POST /kahunacart/reviews/{id}/reject |
kahunacart.products.manage |
Turn a review down, keeping the row and the moderator's name. |
DELETE /kahunacart/reviews/{id} |
kahunacart.products.manage |
Remove a review outright. For spam. |
MCP tools
Each route above has a matching tool: list_customer_addresses, create_customer_address, update_customer_address, delete_customer_address, set_default_customer_address, list_countries, list_customer_groups, create_customer_group, update_customer_group, delete_customer_group, set_customer_group, list_customer_wishlists, get_erasure_blockers, erase_customer, reassign_orders, get_retention_policy, run_retention, list_reviews, approve_review, reject_review, delete_review and rebuild_review_ratings. See Run the store from an AI assistant.
From the command line
| Command | Does |
|---|---|
bin/plugin kahunacart retention:run |
Run the four retention rules now. --dry-run reports and changes nothing; --limit=N overrides retention.chunk. |
bin/plugin kahunacart orders:reassign |
Move every order between two customers. --from and --to take an id or an email; --dry-run lists what would move. |
Both are documented in full in CLI.
Events
Nine events fire from this chapter. Seven are observation only; onKahunaCartCollectErasureBlockers and onKahunaCartCustomerGroupsGranted are extension points.
| Event | Carries |
|---|---|
onKahunaCartWishlistItemAdded |
wishlist_id, customer_id, product_id, variant_id, quantity |
onKahunaCartWishlistMerged |
customer_id, from_wishlist_id, into_wishlist_id, moved |
onKahunaCartReviewSubmitted |
review_id, product_id, customer_id, rating, state, verified |
onKahunaCartReviewModerated |
review_id, product_id, state, previous_state, moderated_by |
onKahunaCartCollectErasureBlockers |
customer_id, customer, blockers |
onKahunaCartCustomerGroupsGranted |
customer_id, grants |
onKahunaCartCustomerErased |
customer_id, orders |
onKahunaCartOrdersReassigned |
from_customer_id, to_customer_id, orders |
onKahunaCartRetentionSwept |
dry_run, results |
Related
- Customer accounts — the account area the wishlist page lives in
- Price lists and quantity tiers — what a group-scoped price list is, and what it does not do yet
- Coupons — codes, and how a group narrows one
- Rules and conditions — the vocabulary
customer_groupbelongs to - Products and catalog — reviews on the product page and the aggregate rating in its structured data
- Emails — the review-request message
- Orders and the admin — the Customers section these actions are reached from
- CLI —
retention:runandorders:reassign - Configuration — every key in this chapter with its default
- Events — registering an erasure blocker from an add-on