Tafelrokkenshop Logo

Technische Documentatie

Volledige documentatie voor het Operations Dashboard

Technische Documentatie

Welkom bij de technische documentatie van het Tafelrokkenshop Operations Dashboard. Deze documentatie is bedoeld voor developers en bevat informatie over installatie, configuratie, API referenties en technische implementatie details.

Gebruikersdocumentatie

Voor gebruikersdocumentatie (wat de frontend doet, hoe functies werken, changelog, roadmap), zie de gebruikersdocumentatie pagina.

Snelstart

Volg deze stappen om het platform op te zetten:

1. Vereisten

  • Node.js 18+
  • pnpm (package manager)
  • Docker Desktop (voor PostgreSQL database)
  • PostgreSQL 14+ (via Docker)

2. Repository clonen

git clone  cd Tafelrokkenshop/App

3. Dependencies installeren

pnpm install

4. Environment variabelen configureren

Maak een .env bestand in de root directory:

.env
# Database
DATABASE_URL="postgresql://ops:ops@localhost:5432/ops?schema=public"

# Redis
REDIS_URL="redis://localhost:6379"

# App
NODE_ENV=development
PORT=4000

# Shopify
SHOPIFY_SHOP="jouw-shop.myshopify.com"
SHOPIFY_ACCESS_TOKEN="jouw-access-token"

# Bol.com
BOL_CLIENT_ID="jouw-client-id"
BOL_CLIENT_SECRET="jouw-client-secret"

# Frontend
NEXT_PUBLIC_API_URL="http://localhost:4000/api"

5. Database opzetten

Start Docker containers:

docker compose up -d

Run database migraties:

cd apps/backend
pnpm prisma migrate dev
pnpm db:seed

6. Backend starten

cd apps/backend
pnpm dev

Backend draait nu op http://localhost:4000

7. Frontend starten

In een nieuwe terminal:

cd apps/frontend
pnpm dev

Frontend draait nu op http://localhost:3000

Installatie

Pakketmanager

Het project gebruikt pnpm als package manager. Installeer pnpm als je het nog niet hebt:

npm install -g pnpm

Dependencies installeren

# Root level
pnpm install

# Backend dependencies
cd apps/backend
pnpm install

# Frontend dependencies
cd apps/frontend
pnpm install

Docker Setup

De database draait in Docker. Start de containers:

docker compose up -d

Controleer of containers draaien:

docker ps

Prisma Migraties

Na het opzetten van de database, run migraties:

cd apps/backend
pnpm prisma migrate dev

Seed de database met initiële data:

pnpm db:seed

Configuratie

Environment Variabelen

Het .env bestand bevat alle configuratie. Belangrijke variabelen:

Database

DATABASE_URL="postgresql://ops:ops@localhost:5432/ops?schema=public"

API Endpoints

NEXT_PUBLIC_API_URL="http://localhost:4000/api"

Platform Credentials

Shopify:

SHOPIFY_SHOP="jouw-shop.myshopify.com"
SHOPIFY_ACCESS_TOKEN="jouw-access-token"

Bol.com:

BOL_CLIENT_ID="jouw-client-id"
BOL_CLIENT_SECRET="jouw-client-secret"

Veiligheid

Sla nooit credentials op in versiebeheer. Gebruik altijd .env bestanden die in .gitignore staan.

Backend Configuratie

Backend configuratie staat in apps/backend/src/main.ts. Poort kan aangepast worden via PORT environment variable.

Frontend Configuratie

Frontend configuratie staat in apps/frontend/next.config.js. API URL wordt geconfigureerd via NEXT_PUBLIC_API_URL.

Kernconcepten

Multi-channel Order Management

Het platform ondersteunt orders van meerdere kanalen:

  • Shopify: Orders worden gesynchroniseerd via Shopify Admin API
  • Bol.com: Orders worden gesynchroniseerd via Bol.com Retailer API
  • WooCommerce: (Toekomstige ondersteuning)

Elk kanaal heeft zijn eigen configuratie en credentials in de database.

Voorraadbeheer

Master en Child Varianten

Producten kunnen gekoppeld worden als master/child varianten:

  • Master variant: Hoofdartikel met voorraad in "rokken" (bijv. 4 rokken)
  • Child variant: Individuele varianten die voorraad delen (bijv. verschillende kleuren)

Voorraad wordt berekend als: master_voorraad / variant_itemCount

Single Source of Truth

De database is de single source of truth voor voorraad. Voorraad wordt bijgewerkt via:

  • Inkooporders (bij "ingeboekt" status)
  • Retouren (bij "restocked" status)
  • Verkooporders (bij "fulfilled" status) - automatisch
  • Handmatige correcties

Automatische Voorraadmutaties

Het systeem creëert automatisch voorraadmutaties wanneer:

  • Een order status verandert naar "fulfilled"
  • Een order tracking status "DELIVERED" wordt
  • Een order deliveryDate wordt ingesteld

Als er mutaties ontbreken (bijvoorbeeld door directe database updates), kunnen deze worden aangemaakt via:

  • POST /api/inventory/transactions/create-missing - Creëert ontbrekende mutaties voor fulfilled orders
  • De "Ontbrekende Aanmaken" knop in de Mutaties Log UI

Voorraad Synchronisatie naar Platforms

Het systeem synchroniseert automatisch voorraad naar externe platforms (Shopify, Bol.com) wanneer voorraadmutaties plaatsvinden. De synchronisatie wordt uitgevoerd door de InventorySyncService.

Trigger Momenten

Voorraadsync wordt automatisch getriggerd na:

  • SALE_ORDERED mutaties (nieuwe orders)
  • SALE_FULFILLED mutaties (fulfilled orders)
  • MANUAL_ADJUSTMENT mutaties (handmatige voorraadaanpassingen)
  • RECOUNT operaties (voorraad hertellen)
Implementatie Details
  • Service: apps/backend/src/inventory/inventory-sync.service.ts
  • Shopify API: POST /inventory_levels/set.json (Shopify Admin API v2024-10)
  • Bol.com API: PUT /offers/{offerId}/stock (Bol.com Retailer API v10)
  • Batch Processing: Bol.com wordt in batches verwerkt (10 varianten per batch, 200ms delay)
  • Retry Logic: Retry mechanisme voor SSL/TLS handshake errors (5 retries, 2 seconden delay)
  • Error Handling: Errors worden gelogd maar breken de transactie flow niet af
  • Sync Logging: Alle syncs worden geregistreerd in sync_logs tabel met koppeling naar originele mutatie
Sync Logs

Elke sync operatie wordt geregistreerd in de sync_logs tabel met:

  • syncType: 'INVENTORY_SYNC'
  • status: 'SUCCESS', 'PARTIAL', 'FAILED', of 'RUNNING'
  • shopifyUpdated: Aantal succesvol bijgewerkte Shopify varianten
  • bolUpdated: Aantal succesvol bijgewerkte Bol.com varianten
  • output: Gedetailleerde output met voor/na waarden per variant
  • triggeredByTransactionId: Koppeling naar de inventory_transaction die de sync heeft getriggerd
  • duration: Duur van de sync operatie in milliseconden
  • errorMessage en errorDetails: Foutmeldingen indien van toepassing
Lokale Blokkering

Voorraadsync naar platforms is uitgeschakeld in development mode om te voorkomen dat lokale testdata live platforms beïnvloedt. De blokkering wordt gecontroleerd via:

  • NODE_ENV === 'development' - Blokkeert sync in development
  • ENABLE_INVENTORY_SYNC === 'false' - Expliciete flag om sync uit te schakelen

Belangrijk: Data ophalen van platforms (orders, retouren, voorraad checks) is wel toegestaan op lokaal. Alleen sync naar platforms (push) is geblokkeerd.

API Endpoints
  • POST /api/inventory/sync-to-platforms - Handmatig triggeren van voorraadsync
  • POST /api/inventory/check-and-fix-differences - Controleer verschillen en sync automatisch
  • POST /api/inventory/recount - Herbereken voorraad op basis van alle transacties (alle locaties)
  • POST /api/inventory/transactions/create-missing - Creëer ontbrekende transacties voor orders (alleen vanaf 2025-01-01)

Bezza Media Factuurnummer Beheer

Nieuw! Het systeem ondersteunt nu bulk updates van Bezza Media factuurnummers voor zowel verkooporders als inkooporders.

Database Schema

Beide tabellen hebben een supplierInvoiceNo kolom:

  • orders tabel: supplierInvoiceNo TEXT - Voor verkooporders (Shopify, Bol.com)
  • purchases tabel: supplierInvoiceNo TEXT - Voor inkooporders (toegevoegd in migratie 20251205010857_add_supplier_invoice_no_to_purchases)

API Endpoints

Verkooporders (Orders)
  • POST /api/orders/bulk-update-supplier-invoices
    • Body: { updates: Array<{ orderIdentifier: string, supplierInvoiceNo: string }> }
    • Matching: Orders worden gematcht op orderNo (Shopify) of externalId (Bol.com)
    • Response: { success: number, failed: number, details: Array }
    • Validatie: Automatische trimming van identifiers en invoice numbers, empty check
Inkooporders (Purchases)
  • POST /api/purchases/bulk-update-supplier-invoices
    • Body: { updates: Array<{ purchaseId: string, supplierInvoiceNo: string }> }
    • Matching: Direct op purchaseId
    • Response: { success: number, failed: number, details: Array }
    • Duplicate Detection: Frontend detecteert duplicates voordat data naar backend wordt gestuurd
    • Validatie: Automatische trimming en empty checks

Implementatie Details

  • Backend Services:
    • apps/backend/src/orders/orders.service.ts - bulkUpdateSupplierInvoices() methode
    • apps/backend/src/purchasing/purchasing.service.ts - bulkUpdateSupplierInvoices() methode
  • Backend Controllers:
    • apps/backend/src/orders/orders.controller.ts - @Post('bulk-update-supplier-invoices')
    • apps/backend/src/purchasing/purchasing.controller.ts - @Post('bulk-update-supplier-invoices')
  • Frontend Components:
    • apps/frontend/src/app/orders/page.tsx - Import modal voor verkooporders
    • apps/frontend/src/app/purchasing/page.tsx - Import modal voor inkooporders
    • apps/frontend/src/app/orders/manage/page.tsx - "Interne Factuur" kolom weergave
  • Input Parsing:
    • Ondersteunt tab, komma, en pipe (|) als scheidingstekens
    • Automatische verwijdering van "#" prefix voor Shopify ordernummers
    • Whitespace trimming voor alle identifiers
Toekomstige Verbeteringen

Zie TODO.md sectie "Inventory Sync Verbeteringen" voor geplande optimalisaties zoals:

  • Incremental sync (alleen wijzigingen syncen)
  • Batch processing voor Shopify
  • Exponential backoff retry
  • Circuit breaker pattern
  • Connection pooling
  • Queue system voor betere betrouwbaarheid

Retourverwerking

Retouren kunnen worden geïmporteerd via:

  • Bol.com API: Automatische import via pnpm import:bol-returns
  • Handmatige aanmaak: Via de UI voor andere kanalen

Retouren ondersteunen:

  • Gedeeltelijke retouren (bijv. 2 van 4 rokken)
  • Status tracking (Pending, Goedgekeurd, Afgewezen, Ingeboekt)
  • Track & Trace integratie

Track & Trace Status Ophalen

Het systeem kan automatisch tracking status ophalen van verschillende verzendpartijen:PostNL, DPD, en Bpost.

Algemeen Werkingsprincipe

Het ophalen van tracking status werkt als volgt:

  1. Frontend trigger: Gebruiker klikt op "Update status" knop in order edit pagina
  2. API call: Frontend doet GET /api/orders/:id/tracking-status
  3. Backend detectie: Backend bepaalt welke verzendpartij te gebruiken op basis van Track & Trace nummer, link, en transporter naam
  4. Tracking service: Juiste service wordt aangeroepen (PostNL, DPD, of Bpost)
  5. Data extractie: Service gebruikt Puppeteer (headless browser) om tracking pagina te bezoeken en data te extraheren
  6. Database update: Tracking status, statusMessage, deliveryDate en events worden opgeslagen in order
  7. Response: Backend retourneert tracking data naar frontend

Detectie Logica

De backend gebruikt een prioriteitsvolgorde om te bepalen welke verzendpartij te gebruiken:

  1. 14 cijfersDPD (altijd, geen andere checks)
  2. Bpost patroon → Eindigt op BE (bijv. CD104493459BE) of patroon [A-Z]2\d+[A-Z]2
  3. PostNL patroon → 13 cijfers of start met 3S
  4. Track & Trace link → Detecteert uit URL:
    • dpdgroup.com of dpd.nl → DPD
    • postnl.nl of tracking.postnl → PostNL
    • track.bpost.cloud of bpost.cloud → Bpost
  5. Transporter naam → Fallback (DPD, PostNL, Bpost)

Variabelen voor Detectie

De volgende variabelen worden gebruikt om de verzendpartij te bepalen:

  • trackAndTrace (string): Het Track & Trace nummer zelf
    • Patroon matching: /^\d14$/ voor DPD, /^\d13$/ of startsWith('3S') voor PostNL
    • Bpost: /^[A-Z0-9]+BE$/i of /^[A-Z]2\d+[A-Z]2$/i
  • trackAndTraceLink (string, optioneel): De tracking URL
    • Wordt gecontroleerd op domein namen (dpdgroup.com, postnl.nl, bpost.cloud)
  • transporterName (string, optioneel): Naam van de verzendpartij
    • Wordt gecontroleerd op "DPD", "POSTNL", "BPOST"
    • Minder betrouwbaar, wordt alleen gebruikt als fallback
  • transporterCode (string, optioneel): Code van de verzendpartij
    • Wordt gebruikt als transporterName niet beschikbaar is
  • countryCode (string, default: "NL"): Landcode voor PostNL tracking
    • Wordt gehaald uit customer.shippingAddress.country_code, customer.billingAddress.country_code, of customer.country
  • postalCode (string, optioneel): Postcode voor PostNL tracking
    • Wordt gehaald uit customer.shippingAddress.postalCode, customer.billingAddress.postalCode, of geëxtraheerd uit trackAndTraceLink

PostNL Tracking Service

Service: PostNLTrackingService (apps/backend/src/orders/postnl-tracking.service.ts)

Methode: Puppeteer (headless browser) - PostNL gebruikt Angular client-side rendering

URL Format:

https://tracking.postnl.nl/track-and-trace/{trackAndTrace}-{countryCode}-{postalCode}?language=nl

Werkingsprincipe:

  1. Puppeteer laadt tracking pagina
  2. Wacht 5 seconden voor Angular app te laden en API calls te maken
  3. Extraheert data uit:
    • window.__INITIAL_STATE__ (Angular state)
    • DOM tekst (zoekt naar "pakket is bezorgd", "bezorgd op", etc.)
    • Status elementen in de pagina
  4. Prioriteit check voor pickup point pattern:
    • Zoekt eerst naar "Pakket ligt klaar bij PostNL-punt Tot[datum]" pattern
    • Parseert Nederlandse maandnamen (januari t/m december)
    • Converteert naar ISO datum formaat (YYYY-MM-DD) en DD-MM-YYYY voor display
    • Stelt status in op DELIVERED_TO_PICKUP_POINT
    • Formatteert status message als "Pakket ligt klaar bij PostNL tot DD-MM-YYYY"
  5. Als pickup point niet gevonden, parseert normale status en bezorgdatum uit pagina tekst
  6. Retourneert PostNLTrackingStatus object

Pickup Point Pattern Matching:

Het systeem detecteert automatisch wanneer een pakket klaar ligt bij een PostNL-punt door te zoeken naar het volgende pattern in de pagina tekst:

/pakket ligt klaar bij postnl[^d]*tots*(d{1,2})s+(januari|februari|maart|april|mei|juni|juli|augustus|september|oktober|november|december)s+(d{4})/i

Wanneer dit pattern wordt gevonden:

  • Status wordt ingesteld op DELIVERED_TO_PICKUP_POINT
  • Status message wordt geformatteerd als "Pakket ligt klaar bij PostNL tot DD-MM-YYYY"
  • Delivery date wordt gezet op de ophaaldatum (ISO formaat: YYYY-MM-DD)
  • Frontend past automatisch het label aan van "Bezorgdatum" naar "Ophaaldatum"

Response Format:

{
  status: "DELIVERED" | "DELIVERED_TO_PICKUP_POINT" | "IN_TRANSIT" | "UNKNOWN",
  statusMessage: "Pakket is bezorgd" | "Pakket ligt klaar bij PostNL tot 07-12-2025" | "Status niet beschikbaar via PostNL tracking",
  deliveryDate: "2025-11-14" | "2025-12-07" | null,
  events: [
    {
      timestamp: "2025-11-14T10:00:00Z",
      status: "DELIVERED",
      location: "Amsterdam",
      description: "Pakket is bezorgd"
    }
  ],
  rawPuppeteerData: { ... }
}

Pickup Point Detectie

Sinds 2025-12-02 heeft het systeem prioriteit check voor PostNL pickup point status. Wanneer een pakket klaar ligt bij een PostNL-punt, wordt dit automatisch gedetecteerd en de status message wordt geformatteerd met de ophaaldatum. De frontend past automatisch het label aan van "Bezorgdatum" naar "Ophaaldatum" voor betere gebruikerservaring.

DPD Tracking Service

Service: DPDTrackingService (apps/backend/src/orders/dpd-tracking.service.ts)

Methode: Puppeteer (bypass Cloudflare protection) - DPD heeft Cloudflare bescherming

URL Format:

https://www.dpdgroup.com/nl/mydpd/my-parcels/search?lang=nl&parcelNumber={trackAndTrace}

Werkingsprincipe:

  1. Puppeteer laadt tracking pagina
  2. Wacht 5 seconden voor Cloudflare check te passeren
  3. Extraheert data uit:
    • HTML tabellen met tracking events
    • Status elementen in de DOM
    • Pagina tekst
  4. Parseert events uit tracking tabel
  5. Retourneert DPDTrackingStatus object

Response Format:

{
  status: "DELIVERED" | "IN_TRANSIT" | "UNKNOWN",
  statusMessage: "Pakket is bezorgd" | string,
  deliveryDate: "2025-11-14" | null,
  events: [
    {
      status: "DELIVERED",
      date: "2025-11-14"
    }
  ]
}

Bpost Tracking Service

Service: BpostTrackingService (apps/backend/src/orders/bpost-tracking.service.ts)

Methode: Puppeteer + API call - Bpost heeft een web interface die JavaScript nodig heeft

URL Format:

https://track.bpost.cloud/btr/web/#/search?itemCode={trackAndTrace}&lang=nl&postalCode={postalCode}

Werkingsprincipe:

  1. Puppeteer laadt tracking pagina
  2. Wacht voor pagina te laden
  3. Probeert JSON data op te halen via API endpoint
  4. Extraheert tracking events en status
  5. Retourneert BpostTrackingStatus object

Response Format:

{
  status: "DELIVERED" | "IN_TRANSIT" | "UNKNOWN",
  statusMessage: "Pakket is bezorgd" | string,
  deliveryDate: "2025-11-14" | null,
  events: [
    {
      status: "DELIVERED",
      date: "2025-11-14"
    }
  ]
}

Fallback Logica

Als een tracking service faalt, wordt er een fallback uitgevoerd:

  • PostNL faalt → Probeer DPD (vooral bij 14 cijfers of als PostNL aangeeft dat het geen PostNL nummer is)
  • DPD faalt → Return error: { error: "Kon tracking status niet ophalen van DPD" }
  • Geen service werkt → Return { status: "UNKNOWN", message: "Kon tracking status niet automatisch ophalen..." }

Database Update

Als tracking succesvol is, worden de volgende velden bijgewerkt in de Order tabel:

  • trackingStatus (string): Status (DELIVERED, IN_TRANSIT, UNKNOWN, etc.)
  • trackingStatusMessage (string): Menselijke leesbare status (bijv. "Pakket is bezorgd")
  • deliveryDate (DateTime): Bezorgdatum
  • customer.trackingEvents (JSON): Array van tracking events

API Endpoint

GET /api/orders/:id/tracking-status

Haalt tracking status op voor een specifieke order.

const response = await fetch('http://localhost:4000/api/orders/{orderId}/tracking-status');
const data = await response.json();

// Success response - Normal delivery
{
  status: "DELIVERED",
  statusMessage: "Pakket is bezorgd",
  deliveryDate: "2025-11-14",
  events: [...],
  message: "Tracking status succesvol opgehaald en opgeslagen."
}

// Success response - Pickup point (sinds 2025-12-02)
{
  status: "DELIVERED_TO_PICKUP_POINT",
  statusMessage: "Pakket ligt klaar bij PostNL tot 07-12-2025",
  deliveryDate: "2025-12-07",
  events: [...],
  message: "Tracking status succesvol opgehaald en opgeslagen."
}

// Error response
{
  error: "Kon tracking status niet ophalen van DPD"
}

// UNKNOWN response (geen automatische tracking mogelijk)
{
  status: "UNKNOWN",
  statusMessage: "Status niet beschikbaar via PostNL tracking",
  message: "Kon tracking status niet automatisch ophalen. Voer status en bezorgdatum handmatig in."
}

Frontend Verwerking

De frontend controleert de response en toont alleen success als er geldige tracking data is:

  1. Controleert op data.error → toon error toast
  2. Controleert op data.status === 'UNKNOWN' → toon error toast
  3. Controleert op data.statusMessage met "niet beschikbaar" → toon error toast
  4. Alleen bij geldige tracking data → toon success toast en refresh order

Dynamische Label Aanpassing (sinds 2025-12-02):

Wanneer de tracking status een pickup point detecteert, past de frontend automatisch het label aan:

  • Controleert of trackingStatusMessage bevat: "Pakket ligt klaar bij PostNL"
  • Als true: label wordt "Ophaaldatum" in plaats van "Bezorgdatum"
  • Geïmplementeerd op alle order pagina's:
    • /orders/[id]/edit/admin - Admin edit pagina
    • /orders/[id]/edit - Publieke edit pagina
    • /orders - Orders overzicht
    • /orders/manage - Bulk manage pagina

Handmatige Invoer Verbeteringen (sinds 2025-12-02):

  • Wanneer automatische tracking faalt, worden handmatige invoer velden visueel gemarkeerd:
    • Gele border en achtergrond voor gewijzigde velden
    • Automatisch focus op status veld voor snellere invoer
    • Duidelijke waarschuwing: "(Handmatig invoeren)" bij label
  • State management: showManualEntry flag activeert visuele feedback

Waarom Puppeteer?

Alle tracking services gebruiken Puppeteer (headless browser) omdat:
  • PostNL gebruikt Angular (client-side rendering) - data is niet direct beschikbaar in HTML
  • DPD heeft Cloudflare bescherming - normale HTTP requests worden geblokkeerd
  • Bpost heeft een web interface die JavaScript nodig heeft
Puppeteer simuleert een echte browser, waardoor deze sites kunnen worden benaderd.

Inkooporder Workflow

  1. Aanmaken: Nieuwe inkooporder aanmaken met leverancier en producten
  2. Ontvangen: Order status wijzigen naar "Ontvangen" met ontvangstdatum
  3. Inboeken: Status wijzigen naar "Ingeboekt" - voorraad wordt automatisch bijgewerkt

Voorraad wordt geboekt op master varianten en automatisch doorgerekend naar child varianten.

Kostenbeheer Systeem

Het kostenbeheer systeem is volledig herzien met een kostenposten systeem (zoals in een boekhoudsysteem). Alle kosten worden gekoppeld aan kostenposten in plaats van directe categorieën.

Database Schema

Het systeem bevat twee hoofdmodellen:

CostAccount Model (kostenposten):

  • id, orgId, code (unieke code, bijv. "6001", "7001")
  • name (naam van kostenpost, bijv. "Inkoopkosten", "Verzendkosten")
  • description (optioneel)
  • costType: PURCHASE, PLATFORM_FEE, SHIPPING, ADVERTISING, GENERAL
  • parentAccountId (optioneel, voor hiërarchie)
  • isActive (boolean, default: true)
  • sortOrder (voor weergave volgorde)
  • vatRate (BTW tarief: 0, 9, 21, of null voor BTW-vrij)
  • vatIncluded (boolean, is bedrag incl. of excl. BTW)
  • createdAt, updatedAt

Cost Model (aangepast):

  • id, orgId, costType: PURCHASE, PLATFORM_FEE, SHIPPING, ADVERTISING, GENERAL
  • costAccountId (verplicht, foreign key naar cost_accounts) - NIEUW: vervangt directe category
  • category (optioneel, voor backward compatibility/migratie)
  • description, amount, currency (default: EUR)
  • orderId, channelId (optioneel)
  • date, invoiceNumber, supplierInvoiceNo
  • isRecurring, recurringFrequency
  • tags (array voor categorisatie)
  • advertisingCampaign, advertisingMetrics (voor advertentiekosten)
  • createdAt, updatedAt

API Endpoints - Kostenposten

  • GET /api/cost-accounts - Lijst van alle kostenposten (met filters: costType, isActive)
  • GET /api/cost-accounts/:id - Kostenpost detail
  • POST /api/cost-accounts - Nieuwe kostenpost aanmaken
  • PUT /api/cost-accounts/:id - Kostenpost bewerken
  • DELETE /api/cost-accounts/:id - Kostenpost verwijderen (alleen als geen kosten gekoppeld)
  • GET /api/cost-accounts/grouped - Kostenposten gegroepeerd per costType

API Endpoints - Kosten

  • GET /api/costs - Lijst van alle kosten met filters (type, costAccountId, date range, orderId, channelId)
  • GET /api/costs/log - NIEUW: Kosten Mutaties Log (zoals inventory/transactions endpoint)
    • Paginering (skip, take)
    • Filters: costAccountId, costType, startDate, endDate, orderId, search
    • Sorteerbaar op datum (desc)
    • Retourneert alle kosten met volledige details (costAccount, order, channel, etc.)
  • POST /api/costs - Nieuwe kosten toevoegen (verplicht: costAccountId)
  • PUT /api/costs/:id - Kosten bewerken
  • DELETE /api/costs/:id - Kosten verwijderen
  • POST /api/costs/import/bol-csv - Import Bol.com CSV kosten specificaties
  • GET /api/costs/impact/period - Kosten impact voor periode
  • GET /api/costs/stats - Kosten statistieken

Virtuele Kosten (Virtual Costs)

Het systeem ondersteunt virtuele kosten die automatisch worden gegenereerd uit orderdata. Deze worden alleen getoond als er geen echte cost entry bestaat voor dezelfde order en costType.

Implementatie

In CostsService.findAll() worden virtuele kosten gegenereerd uit:

  • Virtual Platform Fees: Van order.totals.fees
    • Worden alleen getoond als er geen echte PLATFORM_FEE cost bestaat voor die order
    • Category wordt automatisch bepaald op basis van channel type (SHOPIFY_FEE, BOL_FEE, OVERIG)
    • Description: "{channelType} fee voor order {orderNo}"
  • Virtual Shipping Costs: Van order.totals.shipping
    • Worden alleen getoond als er geen echte SHIPPING cost bestaat voor die order
    • Category: altijd VERZENDKOSTEN
    • Description: "Verzendkosten voor order {orderNo}"
  • Virtual Additional Costs: Van order.additionalCosts
    • Worden alleen getoond als er geen echte GENERAL cost bestaat voor die order
    • Category: altijd OVERIG
    • Description: "Additionele kosten voor order {orderNo}"
Duplicaat Preventie

Om duplicaten te voorkomen, worden de volgende Sets gebruikt:

// In CostsService.findAll()
const ordersWithRealPlatformFees = new Set(
  costs
    .filter(cost => cost.orderId && cost.costType === CostType.PLATFORM_FEE)
    .map(cost => cost.orderId)
);

const ordersWithRealShipping = new Set(
  costs
    .filter(cost => cost.orderId && cost.costType === CostType.SHIPPING)
    .map(cost => cost.orderId)
);

// Virtual costs worden alleen toegevoegd als order.id NIET in de Set staat
if (fees > 0 && !ordersWithRealPlatformFees.has(order.id)) {
  // Voeg virtual platform fee toe
}
Virtual Cost Object Structuur
{
  id: `virtual-{type}-{order.id}`, // Bijv. "virtual-fee-123" of "virtual-shipping-456"
  orgId: string,
  costType: CostType.PLATFORM_FEE | CostType.SHIPPING | CostType.GENERAL,
  category: CostCategory, // Automatisch bepaald
  description: string, // Automatisch gegenereerd
  amount: number, // Van order.totals.fees/shipping of order.additionalCosts
  currency: 'EUR',
  date: order.orderDate,
  orderId: order.id,
  channelId: order.channels?.id,
  orders: { id, orderNo, externalId },
  channels: { id, name, type },
  suppliers: null,
  isVirtual: true, // Marker dat dit geen echte database entry is
  createdAt: order.orderDate,
  updatedAt: order.orderDate,
  // Geen costAccountId - virtual costs zijn niet gekoppeld aan kostenposten
}

Belangrijk

  • Virtuele kosten hebben geen costAccountId - ze zijn niet gekoppeld aan kostenposten
  • Virtuele kosten kunnen niet worden bewerkt of verwijderd via de API
  • Als je een echte cost entry toevoegt voor een order, verdwijnt de virtuele cost automatisch
  • Virtuele kosten worden alleen getoond in GET /api/costs, niet in GET /api/costs/log

Migratie

Een migratie script (scripts/migrate-costs-to-cost-accounts.ts) is beschikbaar om bestaande kosten te migreren:

  • Maakt standaard kostenposten aan voor elke CostCategory
  • Koppelt bestaande kosten aan kostenposten op basis van hun categorie
  • Uitvoeren: pnpm tsx scripts/migrate-costs-to-cost-accounts.ts

Responsive Design & PWA

Het platform is volledig responsive en ondersteunt PWA functionaliteit:

Responsive Design

  • Tailwind CSS responsive utilities
  • Card-based layouts voor tabellen op mobiel
  • Hamburger menu voor mobiele navigatie
  • Touch-vriendelijke knoppen (minimaal 44x44px)
  • Responsive modals en formulieren
  • Bottom navigation voor mobiele apparaten
  • Sticky actie kolommen voor desktop tabellen
  • Responsive lettertype schaling

PWA Implementatie

  • Manifest: public/manifest.json - App metadata en icons
  • Service Worker: public/sw.js - Network-first caching strategy
  • Offline Support: Basis functionaliteit werkt offline
  • Install Prompt: PWAInstallPrompt component
  • Offline Indicator: OfflineIndicator component

PWA componenten:

  • apps/frontend/src/components/ui/PWAInstallPrompt.tsx
  • apps/frontend/src/components/ui/OfflineIndicator.tsx
  • apps/frontend/src/app/sw-register.tsx

API Referentie

Backend Endpoints

De backend API draait op http://localhost:4000/api (of zoals geconfigureerd in NEXT_PUBLIC_API_URL).

Orders

GET /api/orders

Haal alle orders op.

curl http://localhost:4000/api/orders

GET /api/orders/:id

Haal specifieke order op.

curl http://localhost:4000/api/orders/123e4567-e89b-12d3-a456-426614174000

PUT /api/orders/:id

Update order.

const response = await fetch('http://localhost:4000/api/orders/123e4567-e89b-12d3-a456-426614174000', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    status: 'fulfilled',
    // ... andere velden
  }),
});

if (!response.ok) {
  const error = await response.json();
  console.error('Error:', error.message);
}

Retouren

GET /api/returns

Haal alle retouren op.

curl http://localhost:4000/api/returns

POST /api/returns

Maak nieuwe retour aan.

const response = await fetch('http://localhost:4000/api/returns', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    orderId: '123e4567-e89b-12d3-a456-426614174000',
    returnDate: '2025-01-15',
    lines: [
      {
        variantId: 'variant-id',
        qty: -2,
        unitPrice: 29.99,
      },
    ],
  }),
});

if (!response.ok) {
  const error = await response.json();
  throw new Error(error.message);
}

Producten

GET /api/products

Haal alle producten op.

curl http://localhost:4000/api/products

Kosten

GET /api/costs

Haal alle kosten op met optionele filters.

curl "http://localhost:4000/api/costs?costType=ADVERTISING&channelId=shopify&startDate=2025-01-01&endDate=2025-01-31"

POST /api/costs

Maak nieuwe kosten aan. Verplicht: costAccountId (kostenpost).

const response = await fetch('http://localhost:4000/api/costs', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    costType: 'ADVERTISING',
    costAccountId: 'ca_1234567890_abc123', // Verplicht: ID van kostenpost
    description: 'Shopify Marketing Campaign Q1',
    amount: 500.00,
    currency: 'EUR',
    channelId: 'shopify-channel-id',
    date: '2025-01-15',
    advertisingCampaign: 'Q1 Product Launch',
    advertisingMetrics: {
      impressions: 100000,
      clicks: 5000,
      conversions: 250,
      cpc: 0.10,
      cpa: 2.00
    }
  }),
});

GET /api/costs/log

Haal kosten mutaties log op (zoals inventory transactions log).

const response = await fetch('http://localhost:4000/api/costs/log?skip=0&take=50&costAccountId=ca_123&costType=ADVERTISING&startDate=2025-01-01&endDate=2025-01-31&search=marketing');
const data = await response.json();
// Returns: { costs: [...], total: 100 }
// Each cost includes: cost_accounts, orders, channels, suppliers

GET /api/cost-accounts

Haal alle kostenposten op.

const response = await fetch('http://localhost:4000/api/cost-accounts?isActive=true&costType=ADVERTISING');
const costAccounts = await response.json();
// Returns: Array of cost accounts with code, name, costType, vatRate, etc.

POST /api/cost-accounts

Maak nieuwe kostenpost aan.

const response = await fetch('http://localhost:4000/api/cost-accounts', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    code: '8005',
    name: 'TikTok Advertenties',
    description: 'TikTok marketing kosten',
    costType: 'ADVERTISING',
    vatRate: 21,
    vatIncluded: false,
    isActive: true,
    sortOrder: 0
  }),
});

POST /api/costs/import/bol-csv

Importeer Bol.com kosten specificaties via CSV.

const formData = new FormData();
formData.append('file', csvFile);
formData.append('skipDuplicates', 'true');
formData.append('defaultCategory', 'BOL_FEE');

const response = await fetch('http://localhost:4000/api/costs/import/bol-csv', {
  method: 'POST',
  body: formData,
});

GET /api/costs/impact/period

Haal kosten impact op voor een periode.

const response = await fetch('http://localhost:4000/api/costs/impact/period?startDate=2025-01-01&endDate=2025-01-31&groupBy=order');
const data = await response.json();
// Returns: { totalCosts, totalRevenue, netProfit, margin, costsPerOrder, ... }

Voorraad Transacties

POST /api/inventory/transactions/create-missing

Creëer ontbrekende voorraadmutaties voor fulfilled orders.

const response = await fetch('http://localhost:4000/api/inventory/transactions/create-missing', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    orderDate: '2025-01-01' // Optioneel: alleen orders vanaf deze datum
  }),
});

POST /api/products/prices/fetch

Haal prijzen op van externe platforms.

const response = await fetch('http://localhost:4000/api/products/prices/fetch', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    platform: 'shopify', // of 'bol'
  }),
});

const data = await response.json();
console.log(`Bijgewerkt: ${data.updated}, Overgeslagen: ${data.skipped}`);

Foutafhandeling

API endpoints retourneren gestructureerde error responses:

{
  "statusCode": 400,
  "message": "Validation failed",
  "error": "Bad Request"
}

Voor field-specifieke errors:

{
  "statusCode": 400,
  "message": "returnDate: Invalid date format"
}

Problemen oplossen

Database Connectie Problemen

Probleem: Kan niet verbinden met database.

Oplossing:

  1. Controleer of Docker containers draaien: docker ps
  2. Controleer DATABASE_URL in .env
  3. Test connectie: cd apps/backend && pnpm prisma studio

API Rate Limiting

Probleem: Te veel requests naar externe API's (Bol.com, Shopify).

Oplossing:

  • Scripts hebben ingebouwde rate limiting en retry logic
  • Wacht tussen API calls (automatisch geïmplementeerd)
  • Gebruik paginatie voor grote datasets

Prisma Migratie Issues

Probleem: Migratie faalt of database is inconsistent.

Oplossing:

  1. Maak backup: pnpm db:backup
  2. Check migratie status: cd apps/backend && pnpm prisma migrate status
  3. Fix failed migrations: pnpm db:fix-migration

Frontend laadt niet

Probleem: Frontend blijft op "Laden..." staan.

Oplossing:

  1. Controleer of backend draait op poort 4000
  2. Controleer NEXT_PUBLIC_API_URL in .env
  3. Check browser console voor errors
  4. Clear cache: cd apps/frontend && rm -rf .next

Service Worker Issues

Probleem: PWA werkt niet of service worker registreert niet.

Oplossing:

  1. Controleer of public/sw.js bestaat
  2. Controleer of public/manifest.json bestaat
  3. Clear service worker cache in browser DevTools > Application > Service Workers
  4. Unregister oude service workers en herlaad pagina
  5. Controleer browser console voor service worker errors

Voorraad Hertelling (recountInventory)

De recountInventory() functie herberekent voorraad op basis van alle inventory transacties.

Implementatie:

  • Service: apps/backend/src/inventory/inventory.service.ts
  • API Endpoint: POST /api/inventory/recount
  • Multi-Location Support: Verwerkt alle actieve inventory locaties simultaan (sinds 2025-12-01)
  • Process:
    1. Haalt alle actieve locaties op
    2. Voor elke locatie:
      • Maakt backup van huidige voorraad
      • Reset voorraad naar 0
      • Haalt alle transacties op voor die locatie
      • Berekent voorraad per variant door transacties op te tellen
      • Update inventory_levels met berekende voorraad
    3. Retourneert resultaten per locatie
  • Response Format:
    {
      "success": true,
      "message": "Inventory recounted successfully for 2 location(s)",
      "totalVariants": 47,
      "updatedCount": 47,
      "transactionsProcessed": 611,
      "locations": [
        {
          "location": "Hoofdmagazijn",
          "variants": 24,
          "updated": 24,
          "transactions": 572
        },
        {
          "location": "Voorraad Mitchel",
          "variants": 23,
          "updated": 23,
          "transactions": 39
        }
      ]
    }

Datumcheck Logica voor Oude Orders

Alle voorraad functies controleren nu of orders van vóór 2025-01-01 zijn en skippen deze automatisch. Dit voorkomt dat oude orders de voorraad verstoren.

Cutoff Datum: 2025-01-01T00:00:00Z

Functies met Datumcheck:

  • reserveInventoryForOrder() - apps/backend/src/inventory/inventory-distribution.service.ts:247-254
  • recalculateInventoryAfterSale() - apps/backend/src/inventory/inventory-distribution.service.ts:552-559
  • createMissingTransactions() - apps/backend/src/inventory/inventory.service.ts:600-601
  • reserve-inventory.ts utility - scripts/utils/reserve-inventory.ts:33-40
  • process-missing-order-inventory.ts - scripts/process-missing-order-inventory.ts:26-27

Implementatie Pattern:

const cutoffDate = new Date('2025-01-01T00:00:00Z');
if (order.orderDate < cutoffDate) {
  console.warn(
    `Skipping inventory reservation for old order ${order.orderNo || order.externalId || orderId} from ${order.orderDate.toISOString().split('T')[0]}`,
  );
  return; // Order is too old, skip reservation
}

Fix Scripts voor Oude Orders

Scripts om problematische transacties voor oude orders te detecteren en te fixen:

  • Check Script: scripts/check-old-orders-inventory-live.ts
    • Detecteert oude orders (voor 2025) met transacties aangemaakt na 2025-01-01
    • Toont overzicht van problematische transacties
    • Gebruik: npx tsx scripts/check-old-orders-inventory-live.ts
  • Fix Script: scripts/fix-old-orders-inventory-deductions.ts
    • Verwijdert problematische transacties voor oude orders
    • Boekt voorraad terug naar inventory_levels
    • Vereist bevestiging ("JA") voordat acties worden uitgevoerd
    • Gebruik: npx tsx scripts/fix-old-orders-inventory-deductions.ts
  • Complete Fix Script: scripts/fix-old-orders-on-live-complete.sh
    • Voert alle stappen uit: check, fix, en recount
    • Gebruik: ./scripts/fix-old-orders-on-live-complete.sh

Ontbrekende Voorraadmutaties

Probleem: Voorraadmutaties ontbreken voor fulfilled orders.

Oplossing:

  1. Gebruik POST /api/inventory/transactions/create-missing endpoint
  2. Of gebruik de "Ontbrekende Aanmaken" knop in de Mutaties Log UI
  3. Het systeem controleert automatisch op ontbrekende mutaties bij order updates
  4. Voor historische orders, specificeer orderDate parameter

FAQ

Hoe voeg ik een nieuw kanaal toe?

Ga naar /settings > "Channels" sectie en klik op "Nieuw Kanaal". Vul de benodigde credentials in.

Hoe importeer ik historische orders?

Gebruik de backfill scripts:

  • pnpm backfill:shopify voor Shopify orders
  • pnpm backfill:bol voor Bol.com orders

Kan ik voorraad handmatig aanpassen?

Ja, ga naar /inventory en gebruik de "Voorraad Aanpassen" functionaliteit. Voorraad wordt geboekt op master varianten.

Hoe werkt de variant linking?

Ga naar /products/ean-manager/variants om varianten te koppelen aan master producten. Voorraad wordt automatisch doorgerekend.

Kan ik retouren importeren van andere platforms?

Momenteel alleen Bol.com via API. Voor andere platforms kunnen retouren handmatig aangemaakt worden via /returns/create.

Hoe synchroniseer ik prijzen terug naar platforms?

Ga naar /products/manage, pas prijzen aan en klik op de synchronisatie knop voor het betreffende platform.

Wat gebeurt er bij een database reset?

Maak altijd eerst een backup: pnpm db:backup. Backups worden automatisch gemaakt voor elke commit (pre-commit hook).

Hoe voeg ik nieuwe product types toe?

Ga naar /settings > "Product Types" sectie en voeg nieuwe types toe.

Kan ik custom return statussen gebruiken?

Ja, ga naar /settings > "Return Statussen" en voeg custom statussen toe. Alleen "Ingeboekt" status triggert voorraad bijboeking.

Hoe werkt de voorraad hertelling?

Ga naar /inventory en klik op "Voorraad Hertellen". Dit herberekent voorraad op basis van alle transacties (inkopen, verkopen, retouren).

Multi-Location Support

Sinds 2025-12-01 verwerkt de voorraad hertelling alle actieve inventory locaties simultaan. Elke locatie wordt individueel verwerkt met eigen transacties, waardoor de voorraad correct wordt berekend voor alle locaties.

API Endpoint: POST /api/inventory/recount

Response: Retourneert resultaten per locatie met aantal varianten, updates en transacties.

Multi-Location Support

Sinds 2025-12-01 verwerkt de recountInventory() functie alle actieve inventory locaties simultaan. Elke locatie wordt individueel verwerkt met eigen transacties, waardoor de voorraad correct wordt berekend voor alle locaties.
Datumcheck Logica voor Oude Orders

Alle voorraad functies controleren nu of orders van vóór 2025-01-01 zijn en skippen deze automatisch. Dit voorkomt dat oude orders de voorraad verstoren.

Functies met datumcheck:

  • reserveInventoryForOrder() - skipt orders < 2025-01-01
  • recalculateInventoryAfterSale() - skipt orders < 2025-01-01
  • createMissingTransactions() - verwerkt alleen orders >= 2025-01-01
  • reserve-inventory.ts utility - skipt oude orders
  • process-missing-order-inventory.ts - filtert alleen recente orders

Cutoff datum: 2025-01-01T00:00:00Z

Product Sales Dashboard

API Endpoints

Het Product Sales Dashboard gebruikt de volgende API endpoints:

  • GET /api/products/:id/sales/stats - Verkoop statistieken
  • GET /api/products/:id/sales/chart - Grafiek data (met optionele vorige periode vergelijking)
  • GET /api/products/:id/sales/orders - Orderregels met paginatie
  • GET /api/products/:id/sales/returns - Retour data
  • GET /api/products/:id/sales/trend - Verkoop trend analyse
  • GET /api/products/:id/inventory/prediction - Voorraad voorspelling
  • GET /api/products/:id/purchase-suggestions - Inkoopsuggesties
  • GET /api/products/:id/sales/return-trend - Retour trend over tijd
  • GET /api/products/:id/sales/inventory-movements - Voorraad bewegingen per locatie
  • GET /api/products/:id/sales/transfer-suggestions - Transfer suggesties tussen locaties
  • GET /api/products/:id/low-stock-settings - Low stock alert instellingen
  • PUT /api/products/:id/low-stock-settings - Update low stock alert instellingen

Query Parameters

Alle sales endpoints ondersteunen de volgende query parameters:

  • startDate - Start datum (ISO format)
  • endDate - Eind datum (ISO format)
  • period - Periode (today, mtd, ytd)
  • includeVariants - Include child variant data (true/false)
  • includePreviousPeriod - Include vorige periode data voor vergelijking (true/false)
  • skip - Paginatie offset (voor orders en movements)
  • take - Paginatie limit (voor orders en movements)

Product Sales Service

De ProductSalesService (apps/backend/src/products/product-sales.service.ts) bevat alle business logic voor:

  • Verkoop statistieken berekening
  • Grafiek data generatie
  • Orderregels ophalen met filtering
  • Retour analyse
  • Trend analyse (omzet, verkochte items, marge)
  • Voorraad voorspelling (stockout datum, projecties)
  • Inkoopsuggesties (herbestelpunt, aanbevolen hoeveelheid)
  • Retour trend analyse
  • Voorraad bewegingen per locatie
  • Transfer suggesties tussen locaties

Variant Aggregatie

Wanneer includeVariants=true wordt doorgegeven:

  • Alle child variant IDs worden opgehaald via findMany voor betere performance
  • Verkoopdata van master product en alle child varianten wordt geaggregeerd
  • Voorraad wordt samengevoegd van alle varianten
  • Retour data wordt gecombineerd van alle varianten

Low Stock Alerts

Low Stock Alert instellingen worden opgeslagen in AppConfig met:

  • category: 'low-stock-alerts'
  • key: 'product-{productId}' voor product-specifieke instellingen
  • key: 'defaults' voor globale standaardwaarden

Instellingen bevatten:

  • minStockLevel - Minimum voorraad niveau
  • warningLevel - Waarschuwings niveau
  • leadTimeDays - Levertijd in dagen
  • safetyStockDays - Veiligheidsvoorraad in dagen
  • enableAlerts - Automatische alerts inschakelen
  • emailNotifications - Email notificaties inschakelen
  • notificationEmail - Email adres voor notificaties

Voorraad Locaties

Voorraad locaties worden beheerd via:

  • GET /api/inventory/locations - Lijst van alle locaties
  • POST /api/inventory/locations - Nieuwe locatie aanmaken
  • PUT /api/inventory/locations/:id - Locatie bijwerken
  • DELETE /api/inventory/locations/:id - Locatie verwijderen (alleen als geen voorraad/transacties)
  • GET /api/inventory/locations/duplicates - Detecteer duplicate locaties
  • POST /api/sync/shopify/locations - Synchroniseer Shopify locaties

Shopify locatie synchronisatie:

  • Locaties worden gesynchroniseerd op basis van externalId
  • Handmatig aangepaste namen worden behouden (worden niet overschreven)
  • Alleen locaties met externalId die overeenkomen met Shopify naam worden bijgewerkt

Transfer Suggesties Algoritme

Het transfer suggesties algoritme:

  1. Berekent voorraad per locatie voor het product (inclusief varianten indien includeVariants=true)
  2. Berekent gemiddelde voorraad per locatie
  3. Identificeert locaties met >150% van gemiddelde (hoge voorraad)
  4. Identificeert locaties met <50% van gemiddelde (lage voorraad)
  5. Suggereert transfers van hoge naar lage locaties
  6. Berekent optimale transfer hoeveelheid (max 50% van overschot)

Sync Verbeteringen

Het sync systeem is geoptimaliseerd voor betere performance, betrouwbaarheid en schaalbaarheid.

Incremental Sync

Het systeem synchroniseert alleen varianten waarvan de voorraad is veranderd sinds de laatste sync:

  • Tracking Velden: lastSyncedAtShopify, lastSyncedInventoryShopify, lastSyncedAtBol, lastSyncedInventoryBol in product_variants tabel
  • Filtering: Alleen varianten met gewijzigde available voorraad worden gesynchroniseerd
  • Time Window: Varianten worden gesynchroniseerd als ze in de laatste uur zijn gewijzigd of nog nooit zijn gesynchroniseerd
  • Update Tracking: Na succesvolle sync worden tracking velden bijgewerkt

Voordeel

Incremental sync reduceert het aantal API calls aanzienlijk en verbetert sync performance.

Batch Processing

Shopify varianten worden verwerkt in batches voor betere performance:

  • Batch Size: 20 varianten per batch (configureerbaar)
  • Parallel Processing: Varianten binnen een batch worden parallel verwerkt met Promise.all
  • Rate Limiting: 200ms delay tussen batches om API rate limits te respecteren
  • Error Handling: Fouten in één variant blokkeren niet de hele batch

Exponential Backoff Retry

API calls gebruiken exponential backoff voor retry logic:

  • Initial Delay: 1 seconde
  • Max Retries: 3 pogingen
  • Backoff Formula: delay = baseDelay * Math.pow(2, attempt)
  • Toepassing: Shopify locations fetch, authentication, initial API calls

Circuit Breaker Pattern

Het systeem gebruikt een circuit breaker om cascading failures te voorkomen:

  • States: CLOSED (normaal), OPEN (geblokkeerd), HALF_OPEN (testen)
  • Open Threshold: 5 opeenvolgende failures
  • Reset Timeout: 60 seconden
  • Half-Open Success Threshold: 2 succesvolle requests om terug te gaan naar CLOSED
  • Separate Breakers: Aparte circuit breakers voor Shopify en Bol.com
// Circuit breaker implementation
class CircuitBreaker {
  private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
  private failures = 0;
  private successes = 0;
  private lastFailureTime?: Date;
  
  async execute(fn: () => Promise): Promise {
    if (this.state === 'OPEN') {
      if (Date.now() - this.lastFailureTime!.getTime() > 60000) {
        this.state = 'HALF_OPEN';
        this.successes = 0;
      } else {
        throw new Error('Circuit breaker is OPEN');
      }
    }
    
    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }
}

Caching Strategie

Het systeem cachet frequente API data om performance te verbeteren:

  • Shopify Locations: Gecached in shopifyLocationsCache (Map)
  • Inventory Item IDs: Gecached in shopifyInventoryItemIdsCache (Map)
  • Cache Invalidation: Cache wordt bijgewerkt bij nieuwe syncs
  • Memory Cache: Cache blijft in geheugen tijdens runtime

Dynamic Rate Limiting

Het systeem past automatisch batch sizes en delays aan op basis van API responses:

  • 429 Detection: Detecteert rate limit errors (HTTP 429) van platforms
  • Adaptive Batch Size: Verkleint batch size bij rate limit errors
  • Adaptive Delays: Verhoogt delays tussen batches bij rate limit errors
  • Recovery: Verhoogt batch size en verlaagt delays bij succesvolle syncs

Selective Sync

Alleen relevante varianten worden gesynchroniseerd:

  • Active Products Only: Alleen varianten met products.status === 'active'
  • Zero Stock Skip: Optioneel overslaan van varianten met 0 voorraad (via SKIP_ZERO_STOCK_SYNC env var)
  • Bol.com Offer ID Required: Alleen varianten met geldige Bol.com offer ID worden gesynchroniseerd

Connection Pooling

HTTP connections worden hergebruikt voor betere performance:

  • HTTPS Agent: Configureerd met keepAlive: true
  • Max Sockets: 50 gelijktijdige connections
  • Max Free Sockets: 10 idle connections
  • Timeout: 30 seconden
import https from 'https';

const agent = new https.Agent({
  keepAlive: true,
  maxSockets: 50,
  maxFreeSockets: 10,
  timeout: 30000,
});

Monitoring & Alerting

Het systeem bevat uitgebreide monitoring en alerting functionaliteit voor sync performance.

Sync Performance Metrics

De SyncMonitoringService berekent en levert performance metrics:

  • Overall Metrics: Totale syncs, success rate, error rate, gemiddelde duur
  • Platform Metrics: Per platform (Shopify, Bol.com) metrics
  • Time-based Metrics: Metrics voor 24h, 7d, 30d periodes
  • Trend Analysis: Vergelijking met vorige periodes

Alerts

Het systeem genereert automatisch alerts voor problemen:

  • High Error Rate: Error rate > 10% (severity: WARNING)
  • Very High Error Rate: Error rate > 25% (severity: CRITICAL)
  • Slow Syncs: Gemiddelde duur > 60 seconden (severity: WARNING)
  • Very Slow Syncs: Gemiddelde duur > 120 seconden (severity: CRITICAL)
  • Failed Syncs: Aantal gefaalde syncs in laatste 24h (severity: WARNING/CRITICAL)
  • No Recent Syncs: Geen syncs in laatste 2 uur (severity: WARNING)

API Endpoint

GET /api/sync/metrics

Retourneert sync performance metrics en alerts:

{
  "overall": {
    "totalSyncs": 150,
    "successfulSyncs": 145,
    "failedSyncs": 5,
    "successRate": 0.967,
    "errorRate": 0.033,
    "avgDuration": 45.2
  },
  "platforms": {
    "shopify": { ... },
    "bol": { ... }
  },
  "timeBased": {
    "24h": { ... },
    "7d": { ... },
    "30d": { ... }
  },
  "alerts": [
    {
      "type": "HIGH_ERROR_RATE",
      "severity": "WARNING",
      "message": "Error rate is 12% (threshold: 10%)"
    }
  ]
}

Error Recovery & Dead Letter Queue

Het systeem heeft uitgebreide error recovery en dead letter queue functionaliteit.

Failed Syncs

Gefaalde syncs worden opgeslagen in de sync_logs tabel en fungeren als dead letter queue:

  • Error Logging: Alle sync errors worden gelogd met details (variant ID, error message, timestamp)
  • Retry Functionaliteit: Gefaalde syncs kunnen handmatig worden opnieuw geprobeerd via POST /api/inventory/sync/retry/:syncLogId
  • Frontend UI: Failed syncs worden getoond in /sync/logs met retry knoppen
  • Error Details: Volledige error stack traces en context worden opgeslagen

Async Processing

Syncs worden asynchroon verwerkt om de main application flow niet te blokkeren:

  • Background Processing: Syncs worden gestart in de achtergrond via .catch() handlers
  • Immediate Response: syncAllAsync() retourneert direct een syncLogId
  • Non-blocking: Frontend hoeft niet te wachten op sync completion
  • Status Tracking: Sync status kan worden opgehaald via sync logs

Automatisch Backup Systeem

Het systeem maakt automatisch backups van de database en applicatie bestanden.

Backup Script

Het backup script (scripts/backup-full-server.sh) voert de volgende acties uit:

  • Database Backup: pg_dump van PostgreSQL database, gecomprimeerd met gzip
  • Files Backup: tar.gz archive van applicatie directory
  • Exclusions: node_modules, .git, dist, .next, logs, .env worden uitgesloten
  • Storage Locations:
    • Database: /var/backups/tafelrokkenshop/database/
    • Files: /var/backups/tafelrokkenshop/files/
    • Logs: /var/backups/tafelrokkenshop/logs/
  • Retention Policy: Behoudt laatste 28 backups (7 dagen bij 4 backups per dag)
  • Cleanup: Oude backups worden automatisch verwijderd

Cron Job

Backups worden automatisch uitgevoerd via cron job:

  • Frequency: Elke 6 uur (00:00, 06:00, 12:00, 18:00 UTC)
  • Installation: Via scripts/install-backup-cron.sh
  • Logging: Output wordt gelogd naar /var/backups/tafelrokkenshop/logs/cron.log

Manual Backup

Handmatige backups kunnen worden uitgevoerd:

# Run backup script manually
bash scripts/backup-full-server.sh

# Or via SSH on live server
ssh root@46.224.25.128 "bash /var/www/tafelrokkenshop/App/scripts/backup-full-server.sh"

Restore Procedure

Database restore:

# Extract and restore database backup
gunzip < /var/backups/tafelrokkenshop/database/db-backup-2025-11-25T00-00-00.sql.gz | psql -U ops -d ops

# Or restore files
tar -xzf /var/backups/tafelrokkenshop/files/files-backup-2025-11-25T00-00-00.tar.gz -C /var/www/tafelrokkenshop/

Performance Optimalisaties

Lazy Loading

Zware componenten worden lazy geladen met Next.js dynamic import:

import dynamic from 'next/dynamic';

// Lazy load charts
const BarChart = dynamic(() => import('recharts').then((mod) => mod.BarChart), { ssr: false });
const LineChart = dynamic(() => import('recharts').then((mod) => mod.LineChart), { ssr: false });

Geïmplementeerd in:

  • apps/frontend/src/app/page.tsx - Dashboard grafieken
  • apps/frontend/src/app/products/[id]/sales/page.tsx - Product sales dashboard
  • apps/frontend/src/app/dashboards/page.tsx - Dashboards pagina

Voordeel

Lazy loading reduceert initial bundle size en verbetert First Contentful Paint (FCP) en Time to Interactive (TTI).

Image Optimization

Next.js Image component wordt gebruikt voor automatische optimalisatie:

import Image from 'next/image';

Logo

Geïmplementeerd in:

  • apps/frontend/src/components/ui/PageHeader.tsx - Logo's
  • apps/frontend/src/app/bug-reports/manage/page.tsx - Screenshots

Features:

  • Automatische format conversie (WebP wanneer mogelijk)
  • Responsive image sizing
  • Lazy loading (alleen wanneer in viewport)
  • Blur placeholder support

Virtual Scrolling

Virtual scrolling component voor lange lijsten:

import { VirtualizedList } from '@/components/ui/VirtualizedList';

 (
    
)} />

Component locatie: apps/frontend/src/components/ui/VirtualizedList.tsx

Gebruikt react-window library voor performance:

  • Alleen zichtbare items worden gerenderd
  • O(1) rendering complexity ongeacht lijstgrootte
  • Soepele scroll performance

Wanneer gebruiken?

Gebruik virtual scrolling voor lijsten met 100+ items voor optimale performance.

Sticky Actie Kolom

ResponsiveTable component ondersteunt sticky kolommen:

const headers = [
  { key: 'name', label: 'Naam' },
  { key: 'email', label: 'Email' },
  { key: 'actions', label: 'Acties', sticky: true } // Sticky kolom
];

Implementatie:

  • CSS position: sticky met right: 0
  • Z-index voor correcte stacking
  • Shadow border voor visuele scheiding
  • Hover state behouden met group-hover

Component locatie: apps/frontend/src/components/ui/ResponsiveTable.tsx

Bottom Navigation

Bottom navigation component voor mobiele navigatie:

import { BottomNavigation } from '@/components/ui/BottomNavigation';

// In layout.tsx

Component locatie: apps/frontend/src/components/ui/BottomNavigation.tsx

Features:

  • Fixed position onderaan scherm (mobiel)
  • Active state highlighting op basis van pathname
  • Badge support voor notificaties
  • Safe area insets voor iOS (notch/home indicator)
  • Customizable items via props

CSS voor safe area insets:

:root {
  --safe-area-inset-bottom: env(safe-area-inset-bottom, 0px);
}

.h-safe-area-inset-bottom {
  height: var(--safe-area-inset-bottom);
}

Code Splitting

Next.js automatische code splitting:

  • Route-based splitting: Elke route krijgt eigen bundle
  • Dynamic imports: Componenten worden alleen geladen wanneer nodig
  • Tree shaking: Ongebruikte code wordt verwijderd

Bundle optimalisatie:

  • Automatische chunking per route
  • Shared chunks voor gedeelde dependencies
  • Prefetching voor snellere navigatie