medusa-plugin-ordinant 0.1.1 → 0.1.2
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.
|
@@ -141,6 +141,57 @@ function formatValue(value) {
|
|
|
141
141
|
}
|
|
142
142
|
return labelValue(String(value));
|
|
143
143
|
}
|
|
144
|
+
function CoveragePreview({ attributes }) {
|
|
145
|
+
const category = typeof attributes["category"] === "string" ? attributes["category"] : void 0;
|
|
146
|
+
const { data, isFetching, error } = reactQuery.useQuery({
|
|
147
|
+
queryKey: ["ordinant-coverage", JSON.stringify(attributes)],
|
|
148
|
+
queryFn: () => sdk.client.fetch("/admin/ordinant/coverage", {
|
|
149
|
+
method: "POST",
|
|
150
|
+
body: { attributes }
|
|
151
|
+
}),
|
|
152
|
+
enabled: category !== void 0,
|
|
153
|
+
staleTime: 5 * 60 * 1e3
|
|
154
|
+
});
|
|
155
|
+
if (category === void 0) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
if (isFetching && !data) {
|
|
159
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", leading: "compact", className: "text-ui-fg-muted", children: "Checking where this configuration can ship…" });
|
|
160
|
+
}
|
|
161
|
+
if (error || !data) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
const blocked = data.blocked ?? [];
|
|
165
|
+
const conditional = data.conditional ?? [];
|
|
166
|
+
if (blocked.length === 0 && conditional.length === 0) {
|
|
167
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "xsmall", leading: "compact", className: "text-ui-fg-subtle", children: [
|
|
168
|
+
"Checkout is allowed in all ",
|
|
169
|
+
data.total_states,
|
|
170
|
+
" modeled states with this configuration."
|
|
171
|
+
] });
|
|
172
|
+
}
|
|
173
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-2 rounded-lg border p-3 bg-ui-bg-subtle", children: [
|
|
174
|
+
blocked.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
175
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "xsmall", leading: "compact", weight: "plus", className: "text-ui-fg-error", children: [
|
|
176
|
+
"With this configuration, shipping/checkout will be denied to",
|
|
177
|
+
" ",
|
|
178
|
+
blocked.length,
|
|
179
|
+
" ",
|
|
180
|
+
blocked.length === 1 ? "state" : "states",
|
|
181
|
+
":"
|
|
182
|
+
] }),
|
|
183
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1 mt-1.5", children: blocked.map((s) => /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", color: "red", title: s.name, children: s.state }, s.state)) })
|
|
184
|
+
] }),
|
|
185
|
+
conditional.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
186
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "xsmall", leading: "compact", weight: "plus", className: "text-ui-fg-subtle", children: [
|
|
187
|
+
"Requires buyer verification (FFL / permit / age) in ",
|
|
188
|
+
conditional.length,
|
|
189
|
+
":"
|
|
190
|
+
] }),
|
|
191
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1 mt-1.5", children: conditional.map((s) => /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", color: "orange", title: s.name, children: s.state }, s.state)) })
|
|
192
|
+
] })
|
|
193
|
+
] });
|
|
194
|
+
}
|
|
144
195
|
function AttributeEditor({
|
|
145
196
|
value,
|
|
146
197
|
onChange
|
|
@@ -184,17 +235,20 @@ function AttributeEditor({
|
|
|
184
235
|
{
|
|
185
236
|
def,
|
|
186
237
|
value: value[def.key],
|
|
187
|
-
onChange: (v) => set(def.key, v)
|
|
238
|
+
onChange: (v) => set(def.key, v),
|
|
239
|
+
extraHint: def.key === "nfa_class" && category === "suppressor_part" ? "For a suppressor component, this is what decides whether it can ship: choose “Suppressor” for the sound-reducing core (baffle, monocore, expansion chamber, outer tube). Choose “None” for external accessories that don't reduce sound (end cap, cover, mount, thread adapter)." : void 0
|
|
188
240
|
},
|
|
189
241
|
def.key
|
|
190
|
-
))
|
|
242
|
+
)),
|
|
243
|
+
category !== void 0 && /* @__PURE__ */ jsxRuntime.jsx(CoveragePreview, { attributes: value })
|
|
191
244
|
] });
|
|
192
245
|
}
|
|
193
246
|
const NONE = "__none__";
|
|
194
247
|
function AttributeField({
|
|
195
248
|
def,
|
|
196
249
|
value,
|
|
197
|
-
onChange
|
|
250
|
+
onChange,
|
|
251
|
+
extraHint
|
|
198
252
|
}) {
|
|
199
253
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
200
254
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
@@ -202,6 +256,7 @@ function AttributeField({
|
|
|
202
256
|
def.hard_gate && /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", color: "orange", children: "required" })
|
|
203
257
|
] }),
|
|
204
258
|
def.description && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", leading: "compact", className: "text-ui-fg-muted", children: def.description }),
|
|
259
|
+
extraHint && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", leading: "compact", className: "text-ui-fg-subtle", children: extraHint }),
|
|
205
260
|
/* @__PURE__ */ jsxRuntime.jsx(Control, { def, value, onChange })
|
|
206
261
|
] });
|
|
207
262
|
}
|
|
@@ -138,6 +138,57 @@ function formatValue(value) {
|
|
|
138
138
|
}
|
|
139
139
|
return labelValue(String(value));
|
|
140
140
|
}
|
|
141
|
+
function CoveragePreview({ attributes }) {
|
|
142
|
+
const category = typeof attributes["category"] === "string" ? attributes["category"] : void 0;
|
|
143
|
+
const { data, isFetching, error } = useQuery({
|
|
144
|
+
queryKey: ["ordinant-coverage", JSON.stringify(attributes)],
|
|
145
|
+
queryFn: () => sdk.client.fetch("/admin/ordinant/coverage", {
|
|
146
|
+
method: "POST",
|
|
147
|
+
body: { attributes }
|
|
148
|
+
}),
|
|
149
|
+
enabled: category !== void 0,
|
|
150
|
+
staleTime: 5 * 60 * 1e3
|
|
151
|
+
});
|
|
152
|
+
if (category === void 0) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
if (isFetching && !data) {
|
|
156
|
+
return /* @__PURE__ */ jsx(Text, { size: "xsmall", leading: "compact", className: "text-ui-fg-muted", children: "Checking where this configuration can ship…" });
|
|
157
|
+
}
|
|
158
|
+
if (error || !data) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
const blocked = data.blocked ?? [];
|
|
162
|
+
const conditional = data.conditional ?? [];
|
|
163
|
+
if (blocked.length === 0 && conditional.length === 0) {
|
|
164
|
+
return /* @__PURE__ */ jsxs(Text, { size: "xsmall", leading: "compact", className: "text-ui-fg-subtle", children: [
|
|
165
|
+
"Checkout is allowed in all ",
|
|
166
|
+
data.total_states,
|
|
167
|
+
" modeled states with this configuration."
|
|
168
|
+
] });
|
|
169
|
+
}
|
|
170
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 rounded-lg border p-3 bg-ui-bg-subtle", children: [
|
|
171
|
+
blocked.length > 0 && /* @__PURE__ */ jsxs("div", { children: [
|
|
172
|
+
/* @__PURE__ */ jsxs(Text, { size: "xsmall", leading: "compact", weight: "plus", className: "text-ui-fg-error", children: [
|
|
173
|
+
"With this configuration, shipping/checkout will be denied to",
|
|
174
|
+
" ",
|
|
175
|
+
blocked.length,
|
|
176
|
+
" ",
|
|
177
|
+
blocked.length === 1 ? "state" : "states",
|
|
178
|
+
":"
|
|
179
|
+
] }),
|
|
180
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1 mt-1.5", children: blocked.map((s) => /* @__PURE__ */ jsx(Badge, { size: "2xsmall", color: "red", title: s.name, children: s.state }, s.state)) })
|
|
181
|
+
] }),
|
|
182
|
+
conditional.length > 0 && /* @__PURE__ */ jsxs("div", { children: [
|
|
183
|
+
/* @__PURE__ */ jsxs(Text, { size: "xsmall", leading: "compact", weight: "plus", className: "text-ui-fg-subtle", children: [
|
|
184
|
+
"Requires buyer verification (FFL / permit / age) in ",
|
|
185
|
+
conditional.length,
|
|
186
|
+
":"
|
|
187
|
+
] }),
|
|
188
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1 mt-1.5", children: conditional.map((s) => /* @__PURE__ */ jsx(Badge, { size: "2xsmall", color: "orange", title: s.name, children: s.state }, s.state)) })
|
|
189
|
+
] })
|
|
190
|
+
] });
|
|
191
|
+
}
|
|
141
192
|
function AttributeEditor({
|
|
142
193
|
value,
|
|
143
194
|
onChange
|
|
@@ -181,17 +232,20 @@ function AttributeEditor({
|
|
|
181
232
|
{
|
|
182
233
|
def,
|
|
183
234
|
value: value[def.key],
|
|
184
|
-
onChange: (v) => set(def.key, v)
|
|
235
|
+
onChange: (v) => set(def.key, v),
|
|
236
|
+
extraHint: def.key === "nfa_class" && category === "suppressor_part" ? "For a suppressor component, this is what decides whether it can ship: choose “Suppressor” for the sound-reducing core (baffle, monocore, expansion chamber, outer tube). Choose “None” for external accessories that don't reduce sound (end cap, cover, mount, thread adapter)." : void 0
|
|
185
237
|
},
|
|
186
238
|
def.key
|
|
187
|
-
))
|
|
239
|
+
)),
|
|
240
|
+
category !== void 0 && /* @__PURE__ */ jsx(CoveragePreview, { attributes: value })
|
|
188
241
|
] });
|
|
189
242
|
}
|
|
190
243
|
const NONE = "__none__";
|
|
191
244
|
function AttributeField({
|
|
192
245
|
def,
|
|
193
246
|
value,
|
|
194
|
-
onChange
|
|
247
|
+
onChange,
|
|
248
|
+
extraHint
|
|
195
249
|
}) {
|
|
196
250
|
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
197
251
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
@@ -199,6 +253,7 @@ function AttributeField({
|
|
|
199
253
|
def.hard_gate && /* @__PURE__ */ jsx(Badge, { size: "2xsmall", color: "orange", children: "required" })
|
|
200
254
|
] }),
|
|
201
255
|
def.description && /* @__PURE__ */ jsx(Text, { size: "xsmall", leading: "compact", className: "text-ui-fg-muted", children: def.description }),
|
|
256
|
+
extraHint && /* @__PURE__ */ jsx(Text, { size: "xsmall", leading: "compact", className: "text-ui-fg-subtle", children: extraHint }),
|
|
202
257
|
/* @__PURE__ */ jsx(Control, { def, value, onChange })
|
|
203
258
|
] });
|
|
204
259
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.POST = POST;
|
|
4
|
+
const ordinant_1 = require("../../../../modules/ordinant");
|
|
5
|
+
/**
|
|
6
|
+
* POST /admin/ordinant/coverage
|
|
7
|
+
*
|
|
8
|
+
* Preview where a proposed classification would be denied. Given an attribute
|
|
9
|
+
* set, the engine sweeps every modeled jurisdiction and returns the states
|
|
10
|
+
* where checkout is BLOCKED vs. CONDITIONAL. Powers the editor's "with this
|
|
11
|
+
* configuration, shipping is denied to…" panel. Non-logging, advisory.
|
|
12
|
+
*/
|
|
13
|
+
async function POST(req, res) {
|
|
14
|
+
const attributes = req.body?.attributes;
|
|
15
|
+
if (!attributes || Object.keys(attributes).length === 0) {
|
|
16
|
+
res.status(400).json({ message: "attributes are required to preview coverage" });
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const ordinant = req.scope.resolve(ordinant_1.ORDINANT_MODULE);
|
|
20
|
+
const coverage = await ordinant.client.coverage(attributes);
|
|
21
|
+
res.json(coverage);
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicm91dGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi8uLi9zcmMvYXBpL2FkbWluL29yZGluYW50L2NvdmVyYWdlL3JvdXRlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7O0FBY0Esb0JBWUM7QUF6QkQsMkRBQThEO0FBSzlEOzs7Ozs7O0dBT0c7QUFDSSxLQUFLLFVBQVUsSUFBSSxDQUN4QixHQUE2QyxFQUM3QyxHQUFtQjtJQUVuQixNQUFNLFVBQVUsR0FBRyxHQUFHLENBQUMsSUFBSSxFQUFFLFVBQVUsQ0FBQTtJQUN2QyxJQUFJLENBQUMsVUFBVSxJQUFJLE1BQU0sQ0FBQyxJQUFJLENBQUMsVUFBVSxDQUFDLENBQUMsTUFBTSxLQUFLLENBQUMsRUFBRSxDQUFDO1FBQ3hELEdBQUcsQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLENBQUMsSUFBSSxDQUFDLEVBQUUsT0FBTyxFQUFFLDZDQUE2QyxFQUFFLENBQUMsQ0FBQTtRQUNoRixPQUFNO0lBQ1IsQ0FBQztJQUNELE1BQU0sUUFBUSxHQUFHLEdBQUcsQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUF3QiwwQkFBZSxDQUFDLENBQUE7SUFDMUUsTUFBTSxRQUFRLEdBQUcsTUFBTSxRQUFRLENBQUMsTUFBTSxDQUFDLFFBQVEsQ0FBQyxVQUFVLENBQUMsQ0FBQTtJQUMzRCxHQUFHLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBQyxDQUFBO0FBQ3BCLENBQUMifQ==
|
|
@@ -51,6 +51,16 @@ class OrdinantClient {
|
|
|
51
51
|
}
|
|
52
52
|
return this.call("POST", "/v1/decisions/preview", request, headers);
|
|
53
53
|
}
|
|
54
|
+
/** Sweep a proposed attribute set across every modeled jurisdiction and
|
|
55
|
+
* report where checkout would be denied vs. conditionally allowed. Non-
|
|
56
|
+
* logging — powers the classification editor's "denied to" preview so a
|
|
57
|
+
* merchant sees the shipping impact of a classification before confirming. */
|
|
58
|
+
async coverage(attributes, buyer) {
|
|
59
|
+
return this.call("POST", "/v1/decisions/coverage", {
|
|
60
|
+
attributes,
|
|
61
|
+
buyer,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
54
64
|
async classifyCatalog(items) {
|
|
55
65
|
// Classification makes one LLM call per item (6-way concurrent
|
|
56
66
|
// server-side); a batch legitimately outlasts the checkout timeout.
|
|
@@ -133,4 +143,4 @@ class OrdinantClient {
|
|
|
133
143
|
}
|
|
134
144
|
}
|
|
135
145
|
exports.OrdinantClient = OrdinantClient;
|
|
136
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
146
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xpZW50LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vc3JjL2xpYi9jbGllbnQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUFBOzs7O0dBSUc7OztBQWtLSCxNQUFhLGdCQUFpQixTQUFRLEtBQUs7SUFJekMsWUFBWSxPQUFlLEVBQUUsSUFBWSxFQUFFLE1BQWU7UUFDeEQsS0FBSyxDQUFDLE9BQU8sQ0FBQyxDQUFBO1FBQ2QsSUFBSSxDQUFDLElBQUksR0FBRyxrQkFBa0IsQ0FBQTtRQUM5QixJQUFJLENBQUMsSUFBSSxHQUFHLElBQUksQ0FBQTtRQUNoQixJQUFJLENBQUMsTUFBTSxHQUFHLE1BQU0sQ0FBQTtJQUN0QixDQUFDO0NBQ0Y7QUFWRCw0Q0FVQztBQW9DRCxNQUFhLGNBQWM7SUFVekIsWUFBWSxPQUE4QjtRQUN4QyxJQUFJLENBQUMsVUFBVSxHQUFHLE9BQU8sQ0FBQyxPQUFPLENBQUMsT0FBTyxJQUFJLE9BQU8sQ0FBQyxNQUFNLENBQUMsQ0FBQTtRQUM1RCxJQUFJLENBQUMsT0FBTyxHQUFHLENBQUMsT0FBTyxDQUFDLE9BQU8sSUFBSSxFQUFFLENBQUMsQ0FBQyxPQUFPLENBQUMsTUFBTSxFQUFFLEVBQUUsQ0FBQyxDQUFBO1FBQzFELElBQUksQ0FBQyxNQUFNLEdBQUcsT0FBTyxDQUFDLE1BQU0sSUFBSSxFQUFFLENBQUE7UUFDbEMsSUFBSSxDQUFDLFNBQVMsR0FBRyxPQUFPLENBQUMsU0FBUyxJQUFJLElBQUksQ0FBQTtRQUMxQyxJQUFJLENBQUMsZ0JBQWdCLEdBQUcsT0FBTyxDQUFDLGdCQUFnQixJQUFJLE9BQU8sQ0FBQTtJQUM3RCxDQUFDO0lBRUQsbUVBQW1FO0lBQ25FLEtBQUssQ0FBQyxVQUFVO1FBQ2QsT0FBTyxJQUFJLENBQUMsSUFBSSxDQUFhLEtBQUssRUFBRSxrQkFBa0IsQ0FBQyxDQUFBO0lBQ3pELENBQUM7SUFFRDtzQ0FDa0M7SUFDbEMsS0FBSyxDQUFDLGNBQWM7UUFDbEIs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
|
package/README.md
CHANGED
|
@@ -248,6 +248,14 @@ dictionary change.
|
|
|
248
248
|
Re-classifying a SKU (e.g. after a product edit) resets it to `proposed`: a
|
|
249
249
|
stale attestation never survives changed product data.
|
|
250
250
|
|
|
251
|
+
**Live shipping-impact preview.** While editing a classification, the widget
|
|
252
|
+
shows *"With this configuration, shipping/checkout will be denied to: CA, NY, …"*
|
|
253
|
+
(and a separate "requires FFL/permit/age in …" list), swept across every modeled
|
|
254
|
+
jurisdiction — so you see exactly where a classification will and won't sell
|
|
255
|
+
**before** you attest to it. Fields with subtle legal meaning carry inline
|
|
256
|
+
guidance (e.g. for a suppressor part, `nfa_class` = *Suppressor* for the sound-
|
|
257
|
+
reducing core, *None* for an external accessory like an end cap).
|
|
258
|
+
|
|
251
259
|
## The checkout gate
|
|
252
260
|
|
|
253
261
|
Consumes `completeCartWorkflow.hooks.validate`, which runs before any order
|
|
@@ -418,6 +426,52 @@ self-contained function — copy it from `src/storefront.ts` into your codebase
|
|
|
418
426
|
instead. Either way, anything you don't wire is still caught by the completeCart
|
|
419
427
|
gate; the preview is pure upside.
|
|
420
428
|
|
|
429
|
+
### Also re-check when the shopper *enters* checkout (defense in depth)
|
|
430
|
+
|
|
431
|
+
The address-step check catches a bad address as it's entered. But a shopper can
|
|
432
|
+
set a *compliant* address, then add an item that's illegal to their state, and
|
|
433
|
+
walk up to the review step — where the block would otherwise only appear at
|
|
434
|
+
"Place Order". Close that gap by re-checking the **whole cart** when they land on
|
|
435
|
+
(or return to) the checkout page, and bouncing them back to the shipping step if
|
|
436
|
+
it's blocked.
|
|
437
|
+
|
|
438
|
+
The same preview does it — pass the `cartId` with **no `destination`** and it
|
|
439
|
+
evaluates the cart's saved address against its *current* items server-side:
|
|
440
|
+
|
|
441
|
+
```ts
|
|
442
|
+
import { checkAddressCompliance } from "medusa-plugin-ordinant/storefront"
|
|
443
|
+
|
|
444
|
+
// on entering checkout (any storefront):
|
|
445
|
+
const block = await checkAddressCompliance(sdk, { cartId }) // no destination = full current cart
|
|
446
|
+
if (block) {
|
|
447
|
+
// send them back to the shipping step and show `block`
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
In the Medusa **Next.js starter**, do it in the checkout page (a server
|
|
452
|
+
component) so the redirect happens before payment/review render:
|
|
453
|
+
|
|
454
|
+
```tsx
|
|
455
|
+
// src/app/[countryCode]/(checkout)/checkout/page.tsx
|
|
456
|
+
import { getCartComplianceBlock } from "@lib/data/cart" // thin wrapper over the preview
|
|
457
|
+
import { redirect } from "next/navigation"
|
|
458
|
+
|
|
459
|
+
const complianceBlock = await getCartComplianceBlock(cart.id)
|
|
460
|
+
if (complianceBlock && step !== "address" && step !== "delivery") {
|
|
461
|
+
redirect(`/${countryCode}/checkout?step=delivery`) // bounce back to shipping
|
|
462
|
+
}
|
|
463
|
+
// ...and render `complianceBlock` as a banner above <CheckoutForm />.
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
It only redirects when the shopper is *past* the shipping step, and it targets
|
|
467
|
+
the shipping step — so there's no redirect loop, and they can fix the address or
|
|
468
|
+
remove the item. Fail-open, same as the address check.
|
|
469
|
+
|
|
470
|
+
**The three layers together:** (1) the address step vets the address as it's
|
|
471
|
+
entered, (2) checkout entry re-checks the whole cart, (3) the completeCart gate
|
|
472
|
+
is the non-bypassable backstop at "Place Order". Wire 1 and 2, or neither — 3
|
|
473
|
+
always holds.
|
|
474
|
+
|
|
421
475
|
## Onboarding an existing catalog
|
|
422
476
|
|
|
423
477
|
Products created **after** installation classify automatically. For the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "medusa-plugin-ordinant",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Ordinant compliance for Medusa v2: catalog classification with merchant attestation and a deterministic, citation-bearing checkout gate. Compliance information and automation - not legal advice.",
|
|
5
5
|
"author": "Ordinant (https://ordinant.com)",
|
|
6
6
|
"license": "MIT",
|