i18n B2B e-commerce platform for waterless urinal products across 32 European countries with automated VAT handling, PDF invoicing, and CRM integration.
Internal sales portal that turns a wholesale deal into a full set of trade paperwork — pro forma, contract, commercial invoice, packing list, CMR and more — from a single form, so a rep can quote and close international orders without a lawyer or a customs broker on every deal.
Key Results

Pi-Pi manufactures waterless urinals and sells them wholesale to businesses across Europe — distributors, facility managers, venue operators. The product is easy to sell. The paperwork behind each sale is not.
Every international B2B order drags a stack of documents with it: a pro forma invoice to lock the offer, a technical specification, a supply contract, a commercial invoice for customs, a declaration of conformity, a packing list, and a CMR consignment note for the truck. Seven documents, each carrying the same buyer details, the same prices, the same quantities — and each one previously assembled by hand in Word and Excel.
That manual process cost the business in three ways. It was slow: a rep spent an afternoon per deal copying figures between templates. It was error-prone: one mistyped VAT number, or a price that didn't match between the invoice and the contract, could stall an order at the border or trigger an awkward call with the buyer. And it didn't scale: closing more deals meant more afternoons lost to formatting, not more selling.
The goal was to make the paperwork stop being a job — the rep fills in what actually varies about a deal, and every document generates itself, guaranteed consistent.
I built an internal portal where the whole deal lives in one record. It works like a lightweight CRM — each client holds all of their deals, and each deal remembers the exact buyer details it was issued with — and every trade document derives from that single source of truth. The stack is deliberately lean; the engineering is in the business rules, not the infrastructure.
One deal, seven documents. The pro forma, specification, contract, commercial invoice, conformity declaration, packing list and CMR note are all generated from the same deal record, so a change to a price or a VAT number propagates to all of them at once. This is the guarantee that keeps orders moving through customs: the numbers on the invoice, the contract and the packing list can never disagree, because they read from one place. The rep sees the documents grouped by the stage of the deal — offer, order confirmation, shipment — and downloads any of them as a finished PDF, with Cyrillic support for Eastern-European buyers.
Pricing, VAT and payment terms handled automatically. The commercial rules the rep used to look up by hand are encoded once. VAT is applied per buyer country, the EU cross-border B2B reverse-charge case drops the rate to zero, and the payment-terms wording on every document is generated from the deposit percentage rather than free-typed — so 50% always produces the same correct sentence everywhere. Wholesale quantity-break discount tiers live in one table, so the buyer gets the right price for their volume without the rep pricing it manually.
Shipping figures worked out for you. A freight forwarder and a customs form both need the shipment broken into pallets with weights and volumes — figures the rep used to work out on a calculator. From the order quantities, the portal groups units by product and splits each group into the fewest, evenly balanced pallets. Gross weight and volume are derived automatically and flow straight onto the packing list and CMR note.
No new software to run. There is no separate database to maintain or back up: the deal data lives in the Notion CRM the team already uses. Each buyer's details are snapshotted at deal creation, so a document reissued months later still matches what the customer signed — even if the client record was edited since. No migration, no per-seat licence, no second place to keep customer records in sync.
What used to be an afternoon of copy-paste per order is now a single form and a set of downloads. The rep quotes faster, the documents always agree with each other, and routine international orders go out without pulling in a lawyer or a customs broker to check the paperwork. The technical build is modest by design — its value is entirely in the business it unblocks.
| Metric | Value |
|---|---|
| Documents per deal | 7 (pro forma, spec, contract, commercial invoice, conformity, packing list, CMR) |
| Source of truth | 1 deal record — every document, price and weight derives from it |
| Pricing & VAT | Automatic — wholesale tiers + reverse-charge by buyer country |
| Shipping figures | Pallets, weight and volume computed from order quantities |
| Datastore | Notion — no separate database, no new licence |
| Rendering | Server-side PDFs (@react-pdf/renderer), Cyrillic-capable |
For the technically curious, here is how the core pieces are built. The stack is Next.js App Router with server-rendered PDFs (@react-pdf/renderer) and Notion for storage.
Every document is a pure function of the same Deal object, so a change to a price or a VAT number propagates to all of them at once.
// src/lib/pdf/documents/generate.ts
export async function generateDealDocument(deal: Deal, docType: DocType): Promise<Buffer> {
let element;
switch (docType) {
case "proforma":
element = ProformaInvoicePDF({ deal });
break;
case "specification":
element = SpecificationPDF({ deal });
break;
case "commercial-invoice":
element = CommercialInvoicePDF({ deal });
break;
case "contract":
element = SupplyContractPDF({ deal });
break;
case "packing-list":
element = PackingListPDF({ deal });
break;
case "cmr":
element = CmrNotePDF({ deal });
break;
case "conformity":
element = ConformityPDF({ deal });
break;
}
return Buffer.from(await renderToBuffer(element)); // @react-pdf/renderer, server-side
}VAT rates are static per-country data (no runtime API call in the critical path), the EU cross-border B2B reverse-charge case flips the rate to zero, and the payment-terms sentence on every document is generated from the deposit percentage rather than free-typed.
// src/lib/admin/deal-defaults.ts
// VAT applies to goods + freight; reverse-charge deals carry vatRate = 0.
export function dealVatAmount(deal: Deal): number {
return (dealSubtotal(deal) + dealShipping(deal)) * deal.vatRate;
}
// Payment terms follow the deposit — no free-text field to get wrong.
export function dealPaymentTerms(deal: Deal): string {
const dep = deal.depositPercent;
if (dep >= 100) return `Full prepayment (100%) payable within 7 days...`;
if (dep <= 0) return `Payment in full within 7 days of order confirmation...`;
return `${dep}% deposit within 7 days; remaining ${100 - dep}% before dispatch...`;
}From the order quantities, the portal groups units by SKU (pallet capacity is box-size specific, so SKUs never share a pallet) and splits each group into the fewest, evenly balanced pallets — 100 units become 50 + 50, never 64 + 36.
// src/lib/admin/deal-defaults.ts
// (100, 64) → [50, 50]; (130, 64) → [44, 43, 43]; (46, 45) → [23, 23]
export function balancedLoads(totalUnits: number, max: number): number[] {
if (totalUnits <= 0) return [];
const count = Math.ceil(totalUnits / max);
const base = Math.floor(totalUnits / count);
const remainder = totalUnits - base * count;
return Array.from({ length: count }, (_, i) => base + (i < remainder ? 1 : 0));
}Gross weight and volume are then derived from catalogue mass plus carton and pallet tare, and flow straight onto the packing list and CMR note.
Each client is a Notion page and each deal is persisted as a single JSON blob. The buyer block is snapshotted from the client at deal creation, so a document reissued months later still matches what the customer signed.
// src/lib/admin/deal-defaults.ts
// A new deal copies the client's party fields into its own buyer snapshot.
export function emptyDealForClient(client: DealClient): Deal {
const deal = emptyDeal();
deal.clientId = client.notionPageId;
deal.buyer = {
company: client.company,
email: client.email,
vatNumber: client.vatNumber /* ... */,
};
return deal;
}
AvailableNeed something similar?
I build custom solutions — from APIs to full products. Let's talk about your project.
i18n B2B e-commerce platform for waterless urinal products across 32 European countries with automated VAT handling, PDF invoicing, and CRM integration.
International e-commerce platform with 30 locales, product configurators, AI chatbot, and fully automated order flow: Stripe → Zoho CRM → Airtable → Mailgun →
B2B quote-to-order flow in Next.js: an RFQ → quote → order state machine, per-deal line items, guarded status transitions, and converting an accepted quote
Gated B2B pricing in Next.js: per-account price lists, server-side access control so prices never leak to crawlers, and a deal-record data model where every