pi-pi.ee — B2B E-commerce for Waterless Urinal Systems
Multilingual B2B e-commerce platform for waterless urinal products across 32 European countries with automated VAT handling, PDF invoicing, and CRM integration.
Tech Stack
Frontend
Backend
Database
Infrastructure
Payments
Key Results
- 28 languages with full localization (53KB per locale)
- 32 European markets with automatic VAT calculation
- 6 payment methods via unified Stripe Checkout
- 100% server-rendered for SEO (Next.js App Router)
The Challenge
B2B sales of sanitary equipment across Europe require support for multiple languages, correct VAT calculation for each EU country, VAT number validation via VIES, and various payment methods — from cards to SEPA and bank transfers. Existing solutions are either too expensive (Shopify Plus) or require significant customization.
Requirements:
- Multi-country VAT compliance — different rates per country, reverse charge for B2B
- B2B-first UX — VAT number validation, company address autofill
- 28 language support — professional B2B tone, not machine translation
- Diverse payment methods — cards, SEPA, bank transfers for different markets
- CRM integration — automatic order entry for operations team
The Solution
I built a headless e-commerce on Next.js with focus on B2B functionality:
- Static VAT data — rates stored in JSON, updated by script before build. Calculation happens client-side without API requests.
- VIES integration — VAT number validation with company address autofill from EU registry.
- Server-side PDFs — invoices generated via @react-pdf/renderer and sent via Resend, no client JavaScript required.
- Notion as CRM — orders and customers automatically synced via API, allowing business management through familiar interface.
Type-safe VAT Calculation
Price calculation without API requests — data loaded statically, types guarantee correctness:
// src/lib/pricing/utils.ts
export interface PriceResult {
basePrice: number; // Net price in EUR
vatRate: number; // 0.22 = 22%
vatAmount: number; // VAT amount in EUR
totalPrice: number; // Net + VAT
isEU: boolean; // EU country flag
formattedTotal: string; // "€365.78"
}
export function calculatePrice(countryCode: string): PriceResult {
const vatRate = getVatRate(countryCode); // From static JSON
const basePrice = 299; // EUR
const vatAmount = Math.round(basePrice * vatRate * 100) / 100;
const totalPrice = Math.round((basePrice + vatAmount) * 100) / 100;
return {
basePrice,
vatRate,
vatAmount,
totalPrice,
isEU: isEuCountry(countryCode),
formattedTotal: new Intl.NumberFormat("de-DE", {
style: "currency",
currency: "EUR",
}).format(totalPrice),
};
}
Stripe Webhook Handler with Async Payments
Unified handler for all payment types — instant (cards) and async (SEPA, bank transfers):
// src/app/api/webhooks/stripe/route.ts
const ASYNC_PAYMENT_METHODS = ["sepa_debit", "customer_balance", "multibanco"];
export async function POST(request: NextRequest) {
const event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
switch (event.type) {
case "payment_intent.succeeded": {
const orderData = parseOrderData(paymentIntent);
await sendOrderNotification(orderData); // Warehouse
await sendInvoiceEmail(orderData); // Customer
await upsertOrder({ ...orderData, status: "Paid" }); // CRM
break;
}
case "payment_intent.requires_action": {
// Bank transfer — send instructions
if (nextActionType === "display_bank_transfer_instructions") {
const bankTransfer = getBankTransferDetails(paymentIntent);
await sendOrderConfirmationEmail(orderData, bankTransfer);
}
break;
}
}
}
Server-side PDF Invoice Generation
Invoices generated on server with Cyrillic support, reverse charge for B2B, and payment-specific instructions:
// src/lib/pdf/invoice-pdf.tsx
Font.register({
family: "Roboto",
fonts: [
{ src: "https://fonts.gstatic.com/.../Roboto.ttf", fontWeight: 400 },
{ src: "https://fonts.gstatic.com/.../Roboto-Bold.ttf", fontWeight: 700 },
],
});
export function InvoicePDF({ order, status, bankTransfer }: InvoicePDFProps) {
return (
<Document>
<Page size="A4" style={styles.page}>
{/* Header with logo and company info */}
<View style={styles.header}>...</View>
{/* Status badge: PAID / PENDING / PROCESSING */}
<View style={[styles.statusBadge, statusStyle]}>
<Text>{statusText}</Text>
</View>
{/* Line items table */}
{order.items.map((item) => (
<View style={styles.tableRow}>
<Text>{productNames[item.productId]}</Text>
<Text>{item.quantity}</Text>
<Text>{formatCurrency(item.price * item.quantity)}</Text>
</View>
))}
{/* Bank transfer instructions (if applicable) */}
{bankTransfer && (
<View style={styles.bankTransferBox}>
<Text>IBAN: {bankTransfer.iban}</Text>
<Text>Reference: {bankTransfer.reference}</Text>
</View>
)}
{/* Reverse charge note for B2B */}
{order.isReverseCharge && (
<Text>VAT reverse charge per Article 196 Directive 2006/112/EC</Text>
)}
</Page>
</Document>
);
}
Multi-locale Routing with 28 Languages
Internationalization via next-intl with SEO and server rendering support:
// src/i18n/config.ts
export const locales = [
"bg",
"cs",
"da",
"de",
"el",
"en",
"es",
"et",
"fi",
"fr",
"hr",
"hu",
"it",
"lt",
"lv",
"nl",
"no",
"pl",
"pt",
"ro",
"ru",
"sk",
"sl",
"sv",
"tr",
"uk",
"vi",
"zh",
] as const;
export const countries = [
"AT",
"AX",
"BE",
"BG",
"CH",
"CY",
"CZ",
"DE",
"DK",
"EE",
"ES",
"FI",
"FR",
"GB",
"GR",
"HR",
"HU",
"IE",
"IT",
"LT",
"LU",
"LV",
"MT",
"NL",
"NO",
"PL",
"PT",
"RO",
"SE",
"SI",
"SK",
] as const;
// Each country has VAT pattern, example, phone format
export const countryInfo: Record<Country, CountryConfig> = {
DE: {
vatPrefix: "DE",
vatPattern: /^DE\d{9}$/,
vatExample: "DE123456789",
phoneCode: "+49",
},
// ... 31 more countries
};
Results
The platform is in pre-launch phase:
| Metric | Value |
|---|---|
| Languages | 28 fully localized |
| Markets | 32 European countries |
| Payment methods | 6 (Cards, PayPal, Revolut, SEPA, Bank Transfer, Multibanco) |
| VAT calculation | 0 runtime dependencies (static JSON) |
| PDF invoices | Server-rendered with Cyrillic support |
| SEO | 100% server-rendered (Next.js App Router) |
The B2B-first approach means professional buyers can self-serve — validate VAT numbers, see accurate pricing with country-specific VAT, and pay via their preferred method.
AvailableNeed something similar?
I build custom solutions — from APIs to full products. Let's talk about your project.