punchout-simulator 0.3.1 → 0.5.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 +46 -7
- package/dist/server/cli.js +482 -90
- package/dist/web/assets/{cssMode-uoWbxmRA.js → cssMode-BbEnQNgt.js} +1 -1
- package/dist/web/assets/{freemarker2-DNR9Witk.js → freemarker2-B66AqaBn.js} +1 -1
- package/dist/web/assets/{handlebars-Cz4BZbpY.js → handlebars-VHNJA3L3.js} +1 -1
- package/dist/web/assets/{html-B08afy-k.js → html-DKhtq5mU.js} +1 -1
- package/dist/web/assets/{htmlMode-DwllrnDj.js → htmlMode-DTvp4_of.js} +1 -1
- package/dist/web/assets/{index-BzgSetIt.js → index-DRD_fRQi.js} +195 -193
- package/dist/web/assets/{index-DLIyRG88.css → index-WgWCySdd.css} +1 -1
- package/dist/web/assets/{javascript-CSO3Lh8M.js → javascript-C7niBpqn.js} +1 -1
- package/dist/web/assets/{jsonMode-BQc0D6l3.js → jsonMode-mhDIEB_U.js} +1 -1
- package/dist/web/assets/{liquid-D_Cq8kS0.js → liquid-v92UUGTF.js} +1 -1
- package/dist/web/assets/{mdx-9UNHMbIw.js → mdx-upBohKLC.js} +1 -1
- package/dist/web/assets/{python-BziTtpp-.js → python-BptHZKfu.js} +1 -1
- package/dist/web/assets/{razor-CqSSZBI0.js → razor-D3-LbWWb.js} +1 -1
- package/dist/web/assets/{tsMode-BXILR9lc.js → tsMode-0LcFoT79.js} +1 -1
- package/dist/web/assets/{typescript-lG76zlZP.js → typescript-ClLaLjem.js} +1 -1
- package/dist/web/assets/{xml-D1mrwtCd.js → xml-BIefLbdX.js} +1 -1
- package/dist/web/assets/{yaml-DWKj4dJQ.js → yaml-D3udJL_a.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
package/dist/server/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import { nanoid as nanoid7 } from "nanoid";
|
|
|
8
8
|
|
|
9
9
|
// src/server/app.ts
|
|
10
10
|
import { existsSync as existsSync3 } from "fs";
|
|
11
|
-
import { Hono as
|
|
11
|
+
import { Hono as Hono10 } from "hono";
|
|
12
12
|
import { logger } from "hono/logger";
|
|
13
13
|
import { bodyLimit } from "hono/body-limit";
|
|
14
14
|
import { getCookie } from "hono/cookie";
|
|
@@ -56,7 +56,10 @@ var PROFILE_PRESETS = [
|
|
|
56
56
|
setupOperation: "create",
|
|
57
57
|
attachmentEncoding: "binary",
|
|
58
58
|
cartReturnTransport: "cxml-urlencoded",
|
|
59
|
-
extrinsics: []
|
|
59
|
+
extrinsics: [],
|
|
60
|
+
addressMode: "full",
|
|
61
|
+
shipToInSetup: false,
|
|
62
|
+
contactInSetup: false
|
|
60
63
|
},
|
|
61
64
|
{
|
|
62
65
|
id: "ariba",
|
|
@@ -68,7 +71,12 @@ var PROFILE_PRESETS = [
|
|
|
68
71
|
setupOperation: "create",
|
|
69
72
|
attachmentEncoding: "base64",
|
|
70
73
|
cartReturnTransport: "cxml-urlencoded",
|
|
71
|
-
extrinsics: [{ name: "User", value: "${buyerCookie}", scope: "setup" }]
|
|
74
|
+
extrinsics: [{ name: "User", value: "${buyerCookie}", scope: "setup" }],
|
|
75
|
+
// Ariba sends ShipTo (addressID + postal) already in the SetupRequest so the
|
|
76
|
+
// supplier can return ship-to-specific pricing/availability.
|
|
77
|
+
addressMode: "both",
|
|
78
|
+
shipToInSetup: true,
|
|
79
|
+
contactInSetup: false
|
|
72
80
|
},
|
|
73
81
|
{
|
|
74
82
|
id: "coupa",
|
|
@@ -81,7 +89,10 @@ var PROFILE_PRESETS = [
|
|
|
81
89
|
setupOperation: "create",
|
|
82
90
|
attachmentEncoding: "base64",
|
|
83
91
|
cartReturnTransport: "cxml-urlencoded",
|
|
84
|
-
extrinsics: []
|
|
92
|
+
extrinsics: [],
|
|
93
|
+
addressMode: "both",
|
|
94
|
+
shipToInSetup: false,
|
|
95
|
+
contactInSetup: false
|
|
85
96
|
},
|
|
86
97
|
{
|
|
87
98
|
id: "jaggaer",
|
|
@@ -93,7 +104,10 @@ var PROFILE_PRESETS = [
|
|
|
93
104
|
setupOperation: "create",
|
|
94
105
|
attachmentEncoding: "binary",
|
|
95
106
|
cartReturnTransport: "cxml-urlencoded",
|
|
96
|
-
extrinsics: []
|
|
107
|
+
extrinsics: [],
|
|
108
|
+
addressMode: "full",
|
|
109
|
+
shipToInSetup: false,
|
|
110
|
+
contactInSetup: false
|
|
97
111
|
},
|
|
98
112
|
{
|
|
99
113
|
id: "oracle",
|
|
@@ -105,7 +119,10 @@ var PROFILE_PRESETS = [
|
|
|
105
119
|
setupOperation: "create",
|
|
106
120
|
attachmentEncoding: "binary",
|
|
107
121
|
cartReturnTransport: "cxml-urlencoded",
|
|
108
|
-
extrinsics: []
|
|
122
|
+
extrinsics: [],
|
|
123
|
+
addressMode: "full",
|
|
124
|
+
shipToInSetup: false,
|
|
125
|
+
contactInSetup: false
|
|
109
126
|
},
|
|
110
127
|
{
|
|
111
128
|
id: "sap-srm",
|
|
@@ -120,7 +137,11 @@ var PROFILE_PRESETS = [
|
|
|
120
137
|
setupOperation: "create",
|
|
121
138
|
attachmentEncoding: "base64",
|
|
122
139
|
cartReturnTransport: "cxml-base64",
|
|
123
|
-
extrinsics: []
|
|
140
|
+
extrinsics: [],
|
|
141
|
+
// SAP is location/plant-code centric — addresses by reference.
|
|
142
|
+
addressMode: "id-only",
|
|
143
|
+
shipToInSetup: false,
|
|
144
|
+
contactInSetup: false
|
|
124
145
|
},
|
|
125
146
|
{
|
|
126
147
|
id: "workday",
|
|
@@ -132,7 +153,10 @@ var PROFILE_PRESETS = [
|
|
|
132
153
|
setupOperation: "create",
|
|
133
154
|
attachmentEncoding: "base64",
|
|
134
155
|
cartReturnTransport: "cxml-urlencoded",
|
|
135
|
-
extrinsics: []
|
|
156
|
+
extrinsics: [],
|
|
157
|
+
addressMode: "full",
|
|
158
|
+
shipToInSetup: false,
|
|
159
|
+
contactInSetup: false
|
|
136
160
|
}
|
|
137
161
|
];
|
|
138
162
|
var GENERIC_PROFILE = {
|
|
@@ -148,6 +172,52 @@ function seedBuiltinProfiles(data, now2) {
|
|
|
148
172
|
}
|
|
149
173
|
}
|
|
150
174
|
|
|
175
|
+
// src/server/cxml/product-list-presets.ts
|
|
176
|
+
var PRODUCT_LIST_PRESETS = [
|
|
177
|
+
{
|
|
178
|
+
id: "sample",
|
|
179
|
+
name: "Sample assortment",
|
|
180
|
+
builtin: true,
|
|
181
|
+
description: "A representative office & industrial supplies catalog (~20 items).",
|
|
182
|
+
items: [
|
|
183
|
+
{ 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" },
|
|
184
|
+
{ 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" },
|
|
185
|
+
{ 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" },
|
|
186
|
+
{ 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" },
|
|
187
|
+
{ supplierPartId: "TAPE-RED", supplierPartAuxiliaryId: "TAPE-RED-50M", description: "Industrial Marking Tape, Red", unitPrice: 5.75, currency: "USD", uom: "RL", classifications: [{ domain: "UNSPSC", value: "31201500" }] },
|
|
188
|
+
{ supplierPartId: "TAPE-YEL", supplierPartAuxiliaryId: "TAPE-YEL-50M", description: "Industrial Marking Tape, Yellow", unitPrice: 5.75, currency: "USD", uom: "RL", classifications: [{ domain: "UNSPSC", value: "31201500" }] },
|
|
189
|
+
// Multiple classifications: UNSPSC + a supplier-specific commodity scheme.
|
|
190
|
+
{ 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 },
|
|
191
|
+
{ 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 },
|
|
192
|
+
{ 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 },
|
|
193
|
+
{ 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 },
|
|
194
|
+
{ 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" },
|
|
195
|
+
{ 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" },
|
|
196
|
+
{ supplierPartId: "GOGGLE-STD", description: "Safety Goggles, Anti-fog", unitPrice: 4.6, currency: "GBP", uom: "EA", classifications: [{ domain: "UNSPSC", value: "46181702" }], manufacturerName: "SafeHands" },
|
|
197
|
+
{ supplierPartId: "HELMET-WHT", description: "Hard Hat, White", unitPrice: 9.8, currency: "GBP", uom: "EA", classifications: [{ domain: "UNSPSC", value: "46181701" }], manufacturerName: "SafeHands" },
|
|
198
|
+
{ supplierPartId: "PAPER-A4", description: "Copy Paper A4 80gsm (ream of 500)", unitPrice: 18, currency: "PLN", uom: "RM", classifications: [{ domain: "UNSPSC", value: "14111507" }], manufacturerName: "PaperCo" },
|
|
199
|
+
{ 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" },
|
|
200
|
+
{ supplierPartId: "BINDER-A4", description: "Lever Arch Binder A4, Black", unitPrice: 11.5, currency: "PLN", uom: "EA", classifications: [{ domain: "UNSPSC", value: "44122011" }], manufacturerName: "Officeline" },
|
|
201
|
+
{ 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" },
|
|
202
|
+
{ 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" },
|
|
203
|
+
{ 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" },
|
|
204
|
+
{ 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 }
|
|
205
|
+
]
|
|
206
|
+
}
|
|
207
|
+
];
|
|
208
|
+
var SAMPLE_PRODUCT_LIST = {
|
|
209
|
+
...PRODUCT_LIST_PRESETS[0],
|
|
210
|
+
createdAt: "",
|
|
211
|
+
updatedAt: ""
|
|
212
|
+
};
|
|
213
|
+
function seedBuiltinProductLists(data, now2) {
|
|
214
|
+
for (const preset of PRODUCT_LIST_PRESETS) {
|
|
215
|
+
if (!data.productLists.some((p) => p.id === preset.id)) {
|
|
216
|
+
data.productLists.push({ ...preset, createdAt: now2, updatedAt: now2 });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
151
221
|
// src/server/store/paths.ts
|
|
152
222
|
import { chmodSync, mkdirSync } from "fs";
|
|
153
223
|
import { resolve } from "path";
|
|
@@ -184,15 +254,20 @@ var db = null;
|
|
|
184
254
|
async function initConfig() {
|
|
185
255
|
ensureDirs();
|
|
186
256
|
const adapter = new JSONFile(configPath());
|
|
187
|
-
db = new Low(adapter, { buyers: [], suppliers: [], connections: [], profiles: [] });
|
|
257
|
+
db = new Low(adapter, { buyers: [], suppliers: [], connections: [], profiles: [], productLists: [] });
|
|
188
258
|
await db.read();
|
|
189
|
-
db.data ||= { buyers: [], suppliers: [], connections: [], profiles: [] };
|
|
259
|
+
db.data ||= { buyers: [], suppliers: [], connections: [], profiles: [], productLists: [] };
|
|
190
260
|
db.data.buyers ||= [];
|
|
191
261
|
db.data.suppliers ||= [];
|
|
192
262
|
db.data.connections ||= [];
|
|
193
263
|
db.data.profiles ||= [];
|
|
264
|
+
db.data.productLists ||= [];
|
|
194
265
|
seedBuiltinProfiles(db.data, now());
|
|
266
|
+
migrateProfiles(db.data);
|
|
267
|
+
seedBuiltinProductLists(db.data, now());
|
|
195
268
|
migrateLegacy(db.data);
|
|
269
|
+
migrateInlineCatalogs(db.data);
|
|
270
|
+
migrateClassifications(db.data);
|
|
196
271
|
await db.write();
|
|
197
272
|
try {
|
|
198
273
|
chmodSync2(configPath(), 384);
|
|
@@ -347,12 +422,47 @@ function effectiveProfile(connection, buyer) {
|
|
|
347
422
|
// Connection attachmentEncoding is concrete by construction → explicit override.
|
|
348
423
|
attachmentEncoding: connection.attachmentEncoding ?? p.attachmentEncoding,
|
|
349
424
|
cartReturnTransport: p.cartReturnTransport,
|
|
350
|
-
extrinsics: p.extrinsics
|
|
425
|
+
extrinsics: p.extrinsics,
|
|
426
|
+
addressMode: p.addressMode,
|
|
427
|
+
shipToInSetup: p.shipToInSetup,
|
|
428
|
+
contactInSetup: p.contactInSetup
|
|
351
429
|
};
|
|
352
430
|
}
|
|
353
431
|
function dtdVersionFor(eff, docType) {
|
|
354
432
|
return eff.dtdVersions[docType] ?? eff.dtdVersions.default;
|
|
355
433
|
}
|
|
434
|
+
var listProductLists = () => requireDb().data.productLists;
|
|
435
|
+
var getProductList = (id) => requireDb().data.productLists.find((p) => p.id === id);
|
|
436
|
+
async function createProductList(input) {
|
|
437
|
+
const list = { ...input, id: input.id ?? nanoid(8), createdAt: now(), updatedAt: now() };
|
|
438
|
+
const d = requireDb();
|
|
439
|
+
d.data.productLists.push(list);
|
|
440
|
+
await d.write();
|
|
441
|
+
return list;
|
|
442
|
+
}
|
|
443
|
+
async function updateProductList(id, patch) {
|
|
444
|
+
const d = requireDb();
|
|
445
|
+
const existing = d.data.productLists.find((p) => p.id === id);
|
|
446
|
+
if (!existing) return void 0;
|
|
447
|
+
Object.assign(existing, patch, { id, updatedAt: now() });
|
|
448
|
+
await d.write();
|
|
449
|
+
return existing;
|
|
450
|
+
}
|
|
451
|
+
async function deleteProductList(id) {
|
|
452
|
+
const d = requireDb();
|
|
453
|
+
if (d.data.suppliers.some((s) => s.productListIds?.includes(id))) {
|
|
454
|
+
throw new Error("product list is referenced by a supplier");
|
|
455
|
+
}
|
|
456
|
+
const before = d.data.productLists.length;
|
|
457
|
+
d.data.productLists = d.data.productLists.filter((p) => p.id !== id);
|
|
458
|
+
const removed = d.data.productLists.length < before;
|
|
459
|
+
if (removed) await d.write();
|
|
460
|
+
return removed;
|
|
461
|
+
}
|
|
462
|
+
function catalogForSupplier(supplier) {
|
|
463
|
+
const ids = supplier.productListIds ?? [];
|
|
464
|
+
return ids.flatMap((id) => getProductList(id)?.items ?? []);
|
|
465
|
+
}
|
|
356
466
|
function migrateLegacy(data) {
|
|
357
467
|
const legacy = data.connections.filter((c) => "from" in c || "to" in c);
|
|
358
468
|
if (legacy.length === 0) return;
|
|
@@ -412,6 +522,48 @@ function migrateLegacy(data) {
|
|
|
412
522
|
}
|
|
413
523
|
data.connections = migrated;
|
|
414
524
|
}
|
|
525
|
+
function migrateInlineCatalogs(data) {
|
|
526
|
+
for (const supplier of data.suppliers) {
|
|
527
|
+
const legacy = supplier.catalog;
|
|
528
|
+
if (!legacy || legacy.length === 0) {
|
|
529
|
+
delete supplier.catalog;
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
if (supplier.productListIds && supplier.productListIds.length > 0) {
|
|
533
|
+
delete supplier.catalog;
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
const list = {
|
|
537
|
+
id: nanoid(8),
|
|
538
|
+
name: `${supplier.name} catalog`,
|
|
539
|
+
items: legacy,
|
|
540
|
+
createdAt: now(),
|
|
541
|
+
updatedAt: now()
|
|
542
|
+
};
|
|
543
|
+
data.productLists.push(list);
|
|
544
|
+
supplier.productListIds = [list.id];
|
|
545
|
+
delete supplier.catalog;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
function migrateProfiles(data) {
|
|
549
|
+
for (const p of data.profiles) {
|
|
550
|
+
if (p.addressMode != null && p.shipToInSetup != null && p.contactInSetup != null) continue;
|
|
551
|
+
const preset = PROFILE_PRESETS.find((x) => x.id === p.id);
|
|
552
|
+
p.addressMode ??= preset?.addressMode ?? "full";
|
|
553
|
+
p.shipToInSetup ??= preset?.shipToInSetup ?? false;
|
|
554
|
+
p.contactInSetup ??= preset?.contactInSetup ?? false;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
function migrateClassifications(data) {
|
|
558
|
+
for (const list of data.productLists) {
|
|
559
|
+
for (const item of list.items) {
|
|
560
|
+
if (!Array.isArray(item.classifications) || item.classifications.length === 0) {
|
|
561
|
+
item.classifications = item.unspsc ? [{ domain: "UNSPSC", value: String(item.unspsc) }] : [];
|
|
562
|
+
}
|
|
563
|
+
delete item.unspsc;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
415
567
|
|
|
416
568
|
// src/server/routes/connections.ts
|
|
417
569
|
var connectionsRoute = new Hono();
|
|
@@ -486,12 +638,40 @@ var cred = (c) => ({
|
|
|
486
638
|
domain: String(c?.domain ?? ""),
|
|
487
639
|
identity: String(c?.identity ?? "")
|
|
488
640
|
});
|
|
641
|
+
var str = (v) => v != null && String(v) !== "" ? String(v) : void 0;
|
|
642
|
+
function address(a) {
|
|
643
|
+
if (a == null || typeof a !== "object") return void 0;
|
|
644
|
+
const out = {
|
|
645
|
+
addressId: str(a.addressId),
|
|
646
|
+
addressIdDomain: str(a.addressIdDomain),
|
|
647
|
+
name: str(a.name),
|
|
648
|
+
deliverTo: str(a.deliverTo),
|
|
649
|
+
street: str(a.street),
|
|
650
|
+
city: str(a.city),
|
|
651
|
+
state: str(a.state),
|
|
652
|
+
postalCode: str(a.postalCode),
|
|
653
|
+
countryIsoCode: str(a.countryIsoCode),
|
|
654
|
+
countryName: str(a.countryName),
|
|
655
|
+
email: str(a.email),
|
|
656
|
+
phone: str(a.phone)
|
|
657
|
+
};
|
|
658
|
+
return Object.values(out).some((v) => v != null) ? out : void 0;
|
|
659
|
+
}
|
|
660
|
+
function contact(c) {
|
|
661
|
+
const a = address(c);
|
|
662
|
+
const role = str(c?.role);
|
|
663
|
+
if (!a && !role) return void 0;
|
|
664
|
+
return { ...a ?? {}, role: role ?? "endUser" };
|
|
665
|
+
}
|
|
489
666
|
var buyersRoute = new Hono2();
|
|
490
667
|
function normalizeBuyer(body) {
|
|
491
668
|
return {
|
|
492
669
|
name: String(body?.name ?? "Untitled buyer"),
|
|
493
670
|
identity: cred(body?.identity),
|
|
494
|
-
profileId: body?.profileId ? String(body.profileId) : void 0
|
|
671
|
+
profileId: body?.profileId ? String(body.profileId) : void 0,
|
|
672
|
+
shipTo: address(body?.shipTo),
|
|
673
|
+
billTo: address(body?.billTo),
|
|
674
|
+
contact: contact(body?.contact)
|
|
495
675
|
};
|
|
496
676
|
}
|
|
497
677
|
buyersRoute.get("/", (c) => c.json(listBuyers()));
|
|
@@ -519,22 +699,14 @@ buyersRoute.delete("/:id", async (c) => {
|
|
|
519
699
|
});
|
|
520
700
|
var suppliersRoute = new Hono2();
|
|
521
701
|
function normalizeSupplier(body) {
|
|
522
|
-
const
|
|
523
|
-
supplierPartId: String(it?.supplierPartId ?? ""),
|
|
524
|
-
description: String(it?.description ?? ""),
|
|
525
|
-
unitPrice: Number(it?.unitPrice ?? 0) || 0,
|
|
526
|
-
currency: String(it?.currency ?? "USD"),
|
|
527
|
-
uom: String(it?.uom ?? "EA"),
|
|
528
|
-
unspsc: String(it?.unspsc ?? ""),
|
|
529
|
-
manufacturerPartId: it?.manufacturerPartId ? String(it.manufacturerPartId) : void 0,
|
|
530
|
-
manufacturerName: it?.manufacturerName ? String(it.manufacturerName) : void 0
|
|
531
|
-
})) : void 0;
|
|
702
|
+
const productListIds = Array.isArray(body?.productListIds) ? body.productListIds.map((id) => String(id)) : [];
|
|
532
703
|
return {
|
|
533
704
|
name: String(body?.name ?? "Untitled supplier"),
|
|
534
705
|
identity: cred(body?.identity),
|
|
535
706
|
punchoutUrl: body?.punchoutUrl ? String(body.punchoutUrl) : void 0,
|
|
536
707
|
orderUrl: body?.orderUrl ? String(body.orderUrl) : void 0,
|
|
537
|
-
|
|
708
|
+
productListIds,
|
|
709
|
+
allowMixedCurrency: Boolean(body?.allowMixedCurrency)
|
|
538
710
|
};
|
|
539
711
|
}
|
|
540
712
|
suppliersRoute.get("/", (c) => c.json(listSuppliers()));
|
|
@@ -593,6 +765,7 @@ function normalizeProfile(body) {
|
|
|
593
765
|
const cartReturnTransport = ["cxml-urlencoded", "cxml-base64", "raw"].includes(
|
|
594
766
|
body?.cartReturnTransport
|
|
595
767
|
) ? body.cartReturnTransport : "cxml-urlencoded";
|
|
768
|
+
const addressMode = ["id-only", "full", "both"].includes(body?.addressMode) ? body.addressMode : "full";
|
|
596
769
|
return {
|
|
597
770
|
name: String(body?.name ?? "Untitled profile"),
|
|
598
771
|
platform: body?.platform ? String(body.platform) : void 0,
|
|
@@ -601,7 +774,10 @@ function normalizeProfile(body) {
|
|
|
601
774
|
setupOperation,
|
|
602
775
|
attachmentEncoding,
|
|
603
776
|
cartReturnTransport,
|
|
604
|
-
extrinsics: normalizeExtrinsics(body?.extrinsics)
|
|
777
|
+
extrinsics: normalizeExtrinsics(body?.extrinsics),
|
|
778
|
+
addressMode,
|
|
779
|
+
shipToInSetup: Boolean(body?.shipToInSetup),
|
|
780
|
+
contactInSetup: Boolean(body?.contactInSetup)
|
|
605
781
|
// `builtin` is never set from the wire — only code-seeded presets carry it.
|
|
606
782
|
};
|
|
607
783
|
}
|
|
@@ -630,8 +806,66 @@ profilesRoute.delete("/:id", async (c) => {
|
|
|
630
806
|
});
|
|
631
807
|
profilePresetsRoute.get("/", (c) => c.json(PROFILE_PRESETS));
|
|
632
808
|
|
|
633
|
-
// src/server/routes/
|
|
809
|
+
// src/server/routes/products.ts
|
|
634
810
|
import { Hono as Hono4 } from "hono";
|
|
811
|
+
var productsRoute = new Hono4();
|
|
812
|
+
var productListPresetsRoute = new Hono4();
|
|
813
|
+
function normalizeClassifications(it) {
|
|
814
|
+
if (Array.isArray(it?.classifications)) {
|
|
815
|
+
return it.classifications.map((c) => ({ domain: String(c?.domain ?? "UNSPSC"), value: String(c?.value ?? "") })).filter((c) => c.value.trim().length > 0);
|
|
816
|
+
}
|
|
817
|
+
if (it?.unspsc) return [{ domain: "UNSPSC", value: String(it.unspsc) }];
|
|
818
|
+
return [];
|
|
819
|
+
}
|
|
820
|
+
function normalizeItem(it) {
|
|
821
|
+
return {
|
|
822
|
+
supplierPartId: String(it?.supplierPartId ?? ""),
|
|
823
|
+
supplierPartAuxiliaryId: it?.supplierPartAuxiliaryId ? String(it.supplierPartAuxiliaryId) : void 0,
|
|
824
|
+
description: String(it?.description ?? ""),
|
|
825
|
+
unitPrice: Number(it?.unitPrice ?? 0) || 0,
|
|
826
|
+
currency: String(it?.currency ?? "USD"),
|
|
827
|
+
uom: String(it?.uom ?? "EA"),
|
|
828
|
+
classifications: normalizeClassifications(it),
|
|
829
|
+
manufacturerPartId: it?.manufacturerPartId ? String(it.manufacturerPartId) : void 0,
|
|
830
|
+
manufacturerName: it?.manufacturerName ? String(it.manufacturerName) : void 0,
|
|
831
|
+
allowFractional: Boolean(it?.allowFractional)
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
function normalizeProductList(body) {
|
|
835
|
+
const items = Array.isArray(body?.items) ? body.items.map(normalizeItem) : [];
|
|
836
|
+
return {
|
|
837
|
+
name: String(body?.name ?? "Untitled product list"),
|
|
838
|
+
description: body?.description ? String(body.description) : void 0,
|
|
839
|
+
items
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
productsRoute.get("/", (c) => c.json(listProductLists()));
|
|
843
|
+
productsRoute.post("/", async (c) => {
|
|
844
|
+
const input = normalizeProductList(await c.req.json().catch(() => ({})));
|
|
845
|
+
if (!input.name.trim()) return c.json({ errors: ["name is required"] }, 400);
|
|
846
|
+
return c.json(await createProductList(input), 201);
|
|
847
|
+
});
|
|
848
|
+
productsRoute.get("/:id", (c) => {
|
|
849
|
+
const p = getProductList(c.req.param("id"));
|
|
850
|
+
return p ? c.json(p) : c.json({ error: "not found" }, 404);
|
|
851
|
+
});
|
|
852
|
+
productsRoute.put("/:id", async (c) => {
|
|
853
|
+
if (!getProductList(c.req.param("id"))) return c.json({ error: "not found" }, 404);
|
|
854
|
+
const input = normalizeProductList(await c.req.json().catch(() => ({})));
|
|
855
|
+
return c.json(await updateProductList(c.req.param("id"), input));
|
|
856
|
+
});
|
|
857
|
+
productsRoute.delete("/:id", async (c) => {
|
|
858
|
+
try {
|
|
859
|
+
const ok = await deleteProductList(c.req.param("id"));
|
|
860
|
+
return ok ? c.json({ ok: true }) : c.json({ error: "not found" }, 404);
|
|
861
|
+
} catch (e) {
|
|
862
|
+
return c.json({ error: e instanceof Error ? e.message : String(e) }, 409);
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
productListPresetsRoute.get("/", (c) => c.json(PRODUCT_LIST_PRESETS));
|
|
866
|
+
|
|
867
|
+
// src/server/routes/flow.ts
|
|
868
|
+
import { Hono as Hono5 } from "hono";
|
|
635
869
|
import { nanoid as nanoid5 } from "nanoid";
|
|
636
870
|
|
|
637
871
|
// src/server/store/log.ts
|
|
@@ -944,6 +1178,15 @@ function escapeXml(value) {
|
|
|
944
1178
|
function makePayloadId(host3, nowIso) {
|
|
945
1179
|
return `${nowIso}.${nanoid4(10)}@${host3}`;
|
|
946
1180
|
}
|
|
1181
|
+
function lineItemsTotal(items, headerCurrency) {
|
|
1182
|
+
const currencies = new Set(items.map((it) => it.currency || headerCurrency));
|
|
1183
|
+
if (currencies.size > 1) return 0;
|
|
1184
|
+
return items.reduce((sum, it) => sum + (it.unitPriceAmount ?? 0) * it.quantity, 0);
|
|
1185
|
+
}
|
|
1186
|
+
function classificationBlock(it, indent) {
|
|
1187
|
+
const list = it.classifications && it.classifications.length > 0 ? it.classifications : [{ domain: it.classificationDomain ?? "UNSPSC", value: it.classification ?? "" }];
|
|
1188
|
+
return list.map((c) => `${indent}<Classification domain="${escapeXml(c.domain || "UNSPSC")}">${escapeXml(c.value)}</Classification>`).join("\n");
|
|
1189
|
+
}
|
|
947
1190
|
function credentialBlock(tag, c) {
|
|
948
1191
|
return ` <${tag}>
|
|
949
1192
|
<Credential domain="${escapeXml(c.domain)}">
|
|
@@ -989,6 +1232,11 @@ function extrinsicBlock(items, indent) {
|
|
|
989
1232
|
function buildSetupRequest(o) {
|
|
990
1233
|
const lang = o.lang ?? "en-US";
|
|
991
1234
|
const deployment = o.deploymentMode ? ` deploymentMode="${escapeXml(o.deploymentMode)}"` : "";
|
|
1235
|
+
const mode = o.addressMode ?? "full";
|
|
1236
|
+
const shipTo = o.shipTo ? `
|
|
1237
|
+
${addressBlock("ShipTo", o.shipTo, mode)}` : "";
|
|
1238
|
+
const contact2 = o.contact ? `
|
|
1239
|
+
${contactBlock(o.contact, mode, " ")}` : "";
|
|
992
1240
|
const inner = `${header({
|
|
993
1241
|
from: o.from,
|
|
994
1242
|
to: o.to,
|
|
@@ -1001,26 +1249,51 @@ function buildSetupRequest(o) {
|
|
|
1001
1249
|
<BuyerCookie>${escapeXml(o.buyerCookie)}</BuyerCookie>
|
|
1002
1250
|
<BrowserFormPost>
|
|
1003
1251
|
<URL>${escapeXml(o.browserFormPostUrl)}</URL>
|
|
1004
|
-
</BrowserFormPost>${extrinsicBlock(o.extrinsics, " ")}
|
|
1252
|
+
</BrowserFormPost>${extrinsicBlock(o.extrinsics, " ")}${shipTo}${contact2}
|
|
1005
1253
|
</PunchOutSetupRequest>
|
|
1006
1254
|
</Request>`;
|
|
1007
1255
|
return envelope(o.payloadId, o.timestamp, lang, inner, o.dtdVersion);
|
|
1008
1256
|
}
|
|
1009
|
-
|
|
1257
|
+
var wantsId = (m) => m === "id-only" || m === "both";
|
|
1258
|
+
var wantsPostal = (m) => m === "full" || m === "both";
|
|
1259
|
+
var hasPostal = (a) => !!(a.deliverTo || a.street || a.city || a.state || a.postalCode);
|
|
1260
|
+
function idAttrs(a, mode) {
|
|
1261
|
+
if (!wantsId(mode) || !a.addressId) return "";
|
|
1262
|
+
return ` addressID="${escapeXml(a.addressId)}"${a.addressIdDomain ? ` addressIDDomain="${escapeXml(a.addressIdDomain)}"` : ""}`;
|
|
1263
|
+
}
|
|
1264
|
+
function postalBlock(a, indent) {
|
|
1265
|
+
return `${indent}<PostalAddress>
|
|
1266
|
+
${a.deliverTo ? `${indent} <DeliverTo>${escapeXml(a.deliverTo)}</DeliverTo>
|
|
1267
|
+
` : ""}${indent} <Street>${escapeXml(a.street ?? "")}</Street>
|
|
1268
|
+
${indent} <City>${escapeXml(a.city ?? "")}</City>
|
|
1269
|
+
${indent} <State>${escapeXml(a.state ?? "")}</State>
|
|
1270
|
+
${indent} <PostalCode>${escapeXml(a.postalCode ?? "")}</PostalCode>
|
|
1271
|
+
${indent} <Country isoCountryCode="${escapeXml(a.countryIsoCode ?? "US")}">${escapeXml(a.countryName ?? "United States")}</Country>
|
|
1272
|
+
${indent}</PostalAddress>`;
|
|
1273
|
+
}
|
|
1274
|
+
function contactInfo(a, indent) {
|
|
1275
|
+
const email = a.email ? `
|
|
1276
|
+
${indent}<Email>${escapeXml(a.email)}</Email>` : "";
|
|
1277
|
+
const phone = a.phone ? `
|
|
1278
|
+
${indent}<Phone><TelephoneNumber><Number>${escapeXml(a.phone)}</Number></TelephoneNumber></Phone>` : "";
|
|
1279
|
+
return email + phone;
|
|
1280
|
+
}
|
|
1281
|
+
function addressBlock(tag, a, mode) {
|
|
1282
|
+
const postal = wantsPostal(mode) ? `
|
|
1283
|
+
${postalBlock(a, " ")}` : "";
|
|
1010
1284
|
return ` <${tag}>
|
|
1011
|
-
<Address${a
|
|
1012
|
-
<Name xml:lang="en">${escapeXml(a.name ?? "")}</Name
|
|
1013
|
-
<PostalAddress>
|
|
1014
|
-
${a.deliverTo ? ` <DeliverTo>${escapeXml(a.deliverTo)}</DeliverTo>
|
|
1015
|
-
` : ""} <Street>${escapeXml(a.street ?? "")}</Street>
|
|
1016
|
-
<City>${escapeXml(a.city ?? "")}</City>
|
|
1017
|
-
<State>${escapeXml(a.state ?? "")}</State>
|
|
1018
|
-
<PostalCode>${escapeXml(a.postalCode ?? "")}</PostalCode>
|
|
1019
|
-
<Country isoCountryCode="${escapeXml(a.countryIsoCode ?? "US")}">${escapeXml(a.countryName ?? "United States")}</Country>
|
|
1020
|
-
</PostalAddress>
|
|
1285
|
+
<Address${idAttrs(a, mode)}>
|
|
1286
|
+
<Name xml:lang="en">${escapeXml(a.name ?? "")}</Name>${postal}${contactInfo(a, " ")}
|
|
1021
1287
|
</Address>
|
|
1022
1288
|
</${tag}>`;
|
|
1023
1289
|
}
|
|
1290
|
+
function contactBlock(c, mode, indent) {
|
|
1291
|
+
const postal = mode !== "id-only" && hasPostal(c) ? `
|
|
1292
|
+
${postalBlock(c, indent + " ")}` : "";
|
|
1293
|
+
return `${indent}<Contact role="${escapeXml(c.role || "endUser")}"${idAttrs(c, mode)}>
|
|
1294
|
+
${indent} <Name xml:lang="en">${escapeXml(c.name ?? "")}</Name>${postal}${contactInfo(c, indent + " ")}
|
|
1295
|
+
${indent}</Contact>`;
|
|
1296
|
+
}
|
|
1024
1297
|
function commentsWithAttachments(cids, indent) {
|
|
1025
1298
|
if (cids.length === 0) return "";
|
|
1026
1299
|
const atts = cids.map(
|
|
@@ -1054,14 +1327,15 @@ function buildOrderRequest(o) {
|
|
|
1054
1327
|
</UnitPrice>
|
|
1055
1328
|
<Description xml:lang="en">${escapeXml(it.description ?? "")}</Description>
|
|
1056
1329
|
<UnitOfMeasure>${escapeXml(it.uom ?? "EA")}</UnitOfMeasure>
|
|
1057
|
-
|
|
1058
|
-
it.classification ?? ""
|
|
1059
|
-
)}</Classification>${it.manufacturerPartId ? `
|
|
1330
|
+
${classificationBlock(it, " ")}${it.manufacturerPartId ? `
|
|
1060
1331
|
<ManufacturerPartID>${escapeXml(it.manufacturerPartId)}</ManufacturerPartID>` : ""}${it.manufacturerName ? `
|
|
1061
1332
|
<ManufacturerName>${escapeXml(it.manufacturerName)}</ManufacturerName>` : ""}${commentsWithAttachments(cids, " ")}
|
|
1062
1333
|
</ItemDetail>
|
|
1063
1334
|
</ItemOut>`;
|
|
1064
1335
|
}).join("\n");
|
|
1336
|
+
const mode = o.addressMode ?? "full";
|
|
1337
|
+
const contact2 = o.contact ? `
|
|
1338
|
+
${contactBlock(o.contact, mode, " ")}` : "";
|
|
1065
1339
|
const inner = `${header({
|
|
1066
1340
|
from: o.from,
|
|
1067
1341
|
to: o.to,
|
|
@@ -1077,8 +1351,8 @@ function buildOrderRequest(o) {
|
|
|
1077
1351
|
<Total>
|
|
1078
1352
|
<Money currency="${escapeXml(o.currency)}">${escapeXml(o.total)}</Money>
|
|
1079
1353
|
</Total>
|
|
1080
|
-
${addressBlock("ShipTo", o.shipTo ?? {})}
|
|
1081
|
-
${addressBlock("BillTo", o.billTo ?? {})}${commentsWithAttachments(orderLevelCids, " ")}${extrinsicBlock(
|
|
1354
|
+
${addressBlock("ShipTo", o.shipTo ?? {}, mode)}
|
|
1355
|
+
${addressBlock("BillTo", o.billTo ?? {}, mode)}${contact2}${commentsWithAttachments(orderLevelCids, " ")}${extrinsicBlock(
|
|
1082
1356
|
o.extrinsics,
|
|
1083
1357
|
" "
|
|
1084
1358
|
)}
|
|
@@ -1116,10 +1390,7 @@ function buildResponseStatus(o) {
|
|
|
1116
1390
|
return envelope(o.payloadId, o.timestamp, o.lang ?? "en-US", inner, o.dtdVersion);
|
|
1117
1391
|
}
|
|
1118
1392
|
function buildPunchOutOrderMessage(o) {
|
|
1119
|
-
const total = o.items.
|
|
1120
|
-
(sum, it) => sum + (it.unitPriceAmount ?? 0) * it.quantity,
|
|
1121
|
-
0
|
|
1122
|
-
);
|
|
1393
|
+
const total = lineItemsTotal(o.items, o.currency);
|
|
1123
1394
|
const items = o.items.map(
|
|
1124
1395
|
(it) => ` <ItemIn quantity="${escapeXml(it.quantity)}">
|
|
1125
1396
|
<ItemID>
|
|
@@ -1134,9 +1405,7 @@ function buildPunchOutOrderMessage(o) {
|
|
|
1134
1405
|
</UnitPrice>
|
|
1135
1406
|
<Description xml:lang="en">${escapeXml(it.description ?? "")}</Description>
|
|
1136
1407
|
<UnitOfMeasure>${escapeXml(it.uom ?? "EA")}</UnitOfMeasure>
|
|
1137
|
-
|
|
1138
|
-
it.classification ?? ""
|
|
1139
|
-
)}</Classification>${it.manufacturerPartId ? `
|
|
1408
|
+
${classificationBlock(it, " ")}${it.manufacturerPartId ? `
|
|
1140
1409
|
<ManufacturerPartID>${escapeXml(it.manufacturerPartId)}</ManufacturerPartID>` : ""}
|
|
1141
1410
|
</ItemDetail>
|
|
1142
1411
|
</ItemIn>`
|
|
@@ -1326,8 +1595,8 @@ function parseCart(doc) {
|
|
|
1326
1595
|
const items = asArray(pom?.ItemIn).map((it) => {
|
|
1327
1596
|
const detail = it?.ItemDetail;
|
|
1328
1597
|
const up = money(detail?.UnitPrice);
|
|
1329
|
-
const
|
|
1330
|
-
const classFirst =
|
|
1598
|
+
const classifications = asArray(detail?.Classification).filter((c) => c != null).map((c) => ({ domain: attr(c, "domain") ?? "", value: text(c) ?? "" }));
|
|
1599
|
+
const classFirst = classifications[0];
|
|
1331
1600
|
return {
|
|
1332
1601
|
quantity: Number(attr(it, "quantity") ?? "1") || 1,
|
|
1333
1602
|
supplierPartId: text(it?.ItemID?.SupplierPartID),
|
|
@@ -1336,8 +1605,9 @@ function parseCart(doc) {
|
|
|
1336
1605
|
uom: text(detail?.UnitOfMeasure),
|
|
1337
1606
|
unitPriceAmount: up.amount,
|
|
1338
1607
|
currency: up.currency,
|
|
1339
|
-
|
|
1340
|
-
|
|
1608
|
+
classifications: classifications.length > 0 ? classifications : void 0,
|
|
1609
|
+
classificationDomain: classFirst?.domain,
|
|
1610
|
+
classification: classFirst?.value,
|
|
1341
1611
|
manufacturerPartId: text(detail?.ManufacturerPartID),
|
|
1342
1612
|
manufacturerName: text(detail?.ManufacturerName)
|
|
1343
1613
|
};
|
|
@@ -1500,6 +1770,7 @@ function checkPunchback(doc, ctx, issues) {
|
|
|
1500
1770
|
issues.warn("empty-cart", "PunchOutOrderMessage contains no ItemIn elements", "cXML/.../PunchOutOrderMessage");
|
|
1501
1771
|
}
|
|
1502
1772
|
let lineSum = 0;
|
|
1773
|
+
const lineCurrencies = [];
|
|
1503
1774
|
items.forEach((it, i) => {
|
|
1504
1775
|
const base = `cXML/.../ItemIn[${i + 1}]`;
|
|
1505
1776
|
const qty = num(attr(it, "quantity"));
|
|
@@ -1516,10 +1787,13 @@ function checkPunchback(doc, ctx, issues) {
|
|
|
1516
1787
|
}
|
|
1517
1788
|
const up = detail.UnitPrice?.Money;
|
|
1518
1789
|
const upAmount = num(text(up));
|
|
1790
|
+
const upCurrency = attr(up, "currency");
|
|
1519
1791
|
if (up == null) {
|
|
1520
1792
|
issues.error("item-missing-unitprice", `ItemIn[${i + 1}] is missing ItemDetail/UnitPrice/Money`, `${base}/ItemDetail/UnitPrice`);
|
|
1521
|
-
} else if (!
|
|
1793
|
+
} else if (!upCurrency) {
|
|
1522
1794
|
issues.error("item-missing-currency", `ItemIn[${i + 1}] UnitPrice/Money is missing @currency`, `${base}/ItemDetail/UnitPrice/Money`);
|
|
1795
|
+
} else {
|
|
1796
|
+
lineCurrencies.push(upCurrency);
|
|
1523
1797
|
}
|
|
1524
1798
|
if (!text(detail.Description)) {
|
|
1525
1799
|
issues.error("item-missing-description", `ItemIn[${i + 1}] is missing ItemDetail/Description`, `${base}/ItemDetail/Description`);
|
|
@@ -1535,7 +1809,14 @@ function checkPunchback(doc, ctx, issues) {
|
|
|
1535
1809
|
}
|
|
1536
1810
|
if (qty != null && upAmount != null) lineSum += qty * upAmount;
|
|
1537
1811
|
});
|
|
1538
|
-
|
|
1812
|
+
const singleCurrency = checkSingleCurrency(
|
|
1813
|
+
lineCurrencies,
|
|
1814
|
+
totalCurrency,
|
|
1815
|
+
issues,
|
|
1816
|
+
"cXML/.../PunchOutOrderMessageHeader/Total",
|
|
1817
|
+
ctx.allowMixedCurrency
|
|
1818
|
+
);
|
|
1819
|
+
if (singleCurrency && totalAmount != null && items.length > 0) {
|
|
1539
1820
|
const diff = Math.abs(totalAmount - lineSum);
|
|
1540
1821
|
if (diff > 0.01) {
|
|
1541
1822
|
issues.warn(
|
|
@@ -1546,6 +1827,41 @@ function checkPunchback(doc, ctx, issues) {
|
|
|
1546
1827
|
}
|
|
1547
1828
|
}
|
|
1548
1829
|
}
|
|
1830
|
+
function checkSingleCurrency(lineCurrencies, totalCurrency, issues, path, allowMixed = false) {
|
|
1831
|
+
const all = new Set(lineCurrencies.filter(Boolean));
|
|
1832
|
+
if (totalCurrency) all.add(totalCurrency);
|
|
1833
|
+
if (all.size > 1) {
|
|
1834
|
+
const list = [...all].join(", ");
|
|
1835
|
+
if (allowMixed) {
|
|
1836
|
+
issues.warn(
|
|
1837
|
+
"mixed-currency",
|
|
1838
|
+
`Multiple currencies in one document (${list}); allowed for this supplier. The header Total is a single Money \u2014 rely on the per-line currencies.`,
|
|
1839
|
+
path
|
|
1840
|
+
);
|
|
1841
|
+
} else {
|
|
1842
|
+
issues.error(
|
|
1843
|
+
"mixed-currency",
|
|
1844
|
+
`Multiple currencies in one document (${list}). A cXML Total is a single Money \u2014 all line items and the Total must share one currency.`,
|
|
1845
|
+
path
|
|
1846
|
+
);
|
|
1847
|
+
}
|
|
1848
|
+
return false;
|
|
1849
|
+
}
|
|
1850
|
+
return true;
|
|
1851
|
+
}
|
|
1852
|
+
function checkAddressComplete(addr, label, issues) {
|
|
1853
|
+
if (addr == null) return;
|
|
1854
|
+
const hasId = !!attr(addr, "addressID");
|
|
1855
|
+
const postal = addr.PostalAddress;
|
|
1856
|
+
const hasPostal2 = !!(text(postal?.Street) || text(postal?.City));
|
|
1857
|
+
if (!hasId && !hasPostal2) {
|
|
1858
|
+
issues.warn(
|
|
1859
|
+
`${label.toLowerCase()}-incomplete`,
|
|
1860
|
+
`${label}/Address has neither an addressID nor a Street/City \u2014 the address is empty`,
|
|
1861
|
+
`cXML/.../OrderRequestHeader/${label}/Address`
|
|
1862
|
+
);
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1549
1865
|
function checkOrderRequest(doc, ctx, issues) {
|
|
1550
1866
|
const orderReq = root(doc)?.Request?.OrderRequest;
|
|
1551
1867
|
if (!orderReq) {
|
|
@@ -1564,26 +1880,41 @@ function checkOrderRequest(doc, ctx, issues) {
|
|
|
1564
1880
|
}
|
|
1565
1881
|
if (!header2?.ShipTo) {
|
|
1566
1882
|
issues.warn("missing-shipto", "OrderRequestHeader/ShipTo is missing", "cXML/.../OrderRequestHeader/ShipTo");
|
|
1883
|
+
} else {
|
|
1884
|
+
checkAddressComplete(header2.ShipTo.Address, "ShipTo", issues);
|
|
1567
1885
|
}
|
|
1568
1886
|
if (!header2?.BillTo) {
|
|
1569
1887
|
issues.warn("missing-billto", "OrderRequestHeader/BillTo is missing", "cXML/.../OrderRequestHeader/BillTo");
|
|
1888
|
+
} else {
|
|
1889
|
+
checkAddressComplete(header2.BillTo.Address, "BillTo", issues);
|
|
1890
|
+
}
|
|
1891
|
+
for (const contact2 of asArray(header2?.Contact)) {
|
|
1892
|
+
if (!attr(contact2, "role")) {
|
|
1893
|
+
issues.warn("contact-missing-role", "OrderRequestHeader/Contact is missing @role", "cXML/.../OrderRequestHeader/Contact");
|
|
1894
|
+
}
|
|
1570
1895
|
}
|
|
1571
1896
|
const items = asArray(orderReq.ItemOut);
|
|
1572
1897
|
if (items.length === 0) {
|
|
1573
1898
|
issues.error("no-itemout", "OrderRequest contains no ItemOut elements", "cXML/.../OrderRequest");
|
|
1574
1899
|
}
|
|
1900
|
+
const lineCurrencies = [];
|
|
1575
1901
|
items.forEach((it, i) => {
|
|
1576
1902
|
const base = `cXML/.../ItemOut[${i + 1}]`;
|
|
1577
1903
|
if (!text(it?.ItemID?.SupplierPartID)) {
|
|
1578
1904
|
issues.error("itemout-missing-id", `ItemOut[${i + 1}] is missing ItemID/SupplierPartID`, `${base}/ItemID`);
|
|
1579
1905
|
}
|
|
1580
|
-
|
|
1906
|
+
const up = it?.ItemDetail?.UnitPrice?.Money;
|
|
1907
|
+
if (up == null) {
|
|
1581
1908
|
issues.error("itemout-missing-unitprice", `ItemOut[${i + 1}] is missing ItemDetail/UnitPrice/Money`, `${base}/ItemDetail/UnitPrice`);
|
|
1909
|
+
} else {
|
|
1910
|
+
const cur = attr(up, "currency");
|
|
1911
|
+
if (cur) lineCurrencies.push(cur);
|
|
1582
1912
|
}
|
|
1583
1913
|
if (num(attr(it, "quantity")) == null) {
|
|
1584
1914
|
issues.error("itemout-missing-quantity", `ItemOut[${i + 1}] is missing @quantity`, base);
|
|
1585
1915
|
}
|
|
1586
1916
|
});
|
|
1917
|
+
checkSingleCurrency(lineCurrencies, attr(header2?.Total?.Money, "currency"), issues, "cXML/.../OrderRequestHeader/Total", ctx.allowMixedCurrency);
|
|
1587
1918
|
const refs = collectCidReferences(doc);
|
|
1588
1919
|
const available = ctx.availableContentIds;
|
|
1589
1920
|
const referenced = /* @__PURE__ */ new Set();
|
|
@@ -1665,7 +1996,7 @@ function validateDocument(raw, ctx = {}) {
|
|
|
1665
1996
|
}
|
|
1666
1997
|
|
|
1667
1998
|
// src/server/routes/flow.ts
|
|
1668
|
-
var flowRoute = new
|
|
1999
|
+
var flowRoute = new Hono5();
|
|
1669
2000
|
function host() {
|
|
1670
2001
|
try {
|
|
1671
2002
|
return new URL(getPublicUrl()).host;
|
|
@@ -1690,7 +2021,18 @@ function buyerContext(r) {
|
|
|
1690
2021
|
connectionId: connection.id,
|
|
1691
2022
|
attachmentEncoding: eff.attachmentEncoding,
|
|
1692
2023
|
eff,
|
|
1693
|
-
expected: { from, to, sender, sharedSecret: connection.sharedSecret }
|
|
2024
|
+
expected: { from, to, sender, sharedSecret: connection.sharedSecret },
|
|
2025
|
+
allowMixedCurrency: supplier.allowMixedCurrency ?? false,
|
|
2026
|
+
shipTo: buyer.shipTo,
|
|
2027
|
+
billTo: buyer.billTo,
|
|
2028
|
+
contact: buyer.contact
|
|
2029
|
+
};
|
|
2030
|
+
}
|
|
2031
|
+
function setupAddresses(ctx) {
|
|
2032
|
+
return {
|
|
2033
|
+
shipTo: ctx.eff.shipToInSetup ? ctx.shipTo : void 0,
|
|
2034
|
+
contact: ctx.eff.contactInSetup ? ctx.contact : void 0,
|
|
2035
|
+
addressMode: ctx.eff.addressMode
|
|
1694
2036
|
};
|
|
1695
2037
|
}
|
|
1696
2038
|
function setupExtrinsics(ctx, buyerCookie) {
|
|
@@ -1723,7 +2065,8 @@ flowRoute.get("/:id/setup/preview", (c) => {
|
|
|
1723
2065
|
operation: ctx.eff.setupOperation,
|
|
1724
2066
|
dtdVersion: dtdVersionFor(ctx.eff, "SetupRequest"),
|
|
1725
2067
|
userAgent: ctx.eff.userAgent,
|
|
1726
|
-
extrinsics: setupExtrinsics(ctx, buyerCookie)
|
|
2068
|
+
extrinsics: setupExtrinsics(ctx, buyerCookie),
|
|
2069
|
+
...setupAddresses(ctx)
|
|
1727
2070
|
});
|
|
1728
2071
|
return c.json({ buyerCookie, xml, browserFormPostUrl: browserFormPostUrl() });
|
|
1729
2072
|
});
|
|
@@ -1746,7 +2089,8 @@ flowRoute.post("/:id/setup", async (c) => {
|
|
|
1746
2089
|
operation: ctx.eff.setupOperation,
|
|
1747
2090
|
dtdVersion: dtdVersionFor(ctx.eff, "SetupRequest"),
|
|
1748
2091
|
userAgent: ctx.eff.userAgent,
|
|
1749
|
-
extrinsics: setupExtrinsics(ctx, buyerCookie)
|
|
2092
|
+
extrinsics: setupExtrinsics(ctx, buyerCookie),
|
|
2093
|
+
...setupAddresses(ctx)
|
|
1750
2094
|
});
|
|
1751
2095
|
rememberSessionConnection(buyerCookie, ctx.connectionId);
|
|
1752
2096
|
const reqValidation = validateDocument(xml, { expected: ctx.expected, forceDocType: "SetupRequest" });
|
|
@@ -1789,7 +2133,7 @@ flowRoute.post("/:id/setup", async (c) => {
|
|
|
1789
2133
|
function buildOrderXml(ctx, body) {
|
|
1790
2134
|
const items = body.items ?? [];
|
|
1791
2135
|
const currency = body.currency || items[0]?.currency || "USD";
|
|
1792
|
-
const total = body.total ?? items
|
|
2136
|
+
const total = body.total ?? lineItemsTotal(items, currency);
|
|
1793
2137
|
const orderId = body.orderId || `PO-${nanoid5(8)}`;
|
|
1794
2138
|
const attMeta = (body.attachments ?? []).map((a) => ({
|
|
1795
2139
|
contentId: a.contentId,
|
|
@@ -1808,8 +2152,12 @@ function buildOrderXml(ctx, body) {
|
|
|
1808
2152
|
currency,
|
|
1809
2153
|
total,
|
|
1810
2154
|
items,
|
|
1811
|
-
|
|
1812
|
-
|
|
2155
|
+
// Order addresses default to the buyer's configured defaults when the body
|
|
2156
|
+
// doesn't override them (the UI pre-fills from the buyer, editable per order).
|
|
2157
|
+
shipTo: body.shipTo ?? ctx.shipTo,
|
|
2158
|
+
billTo: body.billTo ?? ctx.billTo,
|
|
2159
|
+
contact: body.contact ?? ctx.contact,
|
|
2160
|
+
addressMode: ctx.eff.addressMode,
|
|
1813
2161
|
attachments: attMeta,
|
|
1814
2162
|
dtdVersion: dtdVersionFor(ctx.eff, "OrderRequest"),
|
|
1815
2163
|
userAgent: ctx.eff.userAgent,
|
|
@@ -1863,7 +2211,8 @@ flowRoute.post("/:id/order", async (c) => {
|
|
|
1863
2211
|
const reqValidation = validateDocument(xml, {
|
|
1864
2212
|
expected: ctx.expected,
|
|
1865
2213
|
forceDocType: "OrderRequest",
|
|
1866
|
-
availableContentIds: inputAtts.length > 0 ? availableContentIds : void 0
|
|
2214
|
+
availableContentIds: inputAtts.length > 0 ? availableContentIds : void 0,
|
|
2215
|
+
allowMixedCurrency: ctx.allowMixedCurrency
|
|
1867
2216
|
});
|
|
1868
2217
|
const reqLog = appendLog({
|
|
1869
2218
|
sessionId,
|
|
@@ -1904,8 +2253,8 @@ flowRoute.post("/:id/order", async (c) => {
|
|
|
1904
2253
|
});
|
|
1905
2254
|
|
|
1906
2255
|
// src/server/routes/punchout-return.ts
|
|
1907
|
-
import { Hono as
|
|
1908
|
-
var punchoutReturnRoute = new
|
|
2256
|
+
import { Hono as Hono6 } from "hono";
|
|
2257
|
+
var punchoutReturnRoute = new Hono6();
|
|
1909
2258
|
async function extractCxml(c) {
|
|
1910
2259
|
const ct = (c.req.header("content-type") ?? "").toLowerCase();
|
|
1911
2260
|
if (ct.includes("application/x-www-form-urlencoded") || ct.includes("multipart/form-data")) {
|
|
@@ -1973,9 +2322,9 @@ punchoutReturnRoute.post("/return", async (c) => {
|
|
|
1973
2322
|
});
|
|
1974
2323
|
|
|
1975
2324
|
// src/server/routes/stream.ts
|
|
1976
|
-
import { Hono as
|
|
2325
|
+
import { Hono as Hono7 } from "hono";
|
|
1977
2326
|
import { streamSSE } from "hono/streaming";
|
|
1978
|
-
var streamRoute = new
|
|
2327
|
+
var streamRoute = new Hono7();
|
|
1979
2328
|
var MAX_STREAMS = 64;
|
|
1980
2329
|
var activeStreams = 0;
|
|
1981
2330
|
streamRoute.get("/stream", (c) => {
|
|
@@ -2007,8 +2356,8 @@ streamRoute.get("/stream", (c) => {
|
|
|
2007
2356
|
});
|
|
2008
2357
|
|
|
2009
2358
|
// src/server/routes/data.ts
|
|
2010
|
-
import { Hono as
|
|
2011
|
-
var dataRoute = new
|
|
2359
|
+
import { Hono as Hono8 } from "hono";
|
|
2360
|
+
var dataRoute = new Hono8();
|
|
2012
2361
|
function rawMessage(record) {
|
|
2013
2362
|
const ct = record.contentType ?? record.headers?.["Content-Type"] ?? record.headers?.["content-type"];
|
|
2014
2363
|
let body = record.body;
|
|
@@ -2060,13 +2409,13 @@ dataRoute.get("/attachments/:hash", (c) => {
|
|
|
2060
2409
|
});
|
|
2061
2410
|
|
|
2062
2411
|
// src/server/routes/sim.ts
|
|
2063
|
-
import { Hono as
|
|
2412
|
+
import { Hono as Hono9 } from "hono";
|
|
2064
2413
|
import { nanoid as nanoid6 } from "nanoid";
|
|
2065
|
-
var simRoute = new
|
|
2414
|
+
var simRoute = new Hono9();
|
|
2066
2415
|
var DEMO_CATALOG = [
|
|
2067
|
-
{ supplierPartId: "WIDGET-001", description: "Premium Steel Widget", unitPrice: 12.5, currency: "USD", uom: "EA",
|
|
2068
|
-
{ supplierPartId: "BOLT-250", description: "M8 Hex Bolt (pack of 250)", unitPrice: 34, currency: "USD", uom: "PK",
|
|
2069
|
-
{ supplierPartId: "TAPE-RED", description: "Industrial Marking Tape, Red", unitPrice: 5.75, currency: "USD", uom: "RL",
|
|
2416
|
+
{ 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" },
|
|
2417
|
+
{ 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" },
|
|
2418
|
+
{ supplierPartId: "TAPE-RED", description: "Industrial Marking Tape, Red", unitPrice: 5.75, currency: "USD", uom: "RL", classifications: [{ domain: "UNSPSC", value: "31201500" }] }
|
|
2070
2419
|
];
|
|
2071
2420
|
function host2() {
|
|
2072
2421
|
try {
|
|
@@ -2075,7 +2424,10 @@ function host2() {
|
|
|
2075
2424
|
return "punchout-simulator";
|
|
2076
2425
|
}
|
|
2077
2426
|
}
|
|
2078
|
-
var catalogOf = (s) =>
|
|
2427
|
+
var catalogOf = (s) => {
|
|
2428
|
+
const items = catalogForSupplier(s);
|
|
2429
|
+
return items.length > 0 ? items : DEMO_CATALOG;
|
|
2430
|
+
};
|
|
2079
2431
|
function safeHttpUrl(u) {
|
|
2080
2432
|
if (!u) return "";
|
|
2081
2433
|
try {
|
|
@@ -2152,13 +2504,15 @@ simRoute.get("/:id/catalog", (c) => {
|
|
|
2152
2504
|
const bd = c.req.query("bd") ?? "";
|
|
2153
2505
|
const bi = c.req.query("bi") ?? "";
|
|
2154
2506
|
const items = catalogOf(supplier);
|
|
2155
|
-
const rows = items.map(
|
|
2156
|
-
(it
|
|
2157
|
-
|
|
2507
|
+
const rows = items.map((it, i) => {
|
|
2508
|
+
const partId = escapeXml(it.supplierPartId) + (it.supplierPartAuxiliaryId ? ` / ${escapeXml(it.supplierPartAuxiliaryId)}` : "");
|
|
2509
|
+
const cls = (it.classifications ?? []).map((c2) => `${escapeXml(c2.domain)} ${escapeXml(c2.value)}`).join(" \xB7 ");
|
|
2510
|
+
return `<tr>
|
|
2511
|
+
<td><strong>${escapeXml(it.description)}</strong><br><small>${partId} \xB7 ${escapeXml(it.uom)}${cls ? ` \xB7 ${cls}` : ""}</small></td>
|
|
2158
2512
|
<td class="price">${escapeXml(it.currency)} ${it.unitPrice.toFixed(2)}</td>
|
|
2159
|
-
<td><input type="number" name="q_${i}" value="0" min="0" step="1" inputmode="numeric"></td>
|
|
2160
|
-
</tr
|
|
2161
|
-
).join("\n");
|
|
2513
|
+
<td><input type="number" name="q_${i}" value="0" min="0" step="${it.allowFractional ? "any" : "1"}" inputmode="${it.allowFractional ? "decimal" : "numeric"}"></td>
|
|
2514
|
+
</tr>`;
|
|
2515
|
+
}).join("\n");
|
|
2162
2516
|
return c.html(`<!doctype html><html lang="en"><head><meta charset="utf-8">
|
|
2163
2517
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2164
2518
|
<title>${escapeXml(supplier.name)} \u2014 mock catalog</title>
|
|
@@ -2197,17 +2551,22 @@ simRoute.post("/:id/checkout", async (c) => {
|
|
|
2197
2551
|
const catalog = catalogOf(supplier);
|
|
2198
2552
|
const items = [];
|
|
2199
2553
|
catalog.forEach((it, i) => {
|
|
2200
|
-
|
|
2554
|
+
let qty = Number(form[`q_${i}`] ?? 0);
|
|
2555
|
+
if (!it.allowFractional) qty = Math.floor(qty);
|
|
2201
2556
|
if (qty > 0) {
|
|
2202
2557
|
items.push({
|
|
2203
2558
|
quantity: qty,
|
|
2204
2559
|
supplierPartId: it.supplierPartId,
|
|
2560
|
+
supplierPartAuxiliaryId: it.supplierPartAuxiliaryId,
|
|
2205
2561
|
description: it.description,
|
|
2206
2562
|
uom: it.uom,
|
|
2207
2563
|
unitPriceAmount: it.unitPrice,
|
|
2208
2564
|
currency: it.currency,
|
|
2209
|
-
|
|
2210
|
-
classification
|
|
2565
|
+
classifications: it.classifications,
|
|
2566
|
+
// Keep the legacy single fields populated from the first classification
|
|
2567
|
+
// for back-compat display (CartView) and any single-domain consumer.
|
|
2568
|
+
classificationDomain: it.classifications[0]?.domain,
|
|
2569
|
+
classification: it.classifications[0]?.value,
|
|
2211
2570
|
manufacturerPartId: it.manufacturerPartId,
|
|
2212
2571
|
manufacturerName: it.manufacturerName
|
|
2213
2572
|
});
|
|
@@ -2233,7 +2592,10 @@ simRoute.post("/:id/checkout", async (c) => {
|
|
|
2233
2592
|
docType: "PunchOutOrderMessage",
|
|
2234
2593
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
2235
2594
|
body: xml,
|
|
2236
|
-
validation: validateDocument(xml, {
|
|
2595
|
+
validation: validateDocument(xml, {
|
|
2596
|
+
forceDocType: "PunchOutOrderMessage",
|
|
2597
|
+
allowMixedCurrency: supplier.allowMixedCurrency
|
|
2598
|
+
})
|
|
2237
2599
|
});
|
|
2238
2600
|
const transport = eff?.cartReturnTransport ?? "cxml-urlencoded";
|
|
2239
2601
|
return c.html(cartReturnPage(formpost, xml, transport));
|
|
@@ -2303,7 +2665,8 @@ simRoute.post("/:id/order", async (c) => {
|
|
|
2303
2665
|
const validation = validateDocument(xml, {
|
|
2304
2666
|
expected: expectedFor(supplier.id, from),
|
|
2305
2667
|
forceDocType: "OrderRequest",
|
|
2306
|
-
availableContentIds: isMultipart(ct) ? availableContentIds : void 0
|
|
2668
|
+
availableContentIds: isMultipart(ct) ? availableContentIds : void 0,
|
|
2669
|
+
allowMixedCurrency: supplier.allowMixedCurrency
|
|
2307
2670
|
});
|
|
2308
2671
|
appendLog({
|
|
2309
2672
|
sessionId,
|
|
@@ -2350,7 +2713,7 @@ function findSessionForOrder(doc) {
|
|
|
2350
2713
|
// src/server/app.ts
|
|
2351
2714
|
import { relative } from "path";
|
|
2352
2715
|
function createApp(opts = {}) {
|
|
2353
|
-
const app = new
|
|
2716
|
+
const app = new Hono10();
|
|
2354
2717
|
if (!opts.quiet) app.use("*", logger());
|
|
2355
2718
|
app.use(
|
|
2356
2719
|
"*",
|
|
@@ -2369,6 +2732,8 @@ function createApp(opts = {}) {
|
|
|
2369
2732
|
app.route("/api/suppliers", suppliersRoute);
|
|
2370
2733
|
app.route("/api/profiles", profilesRoute);
|
|
2371
2734
|
app.route("/api/profile-presets", profilePresetsRoute);
|
|
2735
|
+
app.route("/api/product-lists", productsRoute);
|
|
2736
|
+
app.route("/api/product-list-presets", productListPresetsRoute);
|
|
2372
2737
|
app.route("/api/connections", connectionsRoute);
|
|
2373
2738
|
app.route("/api/connections", flowRoute);
|
|
2374
2739
|
app.route("/api", dataRoute);
|
|
@@ -2402,7 +2767,33 @@ async function seedDemoIfEmpty() {
|
|
|
2402
2767
|
identity: { domain: "DUNS", identity: "123456789" },
|
|
2403
2768
|
// Exercise a non-default platform profile end-to-end (Coupa: per-doc-type
|
|
2404
2769
|
// DTD versions, base64 attachments). Built-in profiles are seeded by initConfig.
|
|
2405
|
-
profileId: "coupa"
|
|
2770
|
+
profileId: "coupa",
|
|
2771
|
+
// Default addresses + end-user contact, so the OrderRequest is populated and
|
|
2772
|
+
// the address feature is exercised out of the box.
|
|
2773
|
+
shipTo: {
|
|
2774
|
+
addressId: "1001",
|
|
2775
|
+
addressIdDomain: "buyerSystemID",
|
|
2776
|
+
name: "Demo Buyer HQ \u2014 Receiving",
|
|
2777
|
+
deliverTo: "Dock 3",
|
|
2778
|
+
street: "1 Market St",
|
|
2779
|
+
city: "San Francisco",
|
|
2780
|
+
state: "CA",
|
|
2781
|
+
postalCode: "94105",
|
|
2782
|
+
countryIsoCode: "US",
|
|
2783
|
+
countryName: "United States"
|
|
2784
|
+
},
|
|
2785
|
+
billTo: {
|
|
2786
|
+
addressId: "9001",
|
|
2787
|
+
addressIdDomain: "buyerSystemID",
|
|
2788
|
+
name: "Demo Buyer Accounts Payable",
|
|
2789
|
+
street: "1 Market St",
|
|
2790
|
+
city: "San Francisco",
|
|
2791
|
+
state: "CA",
|
|
2792
|
+
postalCode: "94105",
|
|
2793
|
+
countryIsoCode: "US",
|
|
2794
|
+
countryName: "United States"
|
|
2795
|
+
},
|
|
2796
|
+
contact: { role: "endUser", name: "Jane Buyer", email: "jane.buyer@demo.example", phone: "+1 555 0100" }
|
|
2406
2797
|
});
|
|
2407
2798
|
const supplier = await createSupplier({
|
|
2408
2799
|
id: "demo-supplier",
|
|
@@ -2410,7 +2801,8 @@ async function seedDemoIfEmpty() {
|
|
|
2410
2801
|
identity: { domain: "DUNS", identity: "987654321" },
|
|
2411
2802
|
punchoutUrl: `${getPublicUrl()}/sim/demo-supplier/punchout`,
|
|
2412
2803
|
orderUrl: `${getPublicUrl()}/sim/demo-supplier/order`,
|
|
2413
|
-
|
|
2804
|
+
// Serve the built-in sample assortment (seeded by initConfig).
|
|
2805
|
+
productListIds: ["sample"]
|
|
2414
2806
|
});
|
|
2415
2807
|
await createConnection({
|
|
2416
2808
|
id: "demo",
|