/**
* 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}`)
}