pi-pi.ee — B2B E-commerce for Waterless Urinal Systems

January 17, 2026

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

Next.js 16React 19Tailwind CSSDaisyUI 5next-intl 4Embla Carousel

Backend

Next.js API RoutesStripe SDK 20@react-pdf/rendererResendlibphonenumber-js

Database

Notion API (CRM)JSON files (products, VAT rates)

Infrastructure

VercelStripeVIES API

Payments

CardsPayPalRevolut PaySEPA Direct DebitBank TransferMultibanco

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:

  1. Multi-country VAT compliance — different rates per country, reverse charge for B2B
  2. B2B-first UX — VAT number validation, company address autofill
  3. 28 language support — professional B2B tone, not machine translation
  4. Diverse payment methods — cards, SEPA, bank transfers for different markets
  5. CRM integration — automatic order entry for operations team

The Solution

I built a headless e-commerce on Next.js with focus on B2B functionality:

  1. Static VAT data — rates stored in JSON, updated by script before build. Calculation happens client-side without API requests.
  2. VIES integration — VAT number validation with company address autofill from EU registry.
  3. Server-side PDFs — invoices generated via @react-pdf/renderer and sent via Resend, no client JavaScript required.
  4. 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:

MetricValue
Languages28 fully localized
Markets32 European countries
Payment methods6 (Cards, PayPal, Revolut, SEPA, Bank Transfer, Multibanco)
VAT calculation0 runtime dependencies (static JSON)
PDF invoicesServer-rendered with Cyrillic support
SEO100% 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.

Iurii RoguliaAvailable

Need something similar?

I build custom solutions — from APIs to full products. Let's talk about your project.

View all projects

Related projects