B2B ecommerce on Solidus: price lists, quotes, net terms and VAT
TL;DR
Difficulty: Intermediate
A trade shop is not a consumer shop with a discount. Make the company the customer, with users, roles and addresses under it. Put contract prices in explicit price lists through the Solidus price selector, never in promotions, which stack. Treat a quote as an order held before checkout with frozen prices and an expiry. Build pay on invoice as a payment method with a credit check against balances from the ERP. Decide VAT per order by delivery address: Irish VAT at home, zero rated EU supplies only with a valid VAT number and correct VIES returns. Store invoices as structured data, because Irish e invoicing starts in November 2028.
Wholesale buying works differently from retail: companies with several buyers, negotiated prices, quotes, invoices on 30 or 60 days and VAT that depends on where the goods go. In this article I map each of those onto Solidus with an invented Cork catering wholesaler, show what Solidus ships today and what is custom code, go through Irish late payment law, intra Community supplies and Revenue's e invoicing phases, and finish with a build order, costs and the mistakes that sink B2B shops.
Wholesale is not retail with a discount
Ask the big platforms about B2B and you tend to get one of two answers. Either it is an app that bolts a login wall and a percentage discount onto a consumer shop and calls it wholesale. Or it is the upper plan, where the features exist but the price grows with them.
Neither fits a wholesaler with a few hundred trade customers. What that business needs is a shop that understands five things: a company is not a person, a price is the result of a negotiation, an order often starts as a quote, most customers pay on invoice, and the tax treatment depends on where the goods actually go.
Take an invented business to keep this concrete. Lee Valley Catering Supply (leevalleysupply.example) is a Cork wholesaler of catering disposables and small kitchen equipment with about 400 trade accounts: independent restaurants, a hotel group in Galway with twelve hotels, school caterers in Limerick, and a handful of customers in the Netherlands and Northern Ireland. Today its sales team works from phone calls, PDFs and a spreadsheet. I will use Lee Valley throughout.
One warning first. Solidus, the open source Rails ecommerce framework, does not have a "B2B mode". The current release line is 4.8, with 4.8.1 out on 5 November 2026.1 What it has is a set of plain models for orders, prices, users, payments and taxes, and an architecture that expects you to extend them. Every feature in this article is something built on top of those models. That is more work than ticking "enable wholesale". It is also why the result can match how your customers actually buy. If you have not read my platform comparison, the short version is that Solidus is the platform you own outright, and B2B is where owning it pays off soonest.
How trade buying differs
List what Lee Valley's sales team does in a day and the gap with a consumer shop becomes obvious.
A customer is a company with several people. The chef who places the order is not the operations manager who approves it, and neither of them pays the invoice. The hotel group has a rule that anything over €5,000 needs a second signature.
Prices are negotiated. One restaurant pays list less 12%. The hotel group pays list less 20% on disposables and list on equipment, because that is what was agreed three years ago. The school caterer has fixed unit prices for a year under a framework contract. The Dutch customer wants its invoices in English with its own purchase order numbers.
Orders often start as quotes. The hotel group asks for a price on two pallets of takeaway packaging delivered to its Galway hotel, a rep works out the freight, sends a quote, the group gets it approved, and three weeks later it becomes an order. The quote number goes on the purchase order, and the invoice has to reference both.
Almost nobody pays at checkout. They pay on invoice, 30 or 60 days, against a credit limit someone in accounts set years ago.
Repeat orders dominate. The same fifty items every fortnight, in cartons of a fixed size, typed in by product code rather than browsed.
Delivery goes to many sites. One account, twelve hotel kitchens, each with its own address and delivery window.
The tax treatment depends on the destination. A delivery to Limerick carries Irish VAT. A delivery to Rotterdam can be a zero rated intra Community supply. A delivery to Britain is an export. Get it wrong and the invoice is wrong.
And all of it has to talk to an ERP or accounting system, because that is where stock, prices and balances actually live. The shop is a window onto it.
The account, not the user, is the customer
The first modelling decision carries everything else, and it is the one consumer platforms get wrong by default. In a consumer shop the user is the customer. In B2B the company is the customer and the users belong to it.
In Solidus I model this as an account (some businesses call it a company or a customer group), and every user belongs to one account with a role: buyer, approver, admin, and sometimes a read only role for someone who needs order history but places no orders. Addresses belong to the account, so a new buyer at a branch sees the right delivery sites at once. Orders belong to the account, with the user recorded as the person who placed them. Price list, credit limit, payment terms, VAT number and the assigned sales rep all sit on the account.
This sounds obvious. It is the one thing I insist on before any other code, because retrofitting it onto a shop where users are customers means touching every order, every address and every price lookup later. That retrofit costs more than building the account model on day one.
Approval chains live here too. The simplest useful version is a threshold on the account: orders above a value go into a pending approval state and the approver gets a notification. Per buyer limits and per product family sign off are possible, but I would not build them before a customer has asked twice. Most trade customers need the threshold and nothing else.
Price lists, and why promotions are the wrong tool
Solidus prices are explicit records. Each variant has prices, and each price carries a currency and, optionally, a country.2 When the shop needs a price, a price selector picks the matching one using a small pricing options object, which by default holds the currency and the country of the tax address. Both classes are configurable: the store setting variant_price_selector_class names the selector, and the selector names its pricing options class.3 That is the extension point. You give the pricing options a third attribute, the account's price list, and teach the selector to prefer a price from that list. Every price lookup in the cart, the admin and the API then resolves the contract price without further work.
So a price list is a named set of explicit prices per variant and currency. Contract tier A gets one, tier B another, and the list price is simply the price with no list attached, which is what a visitor who is not logged in sees.
Is there an extension that does this for you? I checked the Solidus organisation and the community extensions on GitHub in November 2026. There is no maintained price list extension. The nearest maintained piece is solidus_volume_pricing, which sets prices by quantity range per variant and supports Solidus 4.4 Contract price lists are custom code, roughly a few hundred lines with tests.
The alternative you see on many shops that pretend to do B2B is to leave the list price alone and apply a promotion for the customer group. Do not do this. Promotions are built to be temporary, conditional, stackable and shown as a discount. Contract prices are none of those. A chef does not want to see "list €84.00, your discount 12%". They want to see €73.92 on the product page, in search, in the export, on the quote and on the invoice, the same number every time.
Promotions also stack. Solidus now ships a reworked promotion system as its recommended one, which is better structured than the legacy system, but stacking a contract discount with a real campaign is still a business rule nobody wrote down.5 Here is the arithmetic when it goes wrong. Two percentage discounts applied one after the other multiply, as covered in percentages in the Foundations:
where is the price the customer pays, is the list price, is the contract discount and is the campaign discount, both as fractions.
For a product listed at €84.00 with a contract discount of 12% and a free delivery weekend that marketing set up as 10% off everything:
The customer pays €66.53, so : an effective discount of 20.8% that nobody approved, and accounts has to explain why the invoice does not match the contract.
Price lists are also what the ERP understands. Every serious ERP has customer specific prices or price groups, and mapping those onto Solidus price lists is a straightforward sync. Mapping them onto promotion rules is not.
One design rule: keep the number of lists small. One per contract tier, not one per customer. Four hundred customers with four hundred lists is unmaintainable and turns the ERP sync into a bottleneck. Five to ten tiers plus a few account level overrides on specific variants covers most wholesalers.
Pack sizes, minimums and what the warehouse needs
Trade customers buy in units the warehouse can pick: cartons of 500 cups, cases of six pans, a minimum order value per delivery. In Solidus these are variant attributes and cart validations. A variant carries its pack size and minimum, the cart rounds up or rejects quantities that do not fit, and the product page says what a pack contains. Minimum order value per delivery is an account or order rule that gates checkout. Quantity breaks, where 100 cartons cost less each than 10, are what solidus_volume_pricing already does.4
None of this is hard. It is a day of work with tests that saves the warehouse an hour a day, and it is usually missing from the first specification.
Quotes are orders that have not been placed yet
The usual approach to quotes is email. The buyer asks, the rep replies with a PDF, the buyer says yes, someone keys the order by hand. It works until the rep is on holiday, or the PDF and the order differ, or the customer disputes what was quoted.
A Solidus order has a state machine. Its checkout flow moves from cart through address, delivery, payment (only if payment is required) and confirm to complete, and the flow is defined in one block of the order model that a store can change.6 So a quote is an order held before checkout, with a quote record beside it: a quote number, an expiry date, the prices frozen at the moment of quoting, freight worked out and attached, and the rep who prepared it. The buyer sees it in their account. They accept it and it enters the normal checkout with everything filled in, or they ask for changes and it goes back to the rep. When it completes, the order references the quote number, and both appear on the invoice.
The rep builds quotes in the admin: pick an account, add lines, override a price with a reason, add freight, set the expiry, send. That quote builder is the largest single piece of a B2B build, and it is worth the time because the sales team lives in it.
Two details matter. Frozen prices: the quote stores its own prices, so a price list change before acceptance does not alter what was promised. Expiry: quotes lapse automatically, with a reminder a few days before, because an old quote accepted at old prices is how margins quietly disappear.
Net terms are a payment method with a credit limit behind it
At checkout a trade customer does not enter a card. They pick "invoice, 30 days" and expect the order to go through. In Solidus a payment method is a class with a defined interface. The core even ships a minimal one for cheques, which needs no card or other payment source and simply records capture, void and credit as successful.7 A pay on invoice method starts from that idea and adds the one thing a cheque method lacks: a credit check.
The account carries the credit limit and the terms, 30 or 60 days, sometimes 14 for new customers. The payment method compares the open balance plus this order with the limit. If it fits, the payment is authorised and the order completes. If not, the order is held and accounts get a notification. The customer sees a polite message, not a decline.
Solidus also ships store credit as a payment method in its own right.8 That is useful for credit notes and prepayments: a refund as store credit is spent at the next checkout without anyone keying it by hand.
Open balance means the shop must know what has been invoiced and what has been paid. The reliable pattern is that the ERP or accounting system is the source of truth for balances, and the shop syncs them on a schedule and once more at checkout. Making the shop the ledger is a mistake. It is not an accounting system.
Chasing overdue invoices belongs in the accounting system too. What the shop needs is the outcome: an account on hold for overdue invoices cannot place orders on terms until it is cleared. That is a flag on the account and a check in the payment method.
For how buyers in Germany and Austria expect to pay, including pay on invoice for consumers, see my piece on DACH payments in Solidus.
What Irish law gives you when an invoice is paid late
Terms are only half the story. The other half is what happens when they are broken. In Ireland the European Communities (Late Payment in Commercial Transactions) Regulations 2012, S.I. No. 580 of 2012, set the defaults between businesses. Payment is late once 30 days have passed, unless the contract sets another period. The parties can agree up to 60 days, and longer only if it is expressly agreed and not grossly unfair to the supplier.9
Once a payment is late, interest runs at the European Central Bank main refinancing rate plus 8 percentage points, unless otherwise agreed. The supplier can also claim fixed compensation: €40 for a debt up to €1,000, €70 above €1,000 up to €10,000, and €100 above €10,000.9 Daily interest is simple interest, a rate applied pro rata (see ratios and rates):
where is the interest owed, is the debt, is the ECB main refinancing rate as a fraction and is the number of days late.
The hotel group owes Lee Valley €12,400 and pays 45 days late. If the ECB rate were 2%:
So is €152.88 of interest, plus €100 compensation because is above €10,000. Whether Lee Valley actually claims it from a twelve hotel customer is a commercial decision. The shop's job is to put the terms on every invoice so the option exists.
VAT, decided by where the goods go
Now the part that makes tax advisers nervous. The rules below are the general position for an Irish VAT registered seller of goods. Have your adviser confirm them for your products before launch.
Inside the State. Domestic sales between businesses carry Irish VAT at the normal rate, 23% for most goods, whether or not the buyer is registered.10 The buyer's VAT number does not change the rate.
To a business in another EU member state. This is an intra Community supply, and Revenue sets out five conditions for the zero rate: the customer is registered for VAT in another member state, you obtain and keep the customer's VAT number with its country prefix, you quote your VAT number and the customer's on the invoice, the goods are dispatched or transported to another member state, and you make correct VIES returns. If any condition fails, you charge Irish VAT. If the VIES return is not correct, the zero rate cannot apply.11 The customer then accounts for VAT on the acquisition in their own country.
The VAT number check runs through VIES, the European Commission's validation service.12 It is slow and sometimes down, so validate at account creation and periodically, not at every checkout, and store the result and the date as evidence.
Northern Ireland. Under the Windsor Framework, goods moving between Northern Ireland and the EU still follow EU style VAT rules, with Northern Ireland businesses using an XI VAT number.13 A delivery from Cork to a Belfast customer with a valid XI number is treated like an intra Community supply. Confirm the details with your adviser before you build it into a tax zone.
To Great Britain or anywhere outside the EU. This is an export, zero rated when the goods leave the EU and you hold the evidence Revenue expects for the transport used, such as the export notification, the bill of lading or the air waybill.14 The customer deals with import VAT and duty at their end.
The shop logic follows from this. Tax is decided per order by the delivery address, never by the country on the account. If the Dutch customer asks for a delivery to its partner's kitchen in Galway, the goods never leave the State, and Irish VAT applies.
In Solidus, tax comes from tax rates attached to zones (sets of countries or regions) and tax categories, and the class that applies taxes to an order is configurable through the tax_adjuster_class setting.3 The pattern I use is a domestic zone at the normal rates, an EU zone whose zero rate only applies when the account holds a validated VAT number from the delivery country, and an export zone. Each case is a written specification with tests, so the owner can read in plain English which order gets which treatment.
Invoices an accounts department will accept
A consumer shop can get by with an order confirmation. A B2B shop cannot. Revenue lists what a VAT invoice must show, including the date of issue, a unique sequential number, the supplier's full name, address and VAT number, the customer's name and address, the quantity and nature of the goods, the price excluding VAT, the rate and the VAT. For an intra Community supply of goods it must also carry the customer's VAT number and a notation that this is an intra Community supply of goods.15 An invoice that falls short is a problem for the buyer reclaiming input VAT, and a buyer who cannot reclaim will send it back.
Sequential numbering means the shop cannot reuse the order number, because orders get cancelled and numbers get skipped. Invoices are their own model with their own sequence, generated when the order ships, never changed afterwards. Corrections are credit notes with their own sequence.
Then there is e invoicing, and Ireland has a date. From 1 November 2028, VAT registered large corporates must issue e invoices and report in real time for domestic B2B sales, and from the same date every business must be able to receive e invoices. From 1 November 2029 the obligation extends to businesses trading across EU borders with zero rated goods or reverse charge services. From July 2030 the EU wide rules under VAT in the Digital Age apply to all cross border B2B transactions.16 Lee Valley ships zero rated goods to the Netherlands, so its date is November 2029.
The practical consequence: store the invoice as structured data from day one, with every required field held explicitly, so that a PDF or a structured e invoice is a rendering choice and not a rebuild. If the ERP already generates invoices, the shop should not create a second set. It hands the completed order to the ERP and shows the ERP's invoice to the customer. Two invoice sequences for one business is a mess.
Quick order, file upload and reorder
Trade buyers do not browse. Three features cover most of what they do.
A quick order form: a grid where the buyer types or pastes product codes and quantities, the shop checks each line against the catalogue and the account's price list, flags anything unknown or out of stock, and adds the lot to the cart in one go. It belongs on the first page after login.
File upload: the same thing from a CSV file, because some buyers export requirements from their own system. Accept the customer's own product codes as well as yours, with a mapping table on the account, because large accounts will not renumber for you.
Reorder: one click to put a past order's lines in the cart at today's prices, with a clear note on anything that changed in price or availability. With saved lists, this is most of what a fortnightly customer ever does in the shop.
All three are ordinary Rails features on top of the Solidus cart. None comes out of the box, and all three belong in the first phase.
Reps ordering on behalf of customers
A B2B shop does not replace the sales team. It gives them a better tool. Reps need to see their accounts, prepare quotes, place orders for a customer who phoned, and see what is open. That is an admin role scoped to assigned accounts, with an "act as" capability that records which rep placed which order.
The audit trail matters. When a rep places an order for a customer, the order records the rep, the customer contact who authorised it and how (a call, a message reference). When the invoice is disputed six weeks later, that record settles it.
Catalogue visibility, and the SEO mistake that costs the most
The most expensive mistake in B2B shops is a policy decision, not a technical one: hiding the whole catalogue behind a login.
The reasoning is understandable. Trade prices are confidential and the business does not want consumers ordering. So the whole shop disappears behind a login and becomes invisible to search engines, to prospects, and to the buyer at a new hotel group researching suppliers at nine in the evening.
The fix is to separate visibility from price. The catalogue is public: products, specifications, images, availability, everything a prospect needs to decide you are worth a call. Prices show only to logged in accounts, or a list price shows publicly with a note that trade prices are available on application. Ordering needs an account. Every product page stays indexable, the inbound enquiries keep coming, and negotiated prices stay private.
Account specific catalogue restrictions, where a customer only sees the product families in their contract, are a separate and legitimate feature: a per account visibility rule on top of a public catalogue, not a reason to hide everything. What I wrote about B2B buying in Germany, with procurement doing its homework before the first call, applies to catering supplies as much as to software.
ERP integration and who owns what
The integration with the ERP or accounting system is often the biggest single part of a B2B project. What keeps it sane is deciding, for each kind of data, which system is the source of truth.
Products and stock: the ERP, almost always. The shop imports catalogue and stock on a schedule and on webhook where the ERP supports it, and never edits them.
Prices: the ERP, if it has customer specific pricing. The shop imports price lists. If the ERP's pricing is too crude, the shop owns prices and pushes them back, which needs more care.
Accounts: usually the ERP for the account record, credit limit and terms, with the shop owning users, roles and addresses that the ERP does not model.
Orders: created in the shop, pushed to the ERP on completion, with the ERP's order number written back. Fulfilment, invoices and payments flow back for display.
The integration is ordinary Rails background jobs: idempotent, logged, with an admin page that shows every failure and lets someone retry. I wrote about that kind of plumbing in my piece on background workers. Whether the ERP offers a modern API or a CSV file on a server overnight, the pattern is the same and only the transport differs.
Punchout, where a large customer's procurement system opens your shop inside itself and pulls the cart back as a purchase order, comes up with enterprise customers. It uses defined protocols (cXML, or OCI in German speaking markets) and is a well bounded feature to add once a customer asks. Do not build it speculatively.
What to build first
For a wholesaler like Lee Valley, this is the order.
Phase one, the foundation: account model with users, roles and addresses, public catalogue with prices behind login, price lists, pack sizes and minimums, quick order and reorder, pay on invoice with a credit check, VAT number validation and the VAT zones, structured invoices, and a one way ERP sync for products, stock and price lists with orders pushed out. That is a shop existing customers can move onto.
Phase two, the sales team: quote builder and quote to order flow, rep views and act as, approval thresholds, saved lists, file upload with customer product codes, and the sync extended to bring invoices and balances back.
Phase three, when customers ask: per account catalogue restrictions, quantity breaks, punchout, structured e invoice output for the 2028 and 2029 dates if the ERP does not already do it, more currencies, and whatever the first six months of real orders reveal.
Phase one is the part to build carefully and test exhaustively, because it handles money and tax. Every rule in it gets an executable specification the owner can read. Phase three stays vague on purpose, because real customers should drive it.
What it costs
My published rate for development is €195 an hour.17 What a B2B build costs depends on its scope: how many price lists and how they sync, whether quotes are in the first phase, how net terms and credit checks connect to the ERP, and which VAT cases the shop has to handle. A messy ERP pushes every part up. So I do not publish a fixed figure. I scope it with you and quote it at that hourly rate, and the free workshop plus the first two weeks of work is where that scoping happens.
Compare that honestly with the alternatives. Shopify now offers B2B on its Basic, Grow and Advanced plans too, with net payment terms and quantity rules on all of them. But those plans allow at most three active catalogues across B2B markets, and assigning a catalogue directly to a specific company, which is what contract pricing needs, is only on Plus.18 Plus starts at €2,100 a month, €126,000 over five years before any build work.19 Shopware puts its B2B components, such as quotes and purchasing approvals, in its Evolve plan, from €2,400 a month before VAT.20
The money on Solidus goes into your own features, tested and documented, on infrastructure you control, and the bespoke code I write becomes yours on full payment. It is rarely the lowest figure in year one. It is often the lowest by year three, and it is the option where the quote builder works the way your sales team works.
A word on demos: I am building a set of Solidus demo shops, and a trade and wholesale one is planned. They are a work in progress and not open to visitors yet.
The mistakes, collected
Hiding the whole catalogue behind a login. It kills search visibility and inbound enquiries.
One price list per customer. Unmaintainable. Use tiers and account level overrides.
Contract prices as promotions. They stack with real campaigns, confuse the invoice and break the ERP sync.
Quotes as emails. Nobody can find them, prices drift, disputes cannot be settled.
VAT by the account's country instead of the delivery address. It produces wrong invoices the day a customer has a site in another country.
Invoice numbers reused from order numbers. Gaps in the sequence when orders are cancelled.
Invoices as PDF templates instead of structured data. Fine until the first customer asks for an e invoice.
The shop as the ledger. Balances and overdue invoices live in the accounting system, the shop reads them.
Building punchout, quantity breaks and elaborate approval flows before anyone asks. Speculative B2B features are usually wrong in the details, and the details are the whole point.
Card only checkout. The buyer does not have a company card. They have a purchasing process with an invoice in it.
Why Solidus fits this work
All of this could be built on any framework. Solidus fits B2B because it does not fight you. The checkout flow is designed to be changed. The price selector is a named extension point. Payment methods are classes with a defined interface. Tax application is a configurable class. Users, roles and permissions are ordinary Rails. Each feature in this article slots into a place the framework left for it, with tests, without working around a vendor's assumptions about how people shop.
That is what owning the platform means in practice: the shop matches the business instead of the business matching the shop. In B2B, where every customer has their own terms and every order is a small negotiation, that is the difference between a shop the sales team uses and one they work around.
If you would like to go through your price lists, terms and invoicing with me, I run a free workshop, and the first two weeks of work are on me.