Troubleshooting
Start here:
php craft forklift/maintenance/doctor
Every check in it is something that produces no error and no log line but quietly stops Forklift doing what you think it is doing.
Prices
A contract price is not being applied
In order of likelihood:
- The licence is Lite. Price lists are Pro. On Lite they are ignored, not half-applied, and
the price-lists screen and
doctorboth say so. - The row's SKU does not exist in your store. An import keeps rows it could not match, and
they price nothing.
doctorcounts them;forklift/price-lists/resolve-skusre-matches after a catalogue rebuild. - The list is out of its date range, or disabled. The index shows Out of date range rather than Yes in the Active column.
- The company is not assigned to it. Either tick Applies to every company or add them.
- A higher-priority list won. Remember the rule: the highest-priority list with any matching entry governs, and then the most specific entry within it. Check the preview.
Price preview answers all five at once, and it runs the real resolver:
php craft forklift/price-lists/price FIX-M8-100 ACME 250
The price is higher than I expected
Look at suppressedReason in the preview. Two settings can decide against your list:
- Allow a contract price above the list price is off by default, so a row that would charge more than the public pays is ignored and the list price used. Almost always this means two columns were swapped in the import.
- Let a promotion beat a contract price is on by default, so a cheaper public sale wins.
The price in the basket disagrees with the product page
It cannot, if both go through craft.forklift.price(). If they disagree, the product page is
reading variant.price or variant.salePrice directly — those are Commerce's numbers, not the
buyer's.
Checkout
The button says one thing and checkout does another
It cannot, if the button reads craft.forklift.verdict(). A hard-coded "Place order" will
disagree with the gate; verdict.actionLabel will not.
A buyer cannot check out and there is no message
Render the verdict:
{% for message in craft.forklift.verdict().allMessages %}
<p>{{ message }}</p>
{% endfor %}
blockingMessages is only the sentences actually stopping the order; allMessages includes the
ones that are merely true, such as being over a credit limit.
An order needed approval and went through anyway
The licence was Lite at the time. Forklift fails open there deliberately — a store that cannot check out is worse than one that lets an order through — and marks every such order. Find them in Forklift → Approvals, or:
php craft forklift/maintenance/doctor
Nobody is getting approval emails
- Check the company has somebody with an approver or administrator role. An account with a
threshold and no approver strands every order over it, and Forklift logs a warning when it
happens.
doctornames it. - Check Email the approvers is on.
- Check Craft's own mail settings. Forklift's emails are Craft system messages — edit them at Settings → Email → System Messages.
Sending failures are logged, never thrown: an approval that was granted but whose confirmation bounced is an inconvenience, and an exception that rolled the decision back would be a support call.
Credit and terms
The Purchase Order option is not appearing at checkout
It withdraws when any of these is true:
- the licence is Lite (fails closed on purpose — no new credit can be opened);
- the order has no company;
- the company does not have trades on terms ticked;
- the account is on hold or closed;
- the order would take the account past its credit limit.
gateway.getUnavailableReason(order) gives you a sentence for the last three.
An order on account has no charge on the ledger
Post-completion bookkeeping is deliberately wrapped so that a failure cannot take down the request the customer is looking at — which means a failure leaves the invoice unraised. Put it right:
php craft forklift/credit/repair --dryRun
php craft forklift/credit/repair
Idempotent: an order that already has its charge is left alone.
The aged statement does not match what I expect
Payments are allocated oldest charge first, and nothing about the allocation is stored — it is recomputed every time, so a backdated payment re-ages the account. Money left after every charge is settled shows as a negative in the current column, because an account in credit should read as being in credit.
Check the aging buckets ascend and do not repeat. The statement walks them in order.
Quotes
"The quote was saved but its cart could not be built"
The quote keeps its previous status rather than being marked sent, because a quote marked sent
whose link goes nowhere is worse than one still in the queue. The usual cause is a line whose
purchasable has been deleted since the request. Check the Craft log for
Quote Q-…: could not add purchasable.
Free-text lines with no product are skipped when the basket is built — fold them into the delivery figure.
The buyer's link says the cart could not be retrieved
Either the link has expired (Commerce's loadCartUrlExpiry, a week by default) or the quote has
been re-sent since, which discards the previous basket so an older email cannot buy at an older
price. Re-send it.
The quoted prices reverted to list
They should not — quote pins outrank everything in the resolver, and the basket carries the quote
on its Forklift row. If it happens, the basket was copied rather than loaded: the copy has no
quoteId, so nothing knows it came from a quote.
Tax
Tax is still being charged for an exempt customer
Work through:
- Is the certificate approved? Uploaded is not approved.
- Has it expired? An expired certificate exempts nothing from the moment it expires, not from the next sweep.
- Does it cover the delivery address? A country and a state must match both.
- Is Honour exemption certificates at checkout on?
- Is another plugin providing the tax engine? Forklift only replaces Commerce's own. Check the log for "Another plugin owns the tax engine".
Tax was removed and should not have been
Check the certificate's jurisdiction. A certificate with no country covers everywhere, which is right for a charity registration and wrong for a state resale certificate.
The control panel
A screen 403s
Pro-only screens — quotes, approvals, credit — refuse a Lite licence by design. Otherwise check the Forklift permissions on the user's group. Seeing what a customer negotiated, and what they owe, are separate permissions from maintaining an address book on purpose.
The Forklift panel is missing from an order
It only renders when there is something to say: a company, a PO number, an approval, a quote, or a bypassed approval. A retail order shows nothing.
Data
I deleted a price list and an old order changed
It should not have. Forklift's foreign keys null the reference rather than cascading for anything that records something that happened — deleting a price list does not erase the invoice it priced, and deleting an order does not erase the payment that settled it. What does cascade is a row that only exists because of its parent: members with their company, entries with their list, quote lines with their quote.
Uninstalling
Drops Forklift's twelve tables. Commerce orders, customers and products are untouched, but the B2B record is not — which account an order belonged to, its PO number, its approval, the ledger and the certificates. Export first:
php craft forklift/price-lists/export --list=acme prices.csv
php craft forklift/credit/statement ACME