@porulle/core 0.10.6 → 0.11.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/dist/config/types.d.ts +3 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/hooks/checkout.d.ts +2 -0
- package/dist/hooks/checkout.d.ts.map +1 -1
- package/dist/hooks/checkout.js +2 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/interfaces/rest/index.d.ts.map +1 -1
- package/dist/interfaces/rest/index.js +4 -0
- package/dist/interfaces/rest/routes/admin/custom-field-proposals.d.ts +5 -0
- package/dist/interfaces/rest/routes/admin/custom-field-proposals.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/admin/custom-field-proposals.js +22 -0
- package/dist/interfaces/rest/routes/admin/entity-field-definitions.d.ts +5 -0
- package/dist/interfaces/rest/routes/admin/entity-field-definitions.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/admin/entity-field-definitions.js +39 -0
- package/dist/interfaces/rest/routes/catalog.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/catalog.js +37 -2
- package/dist/interfaces/rest/routes/checkout.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/checkout.js +7 -1
- package/dist/interfaces/rest/routes/payments.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/payments.js +48 -30
- package/dist/interfaces/rest/routes/search.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/search.js +12 -0
- package/dist/interfaces/rest/schemas/admin-custom-field-proposals.d.ts +134 -0
- package/dist/interfaces/rest/schemas/admin-custom-field-proposals.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/admin-custom-field-proposals.js +26 -0
- package/dist/interfaces/rest/schemas/admin-entity-field-definitions.d.ts +592 -0
- package/dist/interfaces/rest/schemas/admin-entity-field-definitions.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/admin-entity-field-definitions.js +109 -0
- package/dist/interfaces/rest/schemas/catalog.d.ts +1364 -401
- package/dist/interfaces/rest/schemas/catalog.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/catalog.js +86 -0
- package/dist/interfaces/rest/schemas/pricing.d.ts +1 -0
- package/dist/interfaces/rest/schemas/pricing.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/search.d.ts +1 -0
- package/dist/interfaces/rest/schemas/search.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/search.js +1 -0
- package/dist/kernel/error-mapper.d.ts.map +1 -1
- package/dist/kernel/error-mapper.js +3 -0
- package/dist/modules/catalog/entity-service.d.ts +5 -3
- package/dist/modules/catalog/entity-service.d.ts.map +1 -1
- package/dist/modules/catalog/entity-service.js +73 -22
- package/dist/modules/catalog/module.d.ts +6 -1
- package/dist/modules/catalog/module.d.ts.map +1 -1
- package/dist/modules/catalog/module.js +6 -1
- package/dist/modules/catalog/ownership.d.ts +5 -0
- package/dist/modules/catalog/ownership.d.ts.map +1 -0
- package/dist/modules/catalog/ownership.js +11 -0
- package/dist/modules/catalog/repository/index.d.ts +52 -1
- package/dist/modules/catalog/repository/index.d.ts.map +1 -1
- package/dist/modules/catalog/repository/index.js +321 -2
- package/dist/modules/catalog/revision.d.ts +4 -0
- package/dist/modules/catalog/revision.d.ts.map +1 -0
- package/dist/modules/catalog/revision.js +33 -0
- package/dist/modules/catalog/schema.d.ts +979 -110
- package/dist/modules/catalog/schema.d.ts.map +1 -1
- package/dist/modules/catalog/schema.js +112 -2
- package/dist/modules/catalog/schemas.d.ts +2 -0
- package/dist/modules/catalog/schemas.d.ts.map +1 -1
- package/dist/modules/catalog/schemas.js +2 -0
- package/dist/modules/catalog/service.d.ts +66 -3
- package/dist/modules/catalog/service.d.ts.map +1 -1
- package/dist/modules/catalog/service.js +512 -11
- package/dist/modules/channels/adapter.d.ts +85 -0
- package/dist/modules/channels/adapter.d.ts.map +1 -1
- package/dist/modules/channels/adapter.js +9 -1
- package/dist/modules/media/module.d.ts +68 -0
- package/dist/modules/media/module.d.ts.map +1 -1
- package/dist/modules/media/module.js +2 -0
- package/dist/modules/media/schema.d.ts +70 -0
- package/dist/modules/media/schema.d.ts.map +1 -1
- package/dist/modules/media/schema.js +20 -2
- package/dist/modules/media/service.d.ts +8 -1
- package/dist/modules/media/service.d.ts.map +1 -1
- package/dist/modules/media/service.js +66 -18
- package/dist/modules/orders/service.d.ts +2 -0
- package/dist/modules/orders/service.d.ts.map +1 -1
- package/dist/modules/orders/service.js +1 -0
- package/dist/modules/pricing/schema.d.ts +17 -0
- package/dist/modules/pricing/schema.d.ts.map +1 -1
- package/dist/modules/pricing/schema.js +1 -0
- package/dist/modules/pricing/schemas.d.ts +1 -0
- package/dist/modules/pricing/schemas.d.ts.map +1 -1
- package/dist/modules/pricing/schemas.js +1 -0
- package/dist/modules/pricing/service.d.ts.map +1 -1
- package/dist/modules/pricing/service.js +6 -1
- package/dist/modules/search/adapter.d.ts +2 -0
- package/dist/modules/search/adapter.d.ts.map +1 -1
- package/dist/modules/search/module.d.ts.map +1 -1
- package/dist/modules/search/module.js +2 -0
- package/dist/modules/search/service.d.ts +4 -0
- package/dist/modules/search/service.d.ts.map +1 -1
- package/dist/modules/search/service.js +74 -5
- package/dist/runtime/kernel-modules.d.ts +73 -0
- package/dist/runtime/kernel-modules.d.ts.map +1 -1
- package/dist/utils/id.d.ts +1 -0
- package/dist/utils/id.d.ts.map +1 -1
- package/dist/utils/id.js +8 -0
- package/package.json +1 -1
- package/src/config/types.ts +3 -0
- package/src/hooks/checkout.ts +4 -0
- package/src/index.ts +14 -0
- package/src/interfaces/rest/index.ts +4 -0
- package/src/interfaces/rest/routes/admin/custom-field-proposals.ts +37 -0
- package/src/interfaces/rest/routes/admin/entity-field-definitions.ts +89 -0
- package/src/interfaces/rest/routes/catalog.ts +85 -0
- package/src/interfaces/rest/routes/checkout.ts +7 -1
- package/src/interfaces/rest/routes/payments.ts +55 -35
- package/src/interfaces/rest/routes/search.ts +16 -0
- package/src/interfaces/rest/schemas/admin-custom-field-proposals.ts +29 -0
- package/src/interfaces/rest/schemas/admin-entity-field-definitions.ts +120 -0
- package/src/interfaces/rest/schemas/catalog.ts +92 -0
- package/src/interfaces/rest/schemas/search.ts +2 -0
- package/src/kernel/error-mapper.ts +3 -0
- package/src/modules/catalog/entity-service.ts +73 -20
- package/src/modules/catalog/module.ts +15 -0
- package/src/modules/catalog/ownership.ts +17 -0
- package/src/modules/catalog/repository/index.ts +547 -1
- package/src/modules/catalog/revision.ts +35 -0
- package/src/modules/catalog/schema.ts +169 -1
- package/src/modules/catalog/schemas.ts +2 -0
- package/src/modules/catalog/service.ts +738 -13
- package/src/modules/channels/adapter.ts +107 -1
- package/src/modules/media/module.ts +2 -0
- package/src/modules/media/schema.ts +23 -2
- package/src/modules/media/service.ts +84 -23
- package/src/modules/orders/service.ts +3 -0
- package/src/modules/pricing/schema.ts +1 -0
- package/src/modules/pricing/schemas.ts +1 -0
- package/src/modules/pricing/service.ts +7 -2
- package/src/modules/search/adapter.ts +2 -0
- package/src/modules/search/module.ts +2 -0
- package/src/modules/search/service.ts +87 -6
- package/src/utils/id.ts +11 -0
- package/dist/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
2
|
+
import type { Kernel } from "../../../../runtime/kernel.js";
|
|
3
|
+
import type {
|
|
4
|
+
CreateEntityFieldDefinitionInput,
|
|
5
|
+
UpdateEntityFieldDefinitionInput,
|
|
6
|
+
} from "../../../../modules/catalog/service.js";
|
|
7
|
+
import {
|
|
8
|
+
archiveEntityFieldDefinitionRoute,
|
|
9
|
+
createEntityFieldDefinitionRoute,
|
|
10
|
+
listEntityFieldDefinitionsRoute,
|
|
11
|
+
updateEntityFieldDefinitionRoute,
|
|
12
|
+
} from "../../schemas/admin-entity-field-definitions.js";
|
|
13
|
+
import {
|
|
14
|
+
type AppEnv,
|
|
15
|
+
mapErrorToResponse,
|
|
16
|
+
mapErrorToStatus,
|
|
17
|
+
requirePerm,
|
|
18
|
+
} from "../../utils.js";
|
|
19
|
+
|
|
20
|
+
export function adminEntityFieldDefinitionRoutes(kernel: Kernel) {
|
|
21
|
+
const router = new OpenAPIHono<AppEnv>();
|
|
22
|
+
|
|
23
|
+
router.use("/entity-field-definitions", requirePerm("catalog:update"));
|
|
24
|
+
router.use("/entity-field-definitions/:id", requirePerm("catalog:update"));
|
|
25
|
+
router.use(
|
|
26
|
+
"/entity-field-definitions/:id/archive",
|
|
27
|
+
requirePerm("catalog:update"),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
// @ts-expect-error -- openapi handler union return type
|
|
31
|
+
router.openapi(createEntityFieldDefinitionRoute, async (c) => {
|
|
32
|
+
const result = await kernel.services.catalog.createEntityFieldDefinition(
|
|
33
|
+
c.req.valid("json") as CreateEntityFieldDefinitionInput,
|
|
34
|
+
c.get("actor"),
|
|
35
|
+
);
|
|
36
|
+
if (!result.ok)
|
|
37
|
+
return c.json(
|
|
38
|
+
mapErrorToResponse(result.error),
|
|
39
|
+
mapErrorToStatus(result.error),
|
|
40
|
+
);
|
|
41
|
+
return c.json({ data: result.value }, 201);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// @ts-expect-error -- openapi handler union return type
|
|
45
|
+
router.openapi(listEntityFieldDefinitionsRoute, async (c) => {
|
|
46
|
+
const entityType = c.req.query("entityType");
|
|
47
|
+
const result = await kernel.services.catalog.listEntityFieldDefinitions(
|
|
48
|
+
c.get("actor"),
|
|
49
|
+
entityType,
|
|
50
|
+
);
|
|
51
|
+
if (!result.ok)
|
|
52
|
+
return c.json(
|
|
53
|
+
mapErrorToResponse(result.error),
|
|
54
|
+
mapErrorToStatus(result.error),
|
|
55
|
+
);
|
|
56
|
+
return c.json({ data: result.value });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// @ts-expect-error -- openapi handler union return type
|
|
60
|
+
router.openapi(updateEntityFieldDefinitionRoute, async (c) => {
|
|
61
|
+
const result = await kernel.services.catalog.updateEntityFieldDefinition(
|
|
62
|
+
c.req.param("id"),
|
|
63
|
+
c.req.valid("json") as UpdateEntityFieldDefinitionInput,
|
|
64
|
+
c.get("actor"),
|
|
65
|
+
);
|
|
66
|
+
if (!result.ok)
|
|
67
|
+
return c.json(
|
|
68
|
+
mapErrorToResponse(result.error),
|
|
69
|
+
mapErrorToStatus(result.error),
|
|
70
|
+
);
|
|
71
|
+
return c.json({ data: result.value });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// @ts-expect-error -- openapi handler union return type
|
|
75
|
+
router.openapi(archiveEntityFieldDefinitionRoute, async (c) => {
|
|
76
|
+
const result = await kernel.services.catalog.archiveEntityFieldDefinition(
|
|
77
|
+
c.req.param("id"),
|
|
78
|
+
c.get("actor"),
|
|
79
|
+
);
|
|
80
|
+
if (!result.ok)
|
|
81
|
+
return c.json(
|
|
82
|
+
mapErrorToResponse(result.error),
|
|
83
|
+
mapErrorToStatus(result.error),
|
|
84
|
+
);
|
|
85
|
+
return c.json({ data: result.value });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return router;
|
|
89
|
+
}
|
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
listEntitiesRoute,
|
|
20
20
|
getEntityRoute,
|
|
21
21
|
getEntityAttributesRoute,
|
|
22
|
+
listFieldOwnershipRoute,
|
|
23
|
+
setFieldOwnershipRoute,
|
|
22
24
|
listCategoriesRoute,
|
|
23
25
|
listBrandsRoute,
|
|
24
26
|
deleteEntityRoute,
|
|
@@ -34,6 +36,8 @@ import {
|
|
|
34
36
|
createEntityRoute,
|
|
35
37
|
updateEntityRoute,
|
|
36
38
|
setEntityAttributesRoute,
|
|
39
|
+
approveCustomFieldRoute,
|
|
40
|
+
rejectCustomFieldRoute,
|
|
37
41
|
createCategoryRoute,
|
|
38
42
|
archiveCategoryRoute,
|
|
39
43
|
restoreCategoryRoute,
|
|
@@ -54,11 +58,58 @@ import {
|
|
|
54
58
|
parseInclude,
|
|
55
59
|
parsePagination,
|
|
56
60
|
parseSort,
|
|
61
|
+
requirePerm,
|
|
57
62
|
} from "../utils.js";
|
|
58
63
|
|
|
59
64
|
export function catalogRoutes(kernel: Kernel) {
|
|
60
65
|
const router = new OpenAPIHono<AppEnv>();
|
|
61
66
|
|
|
67
|
+
router.use("/entities/:id/custom-fields/:fieldName/approve", requirePerm("catalog:update"));
|
|
68
|
+
router.use("/entities/:id/custom-fields/:fieldName/reject", requirePerm("catalog:update"));
|
|
69
|
+
|
|
70
|
+
// @ts-expect-error -- openapi handler union return type
|
|
71
|
+
router.openapi(listFieldOwnershipRoute, async (c) => {
|
|
72
|
+
const result = await kernel.services.catalog.listFieldOwnership(
|
|
73
|
+
c.req.param("id"),
|
|
74
|
+
c.get("actor"),
|
|
75
|
+
undefined,
|
|
76
|
+
c.req.query("storeId"),
|
|
77
|
+
);
|
|
78
|
+
if (!result.ok) {
|
|
79
|
+
return c.json(
|
|
80
|
+
mapErrorToResponse(result.error),
|
|
81
|
+
mapErrorToStatus(result.error),
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return c.json({ data: result.value });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// @ts-expect-error -- openapi handler union return type
|
|
88
|
+
router.openapi(setFieldOwnershipRoute, async (c) => {
|
|
89
|
+
const body = c.req.valid("json") as {
|
|
90
|
+
fieldPath: string;
|
|
91
|
+
owner: "platform" | "store" | "shared";
|
|
92
|
+
storeId?: string | null;
|
|
93
|
+
variantId?: string | null;
|
|
94
|
+
};
|
|
95
|
+
const result = await kernel.services.catalog.setFieldOwner(
|
|
96
|
+
c.req.param("id"),
|
|
97
|
+
body.fieldPath,
|
|
98
|
+
body.storeId ?? null,
|
|
99
|
+
body.owner,
|
|
100
|
+
c.get("actor"),
|
|
101
|
+
undefined,
|
|
102
|
+
body.variantId ?? null,
|
|
103
|
+
);
|
|
104
|
+
if (!result.ok) {
|
|
105
|
+
return c.json(
|
|
106
|
+
mapErrorToResponse(result.error),
|
|
107
|
+
mapErrorToStatus(result.error),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return c.json({ data: { updated: true } });
|
|
111
|
+
});
|
|
112
|
+
|
|
62
113
|
// @ts-expect-error -- openapi handler union return type
|
|
63
114
|
router.openapi(listEntitiesRoute, async (c) => {
|
|
64
115
|
try {
|
|
@@ -101,6 +152,7 @@ export function catalogRoutes(kernel: Kernel) {
|
|
|
101
152
|
includeMedia: include.has("media"),
|
|
102
153
|
includeInventory: include.has("inventory"),
|
|
103
154
|
includePricing: include.has("pricing"),
|
|
155
|
+
includeCustomFields: include.has("customFields"),
|
|
104
156
|
};
|
|
105
157
|
const hydrated = await Promise.all(
|
|
106
158
|
result.value.items.map(async (item) => {
|
|
@@ -143,6 +195,7 @@ export function catalogRoutes(kernel: Kernel) {
|
|
|
143
195
|
includeMedia: include.has("media"),
|
|
144
196
|
includeInventory: include.has("inventory"),
|
|
145
197
|
includePricing: include.has("pricing"),
|
|
198
|
+
includeCustomFields: include.has("customFields"),
|
|
146
199
|
};
|
|
147
200
|
|
|
148
201
|
const actor = c.get("actor");
|
|
@@ -268,6 +321,38 @@ export function catalogRoutes(kernel: Kernel) {
|
|
|
268
321
|
return c.json({ data: { updated: true } });
|
|
269
322
|
});
|
|
270
323
|
|
|
324
|
+
// @ts-expect-error -- openapi handler union return type
|
|
325
|
+
router.openapi(approveCustomFieldRoute, async (c) => {
|
|
326
|
+
const result = await kernel.services.catalog.approveCustomField(
|
|
327
|
+
c.req.param("id"),
|
|
328
|
+
c.req.param("fieldName"),
|
|
329
|
+
c.req.query("locale") ?? "en",
|
|
330
|
+
c.get("actor"),
|
|
331
|
+
);
|
|
332
|
+
if (!result.ok)
|
|
333
|
+
return c.json(
|
|
334
|
+
mapErrorToResponse(result.error),
|
|
335
|
+
mapErrorToStatus(result.error),
|
|
336
|
+
);
|
|
337
|
+
return c.json({ data: result.value });
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// @ts-expect-error -- openapi handler union return type
|
|
341
|
+
router.openapi(rejectCustomFieldRoute, async (c) => {
|
|
342
|
+
const result = await kernel.services.catalog.rejectCustomField(
|
|
343
|
+
c.req.param("id"),
|
|
344
|
+
c.req.param("fieldName"),
|
|
345
|
+
c.req.query("locale") ?? "en",
|
|
346
|
+
c.get("actor"),
|
|
347
|
+
);
|
|
348
|
+
if (!result.ok)
|
|
349
|
+
return c.json(
|
|
350
|
+
mapErrorToResponse(result.error),
|
|
351
|
+
mapErrorToStatus(result.error),
|
|
352
|
+
);
|
|
353
|
+
return c.json({ data: result.value });
|
|
354
|
+
});
|
|
355
|
+
|
|
271
356
|
// @ts-expect-error -- openapi handler union return type
|
|
272
357
|
router.openapi(getEntityAttributesRoute, async (c) => {
|
|
273
358
|
const result = await kernel.services.catalog.getAttributes(
|
|
@@ -22,7 +22,8 @@ import type { PluginDb } from "../../../kernel/database/plugin-types.js";
|
|
|
22
22
|
import { type AppEnv, mapErrorToResponse, mapErrorToStatus } from "../utils.js";
|
|
23
23
|
import { isCommerceError } from "../../../kernel/errors.js";
|
|
24
24
|
import { assertPermission } from "../../../auth/permissions.js";
|
|
25
|
-
import {
|
|
25
|
+
import { resolveOrgId } from "../../../auth/org.js";
|
|
26
|
+
import { makeDeterministicId, makeId } from "../../../utils/id.js";
|
|
26
27
|
import type { ShippingAddress } from "../../../modules/shipping/calculator.js";
|
|
27
28
|
import type { Actor } from "../../../auth/types.js";
|
|
28
29
|
|
|
@@ -126,8 +127,12 @@ export function checkoutRoutes(kernel: Kernel) {
|
|
|
126
127
|
}
|
|
127
128
|
}
|
|
128
129
|
|
|
130
|
+
const orderId = body.idempotencyKey
|
|
131
|
+
? await makeDeterministicId(`checkout:${resolveOrgId(actor)}:${body.idempotencyKey}`)
|
|
132
|
+
: makeId();
|
|
129
133
|
const checkoutData: CheckoutData = {
|
|
130
134
|
checkoutId: makeId(),
|
|
135
|
+
orderId,
|
|
131
136
|
cartId: body.cartId,
|
|
132
137
|
currency: body.currency ?? "USD",
|
|
133
138
|
paymentMethodId: body.paymentMethodId,
|
|
@@ -235,6 +240,7 @@ export function checkoutRoutes(kernel: Kernel) {
|
|
|
235
240
|
);
|
|
236
241
|
|
|
237
242
|
const orderPayload = {
|
|
243
|
+
id: processed.orderId ?? orderId,
|
|
238
244
|
...(body.idempotencyKey !== undefined
|
|
239
245
|
? { idempotencyKey: body.idempotencyKey }
|
|
240
246
|
: {}),
|
|
@@ -3,6 +3,8 @@ import type { PgDatabase, PgQueryResultHKT } from "drizzle-orm/pg-core";
|
|
|
3
3
|
import type { Kernel } from "../../../runtime/kernel.js";
|
|
4
4
|
import { type AppEnv, mapErrorToResponse, mapErrorToStatus } from "../utils.js";
|
|
5
5
|
import { processedWebhookEvents } from "../../../modules/webhooks/schema.js";
|
|
6
|
+
import type { Actor } from "../../../auth/types.js";
|
|
7
|
+
import { toCommerceError } from "../../../kernel/errors.js";
|
|
6
8
|
|
|
7
9
|
type Db = PgDatabase<PgQueryResultHKT, Record<string, unknown>>;
|
|
8
10
|
|
|
@@ -16,44 +18,62 @@ export function paymentRoutes(kernel: Kernel) {
|
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
const event = result.value;
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
// Row already existed — this is a duplicate delivery
|
|
37
|
-
return c.json({ data: { received: true, duplicate: true } });
|
|
38
|
-
}
|
|
21
|
+
const outcome = await kernel.database.transaction(async (transaction) => {
|
|
22
|
+
const db = transaction as Db;
|
|
23
|
+
|
|
24
|
+
// Claim and process in one transaction. If reconciliation throws, the
|
|
25
|
+
// claim rolls back so Stripe can safely retry instead of receiving a
|
|
26
|
+
// false 2xx for an event that never changed commerce state.
|
|
27
|
+
const [inserted] = await db
|
|
28
|
+
.insert(processedWebhookEvents)
|
|
29
|
+
.values({
|
|
30
|
+
eventId: event.id,
|
|
31
|
+
provider: "stripe",
|
|
32
|
+
eventType: event.type,
|
|
33
|
+
})
|
|
34
|
+
.onConflictDoNothing()
|
|
35
|
+
.returning({ id: processedWebhookEvents.id });
|
|
36
|
+
|
|
37
|
+
if (!inserted) return { duplicate: true };
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
39
|
+
if (event.type === "payment_intent.succeeded") {
|
|
40
|
+
const data = event.data as Record<string, unknown> | undefined;
|
|
41
|
+
const metadata = data?.metadata as Record<string, unknown> | undefined;
|
|
42
|
+
if (typeof metadata?.orderId === "string") {
|
|
43
|
+
const actor: Actor = {
|
|
44
|
+
type: "api_key",
|
|
45
|
+
userId: "stripe-webhook",
|
|
46
|
+
email: null,
|
|
47
|
+
name: "Stripe webhook",
|
|
48
|
+
vendorId: null,
|
|
49
|
+
organizationId:
|
|
50
|
+
typeof metadata.organizationId === "string"
|
|
51
|
+
? metadata.organizationId
|
|
52
|
+
: null,
|
|
53
|
+
role: "system",
|
|
54
|
+
permissions: ["orders:update"],
|
|
55
|
+
};
|
|
56
|
+
const changed = await kernel.services.orders.changeStatus(
|
|
57
|
+
{
|
|
58
|
+
orderId: metadata.orderId,
|
|
59
|
+
newStatus: "confirmed",
|
|
60
|
+
reason: "stripe_webhook_payment_intent_succeeded",
|
|
61
|
+
},
|
|
62
|
+
actor,
|
|
63
|
+
{
|
|
64
|
+
tx: transaction,
|
|
65
|
+
actor,
|
|
66
|
+
requestId: c.get("requestId"),
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
if (!changed.ok) throw toCommerceError(changed.error);
|
|
70
|
+
}
|
|
53
71
|
}
|
|
54
|
-
}
|
|
55
72
|
|
|
56
|
-
|
|
73
|
+
return { duplicate: false };
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return c.json({ data: { received: true, ...(outcome.duplicate ? { duplicate: true } : {}) } });
|
|
57
77
|
});
|
|
58
78
|
|
|
59
79
|
return router;
|
|
@@ -3,6 +3,8 @@ import type { Kernel } from "../../../runtime/kernel.js";
|
|
|
3
3
|
import { searchRoute, suggestRoute } from "../schemas/search.js";
|
|
4
4
|
import { type AppEnv, mapErrorToResponse, mapErrorToStatus } from "../utils.js";
|
|
5
5
|
|
|
6
|
+
const SAFE_ATTRIBUTE_NAME = /^[A-Za-z0-9_-]+$/;
|
|
7
|
+
|
|
6
8
|
export function searchRoutes(kernel: Kernel) {
|
|
7
9
|
const router = new OpenAPIHono<AppEnv>();
|
|
8
10
|
|
|
@@ -21,6 +23,19 @@ export function searchRoutes(kernel: Kernel) {
|
|
|
21
23
|
.map((facet) => facet.trim())
|
|
22
24
|
.filter(Boolean);
|
|
23
25
|
|
|
26
|
+
const attributes: Record<string, string | string[]> = {};
|
|
27
|
+
for (const [key, values] of Object.entries(c.req.queries())) {
|
|
28
|
+
if (!key.startsWith("attr.")) continue;
|
|
29
|
+
const name = key.slice("attr.".length);
|
|
30
|
+
if (!SAFE_ATTRIBUTE_NAME.test(name)) {
|
|
31
|
+
return c.json(
|
|
32
|
+
{ error: "invalid_argument", field: key, message: `Invalid attribute filter name: ${name}` },
|
|
33
|
+
422,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
attributes[name] = values.length === 1 ? values[0]! : values;
|
|
37
|
+
}
|
|
38
|
+
|
|
24
39
|
const result = await kernel.services.search.query({
|
|
25
40
|
query: q,
|
|
26
41
|
page,
|
|
@@ -30,6 +45,7 @@ export function searchRoutes(kernel: Kernel) {
|
|
|
30
45
|
...(category ? { category } : {}),
|
|
31
46
|
...(brand ? { brand } : {}),
|
|
32
47
|
...(status ? { status } : {}),
|
|
48
|
+
...(Object.keys(attributes).length > 0 ? { attributes } : {}),
|
|
33
49
|
},
|
|
34
50
|
...(facets.length > 0 ? { facets } : {}),
|
|
35
51
|
}, { actor: c.get("actor"), tx: null, requestId: "" });
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createRoute, z } from "@hono/zod-openapi";
|
|
2
|
+
import { errorResponses } from "./shared.js";
|
|
3
|
+
|
|
4
|
+
const ProposalResponse = z.object({
|
|
5
|
+
data: z.object({
|
|
6
|
+
items: z.array(z.any()),
|
|
7
|
+
total: z.number(),
|
|
8
|
+
page: z.number(),
|
|
9
|
+
limit: z.number(),
|
|
10
|
+
}),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const ListQuery = z.object({
|
|
14
|
+
entityType: z.string().min(1).optional(),
|
|
15
|
+
page: z.string().max(10).optional(),
|
|
16
|
+
limit: z.string().max(10).optional(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const listCustomFieldProposalsRoute = createRoute({
|
|
20
|
+
method: "get",
|
|
21
|
+
path: "/custom-field-proposals",
|
|
22
|
+
tags: ["Admin"],
|
|
23
|
+
summary: "List proposed custom-field values",
|
|
24
|
+
request: { query: ListQuery },
|
|
25
|
+
responses: {
|
|
26
|
+
200: { content: { "application/json": { schema: ProposalResponse } }, description: "Proposed custom fields." },
|
|
27
|
+
...errorResponses,
|
|
28
|
+
},
|
|
29
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { createRoute, z } from "@hono/zod-openapi";
|
|
2
|
+
import { errorResponses } from "./shared.js";
|
|
3
|
+
|
|
4
|
+
const FieldType = z.enum([
|
|
5
|
+
"text",
|
|
6
|
+
"number",
|
|
7
|
+
"boolean",
|
|
8
|
+
"date",
|
|
9
|
+
"json",
|
|
10
|
+
"relation",
|
|
11
|
+
"select",
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
const CreateBody = z
|
|
15
|
+
.object({
|
|
16
|
+
entityType: z.string().min(1),
|
|
17
|
+
name: z.string().min(1),
|
|
18
|
+
type: FieldType,
|
|
19
|
+
unit: z.string().nullable().optional(),
|
|
20
|
+
options: z.array(z.string()).nullable().optional(),
|
|
21
|
+
target: z.string().nullable().optional(),
|
|
22
|
+
filterable: z.boolean().optional(),
|
|
23
|
+
localized: z.boolean().optional(),
|
|
24
|
+
sortOrder: z.number().int().optional(),
|
|
25
|
+
})
|
|
26
|
+
.openapi("CreateEntityFieldDefinitionRequest");
|
|
27
|
+
|
|
28
|
+
const UpdateBody = z
|
|
29
|
+
.object({
|
|
30
|
+
entityType: z.string().min(1).optional(),
|
|
31
|
+
name: z.string().min(1).optional(),
|
|
32
|
+
type: FieldType.optional(),
|
|
33
|
+
unit: z.string().nullable().optional(),
|
|
34
|
+
options: z.array(z.string()).nullable().optional(),
|
|
35
|
+
target: z.string().nullable().optional(),
|
|
36
|
+
filterable: z.boolean().optional(),
|
|
37
|
+
localized: z.boolean().optional(),
|
|
38
|
+
sortOrder: z.number().int().optional(),
|
|
39
|
+
})
|
|
40
|
+
.openapi("UpdateEntityFieldDefinitionRequest");
|
|
41
|
+
|
|
42
|
+
const IdParam = z.object({ id: z.uuid() });
|
|
43
|
+
const ListQuery = z.object({ entityType: z.string().min(1).optional() });
|
|
44
|
+
const DataResponse = z
|
|
45
|
+
.object({ data: z.any() })
|
|
46
|
+
.openapi("EntityFieldDefinitionResponse");
|
|
47
|
+
|
|
48
|
+
export const createEntityFieldDefinitionRoute = createRoute({
|
|
49
|
+
method: "post",
|
|
50
|
+
path: "/entity-field-definitions",
|
|
51
|
+
tags: ["Admin"],
|
|
52
|
+
summary: "Create a runtime entity field definition",
|
|
53
|
+
request: {
|
|
54
|
+
body: {
|
|
55
|
+
content: { "application/json": { schema: CreateBody } },
|
|
56
|
+
required: true,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
responses: {
|
|
60
|
+
201: {
|
|
61
|
+
content: { "application/json": { schema: DataResponse } },
|
|
62
|
+
description: "Definition created.",
|
|
63
|
+
},
|
|
64
|
+
...errorResponses,
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export const listEntityFieldDefinitionsRoute = createRoute({
|
|
69
|
+
method: "get",
|
|
70
|
+
path: "/entity-field-definitions",
|
|
71
|
+
tags: ["Admin"],
|
|
72
|
+
summary: "List runtime entity field definitions",
|
|
73
|
+
request: { query: ListQuery },
|
|
74
|
+
responses: {
|
|
75
|
+
200: {
|
|
76
|
+
content: { "application/json": { schema: DataResponse } },
|
|
77
|
+
description: "Definitions.",
|
|
78
|
+
},
|
|
79
|
+
...errorResponses,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
export const updateEntityFieldDefinitionRoute = createRoute({
|
|
84
|
+
method: "patch",
|
|
85
|
+
path: "/entity-field-definitions/{id}",
|
|
86
|
+
tags: ["Admin"],
|
|
87
|
+
summary: "Update a runtime entity field definition",
|
|
88
|
+
request: {
|
|
89
|
+
params: IdParam,
|
|
90
|
+
body: {
|
|
91
|
+
content: { "application/json": { schema: UpdateBody } },
|
|
92
|
+
required: true,
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
responses: {
|
|
96
|
+
200: {
|
|
97
|
+
content: { "application/json": { schema: DataResponse } },
|
|
98
|
+
description: "Definition updated.",
|
|
99
|
+
},
|
|
100
|
+
...errorResponses,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
export const archiveEntityFieldDefinitionRoute = createRoute({
|
|
105
|
+
method: "post",
|
|
106
|
+
path: "/entity-field-definitions/{id}/archive",
|
|
107
|
+
tags: ["Admin"],
|
|
108
|
+
summary: "Archive a runtime entity field definition",
|
|
109
|
+
request: { params: IdParam },
|
|
110
|
+
responses: {
|
|
111
|
+
200: {
|
|
112
|
+
content: { "application/json": { schema: DataResponse } },
|
|
113
|
+
description: "Definition archived.",
|
|
114
|
+
},
|
|
115
|
+
...errorResponses,
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
export type CreateEntityFieldDefinitionBody = z.infer<typeof CreateBody>;
|
|
120
|
+
export type UpdateEntityFieldDefinitionBody = z.infer<typeof UpdateBody>;
|
|
@@ -19,6 +19,17 @@ import {
|
|
|
19
19
|
|
|
20
20
|
const DataResponse = CatalogEntityResponse;
|
|
21
21
|
const DataWithPaginationResponse = CatalogEntityListResponse;
|
|
22
|
+
const FieldOwnershipSchema = z.object({
|
|
23
|
+
id: z.string(),
|
|
24
|
+
organizationId: z.string(),
|
|
25
|
+
entityId: z.string(),
|
|
26
|
+
variantId: z.string().nullable(),
|
|
27
|
+
storeId: z.string().nullable(),
|
|
28
|
+
fieldPath: z.string(),
|
|
29
|
+
owner: z.enum(["platform", "store", "shared"]),
|
|
30
|
+
updatedAt: z.string(),
|
|
31
|
+
}).openapi("CatalogFieldOwnership");
|
|
32
|
+
const FieldOwnershipListResponse = z.object({ data: z.array(FieldOwnershipSchema) }).openapi("CatalogFieldOwnershipListResponse");
|
|
22
33
|
|
|
23
34
|
// ─── Path Params ────────────────────────────────────────────────────────────
|
|
24
35
|
|
|
@@ -35,6 +46,15 @@ const EntityIdLocaleParam = z.object({
|
|
|
35
46
|
locale: z.string().min(1).openapi({ example: "en" }),
|
|
36
47
|
});
|
|
37
48
|
|
|
49
|
+
const EntityCustomFieldParam = z.object({
|
|
50
|
+
id: z.uuid().openapi({ example: "550e8400-e29b-41d4-a716-446655440000" }),
|
|
51
|
+
fieldName: z.string().min(1).openapi({ example: "warranty" }),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const CustomFieldReviewQuery = z.object({
|
|
55
|
+
locale: z.string().min(1).optional().openapi({ example: "en" }),
|
|
56
|
+
});
|
|
57
|
+
|
|
38
58
|
const CategoryIdParam = z.object({
|
|
39
59
|
categoryId: z.uuid().openapi({ example: "550e8400-e29b-41d4-a716-446655440000" }),
|
|
40
60
|
});
|
|
@@ -104,6 +124,48 @@ export const getEntityAttributesRoute = createRoute({
|
|
|
104
124
|
},
|
|
105
125
|
});
|
|
106
126
|
|
|
127
|
+
export const listFieldOwnershipRoute = createRoute({
|
|
128
|
+
method: "get",
|
|
129
|
+
path: "/entities/{id}/field-ownership",
|
|
130
|
+
tags: ["Catalog"],
|
|
131
|
+
summary: "List field ownership for a catalog entity",
|
|
132
|
+
request: {
|
|
133
|
+
params: EntityIdParam,
|
|
134
|
+
query: z.object({ storeId: z.string().optional() }),
|
|
135
|
+
},
|
|
136
|
+
responses: {
|
|
137
|
+
200: { content: { "application/json": { schema: FieldOwnershipListResponse } }, description: "Success" },
|
|
138
|
+
...errorResponses,
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
export const setFieldOwnershipRoute = createRoute({
|
|
143
|
+
method: "put",
|
|
144
|
+
path: "/entities/{id}/field-ownership",
|
|
145
|
+
tags: ["Catalog"],
|
|
146
|
+
summary: "Set field ownership for a catalog entity",
|
|
147
|
+
request: {
|
|
148
|
+
params: EntityIdParam,
|
|
149
|
+
body: {
|
|
150
|
+
content: {
|
|
151
|
+
"application/json": {
|
|
152
|
+
schema: z.object({
|
|
153
|
+
fieldPath: z.string().min(1),
|
|
154
|
+
owner: z.enum(["platform", "store", "shared"]),
|
|
155
|
+
storeId: z.string().min(1).nullable().optional(),
|
|
156
|
+
variantId: z.uuid().nullable().optional(),
|
|
157
|
+
}),
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
required: true,
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
responses: {
|
|
164
|
+
200: { content: { "application/json": { schema: z.object({ data: z.object({ updated: z.literal(true) }) }) } }, description: "Updated" },
|
|
165
|
+
...errorResponses,
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
|
|
107
169
|
export const listCategoriesRoute = createRoute({
|
|
108
170
|
method: "get",
|
|
109
171
|
path: "/categories",
|
|
@@ -312,6 +374,36 @@ export const setEntityAttributesRoute = createRoute({
|
|
|
312
374
|
},
|
|
313
375
|
});
|
|
314
376
|
|
|
377
|
+
export const approveCustomFieldRoute = createRoute({
|
|
378
|
+
method: "post",
|
|
379
|
+
path: "/entities/{id}/custom-fields/{fieldName}/approve",
|
|
380
|
+
tags: ["Catalog"],
|
|
381
|
+
summary: "Approve a proposed custom-field value",
|
|
382
|
+
request: {
|
|
383
|
+
params: EntityCustomFieldParam,
|
|
384
|
+
query: CustomFieldReviewQuery,
|
|
385
|
+
},
|
|
386
|
+
responses: {
|
|
387
|
+
200: { content: { "application/json": { schema: DataResponse } }, description: "Approved" },
|
|
388
|
+
...errorResponses,
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
export const rejectCustomFieldRoute = createRoute({
|
|
393
|
+
method: "post",
|
|
394
|
+
path: "/entities/{id}/custom-fields/{fieldName}/reject",
|
|
395
|
+
tags: ["Catalog"],
|
|
396
|
+
summary: "Reject a proposed custom-field value",
|
|
397
|
+
request: {
|
|
398
|
+
params: EntityCustomFieldParam,
|
|
399
|
+
query: CustomFieldReviewQuery,
|
|
400
|
+
},
|
|
401
|
+
responses: {
|
|
402
|
+
200: { content: { "application/json": { schema: DataResponse } }, description: "Rejected" },
|
|
403
|
+
...errorResponses,
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
|
|
315
407
|
export const createCategoryRoute = createRoute({
|
|
316
408
|
method: "post",
|
|
317
409
|
path: "/categories",
|
|
@@ -8,6 +8,8 @@ export const searchRoute = createRoute({
|
|
|
8
8
|
path: "/",
|
|
9
9
|
tags: ["Search"],
|
|
10
10
|
summary: "Search catalog entities",
|
|
11
|
+
description:
|
|
12
|
+
"Attribute filters use repeatable `attr.<name>=<value>` query parameters: repeated values for one name are ORed, distinct names are ANDed. Attribute names must match `^[A-Za-z0-9_-]+$`; anything else returns 422. Request attribute facet counts by naming the attribute in `facets`.",
|
|
11
13
|
request: {
|
|
12
14
|
query: z.object({
|
|
13
15
|
q: z.string().max(500).optional(),
|
|
@@ -11,6 +11,9 @@ const statusByCode: Record<string, ContentfulStatusCode> = {
|
|
|
11
11
|
CONFLICT: 409,
|
|
12
12
|
INVALID_TRANSITION: 422,
|
|
13
13
|
ORG_RESOLUTION_FAILED: 503,
|
|
14
|
+
WEBHOOK_SIGNATURE_MISSING: 400,
|
|
15
|
+
WEBHOOK_VERIFICATION_FAILED: 400,
|
|
16
|
+
WEBHOOK_SECRET_MISSING: 503,
|
|
14
17
|
};
|
|
15
18
|
|
|
16
19
|
export function mapErrorToStatus(error: unknown): ContentfulStatusCode {
|