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
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 install4. Environment variabelen configureren
Maak een .env bestand in de root directory:
# 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 -dRun database migraties:
cd apps/backend
pnpm prisma migrate dev
pnpm db:seed6. Backend starten
cd apps/backend
pnpm devBackend draait nu op http://localhost:4000
7. Frontend starten
In een nieuwe terminal:
cd apps/frontend
pnpm devFrontend 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 pnpmDependencies installeren
# Root level
pnpm install
# Backend dependencies
cd apps/backend
pnpm install
# Frontend dependencies
cd apps/frontend
pnpm installDocker Setup
De database draait in Docker. Start de containers:
docker compose up -dControleer of containers draaien:
docker psPrisma Migraties
Na het opzetten van de database, run migraties:
cd apps/backend
pnpm prisma migrate devSeed de database met initiële data:
pnpm db:seedConfiguratie
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
.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_logstabel 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 developmentENABLE_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 voorraadsyncPOST /api/inventory/check-and-fix-differences- Controleer verschillen en sync automatischPOST /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 migratie20251205010857_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) ofexternalId(Bol.com) - Response:
{ success: number, failed: number, details: Array } - Validatie: Automatische trimming van identifiers en invoice numbers, empty check
- Body:
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
- Body:
Implementatie Details
- Backend Services:
apps/backend/src/orders/orders.service.ts-bulkUpdateSupplierInvoices()methodeapps/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 verkoopordersapps/frontend/src/app/purchasing/page.tsx- Import modal voor inkoopordersapps/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:
- Frontend trigger: Gebruiker klikt op "Update status" knop in order edit pagina
- API call: Frontend doet
GET /api/orders/:id/tracking-status - Backend detectie: Backend bepaalt welke verzendpartij te gebruiken op basis van Track & Trace nummer, link, en transporter naam
- Tracking service: Juiste service wordt aangeroepen (PostNL, DPD, of Bpost)
- Data extractie: Service gebruikt Puppeteer (headless browser) om tracking pagina te bezoeken en data te extraheren
- Database update: Tracking status, statusMessage, deliveryDate en events worden opgeslagen in order
- Response: Backend retourneert tracking data naar frontend
Detectie Logica
De backend gebruikt een prioriteitsvolgorde om te bepalen welke verzendpartij te gebruiken:
- 14 cijfers → DPD (altijd, geen andere checks)
- Bpost patroon → Eindigt op
BE(bijv.CD104493459BE) of patroon[A-Z]2\d+[A-Z]2 - PostNL patroon → 13 cijfers of start met
3S - Track & Trace link → Detecteert uit URL:
dpdgroup.comofdpd.nl→ DPDpostnl.nloftracking.postnl→ PostNLtrack.bpost.cloudofbpost.cloud→ Bpost
- 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$/ofstartsWith('3S')voor PostNL - Bpost:
/^[A-Z0-9]+BE$/iof/^[A-Z]2\d+[A-Z]2$/i
- Patroon matching:
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
transporterNameniet beschikbaar is
- Wordt gebruikt als
countryCode(string, default: "NL"): Landcode voor PostNL tracking- Wordt gehaald uit
customer.shippingAddress.country_code,customer.billingAddress.country_code, ofcustomer.country
- Wordt gehaald uit
postalCode(string, optioneel): Postcode voor PostNL tracking- Wordt gehaald uit
customer.shippingAddress.postalCode,customer.billingAddress.postalCode, of geëxtraheerd uittrackAndTraceLink
- Wordt gehaald uit
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=nlWerkingsprincipe:
- Puppeteer laadt tracking pagina
- Wacht 5 seconden voor Angular app te laden en API calls te maken
- Extraheert data uit:
window.__INITIAL_STATE__(Angular state)- DOM tekst (zoekt naar "pakket is bezorgd", "bezorgd op", etc.)
- Status elementen in de pagina
- 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"
- Als pickup point niet gevonden, parseert normale status en bezorgdatum uit pagina tekst
- Retourneert
PostNLTrackingStatusobject
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})/iWanneer 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
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:
- Puppeteer laadt tracking pagina
- Wacht 5 seconden voor Cloudflare check te passeren
- Extraheert data uit:
- HTML tabellen met tracking events
- Status elementen in de DOM
- Pagina tekst
- Parseert events uit tracking tabel
- Retourneert
DPDTrackingStatusobject
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:
- Puppeteer laadt tracking pagina
- Wacht voor pagina te laden
- Probeert JSON data op te halen via API endpoint
- Extraheert tracking events en status
- Retourneert
BpostTrackingStatusobject
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): Bezorgdatumcustomer.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:
- Controleert op
data.error→ toon error toast - Controleert op
data.status === 'UNKNOWN'→ toon error toast - Controleert op
data.statusMessagemet "niet beschikbaar" → toon error toast - 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
trackingStatusMessagebevat: "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:
showManualEntryflag activeert visuele feedback
Waarom Puppeteer?
- 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
Inkooporder Workflow
- Aanmaken: Nieuwe inkooporder aanmaken met leverancier en producten
- Ontvangen: Order status wijzigen naar "Ontvangen" met ontvangstdatum
- 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, GENERALparentAccountId(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, GENERALcostAccountId(verplicht, foreign key naarcost_accounts) - NIEUW: vervangt directecategorycategory(optioneel, voor backward compatibility/migratie)description,amount,currency(default: EUR)orderId,channelId(optioneel)date,invoiceNumber,supplierInvoiceNoisRecurring,recurringFrequencytags(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 detailPOST /api/cost-accounts- Nieuwe kostenpost aanmakenPUT /api/cost-accounts/:id- Kostenpost bewerkenDELETE /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 bewerkenDELETE /api/costs/:id- Kosten verwijderenPOST /api/costs/import/bol-csv- Import Bol.com CSV kosten specificatiesGET /api/costs/impact/period- Kosten impact voor periodeGET /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_FEEcost bestaat voor die order - Category wordt automatisch bepaald op basis van channel type (SHOPIFY_FEE, BOL_FEE, OVERIG)
- Description:
"{channelType} fee voor order {orderNo}"
- Worden alleen getoond als er geen echte
- Virtual Shipping Costs: Van
order.totals.shipping- Worden alleen getoond als er geen echte
SHIPPINGcost bestaat voor die order - Category: altijd
VERZENDKOSTEN - Description:
"Verzendkosten voor order {orderNo}"
- Worden alleen getoond als er geen echte
- Virtual Additional Costs: Van
order.additionalCosts- Worden alleen getoond als er geen echte
GENERALcost bestaat voor die order - Category: altijd
OVERIG - Description:
"Additionele kosten voor order {orderNo}"
- Worden alleen getoond als er geen echte
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 inGET /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:
PWAInstallPromptcomponent - Offline Indicator:
OfflineIndicatorcomponent
PWA componenten:
apps/frontend/src/components/ui/PWAInstallPrompt.tsxapps/frontend/src/components/ui/OfflineIndicator.tsxapps/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/ordersGET /api/orders/:id
Haal specifieke order op.
curl http://localhost:4000/api/orders/123e4567-e89b-12d3-a456-426614174000PUT /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/returnsPOST /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/productsKosten
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, suppliersGET /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:
- Controleer of Docker containers draaien:
docker ps - Controleer
DATABASE_URLin.env - 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:
- Maak backup:
pnpm db:backup - Check migratie status:
cd apps/backend && pnpm prisma migrate status - Fix failed migrations:
pnpm db:fix-migration
Frontend laadt niet
Probleem: Frontend blijft op "Laden..." staan.
Oplossing:
- Controleer of backend draait op poort 4000
- Controleer
NEXT_PUBLIC_API_URLin.env - Check browser console voor errors
- Clear cache:
cd apps/frontend && rm -rf .next
Service Worker Issues
Probleem: PWA werkt niet of service worker registreert niet.
Oplossing:
- Controleer of
public/sw.jsbestaat - Controleer of
public/manifest.jsonbestaat - Clear service worker cache in browser DevTools > Application > Service Workers
- Unregister oude service workers en herlaad pagina
- 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:
- Haalt alle actieve locaties op
- 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
- 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-254recalculateInventoryAfterSale()-apps/backend/src/inventory/inventory-distribution.service.ts:552-559createMissingTransactions()-apps/backend/src/inventory/inventory.service.ts:600-601reserve-inventory.tsutility -scripts/utils/reserve-inventory.ts:33-40process-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:
- Gebruik
POST /api/inventory/transactions/create-missingendpoint - Of gebruik de "Ontbrekende Aanmaken" knop in de Mutaties Log UI
- Het systeem controleert automatisch op ontbrekende mutaties bij order updates
- Voor historische orders, specificeer
orderDateparameter
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:shopifyvoor Shopify orderspnpm backfill:bolvoor 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
API Endpoint: POST /api/inventory/recount
Response: Retourneert resultaten per locatie met aantal varianten, updates en transacties.
Multi-Location Support
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-01recalculateInventoryAfterSale()- skipt orders < 2025-01-01createMissingTransactions()- verwerkt alleen orders >= 2025-01-01reserve-inventory.tsutility - skipt oude ordersprocess-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 statistiekenGET /api/products/:id/sales/chart- Grafiek data (met optionele vorige periode vergelijking)GET /api/products/:id/sales/orders- Orderregels met paginatieGET /api/products/:id/sales/returns- Retour dataGET /api/products/:id/sales/trend- Verkoop trend analyseGET /api/products/:id/inventory/prediction- Voorraad voorspellingGET /api/products/:id/purchase-suggestions- InkoopsuggestiesGET /api/products/:id/sales/return-trend- Retour trend over tijdGET /api/products/:id/sales/inventory-movements- Voorraad bewegingen per locatieGET /api/products/:id/sales/transfer-suggestions- Transfer suggesties tussen locatiesGET /api/products/:id/low-stock-settings- Low stock alert instellingenPUT /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
findManyvoor 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 instellingenkey: 'defaults'voor globale standaardwaarden
Instellingen bevatten:
minStockLevel- Minimum voorraad niveauwarningLevel- Waarschuwings niveauleadTimeDays- Levertijd in dagensafetyStockDays- Veiligheidsvoorraad in dagenenableAlerts- Automatische alerts inschakelenemailNotifications- Email notificaties inschakelennotificationEmail- Email adres voor notificaties
Voorraad Locaties
Voorraad locaties worden beheerd via:
GET /api/inventory/locations- Lijst van alle locatiesPOST /api/inventory/locations- Nieuwe locatie aanmakenPUT /api/inventory/locations/:id- Locatie bijwerkenDELETE /api/inventory/locations/:id- Locatie verwijderen (alleen als geen voorraad/transacties)GET /api/inventory/locations/duplicates- Detecteer duplicate locatiesPOST /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
externalIddie overeenkomen met Shopify naam worden bijgewerkt
Transfer Suggesties Algoritme
Het transfer suggesties algoritme:
- Berekent voorraad per locatie voor het product (inclusief varianten indien
includeVariants=true) - Berekent gemiddelde voorraad per locatie
- Identificeert locaties met >150% van gemiddelde (hoge voorraad)
- Identificeert locaties met <50% van gemiddelde (lage voorraad)
- Suggereert transfers van hoge naar lage locaties
- 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,lastSyncedInventoryBolinproduct_variantstabel - Filtering: Alleen varianten met gewijzigde
availablevoorraad 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
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_SYNCenv 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/logsmet 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 eensyncLogId - 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_dumpvan PostgreSQL database, gecomprimeerd metgzip - Files Backup:
tar.gzarchive van applicatie directory - Exclusions:
node_modules,.git,dist,.next, logs,.envworden uitgesloten - Storage Locations:
- Database:
/var/backups/tafelrokkenshop/database/ - Files:
/var/backups/tafelrokkenshop/files/ - Logs:
/var/backups/tafelrokkenshop/logs/
- Database:
- 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 grafiekenapps/frontend/src/app/products/[id]/sales/page.tsx- Product sales dashboardapps/frontend/src/app/dashboards/page.tsx- Dashboards pagina
Voordeel
Image Optimization
Next.js Image component wordt gebruikt voor automatische optimalisatie:
import Image from 'next/image';
Geïmplementeerd in:
apps/frontend/src/components/ui/PageHeader.tsx- Logo'sapps/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?
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: stickymetright: 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