punchout-simulator 0.5.1 → 0.6.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 +40 -3
- package/dist/server/cli.js +138 -25
- package/dist/web/assets/{cssMode-BbEnQNgt.js → cssMode-DipgJtey.js} +1 -1
- package/dist/web/assets/{freemarker2-B66AqaBn.js → freemarker2-Broesno_.js} +1 -1
- package/dist/web/assets/{handlebars-VHNJA3L3.js → handlebars-CjP6B5LA.js} +1 -1
- package/dist/web/assets/{html-DKhtq5mU.js → html-DRw6NWlS.js} +1 -1
- package/dist/web/assets/{htmlMode-DTvp4_of.js → htmlMode-CRd2lx43.js} +1 -1
- package/dist/web/assets/{index-WgWCySdd.css → index-BZV9nM3Z.css} +1 -1
- package/dist/web/assets/{index-DRD_fRQi.js → index-ByIxIbJu.js} +206 -206
- package/dist/web/assets/{javascript-C7niBpqn.js → javascript-C_i2lhIv.js} +1 -1
- package/dist/web/assets/{jsonMode-mhDIEB_U.js → jsonMode-BgryI0EY.js} +1 -1
- package/dist/web/assets/{liquid-v92UUGTF.js → liquid-7FQOG1B6.js} +1 -1
- package/dist/web/assets/{mdx-upBohKLC.js → mdx-qBzkPJqp.js} +1 -1
- package/dist/web/assets/{python-BptHZKfu.js → python-B4DngJbm.js} +1 -1
- package/dist/web/assets/{razor-D3-LbWWb.js → razor-C_lSlak7.js} +1 -1
- package/dist/web/assets/{tsMode-0LcFoT79.js → tsMode-Xnz0Jk77.js} +1 -1
- package/dist/web/assets/{typescript-ClLaLjem.js → typescript-d2XcegVU.js} +1 -1
- package/dist/web/assets/{xml-BIefLbdX.js → xml-D_x-LgOL.js} +1 -1
- package/dist/web/assets/{yaml-D3udJL_a.js → yaml-gU-APWSC.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,6 +18,11 @@ It is role-neutral and runs in two modes (mirror images of each other):
|
|
|
18
18
|
|
|
19
19
|
Protocol scope: **cXML only**.
|
|
20
20
|
|
|
21
|
+
> **New to PunchOut, or want the full picture?** See the in-depth reports in
|
|
22
|
+
> [`docs/`](docs/): the [PunchOut business primer](docs/punchout-business-primer_en.html),
|
|
23
|
+
> the [Architecture reference](docs/architecture_en.html), and the
|
|
24
|
+
> [Operations & usage guide](docs/operations-guide_en.html).
|
|
25
|
+
|
|
21
26
|
---
|
|
22
27
|
|
|
23
28
|
## Quick start
|
|
@@ -30,15 +35,17 @@ That's it — no install, no build. It boots a local server, opens your browser,
|
|
|
30
35
|
and seeds a **built-in demo**: a virtual buyer wired to a built-in mock supplier,
|
|
31
36
|
so you can run the entire roundtrip immediately:
|
|
32
37
|
|
|
33
|
-
1.
|
|
38
|
+
1. On the **Sessions** tab, click **+ New session**, pick the **Demo Buyer → Demo
|
|
39
|
+
Supplier** connection and the **create** operation.
|
|
34
40
|
2. **Send SetupRequest** → the mock supplier replies with a StartPage.
|
|
35
41
|
3. **Open the catalog**, set quantities, **return the cart** — the punchback
|
|
36
42
|
lands back in the app live.
|
|
37
43
|
4. **Build the OrderRequest**, edit the cXML if you want (tweak `<Comments>`,
|
|
38
44
|
addresses, attachment refs), optionally attach files at the **order or item
|
|
39
45
|
level**, optionally flip on the **dangling-`cid` test**, then **send it** and
|
|
40
|
-
inspect the supplier's response.
|
|
41
|
-
|
|
46
|
+
inspect the supplier's response. (A **Validate** button lints the cXML before
|
|
47
|
+
you send.)
|
|
48
|
+
5. If validation fails, **edit and re-send** — the session keeps its log and cart.
|
|
42
49
|
|
|
43
50
|
Every document — inbound and outbound — is validated and logged.
|
|
44
51
|
|
|
@@ -142,6 +149,27 @@ receiver detects the missing attachment. `<Comments>` (and therefore
|
|
|
142
149
|
|
|
143
150
|
---
|
|
144
151
|
|
|
152
|
+
## Sessions
|
|
153
|
+
|
|
154
|
+
A **session** is one PunchOut conversation, keyed by its `BuyerCookie`:
|
|
155
|
+
SetupRequest → catalog → cart → OrderRequest → OrderResponse. The **Sessions** tab
|
|
156
|
+
(under **Run**, separate from the **Configure** sections) is the workspace:
|
|
157
|
+
|
|
158
|
+
- A live **session list** — Mode-A sessions you start *and* Mode-B sessions an
|
|
159
|
+
external buyer initiates against the tool's endpoints (those show as **inbound**).
|
|
160
|
+
- A per-session **message log** (each session keeps its own; no global firehose).
|
|
161
|
+
- **+ New session** picks a connection and the SetupRequest **operation**:
|
|
162
|
+
- **create** — a fresh, empty cart (the default).
|
|
163
|
+
- **edit** — reopen a previous session's returned cart for modification; its
|
|
164
|
+
items are sent as `ItemOut` inside the SetupRequest (`operation="edit"`).
|
|
165
|
+
- **inspect** — view a previously ordered item read-only (`operation="inspect"`),
|
|
166
|
+
carrying that item (or the whole source cart) as `ItemOut`.
|
|
167
|
+
|
|
168
|
+
Connections/Buyers/Suppliers/Products/Profiles remain under **Configure** — you set
|
|
169
|
+
them up there, then run them from **Sessions**.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
145
173
|
## Network reachability
|
|
146
174
|
|
|
147
175
|
- **Mode A rarely needs a tunnel.** The punchback is a browser auto-submit from
|
|
@@ -176,6 +204,15 @@ src/
|
|
|
176
204
|
└─ cxml/ build · parse · validate · multipart · types
|
|
177
205
|
```
|
|
178
206
|
|
|
207
|
+
For a deeper treatment — component breakdown, the full data model, and Mode A /
|
|
208
|
+
Mode B sequence diagrams — see the
|
|
209
|
+
[**Architecture reference**](docs/architecture_en.html) in [`docs/`](docs/).
|
|
210
|
+
The same folder holds a [**PunchOut business primer**](docs/punchout-business-primer_en.html)
|
|
211
|
+
(the business process and where this tool fits) and an
|
|
212
|
+
[**Operations & usage guide**](docs/operations-guide_en.html) (sessions,
|
|
213
|
+
operations, profiles, product lists, validation). The reports are self-contained
|
|
214
|
+
HTML; their canonical Markdown sources sit alongside them.
|
|
215
|
+
|
|
179
216
|
### Data model
|
|
180
217
|
|
|
181
218
|
The config is normalized into five entities:
|
package/dist/server/cli.js
CHANGED
|
@@ -869,7 +869,8 @@ import { Hono as Hono5 } from "hono";
|
|
|
869
869
|
import { nanoid as nanoid5 } from "nanoid";
|
|
870
870
|
|
|
871
871
|
// src/server/store/log.ts
|
|
872
|
-
import { appendFileSync, existsSync, readdirSync, readFileSync } from "fs";
|
|
872
|
+
import { appendFileSync, existsSync, readdirSync, readFileSync, rmSync } from "fs";
|
|
873
|
+
import { resolve as resolve2, sep } from "path";
|
|
873
874
|
import { nanoid as nanoid2 } from "nanoid";
|
|
874
875
|
|
|
875
876
|
// src/server/bus.ts
|
|
@@ -917,6 +918,13 @@ function readSession(sessionId) {
|
|
|
917
918
|
}
|
|
918
919
|
}).filter((r) => r !== null);
|
|
919
920
|
}
|
|
921
|
+
function deleteSession(sessionId) {
|
|
922
|
+
const file = resolve2(sessionFile(sessionId));
|
|
923
|
+
if (!file.startsWith(resolve2(sessionsDir()) + sep)) return false;
|
|
924
|
+
if (!existsSync(file)) return false;
|
|
925
|
+
rmSync(file);
|
|
926
|
+
return true;
|
|
927
|
+
}
|
|
920
928
|
function listSessions() {
|
|
921
929
|
ensureDirs();
|
|
922
930
|
const files = readdirSync(sessionsDir()).filter((f) => f.endsWith(".jsonl"));
|
|
@@ -925,6 +933,8 @@ function listSessions() {
|
|
|
925
933
|
const sessionId = file.replace(/\.jsonl$/, "");
|
|
926
934
|
const records = readSession(sessionId);
|
|
927
935
|
if (records.length === 0) continue;
|
|
936
|
+
const setup = records.find((r) => r.docType === "SetupRequest");
|
|
937
|
+
const operation = setup ? /<PunchOutSetupRequest[^>]*\boperation="([^"]+)"/.exec(setup.body)?.[1] : void 0;
|
|
928
938
|
summaries.push({
|
|
929
939
|
sessionId: records[0].sessionId ?? sessionId,
|
|
930
940
|
connectionId: records[0].connectionId,
|
|
@@ -932,7 +942,8 @@ function listSessions() {
|
|
|
932
942
|
firstTs: records[0].ts,
|
|
933
943
|
lastTs: records[records.length - 1].ts,
|
|
934
944
|
docTypes: [...new Set(records.map((r) => r.docType))],
|
|
935
|
-
hasErrors: records.some((r) => r.validation && !r.validation.ok)
|
|
945
|
+
hasErrors: records.some((r) => r.validation && !r.validation.ok),
|
|
946
|
+
operation
|
|
936
947
|
});
|
|
937
948
|
}
|
|
938
949
|
return summaries.sort((a, b) => (b.lastTs ?? "").localeCompare(a.lastTs ?? ""));
|
|
@@ -944,7 +955,7 @@ function readAllRecent(limit = 200) {
|
|
|
944
955
|
// src/server/store/attachments.ts
|
|
945
956
|
import { createHash } from "crypto";
|
|
946
957
|
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
947
|
-
import { resolve as
|
|
958
|
+
import { resolve as resolve3 } from "path";
|
|
948
959
|
|
|
949
960
|
// src/server/cxml/multipart.ts
|
|
950
961
|
import { nanoid as nanoid3 } from "nanoid";
|
|
@@ -1082,7 +1093,7 @@ function findHeaderBodySplit(buf) {
|
|
|
1082
1093
|
function saveAttachment(data, meta) {
|
|
1083
1094
|
ensureDirs();
|
|
1084
1095
|
const hash = createHash("sha256").update(data).digest("hex");
|
|
1085
|
-
const path =
|
|
1096
|
+
const path = resolve3(attachmentsDir(), hash);
|
|
1086
1097
|
if (!existsSync2(path)) writeFileSync(path, data, { mode: 384 });
|
|
1087
1098
|
return {
|
|
1088
1099
|
contentId: normalizeContentId(meta.contentId),
|
|
@@ -1096,7 +1107,7 @@ function saveAttachment(data, meta) {
|
|
|
1096
1107
|
function readAttachment(hash) {
|
|
1097
1108
|
const safe = hash.replace(/[^a-f0-9]/gi, "");
|
|
1098
1109
|
if (!safe) return void 0;
|
|
1099
|
-
const path =
|
|
1110
|
+
const path = resolve3(attachmentsDir(), safe);
|
|
1100
1111
|
if (!existsSync2(path)) return void 0;
|
|
1101
1112
|
return readFileSync2(path);
|
|
1102
1113
|
}
|
|
@@ -1116,6 +1127,10 @@ function rememberSessionConnection(sessionId, connectionId) {
|
|
|
1116
1127
|
function connectionForSession(sessionId) {
|
|
1117
1128
|
return connectionBySession.get(sessionId);
|
|
1118
1129
|
}
|
|
1130
|
+
function forgetSession(sessionId) {
|
|
1131
|
+
carts.delete(sessionId);
|
|
1132
|
+
connectionBySession.delete(sessionId);
|
|
1133
|
+
}
|
|
1119
1134
|
|
|
1120
1135
|
// src/server/http.ts
|
|
1121
1136
|
function assertSafeOutboundUrl(url) {
|
|
@@ -1229,6 +1244,22 @@ function extrinsicBlock(items, indent) {
|
|
|
1229
1244
|
if (!items || items.length === 0) return "";
|
|
1230
1245
|
return "\n" + items.map((e) => `${indent}<Extrinsic name="${escapeXml(e.name)}">${escapeXml(e.value)}</Extrinsic>`).join("\n");
|
|
1231
1246
|
}
|
|
1247
|
+
function setupItemOut(it, idx) {
|
|
1248
|
+
return ` <ItemOut quantity="${escapeXml(it.quantity)}" lineNumber="${idx}">
|
|
1249
|
+
<ItemID>
|
|
1250
|
+
<SupplierPartID>${escapeXml(it.supplierPartId ?? "")}</SupplierPartID>${it.supplierPartAuxiliaryId ? `
|
|
1251
|
+
<SupplierPartAuxiliaryID>${escapeXml(it.supplierPartAuxiliaryId)}</SupplierPartAuxiliaryID>` : ""}
|
|
1252
|
+
</ItemID>
|
|
1253
|
+
<ItemDetail>
|
|
1254
|
+
<UnitPrice>
|
|
1255
|
+
<Money currency="${escapeXml(it.currency ?? "USD")}">${escapeXml(it.unitPriceAmount ?? 0)}</Money>
|
|
1256
|
+
</UnitPrice>
|
|
1257
|
+
<Description xml:lang="en">${escapeXml(it.description ?? "")}</Description>
|
|
1258
|
+
<UnitOfMeasure>${escapeXml(it.uom ?? "EA")}</UnitOfMeasure>
|
|
1259
|
+
${classificationBlock(it, " ")}
|
|
1260
|
+
</ItemDetail>
|
|
1261
|
+
</ItemOut>`;
|
|
1262
|
+
}
|
|
1232
1263
|
function buildSetupRequest(o) {
|
|
1233
1264
|
const lang = o.lang ?? "en-US";
|
|
1234
1265
|
const deployment = o.deploymentMode ? ` deploymentMode="${escapeXml(o.deploymentMode)}"` : "";
|
|
@@ -1237,6 +1268,8 @@ function buildSetupRequest(o) {
|
|
|
1237
1268
|
${addressBlock("ShipTo", o.shipTo, mode)}` : "";
|
|
1238
1269
|
const contact2 = o.contact ? `
|
|
1239
1270
|
${contactBlock(o.contact, mode, " ")}` : "";
|
|
1271
|
+
const items = o.items && o.items.length > 0 ? `
|
|
1272
|
+
${o.items.map((it, i) => setupItemOut(it, i + 1)).join("\n")}` : "";
|
|
1240
1273
|
const inner = `${header({
|
|
1241
1274
|
from: o.from,
|
|
1242
1275
|
to: o.to,
|
|
@@ -1249,7 +1282,7 @@ ${contactBlock(o.contact, mode, " ")}` : "";
|
|
|
1249
1282
|
<BuyerCookie>${escapeXml(o.buyerCookie)}</BuyerCookie>
|
|
1250
1283
|
<BrowserFormPost>
|
|
1251
1284
|
<URL>${escapeXml(o.browserFormPostUrl)}</URL>
|
|
1252
|
-
</BrowserFormPost>${extrinsicBlock(o.extrinsics, " ")}${shipTo}${contact2}
|
|
1285
|
+
</BrowserFormPost>${extrinsicBlock(o.extrinsics, " ")}${shipTo}${contact2}${items}
|
|
1253
1286
|
</PunchOutSetupRequest>
|
|
1254
1287
|
</Request>`;
|
|
1255
1288
|
return envelope(o.payloadId, o.timestamp, lang, inner, o.dtdVersion);
|
|
@@ -1971,12 +2004,22 @@ function validateDocument(raw, ctx = {}) {
|
|
|
1971
2004
|
const docType = ctx.forceDocType ?? getDocType(doc);
|
|
1972
2005
|
checkGeneral(doc, ctx, issues, docType);
|
|
1973
2006
|
switch (docType) {
|
|
1974
|
-
case "SetupRequest":
|
|
2007
|
+
case "SetupRequest": {
|
|
1975
2008
|
checkSharedSecret(doc, ctx, issues);
|
|
1976
|
-
|
|
2009
|
+
const setup = root(doc)?.Request?.PunchOutSetupRequest;
|
|
2010
|
+
if (!setup?.BrowserFormPost?.URL) {
|
|
1977
2011
|
issues.warn("missing-browserformpost", "PunchOutSetupRequest/BrowserFormPost/URL is missing", "cXML/.../PunchOutSetupRequest/BrowserFormPost/URL");
|
|
1978
2012
|
}
|
|
2013
|
+
const op = attr(setup, "operation") ?? "create";
|
|
2014
|
+
const hasItems = asArray(setup?.ItemOut).length > 0;
|
|
2015
|
+
if ((op === "edit" || op === "inspect") && !hasItems) {
|
|
2016
|
+
issues.warn("setup-missing-items", `operation="${op}" usually carries the prior item(s) as ItemOut, but none are present`, "cXML/.../PunchOutSetupRequest");
|
|
2017
|
+
}
|
|
2018
|
+
if (op === "create" && hasItems) {
|
|
2019
|
+
issues.warn("setup-unexpected-items", 'operation="create" starts an empty cart, but ItemOut elements are present', "cXML/.../PunchOutSetupRequest");
|
|
2020
|
+
}
|
|
1979
2021
|
break;
|
|
2022
|
+
}
|
|
1980
2023
|
case "SetupResponse":
|
|
1981
2024
|
checkSetupResponse(doc, issues);
|
|
1982
2025
|
break;
|
|
@@ -1998,6 +2041,8 @@ function validateDocument(raw, ctx = {}) {
|
|
|
1998
2041
|
}
|
|
1999
2042
|
|
|
2000
2043
|
// src/server/routes/flow.ts
|
|
2044
|
+
var coerceOperation = (v) => v === "create" || v === "edit" || v === "inspect" ? v : void 0;
|
|
2045
|
+
var setupItems = (body) => Array.isArray(body?.items) && body.items.length > 0 ? body.items : void 0;
|
|
2001
2046
|
var flowRoute = new Hono5();
|
|
2002
2047
|
function host() {
|
|
2003
2048
|
try {
|
|
@@ -2049,11 +2094,12 @@ function resolveVirtualBuyer(id) {
|
|
|
2049
2094
|
if (r.connection.mode !== "virtual-buyer") return { error: "connection is not in virtual-buyer mode" };
|
|
2050
2095
|
return { ctx: buyerContext(r) };
|
|
2051
2096
|
}
|
|
2052
|
-
flowRoute.
|
|
2097
|
+
flowRoute.post("/:id/setup/preview", async (c) => {
|
|
2053
2098
|
const r = resolveVirtualBuyer(c.req.param("id"));
|
|
2054
2099
|
if ("error" in r) return c.json({ error: r.error }, 400);
|
|
2055
2100
|
const { ctx } = r;
|
|
2056
|
-
const
|
|
2101
|
+
const body = await c.req.json().catch(() => ({}));
|
|
2102
|
+
const buyerCookie = body.buyerCookie || `pos-${nanoid5(16)}`;
|
|
2057
2103
|
const xml = buildSetupRequest({
|
|
2058
2104
|
from: ctx.from,
|
|
2059
2105
|
to: ctx.to,
|
|
@@ -2064,11 +2110,12 @@ flowRoute.get("/:id/setup/preview", (c) => {
|
|
|
2064
2110
|
payloadId: makePayloadId(host(), (/* @__PURE__ */ new Date()).toISOString()),
|
|
2065
2111
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2066
2112
|
deploymentMode: ctx.deploymentMode,
|
|
2067
|
-
operation: ctx.eff.setupOperation,
|
|
2113
|
+
operation: coerceOperation(body.operation) ?? ctx.eff.setupOperation,
|
|
2068
2114
|
dtdVersion: dtdVersionFor(ctx.eff, "SetupRequest"),
|
|
2069
2115
|
userAgent: ctx.eff.userAgent,
|
|
2070
2116
|
extrinsics: setupExtrinsics(ctx, buyerCookie),
|
|
2071
|
-
...setupAddresses(ctx)
|
|
2117
|
+
...setupAddresses(ctx),
|
|
2118
|
+
items: setupItems(body)
|
|
2072
2119
|
});
|
|
2073
2120
|
return c.json({ buyerCookie, xml, browserFormPostUrl: browserFormPostUrl() });
|
|
2074
2121
|
});
|
|
@@ -2088,11 +2135,12 @@ flowRoute.post("/:id/setup", async (c) => {
|
|
|
2088
2135
|
payloadId: makePayloadId(host(), (/* @__PURE__ */ new Date()).toISOString()),
|
|
2089
2136
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2090
2137
|
deploymentMode: ctx.deploymentMode,
|
|
2091
|
-
operation: ctx.eff.setupOperation,
|
|
2138
|
+
operation: coerceOperation(body.operation) ?? ctx.eff.setupOperation,
|
|
2092
2139
|
dtdVersion: dtdVersionFor(ctx.eff, "SetupRequest"),
|
|
2093
2140
|
userAgent: ctx.eff.userAgent,
|
|
2094
2141
|
extrinsics: setupExtrinsics(ctx, buyerCookie),
|
|
2095
|
-
...setupAddresses(ctx)
|
|
2142
|
+
...setupAddresses(ctx),
|
|
2143
|
+
items: setupItems(body)
|
|
2096
2144
|
});
|
|
2097
2145
|
rememberSessionConnection(buyerCookie, ctx.connectionId);
|
|
2098
2146
|
const reqValidation = validateDocument(xml, { expected: ctx.expected, forceDocType: "SetupRequest" });
|
|
@@ -2132,6 +2180,21 @@ flowRoute.post("/:id/setup", async (c) => {
|
|
|
2132
2180
|
response: respLog
|
|
2133
2181
|
});
|
|
2134
2182
|
});
|
|
2183
|
+
flowRoute.post("/:id/validate", async (c) => {
|
|
2184
|
+
const r = resolveVirtualBuyer(c.req.param("id"));
|
|
2185
|
+
if ("error" in r) return c.json({ error: r.error }, 400);
|
|
2186
|
+
const { ctx } = r;
|
|
2187
|
+
const body = await c.req.json().catch(() => ({}));
|
|
2188
|
+
const xml = String(body.xml ?? "");
|
|
2189
|
+
const docType = body.docType ?? "Unknown";
|
|
2190
|
+
return c.json(
|
|
2191
|
+
validateDocument(xml, {
|
|
2192
|
+
expected: ctx.expected,
|
|
2193
|
+
forceDocType: docType,
|
|
2194
|
+
allowMixedCurrency: ctx.allowMixedCurrency
|
|
2195
|
+
})
|
|
2196
|
+
);
|
|
2197
|
+
});
|
|
2135
2198
|
function buildOrderXml(ctx, body) {
|
|
2136
2199
|
const items = body.items ?? [];
|
|
2137
2200
|
const currency = body.currency || items[0]?.currency || "USD";
|
|
@@ -2387,8 +2450,37 @@ dataRoute.get(
|
|
|
2387
2450
|
"/runtime",
|
|
2388
2451
|
(c) => c.json({ publicUrl: getPublicUrl(), callbackUrl: `${getPublicUrl()}/punchout/return`, version: getVersion() })
|
|
2389
2452
|
);
|
|
2390
|
-
dataRoute.get("/sessions", (c) =>
|
|
2453
|
+
dataRoute.get("/sessions", (c) => {
|
|
2454
|
+
const enriched = listSessions().map((s) => {
|
|
2455
|
+
const conn = s.connectionId ? getConnection(s.connectionId) : void 0;
|
|
2456
|
+
if (conn) {
|
|
2457
|
+
return {
|
|
2458
|
+
...s,
|
|
2459
|
+
connectionName: conn.name,
|
|
2460
|
+
mode: conn.mode,
|
|
2461
|
+
buyerName: getBuyer(conn.buyerId)?.name,
|
|
2462
|
+
supplierName: getSupplier(conn.supplierId)?.name,
|
|
2463
|
+
inbound: false
|
|
2464
|
+
};
|
|
2465
|
+
}
|
|
2466
|
+
const supplier = s.connectionId ? getSupplier(s.connectionId) : void 0;
|
|
2467
|
+
return {
|
|
2468
|
+
...s,
|
|
2469
|
+
supplierName: supplier?.name,
|
|
2470
|
+
mode: supplier ? "virtual-supplier" : void 0,
|
|
2471
|
+
inbound: !!supplier
|
|
2472
|
+
// an external buyer hit our /sim
|
|
2473
|
+
};
|
|
2474
|
+
});
|
|
2475
|
+
return c.json(enriched);
|
|
2476
|
+
});
|
|
2391
2477
|
dataRoute.get("/sessions/:id", (c) => c.json(readSession(c.req.param("id"))));
|
|
2478
|
+
dataRoute.delete("/sessions/:id", (c) => {
|
|
2479
|
+
const id = c.req.param("id");
|
|
2480
|
+
const removed = deleteSession(id);
|
|
2481
|
+
forgetSession(id);
|
|
2482
|
+
return removed ? c.json({ ok: true }) : c.json({ error: "not found" }, 404);
|
|
2483
|
+
});
|
|
2392
2484
|
dataRoute.get("/sessions/:sessionId/records/:recordId/raw", (c) => {
|
|
2393
2485
|
const record = readSession(c.req.param("sessionId")).find((r) => r.id === c.req.param("recordId"));
|
|
2394
2486
|
if (!record) return c.json({ error: "not found" }, 404);
|
|
@@ -2518,17 +2610,35 @@ simRoute.get("/:id/catalog", (c) => {
|
|
|
2518
2610
|
return c.html(`<!doctype html><html lang="en"><head><meta charset="utf-8">
|
|
2519
2611
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2520
2612
|
<title>${escapeXml(supplier.name)} \u2014 mock catalog</title>
|
|
2613
|
+
<script>
|
|
2614
|
+
// Match the main app's theme. The app appends ?theme= to the catalog link
|
|
2615
|
+
// (works even when the SPA is on a different origin, e.g. the Vite dev server);
|
|
2616
|
+
// otherwise fall back to a same-origin 'pos-theme' in localStorage, then the OS
|
|
2617
|
+
// preference. Set before paint to avoid a flash.
|
|
2618
|
+
(function () {
|
|
2619
|
+
try {
|
|
2620
|
+
var q = new URLSearchParams(location.search).get("theme");
|
|
2621
|
+
var t = (q === "light" || q === "dark") ? q : localStorage.getItem("pos-theme");
|
|
2622
|
+
if (t !== "light" && t !== "dark") {
|
|
2623
|
+
t = (window.matchMedia && matchMedia("(prefers-color-scheme: light)").matches) ? "light" : "dark";
|
|
2624
|
+
}
|
|
2625
|
+
document.documentElement.dataset.theme = t;
|
|
2626
|
+
} catch (e) {}
|
|
2627
|
+
})();
|
|
2628
|
+
</script>
|
|
2521
2629
|
<style>
|
|
2522
|
-
|
|
2630
|
+
:root{--bg:#0f172a;--text:#e2e8f0;--muted:#94a3b8;--panel:#1e293b;--th:#0b1220;--border:#334155;--field:#0b1220;--price:#fbbf24;--accent:#6366f1;--accent-hover:#4f46e5}
|
|
2631
|
+
:root[data-theme="light"]{--bg:#f5f7fb;--text:#1e293b;--muted:#4b5a73;--panel:#ffffff;--th:#eef1f7;--border:#d4dae8;--field:#ffffff;--price:#b45309;--accent:#6366f1;--accent-hover:#4f46e5}
|
|
2632
|
+
body{font-family:system-ui,sans-serif;background:var(--bg);color:var(--text);margin:0;padding:2rem}
|
|
2523
2633
|
.wrap{max-width:720px;margin:0 auto}
|
|
2524
|
-
h1{font-size:1.4rem}.sub{color
|
|
2525
|
-
table{width:100%;border-collapse:collapse;background
|
|
2526
|
-
th,td{padding:.75rem 1rem;text-align:left;border-bottom:1px solid
|
|
2527
|
-
th{background
|
|
2528
|
-
.price{white-space:nowrap;color
|
|
2529
|
-
input[type=number]{width:5rem;background
|
|
2530
|
-
button{margin-top:1.5rem;background
|
|
2531
|
-
button:hover{background
|
|
2634
|
+
h1{font-size:1.4rem}.sub{color:var(--muted);margin-bottom:1.5rem}
|
|
2635
|
+
table{width:100%;border-collapse:collapse;background:var(--panel);border-radius:12px;overflow:hidden}
|
|
2636
|
+
th,td{padding:.75rem 1rem;text-align:left;border-bottom:1px solid var(--border)}
|
|
2637
|
+
th{background:var(--th);font-size:.8rem;text-transform:uppercase;letter-spacing:.05em;color:var(--muted)}
|
|
2638
|
+
.price{white-space:nowrap;color:var(--price)}
|
|
2639
|
+
input[type=number]{width:5rem;background:var(--field);border:1px solid var(--border);color:var(--text);border-radius:6px;padding:.35rem .5rem}
|
|
2640
|
+
button{margin-top:1.5rem;background:var(--accent);color:#fff;border:0;border-radius:8px;padding:.7rem 1.4rem;font-size:1rem;cursor:pointer}
|
|
2641
|
+
button:hover{background:var(--accent-hover)}
|
|
2532
2642
|
</style></head><body><div class="wrap">
|
|
2533
2643
|
<h1>${escapeXml(supplier.name)} <small>(virtual supplier)</small></h1>
|
|
2534
2644
|
<div class="sub">Mock catalog served by punchout-simulator. Set quantities and return the cart.</div>
|
|
@@ -2547,7 +2657,10 @@ simRoute.post("/:id/checkout", async (c) => {
|
|
|
2547
2657
|
const supplier = getSupplier(c.req.param("id"));
|
|
2548
2658
|
if (!supplier) return c.text("supplier not found", 404);
|
|
2549
2659
|
const form = await c.req.parseBody();
|
|
2550
|
-
const cookie =
|
|
2660
|
+
const cookie = typeof form.cookie === "string" ? form.cookie.trim() : "";
|
|
2661
|
+
if (!cookie) {
|
|
2662
|
+
return c.text("missing BuyerCookie \u2014 open the catalog from a SetupResponse StartPage", 400);
|
|
2663
|
+
}
|
|
2551
2664
|
const formpost = safeHttpUrl(String(form.formpost ?? ""));
|
|
2552
2665
|
const buyerCred = { domain: String(form.bd ?? ""), identity: String(form.bi ?? "") };
|
|
2553
2666
|
const catalog = catalogOf(supplier);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{m as et}from"./index-
|
|
1
|
+
import{m as et}from"./index-ByIxIbJu.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-ByIxIbJu.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-ByIxIbJu.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-ByIxIbJu.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-ByIxIbJu.js";/*!-----------------------------------------------------------------------------
|
|
2
2
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
3
|
* Version: 0.52.2(404545bded1df6ffa41ea0af4e8ddb219018c6c1)
|
|
4
4
|
* Released under the MIT license
|