/** * One-off / idempotent: fill the five AD_Reference lists of the software-offer * catalog (CUST_SoftwareSupportLevel, CUST_SoftwareItemType, CUST_SoftwareFeatureGroup, * CUST_SoftwareFeatureMode, CUST_SoftwarePriceUnit) with their AD_Ref_List values. * The references themselves are created by hand in iDempiere (empty); this * script only ADDS missing values — it never deletes or renames. * * Same value set as `shared/utils/softwareCatalogDefaults.ts` (SOFTWARE_REF_LISTS) * and the `POST /api/settings/software-catalog/seed-lists` route. * * Run from the repo root (reads .env URLV1 + IDEMPIERETOKEN): * npx tsx reference/test-scripts/software-catalog-seed-lists.mts [--dry-run] */ import { readFileSync } from 'node:fs' import path from 'node:path' const ROOT = process.cwd() const DRY = process.argv.includes('--dry-run') const env = Object.fromEntries( readFileSync(path.join(ROOT, '.env'), 'utf8') .split('\n') .filter((l) => l.includes('=') && !l.startsWith('#')) .map((l) => { const i = l.indexOf('='); return [l.slice(0, i).trim(), l.slice(i + 1).trim().replace(/^["']|["']$/g, '')] }) ) const base = String(env.URLV1 || '').replace(/\/$/, '') const token = env.IDEMPIERETOKEN if (!base || !token) throw new Error('URLV1 / IDEMPIERETOKEN missing in .env') const LISTS: Record> = { CUST_SoftwareSupportLevel: [ { value: 'ticket', name: 'Ticket' }, { value: 'phone', name: 'Ticket + Telefon' }, { value: 'sla', name: 'Ticket + Telefon + SLA' } ], CUST_SoftwareItemType: [ { value: 'feature', name: 'Funktion' }, { value: 'addon', name: 'Zusatzleistung' } ], CUST_SoftwareFeatureGroup: [ { value: 'core', name: 'Kern-WMS' }, { value: 'warehouse', name: 'Lager & Automatisierung' }, { value: 'advanced', name: 'Erweitert & Schnittstellen' }, { value: 'enterprise', name: 'Enterprise' } ], CUST_SoftwareFeatureMode: [ { value: 'standard', name: 'Standard' }, { value: 'onRequest', name: 'möglich / auf Anfrage' }, { value: 'quote', name: 'gegen Angebot' } ], CUST_SoftwarePriceUnit: [ { value: 'month', name: 'pro Monat' }, { value: 'once', name: 'einmalig' }, { value: 'hour', name: 'pro Stunde' }, { value: 'text', name: 'Text / auf Anfrage' } ] } const api = async (method: string, p: string, body?: any) => { const r = await fetch(`${base}/${p}`, { method, headers: { Authorization: `Bearer ${token}`, Accept: 'application/json', ...(body ? { 'Content-Type': 'application/json' } : {}) }, body: body ? JSON.stringify(body) : undefined }) const text = await r.text() let json: any try { json = JSON.parse(text) } catch { json = { raw: text.slice(0, 300) } } return { status: r.status, json } } const enc = encodeURIComponent let failures = 0 for (const [refName, values] of Object.entries(LISTS)) { const ref = await api('GET', `models/ad_reference?$filter=${enc(`Name eq '${refName}'`)}&$top=1`) const refId = Number(ref.json?.records?.[0]?.id) if (!refId) { console.log(`✗ ${refName}: reference not found (status ${ref.status})`); failures++; continue } const existing = await api('GET', `models/ad_ref_list?$filter=${enc(`AD_Reference_ID eq ${refId}`)}&$top=100`) const have = new Map((existing.json?.records || []).map((r: any) => [String(r.Value), r])) console.log(`\n${refName} (AD_Reference_ID ${refId}) — ${have.size} existing value(s)`) for (const v of values) { if (have.has(v.value)) { console.log(` = ${v.value} already present ("${have.get(v.value).Name}")`); continue } if (DRY) { console.log(` + ${v.value} → "${v.name}" (dry run)`); continue } const res = await api('POST', 'models/ad_ref_list', { AD_Reference_ID: { id: refId }, value: v.value, name: v.name, entityType: 'U' }) if (res.status >= 200 && res.status < 300 && res.json?.id) { console.log(` + ${v.value} → "${v.name}" created (id ${res.json.id})`) } else { failures++ console.log(` ✗ ${v.value} → "${v.name}" FAILED ${res.status}: ${JSON.stringify(res.json).slice(0, 300)}`) } } } console.log(failures ? `\n${failures} failure(s) — enter the listed Value/Name pairs by hand in iDempiere (Reference → List Validation).` : '\nAll list values present.') process.exit(failures ? 1 : 0)