B2B Commerce
Version 1.0.0
Added
- GraphQL read expansion and opt-in write mutations.
b2bContextgainsdepartments/departmentBudget,approvalTiers(plus a per-requeststepsladder onmyApprovalRequests),catalogCriteriaandstatement;quotesandmyApprovalRequests/pendingApprovalsgainpoNumber. A new, opt-in and off-by-defaultb2bContext.writeschema component registerssetPoNumber;requestQuote/acceptQuote/declineQuote;submitForApproval/approveOrder/declineOrder; andcreateOrderList/renameOrderList/addOrderListItem. Every mutation requires an authenticated member, accepts no company id, and is a thin wrapper over the existing service — four-eyes, cross-company and budget/credit guards apply unchanged. No schema/table change. - Company-specific catalog: restrict which products a company's members may see and buy via a per-company product condition (control panel). Enforced server-side by an add-to-cart veto across every add path;
craft.b2b.catalogCriteriaprovides convenience storefront filtering. Empty condition = full catalog (dormant until configured; ridesenableCompanies). - Order on behalf of (sales reps). A sales rep can act as a member of a company they are
assigned to and place orders in that member's name, without gaining any of their own elevated
rights: the active identity becomes the member, so every existing storefront guard (budget,
credit limit, approval, PO number, account status) is enforced against the member — a rep can
never place an order the member could not place themselves. Built on Craft's native user
impersonation, so a rep needs both the B2B
Order on behalf of a company(b2b-commerce:orderOnBehalf) permission and Craft's nativeImpersonate userspermission; the plugin's own assignment scope decides which companies a rep may act for, independently of the impersonate permission. Assign or remove reps and review a per-company impersonation audit log on the company's Sales reps page in the control panel. Completed on-behalf orders are stamped with the placing rep and logged. Newb2b_rep_companiesandb2b_impersonation_logtables plus aplacedByRepIdcolumn (schema 1.1.4). - Merchant-initiated quotes. A merchant or sales-rep can build a cart in Commerce's native order editor and click Send as B2B quote to send a price-frozen quote to a customer — linking the customer's company automatically (or via a picker), emailing the accept/decline links plus the quote PDF, and recording the quote's origin. The customer accepts through the existing token flow and checks out at the frozen prices; the same buyer-mutation veto applies.
- Quote and order/invoice PDF documents, rendered via Commerce's native dompdf service, with overridable templates, permission-gated CP downloads and token/member-guarded storefront downloads. No schema change.
Account statements & dunning. A company's account statement — its outstanding invoice orders bucketed by aging (current, 1–30, 31–60, 61–90, 90+ days) — is computed on demand from the existing outstanding-balance logic; there is no statement table. Exposed via
craft.b2b.statementon the storefront (null for a visitor without a company) and a read-only Statement page on the company's control-panel screen (Manage companiespermission), with a Download PDF link rendered through the Phase-16 PDF service (statementPdfTemplatesetting; bundled default ships atsrc/templates/pdf/statement.twig, overridable copy atexamples/templates/b2b/pdf/statement.twig). A newb2b-commerce/dunning/runconsole command emails overdue-invoice payment reminders, gated by theenableDunningsetting (off by default — it is the only feature that autonomously emails customers) and adunningOffsetsday-offset list (default7, 14, 30days past due). Each offset is dunned at most once per invoice, guarded by a newb2b_dunning_logtable (schema 1.1.6) and a run-level mutex; a per-company or per-send failure is logged and counted but never aborts the rest of the run. Cron-able, for example daily:0 6 * * * cd /path/to/project && php craft b2b-commerce/dunning/run >> /dev/null 2>&1
Version 1.0.0-beta.3
Added
- Company-specific pricing. Assign a company to a Craft user group; the plugin keeps every approved member of that company in the group (and never touches their other group memberships). Build a Craft Commerce Catalog Pricing Rule with a customer condition matching that group and each company automatically sees its own wholesale prices — no custom pricing engine, all native Commerce. Members of pending or blocked companies are not placed in the group, so unapproved accounts never get wholesale pricing.
- Per-member spending budgets. A budget caps how much a single team member may spend
for their company within a period, independently of (and on top of) the company credit
limit. Set each member's amount and period (monthly, quarterly, yearly, or a
never-resetting None) on the company's Members page in the control panel — or
leave a member with no budget for unlimited spending. Spend is the member's completed
orders for that company in the current period (on every gateway, minus settled
cancelled/refunded orders). The member's own budget is exposed on the storefront as
craft.b2b.memberBudget({ amount, period, spent, remaining }), with an exampleb2b/account/budget.twigpage. - Payment-time enforcement — the approval and credit gates now refuse the charge up
front. A gated purchaser with no approved approval, an over-budget member, or a
pay-on-account order over the company credit limit is now refused on
Payments::EVENT_BEFORE_PROCESS_PAYMENT, before Commerce creates a transaction or the gateway authorizes — so a buyer paying by card is never charged for an order that cannot be placed. This layers on top of (and shares its decision logic with) the existing completion-time backstop, which still catches zero-payment and free-order completions. Console and control-panel payments and completions bypass both layers by design (the merchant override). - Read-only GraphQL API for headless storefronts. Exposes the
Companyelement type (companies/company/companyCountqueries, including custom fields) and a top-levelb2bContextquery returning the authenticated user's company, role, spending budget, company credit summary, members, quotes, approvals and order lists. Both are gated behind opt-in B2B Commerce schema scopes;b2bContextis always scoped to the caller's own company and never exposes another company's data. No mutations — writes stay on the action controllers. The View companies (b2bCompanies.all) scope exposes only company identity (name, registration number, status); sensitive financial fields (tax ID, credit limit, payment term, pay-on-account, approval threshold) resolve tonullunless the caller reads their own company or the separate, opt-in View company financial fields (b2bCompanies.financials) scope is enabled — so enabling View companies on a public token can no longer leak every company's financials. - Manage a company's contact persons (members) directly from the control panel: add or invite a member with a role, change a member's role, and remove a member — from the company's Members page. Reuses the same guards as the front-end team management (approved company, duplicate-membership check, last-admin protection).
- A "New company" button on the control-panel Companies index, so companies can be created from the CP.
Changed
- The company edit screen now renders the core fields (name, registration number, VAT ID, credit limit, payment term, pay-on-account, approval threshold) in the main content area instead of only the sidebar, so the screen is never empty and the field-layout designer still manages any custom fields you add.
- Quotes and Approvals are now Craft elements. Their control-panel lists (B2B → Quotes,
B2B → Approvals) are native element indexes — status-source sidebar with colored dots,
keyword search, sortable columns and export. The element identity sits around the existing
business record:
orderIdstays the business key every enforcement guard reads, so all quote and approval enforcement is unchanged. The Approvals index stays deliberately read-only monitoring — approval decisions belong to the company's own approvers, never the store operator.
Version 1.0.0-beta.1
First public release. B2B Commerce turns a standard Craft Commerce store into a wholesale/business storefront, built around five pillars: company accounts, quotes, order approvals, pay on account and quick order.
Added
Company accounts
- Company element with its own control-panel section, statuses (pending, approved,
blocked), roles (admin, purchaser, approver) and a
Manage companiespermission. - Frontend registration: businesses register from the storefront, creating a pending company plus its admin user and notifying a store manager by email. Includes a honeypot anti-spam field and a cancelable before-register event for custom checks.
- Approval flow: approve or block a company from the control panel with element
actions. Approving activates its members and emails them the
B2B: company approvedmessage alongside an activation email so they can set a password. - Team management for company admins from the storefront: invite colleagues, change roles and remove members, guarded so a company always keeps at least one admin.
- Shared address book: native Craft
Addresselements owned by the company, so the whole team sees and reuses the same addresses. - Order–company linking: completed orders are linked to the buyer's company, exposed
as
order.b2bCompany, with a checkout backstop that refuses completion for guests and unapproved or blocked accounts. - Control-panel company pages with per-company member and order overviews, plus a configurable custom-field layout.
- Price visibility: optionally hide prices and block ordering for guests and unapproved accounts.
Quotes
- Request for quote: an approved buyer turns a cart into a quote request; the request survives as a non-completed order while the buyer keeps a fresh cart.
- Quote workbench (B2B → Quotes,
Manage quotespermission): every quote newest-first, filterable by status, with Mark sent (optional validity date) and Decline (with reason) actions. - Frozen prices: sending a quote pins the order's prices (Commerce
recalculationMode = none, persisted and restored on load) so the buyer pays exactly what the merchant left on each line item. - Token-authorized accept and decline from an emailed link: accepting adopts the frozen order as the buyer's cart to check out against, declining records a reason. An unknown token and another company's token return the same generic message so a token cannot be probed.
- Cart-mutation guard that vetoes buyer-side line-item changes on an open or accepted quote order, so a reactivated quote cart cannot be re-priced; merchant edits stay free.
- Storefront
craft.b2b.quotesoverview andcraft.b2b.quoteByToken(token)accept data. b2b-commerce/quotes/expireconsole command to lapse overdue quotes, plus lazy expiry on the first accept/decline touch.
Order approvals
- Spending threshold: a per-company
approvalThresholdholds a purchaser's over-threshold order for a company approver. The threshold in force is snapshotted onto the request; anullthreshold runs no gate and the comparison is strictly greater-than. - Submit / approve / decline driven by the company's own approvers, enforcing the four-eyes rule (an approver can never approve their own submission). Approving an invoice order places it on account; approving other orders emails the requester a resume-checkout link; declining records a reason that reaches the requester by email.
- Completion backstop that enforces the gate server-side even when the storefront submit step is bypassed, applied equally to accepted-quote orders. Auto-approves a pending order that no longer needs approval at completion instead of blocking it.
- Monitoring overview (B2B → Approvals,
Manage approvalspermission): read-only, newest-first, filterable by status, showing company, requester, resolver, snapshotted threshold, order total and decline reason. Decisions belong to the customer's approvers. - Storefront
craft.b2b.pendingApprovals(approver queue) andcraft.b2b.myApprovalRequests(a requester's own requests).
Pay on account
- Offline "pay on account" gateway that lets approved companies with pay-on-account enabled check out on invoice; the order completes unpaid so you can capture and invoice out of band.
- Per-company credit limit enforced on the storefront: the gateway is only offered while an order fits inside the remaining credit, and the limit is re-checked under a per-company lock at completion. An empty limit means no credit room, not unlimited.
- Credit summary: outstanding balance and available credit exposed as
craft.b2b.creditSummaryon the storefront and shown to merchants on a company's Orders page, alongside a per-order paid / partially paid / unpaid status. order.b2bPaymentDueDate: a completed invoice order's payment due date, derived from the order date plus the company's payment term.
Quick order
- Paste SKUs (one per line, Excel-style) to add many products to the cart at once, with per-line error reporting keyed to the original line number.
- CSV upload fed through the same SKU parser as the textarea.
- Re-order a completed order's still-available line items — the buyer's own or a colleague's.
- Shared, company-scoped order lists with a create/rename/delete flow and an item
editor, exposed as
craft.b2b.orderListsandcraft.b2b.getOrderListItems.
VAT & platform
- EU VAT ID validation and reverse charge built on Commerce's native VAT support:
validates the company VAT ID against VIES at registration/save time and carries it onto
the order at checkout so Commerce applies the reverse charge automatically. Configurable
VIES outage policy (lenient/strict) and a
b2b-commerce/tax-id/revalidateconsole command. b2b-commerce/team/assign-roleconsole command to recover a company that lost its last admin, andb2b-commerce/seedto bootstrap demo data.- Example storefront templates for registration, price display, team, address book, quick order, re-order, order lists, quotes and approvals.
craft.b2btemplate variables includingcompany,teamMembers,companyAddresses,canViewPricesandcanPurchase.- Dutch translations for all control-panel and frontend strings.
Changed
- The
enableInvoicing,enableQuotes,enableApprovalsandenableQuickOrdersettings are functional: turning one off cleanly disables its endpoints and features (and disarms the approval completion backstop forenableApprovals).
Fixed
- Surface company field-layout save failures instead of discarding them silently.
- Exempt already-paid orders from the checkout purchase backstop, so a blocked company's paid order can still be completed.
- Link orders to their company after completion is persisted, so the association survives the completion transaction.
- Stand down the line-item freeze during storefront completion saves.
markAsComplete()salts the line-item options signature while the stored row still carries the cart signature, which the buyer-mutation veto read as a phantom edit and used to block the completion of every accepted quote and approved order (leaving a paid-but-incomplete order). Completion never changes the line-item set, so the guard now stands down for a completing or completed order. - Default the pay-on-account (invoice) gateway to the authorize-only payment type it
supports. It previously inherited Commerce's
purchasedefault and refused every payment with "Gateway doesn't support purchase".