medusa-plugin-ordinant 0.1.0 → 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.
|
@@ -34,6 +34,7 @@ const CATEGORY_FIELDS = {
|
|
|
34
34
|
"aow",
|
|
35
35
|
"destructive_device",
|
|
36
36
|
"suppressor",
|
|
37
|
+
"suppressor_part",
|
|
37
38
|
"firearm_part"
|
|
38
39
|
],
|
|
39
40
|
mag_capacity: [
|
|
@@ -53,7 +54,8 @@ const CATEGORY_FIELDS = {
|
|
|
53
54
|
"short_barreled_shotgun",
|
|
54
55
|
"machine_gun",
|
|
55
56
|
"aow",
|
|
56
|
-
"suppressor"
|
|
57
|
+
"suppressor",
|
|
58
|
+
"suppressor_part"
|
|
57
59
|
],
|
|
58
60
|
action_type: [
|
|
59
61
|
"handgun",
|
|
@@ -139,6 +141,57 @@ function formatValue(value) {
|
|
|
139
141
|
}
|
|
140
142
|
return labelValue(String(value));
|
|
141
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
|
+
}
|
|
142
195
|
function AttributeEditor({
|
|
143
196
|
value,
|
|
144
197
|
onChange
|
|
@@ -182,17 +235,20 @@ function AttributeEditor({
|
|
|
182
235
|
{
|
|
183
236
|
def,
|
|
184
237
|
value: value[def.key],
|
|
185
|
-
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
|
|
186
240
|
},
|
|
187
241
|
def.key
|
|
188
|
-
))
|
|
242
|
+
)),
|
|
243
|
+
category !== void 0 && /* @__PURE__ */ jsxRuntime.jsx(CoveragePreview, { attributes: value })
|
|
189
244
|
] });
|
|
190
245
|
}
|
|
191
246
|
const NONE = "__none__";
|
|
192
247
|
function AttributeField({
|
|
193
248
|
def,
|
|
194
249
|
value,
|
|
195
|
-
onChange
|
|
250
|
+
onChange,
|
|
251
|
+
extraHint
|
|
196
252
|
}) {
|
|
197
253
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
198
254
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
@@ -200,6 +256,7 @@ function AttributeField({
|
|
|
200
256
|
def.hard_gate && /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", color: "orange", children: "required" })
|
|
201
257
|
] }),
|
|
202
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 }),
|
|
203
260
|
/* @__PURE__ */ jsxRuntime.jsx(Control, { def, value, onChange })
|
|
204
261
|
] });
|
|
205
262
|
}
|
|
@@ -31,6 +31,7 @@ const CATEGORY_FIELDS = {
|
|
|
31
31
|
"aow",
|
|
32
32
|
"destructive_device",
|
|
33
33
|
"suppressor",
|
|
34
|
+
"suppressor_part",
|
|
34
35
|
"firearm_part"
|
|
35
36
|
],
|
|
36
37
|
mag_capacity: [
|
|
@@ -50,7 +51,8 @@ const CATEGORY_FIELDS = {
|
|
|
50
51
|
"short_barreled_shotgun",
|
|
51
52
|
"machine_gun",
|
|
52
53
|
"aow",
|
|
53
|
-
"suppressor"
|
|
54
|
+
"suppressor",
|
|
55
|
+
"suppressor_part"
|
|
54
56
|
],
|
|
55
57
|
action_type: [
|
|
56
58
|
"handgun",
|
|
@@ -136,6 +138,57 @@ function formatValue(value) {
|
|
|
136
138
|
}
|
|
137
139
|
return labelValue(String(value));
|
|
138
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
|
+
}
|
|
139
192
|
function AttributeEditor({
|
|
140
193
|
value,
|
|
141
194
|
onChange
|
|
@@ -179,17 +232,20 @@ function AttributeEditor({
|
|
|
179
232
|
{
|
|
180
233
|
def,
|
|
181
234
|
value: value[def.key],
|
|
182
|
-
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
|
|
183
237
|
},
|
|
184
238
|
def.key
|
|
185
|
-
))
|
|
239
|
+
)),
|
|
240
|
+
category !== void 0 && /* @__PURE__ */ jsx(CoveragePreview, { attributes: value })
|
|
186
241
|
] });
|
|
187
242
|
}
|
|
188
243
|
const NONE = "__none__";
|
|
189
244
|
function AttributeField({
|
|
190
245
|
def,
|
|
191
246
|
value,
|
|
192
|
-
onChange
|
|
247
|
+
onChange,
|
|
248
|
+
extraHint
|
|
193
249
|
}) {
|
|
194
250
|
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
195
251
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
@@ -197,6 +253,7 @@ function AttributeField({
|
|
|
197
253
|
def.hard_gate && /* @__PURE__ */ jsx(Badge, { size: "2xsmall", color: "orange", children: "required" })
|
|
198
254
|
] }),
|
|
199
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 }),
|
|
200
257
|
/* @__PURE__ */ jsx(Control, { def, value, onChange })
|
|
201
258
|
] });
|
|
202
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
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
Ordinant compliance for Medusa v2: AI-proposed product classification with
|
|
4
4
|
merchant attestation, and a deterministic, citation-bearing checkout gate.
|
|
5
5
|
|
|
6
|
+
Published on npm — install with `npm install medusa-plugin-ordinant`
|
|
7
|
+
([npmjs.com/package/medusa-plugin-ordinant](https://www.npmjs.com/package/medusa-plugin-ordinant)).
|
|
8
|
+
|
|
6
9
|
> **Ordinant is a compliance information and automation service — not legal
|
|
7
10
|
> advice.** No attorney–client relationship is created by its use. Merchants
|
|
8
11
|
> attest to the final classification of their products.
|
|
@@ -245,6 +248,14 @@ dictionary change.
|
|
|
245
248
|
Re-classifying a SKU (e.g. after a product edit) resets it to `proposed`: a
|
|
246
249
|
stale attestation never survives changed product data.
|
|
247
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
|
+
|
|
248
259
|
## The checkout gate
|
|
249
260
|
|
|
250
261
|
Consumes `completeCartWorkflow.hooks.validate`, which runs before any order
|
|
@@ -415,6 +426,52 @@ self-contained function — copy it from `src/storefront.ts` into your codebase
|
|
|
415
426
|
instead. Either way, anything you don't wire is still caught by the completeCart
|
|
416
427
|
gate; the preview is pure upside.
|
|
417
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
|
+
|
|
418
475
|
## Onboarding an existing catalog
|
|
419
476
|
|
|
420
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",
|