punchout-simulator 0.4.0 → 0.5.1
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 +14 -2
- package/dist/server/cli.js +200 -32
- package/dist/web/assets/{cssMode-CJ0pl8aC.js → cssMode-BbEnQNgt.js} +1 -1
- package/dist/web/assets/{freemarker2-2fJlciUR.js → freemarker2-B66AqaBn.js} +1 -1
- package/dist/web/assets/{handlebars-5dZcbLn3.js → handlebars-VHNJA3L3.js} +1 -1
- package/dist/web/assets/{html-DGPPwllN.js → html-DKhtq5mU.js} +1 -1
- package/dist/web/assets/{htmlMode-DtXuV7Ej.js → htmlMode-DTvp4_of.js} +1 -1
- package/dist/web/assets/{index-BBqxMuv9.js → index-DRD_fRQi.js} +139 -139
- package/dist/web/assets/{index-B2TOY2Lh.css → index-WgWCySdd.css} +1 -1
- package/dist/web/assets/{javascript-FH1B7R5g.js → javascript-C7niBpqn.js} +1 -1
- package/dist/web/assets/{jsonMode-CV67hBvi.js → jsonMode-mhDIEB_U.js} +1 -1
- package/dist/web/assets/{liquid-DdRsn7Vl.js → liquid-v92UUGTF.js} +1 -1
- package/dist/web/assets/{mdx-Dovg1GA1.js → mdx-upBohKLC.js} +1 -1
- package/dist/web/assets/{python-CzgcLU_4.js → python-BptHZKfu.js} +1 -1
- package/dist/web/assets/{razor-DCGsvo9Y.js → razor-D3-LbWWb.js} +1 -1
- package/dist/web/assets/{tsMode-gs0Re4an.js → tsMode-0LcFoT79.js} +1 -1
- package/dist/web/assets/{typescript-D_0DkNkW.js → typescript-ClLaLjem.js} +1 -1
- package/dist/web/assets/{xml-DOUouEEo.js → xml-BIefLbdX.js} +1 -1
- package/dist/web/assets/{yaml-Co8k5eYC.js → yaml-D3udJL_a.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -100,7 +100,7 @@ Document-specific:
|
|
|
100
100
|
|---|---|
|
|
101
101
|
| **PunchOutSetupResponse** | `Status` present & `200`; valid `StartPage/URL` |
|
|
102
102
|
| **PunchOutOrderMessage** (punchback) | `BuyerCookie` matches the session; header `Total`/`Money`+currency; each `ItemIn` has quantity, `SupplierPartID`, `UnitPrice`+currency, `Description`, `UnitOfMeasure`, `Classification`@domain; **single currency** (mixed-currency is an error unless the supplier allows it); **Total consistency** (sum of line totals) |
|
|
103
|
-
| **OrderRequest** | `orderID`/`orderDate`/`Total`; `ShipTo`/`BillTo`; each `ItemOut`; **single currency** (as above); **attachment `cid:` resolution** (see below) |
|
|
103
|
+
| **OrderRequest** | `orderID`/`orderDate`/`Total`; `ShipTo`/`BillTo` present **and complete** (an `addressID` or a postal Street/City — an empty block is flagged); `Contact@role`; each `ItemOut`; **single currency** (as above); **attachment `cid:` resolution** (see below) |
|
|
104
104
|
| **OrderResponse** | `Status` code present / not an error |
|
|
105
105
|
|
|
106
106
|
> Note: full cXML DTD validation would require a native validating XML parser,
|
|
@@ -180,7 +180,7 @@ src/
|
|
|
180
180
|
|
|
181
181
|
The config is normalized into five entities:
|
|
182
182
|
|
|
183
|
-
- **Buyer** — a reusable party holding its own cXML identity (the `From` credential)
|
|
183
|
+
- **Buyer** — a reusable party holding its own cXML identity (the `From` credential), an optional **platform profile** reference (see below), and optional default **ShipTo / BillTo addresses** and an end-user **Contact** (see below).
|
|
184
184
|
- **Supplier** — a reusable party holding its cXML identity (`To`) plus its **endpoints** (PunchOut URL, Order URL) and the **product lists** it serves as its Mode-B catalog. Endpoints are intrinsic to the supplier — defined once, not per relationship. An optional `allowMixedCurrency` flag relaxes the multi-currency check for this supplier (see below).
|
|
185
185
|
- **Connection** — the edge pairing one Buyer with one Supplier. It holds only what is specific to that pair: which side the tool simulates (`mode`), the `sharedSecret`, an optional per-pair Sender identity override (defaults to the buyer's identity), `deploymentMode`, and `attachmentEncoding`.
|
|
186
186
|
- **Product List** — a reusable, named set of catalog products. Assigned to one or more Suppliers; each supplier serves the **union** of its lists as its mock catalog. See below.
|
|
@@ -213,6 +213,16 @@ meaningless cross-currency sum) and validation raises a `mixed-currency` **error
|
|
|
213
213
|
genuinely handles multi-currency orders can set **`allowMixedCurrency`**, which downgrades that to a
|
|
214
214
|
non-blocking **warning** (the per-line `Money` currencies are always preserved either way).
|
|
215
215
|
|
|
216
|
+
### Addresses & contact
|
|
217
|
+
|
|
218
|
+
A **Buyer** holds default **ShipTo** / **BillTo** addresses and an end-user **Contact** (name, email,
|
|
219
|
+
phone). They pre-fill the OrderRequest (which stays fully editable as cXML before sending) and, when the
|
|
220
|
+
buyer's profile enables it, are also carried in the **PunchOutSetupRequest** (Ariba-style). Each address
|
|
221
|
+
supports a postal block and/or an `addressID` (+ `addressIDDomain`) reference; the **profile's
|
|
222
|
+
`addressMode`** decides which is emitted (`full` / `id-only` / `both`) — matching how platforms differ
|
|
223
|
+
(Jaggaer/Workday send full postal, SAP is plant-code/`addressID`-centric, Ariba sends both and includes
|
|
224
|
+
ShipTo in setup). An emitted `ShipTo`/`BillTo` with neither an `addressID` nor a Street/City is flagged.
|
|
225
|
+
|
|
216
226
|
### Simulating different procurement platforms (Buyer profiles)
|
|
217
227
|
|
|
218
228
|
Real procurement systems emit cXML differently. A **Profile** captures a platform's
|
|
@@ -227,6 +237,8 @@ Ariba, Coupa, Jaggaer, Oracle, SAP, or Workday:
|
|
|
227
237
|
| `attachmentEncoding` | Default `Content-Transfer-Encoding` for OrderRequest attachments (`binary`/`base64`). |
|
|
228
238
|
| `cartReturnTransport` | How the punchback is returned in Mode B: `cxml-urlencoded`, `cxml-base64`, or `raw`. |
|
|
229
239
|
| `extrinsics` | `<Extrinsic>` templates injected into the setup/order documents (values may use `${buyerCookie}` / `${orderId}`). |
|
|
240
|
+
| `addressMode` | How `ShipTo`/`BillTo`/`Contact` are emitted: `full` (PostalAddress), `id-only` (just `addressID`), or `both`. |
|
|
241
|
+
| `shipToInSetup` / `contactInSetup` | Carry the buyer's `ShipTo` / `Contact` already in the **PunchOutSetupRequest** (Ariba-style), so the supplier can price/personalize per ship-to. |
|
|
230
242
|
|
|
231
243
|
Built-in presets (Ariba, Coupa, Jaggaer, Oracle, SAP, Workday, Generic) ship out of the box
|
|
232
244
|
and can be **loaded into the editor** as a starting point, then customized. A Profile holds
|
package/dist/server/cli.js
CHANGED
|
@@ -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 = {
|
|
@@ -239,6 +263,7 @@ async function initConfig() {
|
|
|
239
263
|
db.data.profiles ||= [];
|
|
240
264
|
db.data.productLists ||= [];
|
|
241
265
|
seedBuiltinProfiles(db.data, now());
|
|
266
|
+
migrateProfiles(db.data);
|
|
242
267
|
seedBuiltinProductLists(db.data, now());
|
|
243
268
|
migrateLegacy(db.data);
|
|
244
269
|
migrateInlineCatalogs(db.data);
|
|
@@ -397,7 +422,10 @@ function effectiveProfile(connection, buyer) {
|
|
|
397
422
|
// Connection attachmentEncoding is concrete by construction → explicit override.
|
|
398
423
|
attachmentEncoding: connection.attachmentEncoding ?? p.attachmentEncoding,
|
|
399
424
|
cartReturnTransport: p.cartReturnTransport,
|
|
400
|
-
extrinsics: p.extrinsics
|
|
425
|
+
extrinsics: p.extrinsics,
|
|
426
|
+
addressMode: p.addressMode,
|
|
427
|
+
shipToInSetup: p.shipToInSetup,
|
|
428
|
+
contactInSetup: p.contactInSetup
|
|
401
429
|
};
|
|
402
430
|
}
|
|
403
431
|
function dtdVersionFor(eff, docType) {
|
|
@@ -517,6 +545,15 @@ function migrateInlineCatalogs(data) {
|
|
|
517
545
|
delete supplier.catalog;
|
|
518
546
|
}
|
|
519
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
|
+
}
|
|
520
557
|
function migrateClassifications(data) {
|
|
521
558
|
for (const list of data.productLists) {
|
|
522
559
|
for (const item of list.items) {
|
|
@@ -601,12 +638,40 @@ var cred = (c) => ({
|
|
|
601
638
|
domain: String(c?.domain ?? ""),
|
|
602
639
|
identity: String(c?.identity ?? "")
|
|
603
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
|
+
}
|
|
604
666
|
var buyersRoute = new Hono2();
|
|
605
667
|
function normalizeBuyer(body) {
|
|
606
668
|
return {
|
|
607
669
|
name: String(body?.name ?? "Untitled buyer"),
|
|
608
670
|
identity: cred(body?.identity),
|
|
609
|
-
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)
|
|
610
675
|
};
|
|
611
676
|
}
|
|
612
677
|
buyersRoute.get("/", (c) => c.json(listBuyers()));
|
|
@@ -700,6 +765,7 @@ function normalizeProfile(body) {
|
|
|
700
765
|
const cartReturnTransport = ["cxml-urlencoded", "cxml-base64", "raw"].includes(
|
|
701
766
|
body?.cartReturnTransport
|
|
702
767
|
) ? body.cartReturnTransport : "cxml-urlencoded";
|
|
768
|
+
const addressMode = ["id-only", "full", "both"].includes(body?.addressMode) ? body.addressMode : "full";
|
|
703
769
|
return {
|
|
704
770
|
name: String(body?.name ?? "Untitled profile"),
|
|
705
771
|
platform: body?.platform ? String(body.platform) : void 0,
|
|
@@ -708,7 +774,10 @@ function normalizeProfile(body) {
|
|
|
708
774
|
setupOperation,
|
|
709
775
|
attachmentEncoding,
|
|
710
776
|
cartReturnTransport,
|
|
711
|
-
extrinsics: normalizeExtrinsics(body?.extrinsics)
|
|
777
|
+
extrinsics: normalizeExtrinsics(body?.extrinsics),
|
|
778
|
+
addressMode,
|
|
779
|
+
shipToInSetup: Boolean(body?.shipToInSetup),
|
|
780
|
+
contactInSetup: Boolean(body?.contactInSetup)
|
|
712
781
|
// `builtin` is never set from the wire — only code-seeded presets carry it.
|
|
713
782
|
};
|
|
714
783
|
}
|
|
@@ -1163,6 +1232,11 @@ function extrinsicBlock(items, indent) {
|
|
|
1163
1232
|
function buildSetupRequest(o) {
|
|
1164
1233
|
const lang = o.lang ?? "en-US";
|
|
1165
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, " ")}` : "";
|
|
1166
1240
|
const inner = `${header({
|
|
1167
1241
|
from: o.from,
|
|
1168
1242
|
to: o.to,
|
|
@@ -1175,26 +1249,51 @@ function buildSetupRequest(o) {
|
|
|
1175
1249
|
<BuyerCookie>${escapeXml(o.buyerCookie)}</BuyerCookie>
|
|
1176
1250
|
<BrowserFormPost>
|
|
1177
1251
|
<URL>${escapeXml(o.browserFormPostUrl)}</URL>
|
|
1178
|
-
</BrowserFormPost>${extrinsicBlock(o.extrinsics, " ")}
|
|
1252
|
+
</BrowserFormPost>${extrinsicBlock(o.extrinsics, " ")}${shipTo}${contact2}
|
|
1179
1253
|
</PunchOutSetupRequest>
|
|
1180
1254
|
</Request>`;
|
|
1181
1255
|
return envelope(o.payloadId, o.timestamp, lang, inner, o.dtdVersion);
|
|
1182
1256
|
}
|
|
1183
|
-
|
|
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, " ")}` : "";
|
|
1184
1284
|
return ` <${tag}>
|
|
1185
|
-
<Address${a
|
|
1186
|
-
<Name xml:lang="en">${escapeXml(a.name ?? "")}</Name
|
|
1187
|
-
<PostalAddress>
|
|
1188
|
-
${a.deliverTo ? ` <DeliverTo>${escapeXml(a.deliverTo)}</DeliverTo>
|
|
1189
|
-
` : ""} <Street>${escapeXml(a.street ?? "")}</Street>
|
|
1190
|
-
<City>${escapeXml(a.city ?? "")}</City>
|
|
1191
|
-
<State>${escapeXml(a.state ?? "")}</State>
|
|
1192
|
-
<PostalCode>${escapeXml(a.postalCode ?? "")}</PostalCode>
|
|
1193
|
-
<Country isoCountryCode="${escapeXml(a.countryIsoCode ?? "US")}">${escapeXml(a.countryName ?? "United States")}</Country>
|
|
1194
|
-
</PostalAddress>
|
|
1285
|
+
<Address${idAttrs(a, mode)}>
|
|
1286
|
+
<Name xml:lang="en">${escapeXml(a.name ?? "")}</Name>${postal}${contactInfo(a, " ")}
|
|
1195
1287
|
</Address>
|
|
1196
1288
|
</${tag}>`;
|
|
1197
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
|
+
}
|
|
1198
1297
|
function commentsWithAttachments(cids, indent) {
|
|
1199
1298
|
if (cids.length === 0) return "";
|
|
1200
1299
|
const atts = cids.map(
|
|
@@ -1234,6 +1333,9 @@ ${classificationBlock(it, " ")}${it.manufacturerPartId ? `
|
|
|
1234
1333
|
</ItemDetail>
|
|
1235
1334
|
</ItemOut>`;
|
|
1236
1335
|
}).join("\n");
|
|
1336
|
+
const mode = o.addressMode ?? "full";
|
|
1337
|
+
const contact2 = o.contact ? `
|
|
1338
|
+
${contactBlock(o.contact, mode, " ")}` : "";
|
|
1237
1339
|
const inner = `${header({
|
|
1238
1340
|
from: o.from,
|
|
1239
1341
|
to: o.to,
|
|
@@ -1249,8 +1351,8 @@ ${classificationBlock(it, " ")}${it.manufacturerPartId ? `
|
|
|
1249
1351
|
<Total>
|
|
1250
1352
|
<Money currency="${escapeXml(o.currency)}">${escapeXml(o.total)}</Money>
|
|
1251
1353
|
</Total>
|
|
1252
|
-
${addressBlock("ShipTo", o.shipTo ?? {})}
|
|
1253
|
-
${addressBlock("BillTo", o.billTo ?? {})}${commentsWithAttachments(orderLevelCids, " ")}${extrinsicBlock(
|
|
1354
|
+
${addressBlock("ShipTo", o.shipTo ?? {}, mode)}
|
|
1355
|
+
${addressBlock("BillTo", o.billTo ?? {}, mode)}${contact2}${commentsWithAttachments(orderLevelCids, " ")}${extrinsicBlock(
|
|
1254
1356
|
o.extrinsics,
|
|
1255
1357
|
" "
|
|
1256
1358
|
)}
|
|
@@ -1548,7 +1650,7 @@ function credKnown(c, exp) {
|
|
|
1548
1650
|
if (!c) return false;
|
|
1549
1651
|
return [exp.from, exp.to, exp.sender].some((k) => credEq(c, k));
|
|
1550
1652
|
}
|
|
1551
|
-
function checkGeneral(doc, ctx, issues) {
|
|
1653
|
+
function checkGeneral(doc, ctx, issues, docType) {
|
|
1552
1654
|
const payloadId = getPayloadId(doc);
|
|
1553
1655
|
if (!payloadId) {
|
|
1554
1656
|
issues.error("missing-payloadID", "cXML/@payloadID is missing", "cXML/@payloadID");
|
|
@@ -1563,6 +1665,8 @@ function checkGeneral(doc, ctx, issues) {
|
|
|
1563
1665
|
issues.error("missing-timestamp", "cXML/@timestamp is missing", "cXML/@timestamp");
|
|
1564
1666
|
}
|
|
1565
1667
|
if (!ctx.expected) return;
|
|
1668
|
+
const carriesHeader = docType === "SetupRequest" || docType === "OrderRequest" || docType === "PunchOutOrderMessage";
|
|
1669
|
+
if (!carriesHeader) return;
|
|
1566
1670
|
const exp = ctx.expected;
|
|
1567
1671
|
const creds = getHeaderCredentials(doc);
|
|
1568
1672
|
for (const [name, c] of [
|
|
@@ -1747,6 +1851,19 @@ function checkSingleCurrency(lineCurrencies, totalCurrency, issues, path, allowM
|
|
|
1747
1851
|
}
|
|
1748
1852
|
return true;
|
|
1749
1853
|
}
|
|
1854
|
+
function checkAddressComplete(addr, label, issues) {
|
|
1855
|
+
if (addr == null) return;
|
|
1856
|
+
const hasId = !!attr(addr, "addressID");
|
|
1857
|
+
const postal = addr.PostalAddress;
|
|
1858
|
+
const hasPostal2 = !!(text(postal?.Street) || text(postal?.City));
|
|
1859
|
+
if (!hasId && !hasPostal2) {
|
|
1860
|
+
issues.warn(
|
|
1861
|
+
`${label.toLowerCase()}-incomplete`,
|
|
1862
|
+
`${label}/Address has neither an addressID nor a Street/City \u2014 the address is empty`,
|
|
1863
|
+
`cXML/.../OrderRequestHeader/${label}/Address`
|
|
1864
|
+
);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1750
1867
|
function checkOrderRequest(doc, ctx, issues) {
|
|
1751
1868
|
const orderReq = root(doc)?.Request?.OrderRequest;
|
|
1752
1869
|
if (!orderReq) {
|
|
@@ -1765,9 +1882,18 @@ function checkOrderRequest(doc, ctx, issues) {
|
|
|
1765
1882
|
}
|
|
1766
1883
|
if (!header2?.ShipTo) {
|
|
1767
1884
|
issues.warn("missing-shipto", "OrderRequestHeader/ShipTo is missing", "cXML/.../OrderRequestHeader/ShipTo");
|
|
1885
|
+
} else {
|
|
1886
|
+
checkAddressComplete(header2.ShipTo.Address, "ShipTo", issues);
|
|
1768
1887
|
}
|
|
1769
1888
|
if (!header2?.BillTo) {
|
|
1770
1889
|
issues.warn("missing-billto", "OrderRequestHeader/BillTo is missing", "cXML/.../OrderRequestHeader/BillTo");
|
|
1890
|
+
} else {
|
|
1891
|
+
checkAddressComplete(header2.BillTo.Address, "BillTo", issues);
|
|
1892
|
+
}
|
|
1893
|
+
for (const contact2 of asArray(header2?.Contact)) {
|
|
1894
|
+
if (!attr(contact2, "role")) {
|
|
1895
|
+
issues.warn("contact-missing-role", "OrderRequestHeader/Contact is missing @role", "cXML/.../OrderRequestHeader/Contact");
|
|
1896
|
+
}
|
|
1771
1897
|
}
|
|
1772
1898
|
const items = asArray(orderReq.ItemOut);
|
|
1773
1899
|
if (items.length === 0) {
|
|
@@ -1843,7 +1969,7 @@ function validateDocument(raw, ctx = {}) {
|
|
|
1843
1969
|
return { docType: "Unknown", wellFormed: false, ok: false, issues: issues.list };
|
|
1844
1970
|
}
|
|
1845
1971
|
const docType = ctx.forceDocType ?? getDocType(doc);
|
|
1846
|
-
checkGeneral(doc, ctx, issues);
|
|
1972
|
+
checkGeneral(doc, ctx, issues, docType);
|
|
1847
1973
|
switch (docType) {
|
|
1848
1974
|
case "SetupRequest":
|
|
1849
1975
|
checkSharedSecret(doc, ctx, issues);
|
|
@@ -1898,7 +2024,17 @@ function buyerContext(r) {
|
|
|
1898
2024
|
attachmentEncoding: eff.attachmentEncoding,
|
|
1899
2025
|
eff,
|
|
1900
2026
|
expected: { from, to, sender, sharedSecret: connection.sharedSecret },
|
|
1901
|
-
allowMixedCurrency: supplier.allowMixedCurrency ?? false
|
|
2027
|
+
allowMixedCurrency: supplier.allowMixedCurrency ?? false,
|
|
2028
|
+
shipTo: buyer.shipTo,
|
|
2029
|
+
billTo: buyer.billTo,
|
|
2030
|
+
contact: buyer.contact
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
function setupAddresses(ctx) {
|
|
2034
|
+
return {
|
|
2035
|
+
shipTo: ctx.eff.shipToInSetup ? ctx.shipTo : void 0,
|
|
2036
|
+
contact: ctx.eff.contactInSetup ? ctx.contact : void 0,
|
|
2037
|
+
addressMode: ctx.eff.addressMode
|
|
1902
2038
|
};
|
|
1903
2039
|
}
|
|
1904
2040
|
function setupExtrinsics(ctx, buyerCookie) {
|
|
@@ -1931,7 +2067,8 @@ flowRoute.get("/:id/setup/preview", (c) => {
|
|
|
1931
2067
|
operation: ctx.eff.setupOperation,
|
|
1932
2068
|
dtdVersion: dtdVersionFor(ctx.eff, "SetupRequest"),
|
|
1933
2069
|
userAgent: ctx.eff.userAgent,
|
|
1934
|
-
extrinsics: setupExtrinsics(ctx, buyerCookie)
|
|
2070
|
+
extrinsics: setupExtrinsics(ctx, buyerCookie),
|
|
2071
|
+
...setupAddresses(ctx)
|
|
1935
2072
|
});
|
|
1936
2073
|
return c.json({ buyerCookie, xml, browserFormPostUrl: browserFormPostUrl() });
|
|
1937
2074
|
});
|
|
@@ -1954,7 +2091,8 @@ flowRoute.post("/:id/setup", async (c) => {
|
|
|
1954
2091
|
operation: ctx.eff.setupOperation,
|
|
1955
2092
|
dtdVersion: dtdVersionFor(ctx.eff, "SetupRequest"),
|
|
1956
2093
|
userAgent: ctx.eff.userAgent,
|
|
1957
|
-
extrinsics: setupExtrinsics(ctx, buyerCookie)
|
|
2094
|
+
extrinsics: setupExtrinsics(ctx, buyerCookie),
|
|
2095
|
+
...setupAddresses(ctx)
|
|
1958
2096
|
});
|
|
1959
2097
|
rememberSessionConnection(buyerCookie, ctx.connectionId);
|
|
1960
2098
|
const reqValidation = validateDocument(xml, { expected: ctx.expected, forceDocType: "SetupRequest" });
|
|
@@ -2016,8 +2154,12 @@ function buildOrderXml(ctx, body) {
|
|
|
2016
2154
|
currency,
|
|
2017
2155
|
total,
|
|
2018
2156
|
items,
|
|
2019
|
-
|
|
2020
|
-
|
|
2157
|
+
// Order addresses default to the buyer's configured defaults when the body
|
|
2158
|
+
// doesn't override them (the UI pre-fills from the buyer, editable per order).
|
|
2159
|
+
shipTo: body.shipTo ?? ctx.shipTo,
|
|
2160
|
+
billTo: body.billTo ?? ctx.billTo,
|
|
2161
|
+
contact: body.contact ?? ctx.contact,
|
|
2162
|
+
addressMode: ctx.eff.addressMode,
|
|
2021
2163
|
attachments: attMeta,
|
|
2022
2164
|
dtdVersion: dtdVersionFor(ctx.eff, "OrderRequest"),
|
|
2023
2165
|
userAgent: ctx.eff.userAgent,
|
|
@@ -2627,7 +2769,33 @@ async function seedDemoIfEmpty() {
|
|
|
2627
2769
|
identity: { domain: "DUNS", identity: "123456789" },
|
|
2628
2770
|
// Exercise a non-default platform profile end-to-end (Coupa: per-doc-type
|
|
2629
2771
|
// DTD versions, base64 attachments). Built-in profiles are seeded by initConfig.
|
|
2630
|
-
profileId: "coupa"
|
|
2772
|
+
profileId: "coupa",
|
|
2773
|
+
// Default addresses + end-user contact, so the OrderRequest is populated and
|
|
2774
|
+
// the address feature is exercised out of the box.
|
|
2775
|
+
shipTo: {
|
|
2776
|
+
addressId: "1001",
|
|
2777
|
+
addressIdDomain: "buyerSystemID",
|
|
2778
|
+
name: "Demo Buyer HQ \u2014 Receiving",
|
|
2779
|
+
deliverTo: "Dock 3",
|
|
2780
|
+
street: "1 Market St",
|
|
2781
|
+
city: "San Francisco",
|
|
2782
|
+
state: "CA",
|
|
2783
|
+
postalCode: "94105",
|
|
2784
|
+
countryIsoCode: "US",
|
|
2785
|
+
countryName: "United States"
|
|
2786
|
+
},
|
|
2787
|
+
billTo: {
|
|
2788
|
+
addressId: "9001",
|
|
2789
|
+
addressIdDomain: "buyerSystemID",
|
|
2790
|
+
name: "Demo Buyer Accounts Payable",
|
|
2791
|
+
street: "1 Market St",
|
|
2792
|
+
city: "San Francisco",
|
|
2793
|
+
state: "CA",
|
|
2794
|
+
postalCode: "94105",
|
|
2795
|
+
countryIsoCode: "US",
|
|
2796
|
+
countryName: "United States"
|
|
2797
|
+
},
|
|
2798
|
+
contact: { role: "endUser", name: "Jane Buyer", email: "jane.buyer@demo.example", phone: "+1 555 0100" }
|
|
2631
2799
|
});
|
|
2632
2800
|
const supplier = await createSupplier({
|
|
2633
2801
|
id: "demo-supplier",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{m as et}from"./index-
|
|
1
|
+
import{m as et}from"./index-DRD_fRQi.js";/*!-----------------------------------------------------------------------------
|
|
2
2
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
3
|
* Version: 0.52.2(404545bded1df6ffa41ea0af4e8ddb219018c6c1)
|
|
4
4
|
* Released under the MIT license
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{m as f}from"./index-
|
|
1
|
+
import{m as f}from"./index-DRD_fRQi.js";/*!-----------------------------------------------------------------------------
|
|
2
2
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
3
|
* Version: 0.52.2(404545bded1df6ffa41ea0af4e8ddb219018c6c1)
|
|
4
4
|
* Released under the MIT license
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{m as l}from"./index-
|
|
1
|
+
import{m as l}from"./index-DRD_fRQi.js";/*!-----------------------------------------------------------------------------
|
|
2
2
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
3
|
* Version: 0.52.2(404545bded1df6ffa41ea0af4e8ddb219018c6c1)
|
|
4
4
|
* Released under the MIT license
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{m as s}from"./index-
|
|
1
|
+
import{m as s}from"./index-DRD_fRQi.js";/*!-----------------------------------------------------------------------------
|
|
2
2
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
3
|
* Version: 0.52.2(404545bded1df6ffa41ea0af4e8ddb219018c6c1)
|
|
4
4
|
* Released under the MIT license
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{m as lt}from"./index-
|
|
1
|
+
import{m as lt}from"./index-DRD_fRQi.js";/*!-----------------------------------------------------------------------------
|
|
2
2
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
3
|
* Version: 0.52.2(404545bded1df6ffa41ea0af4e8ddb219018c6c1)
|
|
4
4
|
* Released under the MIT license
|