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 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) and an optional **platform profile** reference (see below).
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
@@ -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
- function addressBlock(tag, a) {
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.addressId ? ` addressID="${escapeXml(a.addressId)}"` : ""}>
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
- shipTo: body.shipTo,
2020
- billTo: body.billTo,
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-BBqxMuv9.js";/*!-----------------------------------------------------------------------------
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-BBqxMuv9.js";/*!-----------------------------------------------------------------------------
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-BBqxMuv9.js";/*!-----------------------------------------------------------------------------
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-BBqxMuv9.js";/*!-----------------------------------------------------------------------------
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-BBqxMuv9.js";/*!-----------------------------------------------------------------------------
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