punchout-simulator 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -6
- package/dist/server/cli.js +300 -63
- package/dist/web/assets/{cssMode-UKotGf5X.js → cssMode-CJ0pl8aC.js} +1 -1
- package/dist/web/assets/{freemarker2-0DRATxLT.js → freemarker2-2fJlciUR.js} +1 -1
- package/dist/web/assets/{handlebars-U7ip3Hjn.js → handlebars-5dZcbLn3.js} +1 -1
- package/dist/web/assets/{html-pMwW-Db1.js → html-DGPPwllN.js} +1 -1
- package/dist/web/assets/{htmlMode-BZiR_DER.js → htmlMode-DtXuV7Ej.js} +1 -1
- package/dist/web/assets/{index-sN4D-IAg.css → index-B2TOY2Lh.css} +1 -1
- package/dist/web/assets/{index-fwAo3vG0.js → index-BBqxMuv9.js} +194 -192
- package/dist/web/assets/{javascript-BNQzFVRF.js → javascript-FH1B7R5g.js} +1 -1
- package/dist/web/assets/{jsonMode-CSAIHDlB.js → jsonMode-CV67hBvi.js} +1 -1
- package/dist/web/assets/{liquid--vvS9GUJ.js → liquid-DdRsn7Vl.js} +1 -1
- package/dist/web/assets/{mdx-BJDQflEF.js → mdx-Dovg1GA1.js} +1 -1
- package/dist/web/assets/{python-CepodNht.js → python-CzgcLU_4.js} +1 -1
- package/dist/web/assets/{razor-BQksENSs.js → razor-DCGsvo9Y.js} +1 -1
- package/dist/web/assets/{tsMode-BRKq1Rx4.js → tsMode-gs0Re4an.js} +1 -1
- package/dist/web/assets/{typescript-V-bP8GlZ.js → typescript-D_0DkNkW.js} +1 -1
- package/dist/web/assets/{xml-Bgme81_M.js → xml-DOUouEEo.js} +1 -1
- package/dist/web/assets/{yaml-CkpLYd3y.js → yaml-Co8k5eYC.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
package/dist/server/cli.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/server/cli.ts
|
|
4
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
4
5
|
import { fileURLToPath } from "url";
|
|
5
6
|
import { serve } from "@hono/node-server";
|
|
6
7
|
import { nanoid as nanoid7 } from "nanoid";
|
|
7
8
|
|
|
8
9
|
// src/server/app.ts
|
|
9
10
|
import { existsSync as existsSync3 } from "fs";
|
|
10
|
-
import { Hono as
|
|
11
|
+
import { Hono as Hono10 } from "hono";
|
|
11
12
|
import { logger } from "hono/logger";
|
|
12
13
|
import { bodyLimit } from "hono/body-limit";
|
|
13
14
|
import { getCookie } from "hono/cookie";
|
|
@@ -24,6 +25,9 @@ function setRuntime(r) {
|
|
|
24
25
|
function getToken() {
|
|
25
26
|
return runtime.token;
|
|
26
27
|
}
|
|
28
|
+
function getVersion() {
|
|
29
|
+
return runtime.version;
|
|
30
|
+
}
|
|
27
31
|
function getPublicUrl() {
|
|
28
32
|
return runtime.publicUrl.replace(/\/$/, "");
|
|
29
33
|
}
|
|
@@ -144,6 +148,52 @@ function seedBuiltinProfiles(data, now2) {
|
|
|
144
148
|
}
|
|
145
149
|
}
|
|
146
150
|
|
|
151
|
+
// src/server/cxml/product-list-presets.ts
|
|
152
|
+
var PRODUCT_LIST_PRESETS = [
|
|
153
|
+
{
|
|
154
|
+
id: "sample",
|
|
155
|
+
name: "Sample assortment",
|
|
156
|
+
builtin: true,
|
|
157
|
+
description: "A representative office & industrial supplies catalog (~20 items).",
|
|
158
|
+
items: [
|
|
159
|
+
{ supplierPartId: "WIDGET-001", supplierPartAuxiliaryId: "WIDGET-001-STD", description: "Premium Steel Widget", unitPrice: 12.5, currency: "USD", uom: "EA", classifications: [{ domain: "UNSPSC", value: "31161500" }], manufacturerPartId: "MFR-W001", manufacturerName: "Acme Manufacturing" },
|
|
160
|
+
{ supplierPartId: "BOLT-250", supplierPartAuxiliaryId: "BOLT-250-CONTRACT-A", description: "M8 Hex Bolt (pack of 250)", unitPrice: 34, currency: "USD", uom: "PK", classifications: [{ domain: "UNSPSC", value: "31161600" }], manufacturerPartId: "MFR-B250", manufacturerName: "Acme Manufacturing" },
|
|
161
|
+
{ supplierPartId: "NUT-M8-500", description: "M8 Hex Nut (pack of 500)", unitPrice: 28.5, currency: "USD", uom: "PK", classifications: [{ domain: "UNSPSC", value: "31161700" }], manufacturerPartId: "MFR-N8500", manufacturerName: "Acme Manufacturing" },
|
|
162
|
+
{ supplierPartId: "WASHER-M8", description: "M8 Flat Washer (pack of 1000)", unitPrice: 19.95, currency: "USD", uom: "PK", classifications: [{ domain: "UNSPSC", value: "31161800" }], manufacturerName: "Acme Manufacturing" },
|
|
163
|
+
{ supplierPartId: "TAPE-RED", supplierPartAuxiliaryId: "TAPE-RED-50M", description: "Industrial Marking Tape, Red", unitPrice: 5.75, currency: "USD", uom: "RL", classifications: [{ domain: "UNSPSC", value: "31201500" }] },
|
|
164
|
+
{ supplierPartId: "TAPE-YEL", supplierPartAuxiliaryId: "TAPE-YEL-50M", description: "Industrial Marking Tape, Yellow", unitPrice: 5.75, currency: "USD", uom: "RL", classifications: [{ domain: "UNSPSC", value: "31201500" }] },
|
|
165
|
+
// Multiple classifications: UNSPSC + a supplier-specific commodity scheme.
|
|
166
|
+
{ supplierPartId: "CABLE-CU-2.5", supplierPartAuxiliaryId: "CABLE-CU-2.5-RAL", description: "Copper Cable 2.5mm\xB2 (by the metre)", unitPrice: 1.15, currency: "EUR", uom: "MTR", classifications: [{ domain: "UNSPSC", value: "26121600" }, { domain: "eCl@ss", value: "27-06-01-01" }], manufacturerPartId: "WIRE-25", manufacturerName: "Volt Industries", allowFractional: true },
|
|
167
|
+
{ supplierPartId: "CABLE-CU-4.0", supplierPartAuxiliaryId: "CABLE-CU-4.0-RAL", description: "Copper Cable 4.0mm\xB2 (by the metre)", unitPrice: 1.8, currency: "EUR", uom: "MTR", classifications: [{ domain: "UNSPSC", value: "26121600" }, { domain: "eCl@ss", value: "27-06-01-02" }], manufacturerPartId: "WIRE-40", manufacturerName: "Volt Industries", allowFractional: true },
|
|
168
|
+
{ supplierPartId: "PAINT-WHT", description: "Acrylic Wall Paint, White (by the litre)", unitPrice: 8.4, currency: "USD", uom: "LTR", classifications: [{ domain: "UNSPSC", value: "31211500" }], manufacturerName: "ColorWorks", allowFractional: true },
|
|
169
|
+
{ supplierPartId: "LUBE-OIL", description: "Machine Lubricating Oil (by the litre)", unitPrice: 6.9, currency: "USD", uom: "LTR", classifications: [{ domain: "UNSPSC", value: "15121800" }], manufacturerName: "SlickPro", allowFractional: true },
|
|
170
|
+
{ supplierPartId: "GLOVE-NIT-M", supplierPartAuxiliaryId: "GLOVE-NIT-M-CE", description: "Nitrile Gloves, Medium (box of 100)", unitPrice: 9.95, currency: "GBP", uom: "BX", classifications: [{ domain: "UNSPSC", value: "46181504" }], manufacturerName: "SafeHands" },
|
|
171
|
+
{ supplierPartId: "GLOVE-NIT-L", supplierPartAuxiliaryId: "GLOVE-NIT-L-CE", description: "Nitrile Gloves, Large (box of 100)", unitPrice: 9.95, currency: "GBP", uom: "BX", classifications: [{ domain: "UNSPSC", value: "46181504" }], manufacturerName: "SafeHands" },
|
|
172
|
+
{ supplierPartId: "GOGGLE-STD", description: "Safety Goggles, Anti-fog", unitPrice: 4.6, currency: "GBP", uom: "EA", classifications: [{ domain: "UNSPSC", value: "46181702" }], manufacturerName: "SafeHands" },
|
|
173
|
+
{ supplierPartId: "HELMET-WHT", description: "Hard Hat, White", unitPrice: 9.8, currency: "GBP", uom: "EA", classifications: [{ domain: "UNSPSC", value: "46181701" }], manufacturerName: "SafeHands" },
|
|
174
|
+
{ supplierPartId: "PAPER-A4", description: "Copy Paper A4 80gsm (ream of 500)", unitPrice: 18, currency: "PLN", uom: "RM", classifications: [{ domain: "UNSPSC", value: "14111507" }], manufacturerName: "PaperCo" },
|
|
175
|
+
{ supplierPartId: "PEN-BLU-12", description: "Ballpoint Pens, Blue (pack of 12)", unitPrice: 12.99, currency: "PLN", uom: "PK", classifications: [{ domain: "UNSPSC", value: "44121701" }], manufacturerName: "WriteRight" },
|
|
176
|
+
{ supplierPartId: "BINDER-A4", description: "Lever Arch Binder A4, Black", unitPrice: 11.5, currency: "PLN", uom: "EA", classifications: [{ domain: "UNSPSC", value: "44122011" }], manufacturerName: "Officeline" },
|
|
177
|
+
{ supplierPartId: "TONER-BLK", supplierPartAuxiliaryId: "TN-1000-OEM", description: "Laser Toner Cartridge, Black", unitPrice: 64, currency: "USD", uom: "EA", classifications: [{ domain: "UNSPSC", value: "44103105" }], manufacturerPartId: "TN-1000", manufacturerName: "PrintMax" },
|
|
178
|
+
{ supplierPartId: "BATT-AA-24", description: "Alkaline Batteries AA (pack of 24)", unitPrice: 14.5, currency: "USD", uom: "PK", classifications: [{ domain: "UNSPSC", value: "26111702" }], manufacturerName: "PowerCell" },
|
|
179
|
+
{ supplierPartId: "DRILL-BIT-SET", supplierPartAuxiliaryId: "DBS-19-HSS", description: "HSS Drill Bit Set (19 pieces)", unitPrice: 22.75, currency: "USD", uom: "ST", classifications: [{ domain: "UNSPSC", value: "27112700" }], manufacturerPartId: "DBS-19", manufacturerName: "Acme Manufacturing" },
|
|
180
|
+
{ supplierPartId: "GAFF-CLEAN", description: "All-purpose Cleaner Concentrate (by the litre)", unitPrice: 3.6, currency: "USD", uom: "LTR", classifications: [{ domain: "UNSPSC", value: "47131800" }], manufacturerName: "CleanLine", allowFractional: true }
|
|
181
|
+
]
|
|
182
|
+
}
|
|
183
|
+
];
|
|
184
|
+
var SAMPLE_PRODUCT_LIST = {
|
|
185
|
+
...PRODUCT_LIST_PRESETS[0],
|
|
186
|
+
createdAt: "",
|
|
187
|
+
updatedAt: ""
|
|
188
|
+
};
|
|
189
|
+
function seedBuiltinProductLists(data, now2) {
|
|
190
|
+
for (const preset of PRODUCT_LIST_PRESETS) {
|
|
191
|
+
if (!data.productLists.some((p) => p.id === preset.id)) {
|
|
192
|
+
data.productLists.push({ ...preset, createdAt: now2, updatedAt: now2 });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
147
197
|
// src/server/store/paths.ts
|
|
148
198
|
import { chmodSync, mkdirSync } from "fs";
|
|
149
199
|
import { resolve } from "path";
|
|
@@ -180,15 +230,19 @@ var db = null;
|
|
|
180
230
|
async function initConfig() {
|
|
181
231
|
ensureDirs();
|
|
182
232
|
const adapter = new JSONFile(configPath());
|
|
183
|
-
db = new Low(adapter, { buyers: [], suppliers: [], connections: [], profiles: [] });
|
|
233
|
+
db = new Low(adapter, { buyers: [], suppliers: [], connections: [], profiles: [], productLists: [] });
|
|
184
234
|
await db.read();
|
|
185
|
-
db.data ||= { buyers: [], suppliers: [], connections: [], profiles: [] };
|
|
235
|
+
db.data ||= { buyers: [], suppliers: [], connections: [], profiles: [], productLists: [] };
|
|
186
236
|
db.data.buyers ||= [];
|
|
187
237
|
db.data.suppliers ||= [];
|
|
188
238
|
db.data.connections ||= [];
|
|
189
239
|
db.data.profiles ||= [];
|
|
240
|
+
db.data.productLists ||= [];
|
|
190
241
|
seedBuiltinProfiles(db.data, now());
|
|
242
|
+
seedBuiltinProductLists(db.data, now());
|
|
191
243
|
migrateLegacy(db.data);
|
|
244
|
+
migrateInlineCatalogs(db.data);
|
|
245
|
+
migrateClassifications(db.data);
|
|
192
246
|
await db.write();
|
|
193
247
|
try {
|
|
194
248
|
chmodSync2(configPath(), 384);
|
|
@@ -349,6 +403,38 @@ function effectiveProfile(connection, buyer) {
|
|
|
349
403
|
function dtdVersionFor(eff, docType) {
|
|
350
404
|
return eff.dtdVersions[docType] ?? eff.dtdVersions.default;
|
|
351
405
|
}
|
|
406
|
+
var listProductLists = () => requireDb().data.productLists;
|
|
407
|
+
var getProductList = (id) => requireDb().data.productLists.find((p) => p.id === id);
|
|
408
|
+
async function createProductList(input) {
|
|
409
|
+
const list = { ...input, id: input.id ?? nanoid(8), createdAt: now(), updatedAt: now() };
|
|
410
|
+
const d = requireDb();
|
|
411
|
+
d.data.productLists.push(list);
|
|
412
|
+
await d.write();
|
|
413
|
+
return list;
|
|
414
|
+
}
|
|
415
|
+
async function updateProductList(id, patch) {
|
|
416
|
+
const d = requireDb();
|
|
417
|
+
const existing = d.data.productLists.find((p) => p.id === id);
|
|
418
|
+
if (!existing) return void 0;
|
|
419
|
+
Object.assign(existing, patch, { id, updatedAt: now() });
|
|
420
|
+
await d.write();
|
|
421
|
+
return existing;
|
|
422
|
+
}
|
|
423
|
+
async function deleteProductList(id) {
|
|
424
|
+
const d = requireDb();
|
|
425
|
+
if (d.data.suppliers.some((s) => s.productListIds?.includes(id))) {
|
|
426
|
+
throw new Error("product list is referenced by a supplier");
|
|
427
|
+
}
|
|
428
|
+
const before = d.data.productLists.length;
|
|
429
|
+
d.data.productLists = d.data.productLists.filter((p) => p.id !== id);
|
|
430
|
+
const removed = d.data.productLists.length < before;
|
|
431
|
+
if (removed) await d.write();
|
|
432
|
+
return removed;
|
|
433
|
+
}
|
|
434
|
+
function catalogForSupplier(supplier) {
|
|
435
|
+
const ids = supplier.productListIds ?? [];
|
|
436
|
+
return ids.flatMap((id) => getProductList(id)?.items ?? []);
|
|
437
|
+
}
|
|
352
438
|
function migrateLegacy(data) {
|
|
353
439
|
const legacy = data.connections.filter((c) => "from" in c || "to" in c);
|
|
354
440
|
if (legacy.length === 0) return;
|
|
@@ -408,6 +494,39 @@ function migrateLegacy(data) {
|
|
|
408
494
|
}
|
|
409
495
|
data.connections = migrated;
|
|
410
496
|
}
|
|
497
|
+
function migrateInlineCatalogs(data) {
|
|
498
|
+
for (const supplier of data.suppliers) {
|
|
499
|
+
const legacy = supplier.catalog;
|
|
500
|
+
if (!legacy || legacy.length === 0) {
|
|
501
|
+
delete supplier.catalog;
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
if (supplier.productListIds && supplier.productListIds.length > 0) {
|
|
505
|
+
delete supplier.catalog;
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
const list = {
|
|
509
|
+
id: nanoid(8),
|
|
510
|
+
name: `${supplier.name} catalog`,
|
|
511
|
+
items: legacy,
|
|
512
|
+
createdAt: now(),
|
|
513
|
+
updatedAt: now()
|
|
514
|
+
};
|
|
515
|
+
data.productLists.push(list);
|
|
516
|
+
supplier.productListIds = [list.id];
|
|
517
|
+
delete supplier.catalog;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
function migrateClassifications(data) {
|
|
521
|
+
for (const list of data.productLists) {
|
|
522
|
+
for (const item of list.items) {
|
|
523
|
+
if (!Array.isArray(item.classifications) || item.classifications.length === 0) {
|
|
524
|
+
item.classifications = item.unspsc ? [{ domain: "UNSPSC", value: String(item.unspsc) }] : [];
|
|
525
|
+
}
|
|
526
|
+
delete item.unspsc;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
411
530
|
|
|
412
531
|
// src/server/routes/connections.ts
|
|
413
532
|
var connectionsRoute = new Hono();
|
|
@@ -515,22 +634,14 @@ buyersRoute.delete("/:id", async (c) => {
|
|
|
515
634
|
});
|
|
516
635
|
var suppliersRoute = new Hono2();
|
|
517
636
|
function normalizeSupplier(body) {
|
|
518
|
-
const
|
|
519
|
-
supplierPartId: String(it?.supplierPartId ?? ""),
|
|
520
|
-
description: String(it?.description ?? ""),
|
|
521
|
-
unitPrice: Number(it?.unitPrice ?? 0) || 0,
|
|
522
|
-
currency: String(it?.currency ?? "USD"),
|
|
523
|
-
uom: String(it?.uom ?? "EA"),
|
|
524
|
-
unspsc: String(it?.unspsc ?? ""),
|
|
525
|
-
manufacturerPartId: it?.manufacturerPartId ? String(it.manufacturerPartId) : void 0,
|
|
526
|
-
manufacturerName: it?.manufacturerName ? String(it.manufacturerName) : void 0
|
|
527
|
-
})) : void 0;
|
|
637
|
+
const productListIds = Array.isArray(body?.productListIds) ? body.productListIds.map((id) => String(id)) : [];
|
|
528
638
|
return {
|
|
529
639
|
name: String(body?.name ?? "Untitled supplier"),
|
|
530
640
|
identity: cred(body?.identity),
|
|
531
641
|
punchoutUrl: body?.punchoutUrl ? String(body.punchoutUrl) : void 0,
|
|
532
642
|
orderUrl: body?.orderUrl ? String(body.orderUrl) : void 0,
|
|
533
|
-
|
|
643
|
+
productListIds,
|
|
644
|
+
allowMixedCurrency: Boolean(body?.allowMixedCurrency)
|
|
534
645
|
};
|
|
535
646
|
}
|
|
536
647
|
suppliersRoute.get("/", (c) => c.json(listSuppliers()));
|
|
@@ -626,8 +737,66 @@ profilesRoute.delete("/:id", async (c) => {
|
|
|
626
737
|
});
|
|
627
738
|
profilePresetsRoute.get("/", (c) => c.json(PROFILE_PRESETS));
|
|
628
739
|
|
|
629
|
-
// src/server/routes/
|
|
740
|
+
// src/server/routes/products.ts
|
|
630
741
|
import { Hono as Hono4 } from "hono";
|
|
742
|
+
var productsRoute = new Hono4();
|
|
743
|
+
var productListPresetsRoute = new Hono4();
|
|
744
|
+
function normalizeClassifications(it) {
|
|
745
|
+
if (Array.isArray(it?.classifications)) {
|
|
746
|
+
return it.classifications.map((c) => ({ domain: String(c?.domain ?? "UNSPSC"), value: String(c?.value ?? "") })).filter((c) => c.value.trim().length > 0);
|
|
747
|
+
}
|
|
748
|
+
if (it?.unspsc) return [{ domain: "UNSPSC", value: String(it.unspsc) }];
|
|
749
|
+
return [];
|
|
750
|
+
}
|
|
751
|
+
function normalizeItem(it) {
|
|
752
|
+
return {
|
|
753
|
+
supplierPartId: String(it?.supplierPartId ?? ""),
|
|
754
|
+
supplierPartAuxiliaryId: it?.supplierPartAuxiliaryId ? String(it.supplierPartAuxiliaryId) : void 0,
|
|
755
|
+
description: String(it?.description ?? ""),
|
|
756
|
+
unitPrice: Number(it?.unitPrice ?? 0) || 0,
|
|
757
|
+
currency: String(it?.currency ?? "USD"),
|
|
758
|
+
uom: String(it?.uom ?? "EA"),
|
|
759
|
+
classifications: normalizeClassifications(it),
|
|
760
|
+
manufacturerPartId: it?.manufacturerPartId ? String(it.manufacturerPartId) : void 0,
|
|
761
|
+
manufacturerName: it?.manufacturerName ? String(it.manufacturerName) : void 0,
|
|
762
|
+
allowFractional: Boolean(it?.allowFractional)
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
function normalizeProductList(body) {
|
|
766
|
+
const items = Array.isArray(body?.items) ? body.items.map(normalizeItem) : [];
|
|
767
|
+
return {
|
|
768
|
+
name: String(body?.name ?? "Untitled product list"),
|
|
769
|
+
description: body?.description ? String(body.description) : void 0,
|
|
770
|
+
items
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
productsRoute.get("/", (c) => c.json(listProductLists()));
|
|
774
|
+
productsRoute.post("/", async (c) => {
|
|
775
|
+
const input = normalizeProductList(await c.req.json().catch(() => ({})));
|
|
776
|
+
if (!input.name.trim()) return c.json({ errors: ["name is required"] }, 400);
|
|
777
|
+
return c.json(await createProductList(input), 201);
|
|
778
|
+
});
|
|
779
|
+
productsRoute.get("/:id", (c) => {
|
|
780
|
+
const p = getProductList(c.req.param("id"));
|
|
781
|
+
return p ? c.json(p) : c.json({ error: "not found" }, 404);
|
|
782
|
+
});
|
|
783
|
+
productsRoute.put("/:id", async (c) => {
|
|
784
|
+
if (!getProductList(c.req.param("id"))) return c.json({ error: "not found" }, 404);
|
|
785
|
+
const input = normalizeProductList(await c.req.json().catch(() => ({})));
|
|
786
|
+
return c.json(await updateProductList(c.req.param("id"), input));
|
|
787
|
+
});
|
|
788
|
+
productsRoute.delete("/:id", async (c) => {
|
|
789
|
+
try {
|
|
790
|
+
const ok = await deleteProductList(c.req.param("id"));
|
|
791
|
+
return ok ? c.json({ ok: true }) : c.json({ error: "not found" }, 404);
|
|
792
|
+
} catch (e) {
|
|
793
|
+
return c.json({ error: e instanceof Error ? e.message : String(e) }, 409);
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
productListPresetsRoute.get("/", (c) => c.json(PRODUCT_LIST_PRESETS));
|
|
797
|
+
|
|
798
|
+
// src/server/routes/flow.ts
|
|
799
|
+
import { Hono as Hono5 } from "hono";
|
|
631
800
|
import { nanoid as nanoid5 } from "nanoid";
|
|
632
801
|
|
|
633
802
|
// src/server/store/log.ts
|
|
@@ -940,6 +1109,15 @@ function escapeXml(value) {
|
|
|
940
1109
|
function makePayloadId(host3, nowIso) {
|
|
941
1110
|
return `${nowIso}.${nanoid4(10)}@${host3}`;
|
|
942
1111
|
}
|
|
1112
|
+
function lineItemsTotal(items, headerCurrency) {
|
|
1113
|
+
const currencies = new Set(items.map((it) => it.currency || headerCurrency));
|
|
1114
|
+
if (currencies.size > 1) return 0;
|
|
1115
|
+
return items.reduce((sum, it) => sum + (it.unitPriceAmount ?? 0) * it.quantity, 0);
|
|
1116
|
+
}
|
|
1117
|
+
function classificationBlock(it, indent) {
|
|
1118
|
+
const list = it.classifications && it.classifications.length > 0 ? it.classifications : [{ domain: it.classificationDomain ?? "UNSPSC", value: it.classification ?? "" }];
|
|
1119
|
+
return list.map((c) => `${indent}<Classification domain="${escapeXml(c.domain || "UNSPSC")}">${escapeXml(c.value)}</Classification>`).join("\n");
|
|
1120
|
+
}
|
|
943
1121
|
function credentialBlock(tag, c) {
|
|
944
1122
|
return ` <${tag}>
|
|
945
1123
|
<Credential domain="${escapeXml(c.domain)}">
|
|
@@ -1050,9 +1228,7 @@ function buildOrderRequest(o) {
|
|
|
1050
1228
|
</UnitPrice>
|
|
1051
1229
|
<Description xml:lang="en">${escapeXml(it.description ?? "")}</Description>
|
|
1052
1230
|
<UnitOfMeasure>${escapeXml(it.uom ?? "EA")}</UnitOfMeasure>
|
|
1053
|
-
|
|
1054
|
-
it.classification ?? ""
|
|
1055
|
-
)}</Classification>${it.manufacturerPartId ? `
|
|
1231
|
+
${classificationBlock(it, " ")}${it.manufacturerPartId ? `
|
|
1056
1232
|
<ManufacturerPartID>${escapeXml(it.manufacturerPartId)}</ManufacturerPartID>` : ""}${it.manufacturerName ? `
|
|
1057
1233
|
<ManufacturerName>${escapeXml(it.manufacturerName)}</ManufacturerName>` : ""}${commentsWithAttachments(cids, " ")}
|
|
1058
1234
|
</ItemDetail>
|
|
@@ -1112,10 +1288,7 @@ function buildResponseStatus(o) {
|
|
|
1112
1288
|
return envelope(o.payloadId, o.timestamp, o.lang ?? "en-US", inner, o.dtdVersion);
|
|
1113
1289
|
}
|
|
1114
1290
|
function buildPunchOutOrderMessage(o) {
|
|
1115
|
-
const total = o.items.
|
|
1116
|
-
(sum, it) => sum + (it.unitPriceAmount ?? 0) * it.quantity,
|
|
1117
|
-
0
|
|
1118
|
-
);
|
|
1291
|
+
const total = lineItemsTotal(o.items, o.currency);
|
|
1119
1292
|
const items = o.items.map(
|
|
1120
1293
|
(it) => ` <ItemIn quantity="${escapeXml(it.quantity)}">
|
|
1121
1294
|
<ItemID>
|
|
@@ -1130,9 +1303,7 @@ function buildPunchOutOrderMessage(o) {
|
|
|
1130
1303
|
</UnitPrice>
|
|
1131
1304
|
<Description xml:lang="en">${escapeXml(it.description ?? "")}</Description>
|
|
1132
1305
|
<UnitOfMeasure>${escapeXml(it.uom ?? "EA")}</UnitOfMeasure>
|
|
1133
|
-
|
|
1134
|
-
it.classification ?? ""
|
|
1135
|
-
)}</Classification>${it.manufacturerPartId ? `
|
|
1306
|
+
${classificationBlock(it, " ")}${it.manufacturerPartId ? `
|
|
1136
1307
|
<ManufacturerPartID>${escapeXml(it.manufacturerPartId)}</ManufacturerPartID>` : ""}
|
|
1137
1308
|
</ItemDetail>
|
|
1138
1309
|
</ItemIn>`
|
|
@@ -1322,8 +1493,8 @@ function parseCart(doc) {
|
|
|
1322
1493
|
const items = asArray(pom?.ItemIn).map((it) => {
|
|
1323
1494
|
const detail = it?.ItemDetail;
|
|
1324
1495
|
const up = money(detail?.UnitPrice);
|
|
1325
|
-
const
|
|
1326
|
-
const classFirst =
|
|
1496
|
+
const classifications = asArray(detail?.Classification).filter((c) => c != null).map((c) => ({ domain: attr(c, "domain") ?? "", value: text(c) ?? "" }));
|
|
1497
|
+
const classFirst = classifications[0];
|
|
1327
1498
|
return {
|
|
1328
1499
|
quantity: Number(attr(it, "quantity") ?? "1") || 1,
|
|
1329
1500
|
supplierPartId: text(it?.ItemID?.SupplierPartID),
|
|
@@ -1332,8 +1503,9 @@ function parseCart(doc) {
|
|
|
1332
1503
|
uom: text(detail?.UnitOfMeasure),
|
|
1333
1504
|
unitPriceAmount: up.amount,
|
|
1334
1505
|
currency: up.currency,
|
|
1335
|
-
|
|
1336
|
-
|
|
1506
|
+
classifications: classifications.length > 0 ? classifications : void 0,
|
|
1507
|
+
classificationDomain: classFirst?.domain,
|
|
1508
|
+
classification: classFirst?.value,
|
|
1337
1509
|
manufacturerPartId: text(detail?.ManufacturerPartID),
|
|
1338
1510
|
manufacturerName: text(detail?.ManufacturerName)
|
|
1339
1511
|
};
|
|
@@ -1496,6 +1668,7 @@ function checkPunchback(doc, ctx, issues) {
|
|
|
1496
1668
|
issues.warn("empty-cart", "PunchOutOrderMessage contains no ItemIn elements", "cXML/.../PunchOutOrderMessage");
|
|
1497
1669
|
}
|
|
1498
1670
|
let lineSum = 0;
|
|
1671
|
+
const lineCurrencies = [];
|
|
1499
1672
|
items.forEach((it, i) => {
|
|
1500
1673
|
const base = `cXML/.../ItemIn[${i + 1}]`;
|
|
1501
1674
|
const qty = num(attr(it, "quantity"));
|
|
@@ -1512,10 +1685,13 @@ function checkPunchback(doc, ctx, issues) {
|
|
|
1512
1685
|
}
|
|
1513
1686
|
const up = detail.UnitPrice?.Money;
|
|
1514
1687
|
const upAmount = num(text(up));
|
|
1688
|
+
const upCurrency = attr(up, "currency");
|
|
1515
1689
|
if (up == null) {
|
|
1516
1690
|
issues.error("item-missing-unitprice", `ItemIn[${i + 1}] is missing ItemDetail/UnitPrice/Money`, `${base}/ItemDetail/UnitPrice`);
|
|
1517
|
-
} else if (!
|
|
1691
|
+
} else if (!upCurrency) {
|
|
1518
1692
|
issues.error("item-missing-currency", `ItemIn[${i + 1}] UnitPrice/Money is missing @currency`, `${base}/ItemDetail/UnitPrice/Money`);
|
|
1693
|
+
} else {
|
|
1694
|
+
lineCurrencies.push(upCurrency);
|
|
1519
1695
|
}
|
|
1520
1696
|
if (!text(detail.Description)) {
|
|
1521
1697
|
issues.error("item-missing-description", `ItemIn[${i + 1}] is missing ItemDetail/Description`, `${base}/ItemDetail/Description`);
|
|
@@ -1531,7 +1707,14 @@ function checkPunchback(doc, ctx, issues) {
|
|
|
1531
1707
|
}
|
|
1532
1708
|
if (qty != null && upAmount != null) lineSum += qty * upAmount;
|
|
1533
1709
|
});
|
|
1534
|
-
|
|
1710
|
+
const singleCurrency = checkSingleCurrency(
|
|
1711
|
+
lineCurrencies,
|
|
1712
|
+
totalCurrency,
|
|
1713
|
+
issues,
|
|
1714
|
+
"cXML/.../PunchOutOrderMessageHeader/Total",
|
|
1715
|
+
ctx.allowMixedCurrency
|
|
1716
|
+
);
|
|
1717
|
+
if (singleCurrency && totalAmount != null && items.length > 0) {
|
|
1535
1718
|
const diff = Math.abs(totalAmount - lineSum);
|
|
1536
1719
|
if (diff > 0.01) {
|
|
1537
1720
|
issues.warn(
|
|
@@ -1542,6 +1725,28 @@ function checkPunchback(doc, ctx, issues) {
|
|
|
1542
1725
|
}
|
|
1543
1726
|
}
|
|
1544
1727
|
}
|
|
1728
|
+
function checkSingleCurrency(lineCurrencies, totalCurrency, issues, path, allowMixed = false) {
|
|
1729
|
+
const all = new Set(lineCurrencies.filter(Boolean));
|
|
1730
|
+
if (totalCurrency) all.add(totalCurrency);
|
|
1731
|
+
if (all.size > 1) {
|
|
1732
|
+
const list = [...all].join(", ");
|
|
1733
|
+
if (allowMixed) {
|
|
1734
|
+
issues.warn(
|
|
1735
|
+
"mixed-currency",
|
|
1736
|
+
`Multiple currencies in one document (${list}); allowed for this supplier. The header Total is a single Money \u2014 rely on the per-line currencies.`,
|
|
1737
|
+
path
|
|
1738
|
+
);
|
|
1739
|
+
} else {
|
|
1740
|
+
issues.error(
|
|
1741
|
+
"mixed-currency",
|
|
1742
|
+
`Multiple currencies in one document (${list}). A cXML Total is a single Money \u2014 all line items and the Total must share one currency.`,
|
|
1743
|
+
path
|
|
1744
|
+
);
|
|
1745
|
+
}
|
|
1746
|
+
return false;
|
|
1747
|
+
}
|
|
1748
|
+
return true;
|
|
1749
|
+
}
|
|
1545
1750
|
function checkOrderRequest(doc, ctx, issues) {
|
|
1546
1751
|
const orderReq = root(doc)?.Request?.OrderRequest;
|
|
1547
1752
|
if (!orderReq) {
|
|
@@ -1568,18 +1773,24 @@ function checkOrderRequest(doc, ctx, issues) {
|
|
|
1568
1773
|
if (items.length === 0) {
|
|
1569
1774
|
issues.error("no-itemout", "OrderRequest contains no ItemOut elements", "cXML/.../OrderRequest");
|
|
1570
1775
|
}
|
|
1776
|
+
const lineCurrencies = [];
|
|
1571
1777
|
items.forEach((it, i) => {
|
|
1572
1778
|
const base = `cXML/.../ItemOut[${i + 1}]`;
|
|
1573
1779
|
if (!text(it?.ItemID?.SupplierPartID)) {
|
|
1574
1780
|
issues.error("itemout-missing-id", `ItemOut[${i + 1}] is missing ItemID/SupplierPartID`, `${base}/ItemID`);
|
|
1575
1781
|
}
|
|
1576
|
-
|
|
1782
|
+
const up = it?.ItemDetail?.UnitPrice?.Money;
|
|
1783
|
+
if (up == null) {
|
|
1577
1784
|
issues.error("itemout-missing-unitprice", `ItemOut[${i + 1}] is missing ItemDetail/UnitPrice/Money`, `${base}/ItemDetail/UnitPrice`);
|
|
1785
|
+
} else {
|
|
1786
|
+
const cur = attr(up, "currency");
|
|
1787
|
+
if (cur) lineCurrencies.push(cur);
|
|
1578
1788
|
}
|
|
1579
1789
|
if (num(attr(it, "quantity")) == null) {
|
|
1580
1790
|
issues.error("itemout-missing-quantity", `ItemOut[${i + 1}] is missing @quantity`, base);
|
|
1581
1791
|
}
|
|
1582
1792
|
});
|
|
1793
|
+
checkSingleCurrency(lineCurrencies, attr(header2?.Total?.Money, "currency"), issues, "cXML/.../OrderRequestHeader/Total", ctx.allowMixedCurrency);
|
|
1583
1794
|
const refs = collectCidReferences(doc);
|
|
1584
1795
|
const available = ctx.availableContentIds;
|
|
1585
1796
|
const referenced = /* @__PURE__ */ new Set();
|
|
@@ -1661,7 +1872,7 @@ function validateDocument(raw, ctx = {}) {
|
|
|
1661
1872
|
}
|
|
1662
1873
|
|
|
1663
1874
|
// src/server/routes/flow.ts
|
|
1664
|
-
var flowRoute = new
|
|
1875
|
+
var flowRoute = new Hono5();
|
|
1665
1876
|
function host() {
|
|
1666
1877
|
try {
|
|
1667
1878
|
return new URL(getPublicUrl()).host;
|
|
@@ -1686,7 +1897,8 @@ function buyerContext(r) {
|
|
|
1686
1897
|
connectionId: connection.id,
|
|
1687
1898
|
attachmentEncoding: eff.attachmentEncoding,
|
|
1688
1899
|
eff,
|
|
1689
|
-
expected: { from, to, sender, sharedSecret: connection.sharedSecret }
|
|
1900
|
+
expected: { from, to, sender, sharedSecret: connection.sharedSecret },
|
|
1901
|
+
allowMixedCurrency: supplier.allowMixedCurrency ?? false
|
|
1690
1902
|
};
|
|
1691
1903
|
}
|
|
1692
1904
|
function setupExtrinsics(ctx, buyerCookie) {
|
|
@@ -1785,7 +1997,7 @@ flowRoute.post("/:id/setup", async (c) => {
|
|
|
1785
1997
|
function buildOrderXml(ctx, body) {
|
|
1786
1998
|
const items = body.items ?? [];
|
|
1787
1999
|
const currency = body.currency || items[0]?.currency || "USD";
|
|
1788
|
-
const total = body.total ?? items
|
|
2000
|
+
const total = body.total ?? lineItemsTotal(items, currency);
|
|
1789
2001
|
const orderId = body.orderId || `PO-${nanoid5(8)}`;
|
|
1790
2002
|
const attMeta = (body.attachments ?? []).map((a) => ({
|
|
1791
2003
|
contentId: a.contentId,
|
|
@@ -1859,7 +2071,8 @@ flowRoute.post("/:id/order", async (c) => {
|
|
|
1859
2071
|
const reqValidation = validateDocument(xml, {
|
|
1860
2072
|
expected: ctx.expected,
|
|
1861
2073
|
forceDocType: "OrderRequest",
|
|
1862
|
-
availableContentIds: inputAtts.length > 0 ? availableContentIds : void 0
|
|
2074
|
+
availableContentIds: inputAtts.length > 0 ? availableContentIds : void 0,
|
|
2075
|
+
allowMixedCurrency: ctx.allowMixedCurrency
|
|
1863
2076
|
});
|
|
1864
2077
|
const reqLog = appendLog({
|
|
1865
2078
|
sessionId,
|
|
@@ -1900,8 +2113,8 @@ flowRoute.post("/:id/order", async (c) => {
|
|
|
1900
2113
|
});
|
|
1901
2114
|
|
|
1902
2115
|
// src/server/routes/punchout-return.ts
|
|
1903
|
-
import { Hono as
|
|
1904
|
-
var punchoutReturnRoute = new
|
|
2116
|
+
import { Hono as Hono6 } from "hono";
|
|
2117
|
+
var punchoutReturnRoute = new Hono6();
|
|
1905
2118
|
async function extractCxml(c) {
|
|
1906
2119
|
const ct = (c.req.header("content-type") ?? "").toLowerCase();
|
|
1907
2120
|
if (ct.includes("application/x-www-form-urlencoded") || ct.includes("multipart/form-data")) {
|
|
@@ -1969,9 +2182,9 @@ punchoutReturnRoute.post("/return", async (c) => {
|
|
|
1969
2182
|
});
|
|
1970
2183
|
|
|
1971
2184
|
// src/server/routes/stream.ts
|
|
1972
|
-
import { Hono as
|
|
2185
|
+
import { Hono as Hono7 } from "hono";
|
|
1973
2186
|
import { streamSSE } from "hono/streaming";
|
|
1974
|
-
var streamRoute = new
|
|
2187
|
+
var streamRoute = new Hono7();
|
|
1975
2188
|
var MAX_STREAMS = 64;
|
|
1976
2189
|
var activeStreams = 0;
|
|
1977
2190
|
streamRoute.get("/stream", (c) => {
|
|
@@ -2003,8 +2216,8 @@ streamRoute.get("/stream", (c) => {
|
|
|
2003
2216
|
});
|
|
2004
2217
|
|
|
2005
2218
|
// src/server/routes/data.ts
|
|
2006
|
-
import { Hono as
|
|
2007
|
-
var dataRoute = new
|
|
2219
|
+
import { Hono as Hono8 } from "hono";
|
|
2220
|
+
var dataRoute = new Hono8();
|
|
2008
2221
|
function rawMessage(record) {
|
|
2009
2222
|
const ct = record.contentType ?? record.headers?.["Content-Type"] ?? record.headers?.["content-type"];
|
|
2010
2223
|
let body = record.body;
|
|
@@ -2030,7 +2243,7 @@ ${body}` : body;
|
|
|
2030
2243
|
dataRoute.get("/health", (c) => c.json({ ok: true }));
|
|
2031
2244
|
dataRoute.get(
|
|
2032
2245
|
"/runtime",
|
|
2033
|
-
(c) => c.json({ publicUrl: getPublicUrl(), callbackUrl: `${getPublicUrl()}/punchout/return
|
|
2246
|
+
(c) => c.json({ publicUrl: getPublicUrl(), callbackUrl: `${getPublicUrl()}/punchout/return`, version: getVersion() })
|
|
2034
2247
|
);
|
|
2035
2248
|
dataRoute.get("/sessions", (c) => c.json(listSessions()));
|
|
2036
2249
|
dataRoute.get("/sessions/:id", (c) => c.json(readSession(c.req.param("id"))));
|
|
@@ -2056,13 +2269,13 @@ dataRoute.get("/attachments/:hash", (c) => {
|
|
|
2056
2269
|
});
|
|
2057
2270
|
|
|
2058
2271
|
// src/server/routes/sim.ts
|
|
2059
|
-
import { Hono as
|
|
2272
|
+
import { Hono as Hono9 } from "hono";
|
|
2060
2273
|
import { nanoid as nanoid6 } from "nanoid";
|
|
2061
|
-
var simRoute = new
|
|
2274
|
+
var simRoute = new Hono9();
|
|
2062
2275
|
var DEMO_CATALOG = [
|
|
2063
|
-
{ supplierPartId: "WIDGET-001", description: "Premium Steel Widget", unitPrice: 12.5, currency: "USD", uom: "EA",
|
|
2064
|
-
{ supplierPartId: "BOLT-250", description: "M8 Hex Bolt (pack of 250)", unitPrice: 34, currency: "USD", uom: "PK",
|
|
2065
|
-
{ supplierPartId: "TAPE-RED", description: "Industrial Marking Tape, Red", unitPrice: 5.75, currency: "USD", uom: "RL",
|
|
2276
|
+
{ supplierPartId: "WIDGET-001", description: "Premium Steel Widget", unitPrice: 12.5, currency: "USD", uom: "EA", classifications: [{ domain: "UNSPSC", value: "31161500" }], manufacturerPartId: "MFR-W001", manufacturerName: "Acme Manufacturing" },
|
|
2277
|
+
{ supplierPartId: "BOLT-250", description: "M8 Hex Bolt (pack of 250)", unitPrice: 34, currency: "USD", uom: "PK", classifications: [{ domain: "UNSPSC", value: "31161600" }], manufacturerPartId: "MFR-B250", manufacturerName: "Acme Manufacturing" },
|
|
2278
|
+
{ supplierPartId: "TAPE-RED", description: "Industrial Marking Tape, Red", unitPrice: 5.75, currency: "USD", uom: "RL", classifications: [{ domain: "UNSPSC", value: "31201500" }] }
|
|
2066
2279
|
];
|
|
2067
2280
|
function host2() {
|
|
2068
2281
|
try {
|
|
@@ -2071,7 +2284,10 @@ function host2() {
|
|
|
2071
2284
|
return "punchout-simulator";
|
|
2072
2285
|
}
|
|
2073
2286
|
}
|
|
2074
|
-
var catalogOf = (s) =>
|
|
2287
|
+
var catalogOf = (s) => {
|
|
2288
|
+
const items = catalogForSupplier(s);
|
|
2289
|
+
return items.length > 0 ? items : DEMO_CATALOG;
|
|
2290
|
+
};
|
|
2075
2291
|
function safeHttpUrl(u) {
|
|
2076
2292
|
if (!u) return "";
|
|
2077
2293
|
try {
|
|
@@ -2148,13 +2364,15 @@ simRoute.get("/:id/catalog", (c) => {
|
|
|
2148
2364
|
const bd = c.req.query("bd") ?? "";
|
|
2149
2365
|
const bi = c.req.query("bi") ?? "";
|
|
2150
2366
|
const items = catalogOf(supplier);
|
|
2151
|
-
const rows = items.map(
|
|
2152
|
-
(it
|
|
2153
|
-
|
|
2367
|
+
const rows = items.map((it, i) => {
|
|
2368
|
+
const partId = escapeXml(it.supplierPartId) + (it.supplierPartAuxiliaryId ? ` / ${escapeXml(it.supplierPartAuxiliaryId)}` : "");
|
|
2369
|
+
const cls = (it.classifications ?? []).map((c2) => `${escapeXml(c2.domain)} ${escapeXml(c2.value)}`).join(" \xB7 ");
|
|
2370
|
+
return `<tr>
|
|
2371
|
+
<td><strong>${escapeXml(it.description)}</strong><br><small>${partId} \xB7 ${escapeXml(it.uom)}${cls ? ` \xB7 ${cls}` : ""}</small></td>
|
|
2154
2372
|
<td class="price">${escapeXml(it.currency)} ${it.unitPrice.toFixed(2)}</td>
|
|
2155
|
-
<td><input type="number" name="q_${i}" value="0" min="0" step="1" inputmode="numeric"></td>
|
|
2156
|
-
</tr
|
|
2157
|
-
).join("\n");
|
|
2373
|
+
<td><input type="number" name="q_${i}" value="0" min="0" step="${it.allowFractional ? "any" : "1"}" inputmode="${it.allowFractional ? "decimal" : "numeric"}"></td>
|
|
2374
|
+
</tr>`;
|
|
2375
|
+
}).join("\n");
|
|
2158
2376
|
return c.html(`<!doctype html><html lang="en"><head><meta charset="utf-8">
|
|
2159
2377
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2160
2378
|
<title>${escapeXml(supplier.name)} \u2014 mock catalog</title>
|
|
@@ -2193,17 +2411,22 @@ simRoute.post("/:id/checkout", async (c) => {
|
|
|
2193
2411
|
const catalog = catalogOf(supplier);
|
|
2194
2412
|
const items = [];
|
|
2195
2413
|
catalog.forEach((it, i) => {
|
|
2196
|
-
|
|
2414
|
+
let qty = Number(form[`q_${i}`] ?? 0);
|
|
2415
|
+
if (!it.allowFractional) qty = Math.floor(qty);
|
|
2197
2416
|
if (qty > 0) {
|
|
2198
2417
|
items.push({
|
|
2199
2418
|
quantity: qty,
|
|
2200
2419
|
supplierPartId: it.supplierPartId,
|
|
2420
|
+
supplierPartAuxiliaryId: it.supplierPartAuxiliaryId,
|
|
2201
2421
|
description: it.description,
|
|
2202
2422
|
uom: it.uom,
|
|
2203
2423
|
unitPriceAmount: it.unitPrice,
|
|
2204
2424
|
currency: it.currency,
|
|
2205
|
-
|
|
2206
|
-
classification
|
|
2425
|
+
classifications: it.classifications,
|
|
2426
|
+
// Keep the legacy single fields populated from the first classification
|
|
2427
|
+
// for back-compat display (CartView) and any single-domain consumer.
|
|
2428
|
+
classificationDomain: it.classifications[0]?.domain,
|
|
2429
|
+
classification: it.classifications[0]?.value,
|
|
2207
2430
|
manufacturerPartId: it.manufacturerPartId,
|
|
2208
2431
|
manufacturerName: it.manufacturerName
|
|
2209
2432
|
});
|
|
@@ -2229,7 +2452,10 @@ simRoute.post("/:id/checkout", async (c) => {
|
|
|
2229
2452
|
docType: "PunchOutOrderMessage",
|
|
2230
2453
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
2231
2454
|
body: xml,
|
|
2232
|
-
validation: validateDocument(xml, {
|
|
2455
|
+
validation: validateDocument(xml, {
|
|
2456
|
+
forceDocType: "PunchOutOrderMessage",
|
|
2457
|
+
allowMixedCurrency: supplier.allowMixedCurrency
|
|
2458
|
+
})
|
|
2233
2459
|
});
|
|
2234
2460
|
const transport = eff?.cartReturnTransport ?? "cxml-urlencoded";
|
|
2235
2461
|
return c.html(cartReturnPage(formpost, xml, transport));
|
|
@@ -2299,7 +2525,8 @@ simRoute.post("/:id/order", async (c) => {
|
|
|
2299
2525
|
const validation = validateDocument(xml, {
|
|
2300
2526
|
expected: expectedFor(supplier.id, from),
|
|
2301
2527
|
forceDocType: "OrderRequest",
|
|
2302
|
-
availableContentIds: isMultipart(ct) ? availableContentIds : void 0
|
|
2528
|
+
availableContentIds: isMultipart(ct) ? availableContentIds : void 0,
|
|
2529
|
+
allowMixedCurrency: supplier.allowMixedCurrency
|
|
2303
2530
|
});
|
|
2304
2531
|
appendLog({
|
|
2305
2532
|
sessionId,
|
|
@@ -2346,7 +2573,7 @@ function findSessionForOrder(doc) {
|
|
|
2346
2573
|
// src/server/app.ts
|
|
2347
2574
|
import { relative } from "path";
|
|
2348
2575
|
function createApp(opts = {}) {
|
|
2349
|
-
const app = new
|
|
2576
|
+
const app = new Hono10();
|
|
2350
2577
|
if (!opts.quiet) app.use("*", logger());
|
|
2351
2578
|
app.use(
|
|
2352
2579
|
"*",
|
|
@@ -2365,6 +2592,8 @@ function createApp(opts = {}) {
|
|
|
2365
2592
|
app.route("/api/suppliers", suppliersRoute);
|
|
2366
2593
|
app.route("/api/profiles", profilesRoute);
|
|
2367
2594
|
app.route("/api/profile-presets", profilePresetsRoute);
|
|
2595
|
+
app.route("/api/product-lists", productsRoute);
|
|
2596
|
+
app.route("/api/product-list-presets", productListPresetsRoute);
|
|
2368
2597
|
app.route("/api/connections", connectionsRoute);
|
|
2369
2598
|
app.route("/api/connections", flowRoute);
|
|
2370
2599
|
app.route("/api", dataRoute);
|
|
@@ -2406,7 +2635,8 @@ async function seedDemoIfEmpty() {
|
|
|
2406
2635
|
identity: { domain: "DUNS", identity: "987654321" },
|
|
2407
2636
|
punchoutUrl: `${getPublicUrl()}/sim/demo-supplier/punchout`,
|
|
2408
2637
|
orderUrl: `${getPublicUrl()}/sim/demo-supplier/order`,
|
|
2409
|
-
|
|
2638
|
+
// Serve the built-in sample assortment (seeded by initConfig).
|
|
2639
|
+
productListIds: ["sample"]
|
|
2410
2640
|
});
|
|
2411
2641
|
await createConnection({
|
|
2412
2642
|
id: "demo",
|
|
@@ -2477,6 +2707,13 @@ function hostnameOf(url) {
|
|
|
2477
2707
|
return "";
|
|
2478
2708
|
}
|
|
2479
2709
|
}
|
|
2710
|
+
function readVersion() {
|
|
2711
|
+
try {
|
|
2712
|
+
return JSON.parse(readFileSync3(new URL("../../package.json", import.meta.url), "utf8")).version;
|
|
2713
|
+
} catch {
|
|
2714
|
+
return void 0;
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2480
2717
|
function printHelp() {
|
|
2481
2718
|
console.log(`punchout-simulator \u2014 test cXML PunchOut integrations as a virtual counterparty
|
|
2482
2719
|
|
|
@@ -2503,7 +2740,7 @@ async function main() {
|
|
|
2503
2740
|
const exposed = !LOOPBACK.has(hostnameOf(publicUrl)) || !LOOPBACK.has(bindHost);
|
|
2504
2741
|
const token = flags.token || (exposed ? nanoid7(24) : void 0);
|
|
2505
2742
|
setDataDir(flags.dataDir);
|
|
2506
|
-
setRuntime({ port: flags.port, publicUrl, token });
|
|
2743
|
+
setRuntime({ port: flags.port, publicUrl, token, version: readVersion() });
|
|
2507
2744
|
await initConfig();
|
|
2508
2745
|
if (flags.seed) await seedDemoIfEmpty();
|
|
2509
2746
|
const webRoot = flags.dev ? void 0 : fileURLToPath(new URL("../web", import.meta.url));
|