punchout-simulator 0.4.0 → 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 +14 -2
- package/dist/server/cli.js +196 -30
- 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
|
)}
|
|
@@ -1747,6 +1849,19 @@ function checkSingleCurrency(lineCurrencies, totalCurrency, issues, path, allowM
|
|
|
1747
1849
|
}
|
|
1748
1850
|
return true;
|
|
1749
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
|
+
}
|
|
1750
1865
|
function checkOrderRequest(doc, ctx, issues) {
|
|
1751
1866
|
const orderReq = root(doc)?.Request?.OrderRequest;
|
|
1752
1867
|
if (!orderReq) {
|
|
@@ -1765,9 +1880,18 @@ function checkOrderRequest(doc, ctx, issues) {
|
|
|
1765
1880
|
}
|
|
1766
1881
|
if (!header2?.ShipTo) {
|
|
1767
1882
|
issues.warn("missing-shipto", "OrderRequestHeader/ShipTo is missing", "cXML/.../OrderRequestHeader/ShipTo");
|
|
1883
|
+
} else {
|
|
1884
|
+
checkAddressComplete(header2.ShipTo.Address, "ShipTo", issues);
|
|
1768
1885
|
}
|
|
1769
1886
|
if (!header2?.BillTo) {
|
|
1770
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
|
+
}
|
|
1771
1895
|
}
|
|
1772
1896
|
const items = asArray(orderReq.ItemOut);
|
|
1773
1897
|
if (items.length === 0) {
|
|
@@ -1898,7 +2022,17 @@ function buyerContext(r) {
|
|
|
1898
2022
|
attachmentEncoding: eff.attachmentEncoding,
|
|
1899
2023
|
eff,
|
|
1900
2024
|
expected: { from, to, sender, sharedSecret: connection.sharedSecret },
|
|
1901
|
-
allowMixedCurrency: supplier.allowMixedCurrency ?? false
|
|
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
|
|
1902
2036
|
};
|
|
1903
2037
|
}
|
|
1904
2038
|
function setupExtrinsics(ctx, buyerCookie) {
|
|
@@ -1931,7 +2065,8 @@ flowRoute.get("/:id/setup/preview", (c) => {
|
|
|
1931
2065
|
operation: ctx.eff.setupOperation,
|
|
1932
2066
|
dtdVersion: dtdVersionFor(ctx.eff, "SetupRequest"),
|
|
1933
2067
|
userAgent: ctx.eff.userAgent,
|
|
1934
|
-
extrinsics: setupExtrinsics(ctx, buyerCookie)
|
|
2068
|
+
extrinsics: setupExtrinsics(ctx, buyerCookie),
|
|
2069
|
+
...setupAddresses(ctx)
|
|
1935
2070
|
});
|
|
1936
2071
|
return c.json({ buyerCookie, xml, browserFormPostUrl: browserFormPostUrl() });
|
|
1937
2072
|
});
|
|
@@ -1954,7 +2089,8 @@ flowRoute.post("/:id/setup", async (c) => {
|
|
|
1954
2089
|
operation: ctx.eff.setupOperation,
|
|
1955
2090
|
dtdVersion: dtdVersionFor(ctx.eff, "SetupRequest"),
|
|
1956
2091
|
userAgent: ctx.eff.userAgent,
|
|
1957
|
-
extrinsics: setupExtrinsics(ctx, buyerCookie)
|
|
2092
|
+
extrinsics: setupExtrinsics(ctx, buyerCookie),
|
|
2093
|
+
...setupAddresses(ctx)
|
|
1958
2094
|
});
|
|
1959
2095
|
rememberSessionConnection(buyerCookie, ctx.connectionId);
|
|
1960
2096
|
const reqValidation = validateDocument(xml, { expected: ctx.expected, forceDocType: "SetupRequest" });
|
|
@@ -2016,8 +2152,12 @@ function buildOrderXml(ctx, body) {
|
|
|
2016
2152
|
currency,
|
|
2017
2153
|
total,
|
|
2018
2154
|
items,
|
|
2019
|
-
|
|
2020
|
-
|
|
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,
|
|
2021
2161
|
attachments: attMeta,
|
|
2022
2162
|
dtdVersion: dtdVersionFor(ctx.eff, "OrderRequest"),
|
|
2023
2163
|
userAgent: ctx.eff.userAgent,
|
|
@@ -2627,7 +2767,33 @@ async function seedDemoIfEmpty() {
|
|
|
2627
2767
|
identity: { domain: "DUNS", identity: "123456789" },
|
|
2628
2768
|
// Exercise a non-default platform profile end-to-end (Coupa: per-doc-type
|
|
2629
2769
|
// DTD versions, base64 attachments). Built-in profiles are seeded by initConfig.
|
|
2630
|
-
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" }
|
|
2631
2797
|
});
|
|
2632
2798
|
const supplier = await createSupplier({
|
|
2633
2799
|
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
|