/** * Smoke test for the shipping price sheet PDF (server/utils/shipping/priceSheetPdf.ts). * Builds a synthetic PriceSheet from the DHL contract fixture (all services, 15 % markup, * default weight columns) and renders it in German and English. No server, no iDempiere. * * npx tsx reference/test-scripts/price-sheet-pdf-smoke.mts [--out ] [--merchant "Name"] [--lang de|en] * * Default output dir: $PRICE_SHEET_OUT or ./reference/test-scripts/out */ import { readFileSync, writeFileSync, mkdirSync } from 'node:fs' import path from 'node:path' const root = process.cwd() const args = process.argv.slice(2) const arg = (k: string, d: string | null = null) => { const i = args.indexOf(k); return i >= 0 && args[i + 1] && !args[i + 1].startsWith('--') ? args[i + 1] : d } const outDir = arg('--out', process.env.PRICE_SHEET_OUT || path.join(root, 'reference/test-scripts/out'))! const merchantName = arg('--merchant', 'Musterhändler GmbH')! const onlyLang = arg('--lang') // Nitro global used by getQuoteLogos() / flagImages: serve server/assets/ (assets:server) and // public/flags/ (assets:flags) from disk. ;(globalThis as any).useStorage = (base: string = 'assets:server') => ({ getItemRaw: async (key: string) => { try { return readFileSync(path.join(root, base === 'assets:flags' ? 'public/flags' : base === 'assets:shipper' ? 'public/assets/shipper' : 'server/assets', key)) } catch { return null } } }) const { EU_COUNTRY_CODES } = await import('../../app/utils/shippingCountryMap') const sheetUtil = await import('../../app/utils/shippingPriceSheet') const { generatePriceSheetPdf } = await import('../../server/utils/shipping/priceSheetPdf') type CarrierRateFixture = import('../../app/utils/shippingPriceSheet').CarrierRateFixture type PriceSheet = import('../../app/utils/shippingPriceSheet').PriceSheet type SheetRow = import('../../app/utils/shippingPriceSheet').SheetRow type SurchargeRow = import('../../app/utils/shippingPriceSheet').SurchargeRow type SectionKey = import('../../app/utils/shippingPriceSheet').SectionKey const { priceAt, applyMarkup, weightMatches, sectionOfService, defaultSheetOptions, DEFAULT_TIER_COLUMNS } = sheetUtil const fixture: CarrierRateFixture = JSON.parse(readFileSync(path.join(root, 'server/assets/shipping/dhl-contract-2025-07.json'), 'utf8')) const MARKUP = 15 const countryName = (code: string, fallbackDe: string | undefined, lang: 'de' | 'en'): string => { try { const n = new Intl.DisplayNames([lang === 'en' ? 'en' : 'de'], { type: 'region' }).of(code) if (n && n !== code) return n } catch { /* fall through */ } return fallbackDe || code } const buildSheet = (lang: 'de' | 'en'): PriceSheet => { const columns = [...DEFAULT_TIER_COLUMNS] const sections: Record = { national: [], eu: [], nonEu: [], warenpost: [], surcharges: [] } const base = fixture.rates.filter(r => r.rateType === 'base') const services = Object.fromEntries(fixture.services.map(s => [s.code, true])) // National tier services: one row per service, cells from the matching tier range. for (const svc of fixture.services.filter(s => s.scope === 'national')) { const tiers = base.filter(r => r.serviceCode === svc.code) if (!tiers.length) continue sections.national.push({ key: svc.code, serviceCode: svc.code, serviceName: svc.name, section: 'national', countryId: 101, countryCode: 'DE', countryName: lang === 'en' ? 'Germany' : 'Deutschland', zone: '', isEu: true, pricing: 'tier', maxWeight: svc.maxWeightKg, cells: columns.map(w => { const t = w <= svc.maxWeightKg ? tiers.find(r => weightMatches(w, r.rangeFrom ?? 0, r.rangeTo ?? 0)) : null return t ? { weight: w, price: applyMarkup(t.basePrice, MARKUP), source: 'derived', carrierPrice: t.basePrice } : { weight: w, price: null, source: 'none', carrierPrice: null } }), note: tiers[0]?.description || undefined }) } // International per-kg services: one row per service × country. const countrySurcharges = fixture.rates.filter(r => r.rateType === 'surcharge' && r.countryCode) for (const r of base.filter(r => r.countryCode && r.countryCode !== 'DE')) { const svc = fixture.services.find(s => s.code === r.serviceCode)! const isEu = EU_COUNTRY_CODES.includes(r.countryCode!) const section = sectionOfService(r.serviceCode, r.countryCode!, isEu) if (!section || section === 'surcharges' || section === 'national') continue const baseSell = applyMarkup(r.basePrice, MARKUP), perKgSell = applyMarkup(r.pricePerKg || 0, MARKUP) const extra = countrySurcharges.filter(s => s.serviceCode === r.serviceCode && s.countryCode === r.countryCode) sections[section].push({ key: `${r.serviceCode}:${r.countryCode}`, serviceCode: r.serviceCode, serviceName: svc?.name || r.serviceCode, section, countryId: null, countryCode: r.countryCode!, countryName: countryName(r.countryCode!, r.countryNameDe, lang), zone: r.zone || '', isEu, pricing: 'perkg', maxWeight: svc?.maxWeightKg || r.rangeTo || 31.5, base: { value: baseSell, source: 'derived', carrier: r.basePrice }, perKg: { value: perKgSell, source: 'derived', carrier: r.pricePerKg || 0 }, cells: columns.map(w => (w <= (svc?.maxWeightKg || 31.5) ? { weight: w, price: applyMarkup(priceAt(r.basePrice, r.pricePerKg || 0, w), MARKUP), source: 'derived', carrierPrice: priceAt(r.basePrice, r.pricePerKg || 0, w) } : { weight: w, price: null, source: 'none', carrierPrice: null })), surcharges: extra.length ? extra.map(s => ({ label: s.description || 'Zuschlag', price: applyMarkup(s.basePrice, MARKUP) })) : undefined }) } // Surcharges & services without a country (contract annex) → surcharge section. const surcharges: SurchargeRow[] = fixture.rates .filter(r => (r.rateType === 'surcharge' || r.rateType === 'service') && !r.countryCode) .map(r => ({ serviceCode: r.serviceCode, serviceName: fixture.services.find(s => s.code === r.serviceCode)?.name || (r.serviceCode.endsWith('_GENERAL') ? 'Allgemein' : r.serviceCode), rateType: r.rateType, label: r.description || r.rateType, carrierPrice: r.basePrice, price: r.isPercentage ? r.basePrice : applyMarkup(r.basePrice, MARKUP), isPercentage: !!r.isPercentage, description: r.sourceRef?.startsWith('VERTRAG') ? '' : r.sourceRef })) return { options: defaultSheetOptions({ organizationId: 1000001, bpartnerId: 1000123, shipperId: fixture.shipperId, language: lang, columns, markupPct: { national: MARKUP, eu: MARKUP, nonEu: MARKUP, warenpost: MARKUP, surcharges: 0 }, services, validOn: '2026-11-01', includeSurcharges: true, includeHaustuer: true, notes: lang === 'en' ? 'Prices agreed in the fulfillment contract of 2026-10-01.\nPeak surcharge applies 15 Nov – 31 Dec.' : 'Preise gemäß Fulfillment-Vertrag vom 01.10.2026.\nPeakzuschlag gilt vom 15.11. bis 31.12.' }), generatedAt: '2026-10-28T12:00:00.000Z', merchant: { name: merchantName, orgId: 1000001, bpartnerId: 1000123 }, shipperName: fixture.shipperName, services: fixture.services, sections, surcharges, warnings: ['internal: not printed'], tierCounts: { tier1: 0, tier2: 0, carrier: base.length } } } mkdirSync(outDir, { recursive: true }) for (const lang of (['de', 'en'] as const).filter(l => !onlyLang || l === onlyLang)) { const sheet = buildSheet(lang) const t0 = Date.now() const { buffer, filename } = await generatePriceSheetPdf(sheet) const target = path.join(outDir, `price-sheet-${lang}.pdf`) writeFileSync(target, buffer) const counts = Object.fromEntries(Object.entries(sheet.sections).map(([k, v]) => [k, v.length])) console.log(`${lang}: ${filename} → ${target} (${(buffer.length / 1024).toFixed(0)} kB, ${Date.now() - t0} ms) sections=${JSON.stringify(counts)} surcharges=${sheet.surcharges.length}`) }