@porulle/core 0.10.0 → 0.10.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.
- package/dist/interfaces/rest/routes/promotions.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/promotions.js +6 -1
- package/dist/interfaces/rest/schemas/promotions.d.ts +34 -1
- package/dist/interfaces/rest/schemas/promotions.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/promotions.js +26 -2
- package/dist/kernel/database/adapter.d.ts +8 -0
- package/dist/kernel/database/adapter.d.ts.map +1 -1
- package/dist/kernel/database/migrate.d.ts +1 -1
- package/dist/kernel/database/migrate.d.ts.map +1 -1
- package/dist/kernel/database/migrate.js +5 -2
- package/dist/runtime/commerce.d.ts.map +1 -1
- package/dist/runtime/commerce.js +9 -0
- package/package.json +3 -3
- package/src/interfaces/rest/routes/promotions.ts +6 -1
- package/src/interfaces/rest/schemas/promotions.ts +27 -2
- package/src/kernel/database/adapter.ts +8 -0
- package/src/kernel/database/migrate.ts +8 -2
- package/src/runtime/commerce.ts +10 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"promotions.d.ts","sourceRoot":"","sources":["../../../../src/interfaces/rest/routes/promotions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAGzD,OAAO,EAAE,KAAK,MAAM,EAAqD,MAAM,aAAa,CAAC;AAG7F,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"promotions.d.ts","sourceRoot":"","sources":["../../../../src/interfaces/rest/routes/promotions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAGzD,OAAO,EAAE,KAAK,MAAM,EAAqD,MAAM,aAAa,CAAC;AAG7F,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,gCAiH7C"}
|
|
@@ -46,7 +46,12 @@ export function promotionRoutes(kernel) {
|
|
|
46
46
|
const payload = c.req.valid("json");
|
|
47
47
|
const actor = c.get("actor");
|
|
48
48
|
const orgId = resolveOrgId(actor);
|
|
49
|
-
|
|
49
|
+
// Apply (not just validate) so the response carries the authoritative
|
|
50
|
+
// discount the cart would receive — the same computation checkout runs.
|
|
51
|
+
// Returning only the promotion forces callers to re-derive the amount, which
|
|
52
|
+
// drifts from checkout and shows customers a wrong number. apply() still
|
|
53
|
+
// Err's on an invalid / inapplicable code (it validates first).
|
|
54
|
+
const result = await kernel.services.promotions.apply(payload.code, {
|
|
50
55
|
orgId,
|
|
51
56
|
currency: payload.currency,
|
|
52
57
|
subtotal: payload.subtotal,
|
|
@@ -48,6 +48,24 @@ export declare const ValidatePromotionBodySchema: z.ZodObject<{
|
|
|
48
48
|
export declare const PromotionResponseSchema: z.ZodObject<{
|
|
49
49
|
data: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
50
50
|
}, z.core.$strip>;
|
|
51
|
+
export declare const PromotionValidationResultSchema: z.ZodObject<{
|
|
52
|
+
data: z.ZodObject<{
|
|
53
|
+
totalDiscount: z.ZodNumber;
|
|
54
|
+
freeShipping: z.ZodBoolean;
|
|
55
|
+
applied: z.ZodArray<z.ZodObject<{
|
|
56
|
+
promotionId: z.ZodString;
|
|
57
|
+
code: z.ZodOptional<z.ZodString>;
|
|
58
|
+
type: z.ZodString;
|
|
59
|
+
discountAmount: z.ZodNumber;
|
|
60
|
+
freeShipping: z.ZodBoolean;
|
|
61
|
+
description: z.ZodString;
|
|
62
|
+
}, z.core.$strip>>;
|
|
63
|
+
rejectedCodes: z.ZodArray<z.ZodObject<{
|
|
64
|
+
code: z.ZodString;
|
|
65
|
+
reason: z.ZodString;
|
|
66
|
+
}, z.core.$strip>>;
|
|
67
|
+
}, z.core.$strip>;
|
|
68
|
+
}, z.core.$strip>;
|
|
51
69
|
export declare const listPromotionsRoute: {
|
|
52
70
|
method: "get";
|
|
53
71
|
path: "/";
|
|
@@ -371,7 +389,22 @@ export declare const validatePromotionRoute: {
|
|
|
371
389
|
content: {
|
|
372
390
|
"application/json": {
|
|
373
391
|
schema: z.ZodObject<{
|
|
374
|
-
data: z.
|
|
392
|
+
data: z.ZodObject<{
|
|
393
|
+
totalDiscount: z.ZodNumber;
|
|
394
|
+
freeShipping: z.ZodBoolean;
|
|
395
|
+
applied: z.ZodArray<z.ZodObject<{
|
|
396
|
+
promotionId: z.ZodString;
|
|
397
|
+
code: z.ZodOptional<z.ZodString>;
|
|
398
|
+
type: z.ZodString;
|
|
399
|
+
discountAmount: z.ZodNumber;
|
|
400
|
+
freeShipping: z.ZodBoolean;
|
|
401
|
+
description: z.ZodString;
|
|
402
|
+
}, z.core.$strip>>;
|
|
403
|
+
rejectedCodes: z.ZodArray<z.ZodObject<{
|
|
404
|
+
code: z.ZodString;
|
|
405
|
+
reason: z.ZodString;
|
|
406
|
+
}, z.core.$strip>>;
|
|
407
|
+
}, z.core.$strip>;
|
|
375
408
|
}, z.core.$strip>;
|
|
376
409
|
};
|
|
377
410
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"promotions.d.ts","sourceRoot":"","sources":["../../../../src/interfaces/rest/schemas/promotions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAe,MAAM,mBAAmB,CAAC;AAKnD,OAAO,EACL,yBAAyB,EAE1B,MAAM,wCAAwC,CAAC;AAChD,OAAO,EAAE,yBAAyB,EAAE,CAAC;AAErC,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAErC,CAAC;AAEF,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;iBAaF,CAAC;AAIvC,eAAO,MAAM,uBAAuB;;iBAEL,CAAC;
|
|
1
|
+
{"version":3,"file":"promotions.d.ts","sourceRoot":"","sources":["../../../../src/interfaces/rest/schemas/promotions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAe,MAAM,mBAAmB,CAAC;AAKnD,OAAO,EACL,yBAAyB,EAE1B,MAAM,wCAAwC,CAAC;AAChD,OAAO,EAAE,yBAAyB,EAAE,CAAC;AAErC,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAErC,CAAC;AAEF,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;iBAaF,CAAC;AAIvC,eAAO,MAAM,uBAAuB;;iBAEL,CAAC;AAKhC,eAAO,MAAM,+BAA+B;;;;;;;;;;;;;;;;;iBAoBL,CAAC;AAUxC,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiB9B,CAAC;AAEH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoB/B,CAAC;AAEH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoBjC,CAAC;AAEH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsB/B,CAAC;AAEH,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAenC,CAAC"}
|
|
@@ -22,6 +22,30 @@ export const ValidatePromotionBodySchema = z.object({
|
|
|
22
22
|
export const PromotionResponseSchema = z.object({
|
|
23
23
|
data: z.record(z.string(), z.unknown()),
|
|
24
24
|
}).openapi("PromotionResponse");
|
|
25
|
+
// The authoritative result of applying a code to a cart — the SAME computation
|
|
26
|
+
// checkout uses, so a storefront can show the exact discount the order will get
|
|
27
|
+
// (no client-side re-derivation, which would drift from checkout).
|
|
28
|
+
export const PromotionValidationResultSchema = z.object({
|
|
29
|
+
data: z.object({
|
|
30
|
+
totalDiscount: z.number().openapi({
|
|
31
|
+
example: 1000,
|
|
32
|
+
description: "Discount this code applies to the cart, in the cart's minor units (e.g. cents).",
|
|
33
|
+
}),
|
|
34
|
+
freeShipping: z.boolean().openapi({ example: false }),
|
|
35
|
+
applied: z.array(z.object({
|
|
36
|
+
promotionId: z.string(),
|
|
37
|
+
code: z.string().optional(),
|
|
38
|
+
type: z.string(),
|
|
39
|
+
discountAmount: z.number(),
|
|
40
|
+
freeShipping: z.boolean(),
|
|
41
|
+
description: z.string(),
|
|
42
|
+
})).openapi({ description: "The promotion(s) applied by this code." }),
|
|
43
|
+
rejectedCodes: z.array(z.object({
|
|
44
|
+
code: z.string(),
|
|
45
|
+
reason: z.string(),
|
|
46
|
+
})),
|
|
47
|
+
}),
|
|
48
|
+
}).openapi("PromotionValidationResult");
|
|
25
49
|
// ─── Path Params ────────────────────────────────────────────────────────────
|
|
26
50
|
const PromotionIdParam = z.object({
|
|
27
51
|
id: z.uuid().openapi({ example: "550e8400-e29b-41d4-a716-446655440000" }),
|
|
@@ -81,8 +105,8 @@ export const validatePromotionRoute = createRoute({
|
|
|
81
105
|
},
|
|
82
106
|
responses: {
|
|
83
107
|
200: {
|
|
84
|
-
content: { "application/json": { schema:
|
|
85
|
-
description: "
|
|
108
|
+
content: { "application/json": { schema: PromotionValidationResultSchema } },
|
|
109
|
+
description: "The code is valid for this cart: the authoritative discount it applies (same computation as checkout).",
|
|
86
110
|
},
|
|
87
111
|
...errorResponses,
|
|
88
112
|
},
|
|
@@ -10,6 +10,14 @@ export interface DatabaseAdapter<TDatabase = unknown, TTransaction = unknown> {
|
|
|
10
10
|
provider: string;
|
|
11
11
|
db: TDatabase;
|
|
12
12
|
transaction<T>(fn: (tx: TTransaction) => Promise<T>): Promise<T>;
|
|
13
|
+
/**
|
|
14
|
+
* Zero-migration adapters (e.g. PGlite) set this so the runtime pushes any
|
|
15
|
+
* plugin-declared tables (`config.customSchemas`) at boot. Plugin schemas
|
|
16
|
+
* only exist after plugins run in `defineConfig` — i.e. after this adapter was
|
|
17
|
+
* constructed — so the adapter cannot create them itself. Omit/false when the
|
|
18
|
+
* consumer manages migrations (e.g. production Postgres via drizzle-kit).
|
|
19
|
+
*/
|
|
20
|
+
autoMigrate?: boolean;
|
|
13
21
|
}
|
|
14
22
|
export interface DatabaseConnectionFactoryInput {
|
|
15
23
|
adapter: DatabaseAdapter;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../../src/kernel/database/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe,CAAC,SAAS,GAAG,OAAO,EAAE,YAAY,GAAG,OAAO;IAC1E,QAAQ,EAAE,MAAM,CAAC;IACjB,EAAE,EAAE,SAAS,CAAC;IACd,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,YAAY,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../../src/kernel/database/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe,CAAC,SAAS,GAAG,OAAO,EAAE,YAAY,GAAG,OAAO;IAC1E,QAAQ,EAAE,MAAM,CAAC;IACjB,EAAE,EAAE,SAAS,CAAC;IACd,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,YAAY,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACjE;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,8BAA8B;IAC7C,OAAO,EAAE,eAAe,CAAC;CAC1B;AASD;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,CAMpC;AAsCD,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,8BAA8B,GAAG,eAAe,CAS/F"}
|
|
@@ -54,5 +54,5 @@ export declare function getSchemaFiles(): string[];
|
|
|
54
54
|
* Drizzle instance bound to your database. `drizzle-kit` must be available
|
|
55
55
|
* (it is the standard schema tool); a clear error is thrown if it is not.
|
|
56
56
|
*/
|
|
57
|
-
export declare function pushSchema(drizzleInstance: unknown): Promise<void>;
|
|
57
|
+
export declare function pushSchema(drizzleInstance: unknown, config?: CommerceConfig): Promise<void>;
|
|
58
58
|
//# sourceMappingURL=migrate.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../../../src/kernel/database/migrate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,OAAO,KAAK,MAAM,MAAM,aAAa,CAAC;AAEtC;;;GAGG;AACH,wBAAgB,SAAS,kBAExB;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,MAAM,CAAC,EAAE,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAwB5E;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,MAAM,EAAE,CASxC;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,IAAI,MAAM,EAAE,CAKzC;AAED;;;;;;;;GAQG;AACH,wBAAsB,UAAU,
|
|
1
|
+
{"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../../../src/kernel/database/migrate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,OAAO,KAAK,MAAM,MAAM,aAAa,CAAC;AAEtC;;;GAGG;AACH,wBAAgB,SAAS,kBAExB;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,MAAM,CAAC,EAAE,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAwB5E;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,MAAM,EAAE,CASxC;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,IAAI,MAAM,EAAE,CAKzC;AAED;;;;;;;;GAQG;AACH,wBAAsB,UAAU,CAC9B,eAAe,EAAE,OAAO,EACxB,MAAM,CAAC,EAAE,cAAc,GACtB,OAAO,CAAC,IAAI,CAAC,CAqBf"}
|
|
@@ -86,7 +86,7 @@ export function getSchemaFiles() {
|
|
|
86
86
|
* Drizzle instance bound to your database. `drizzle-kit` must be available
|
|
87
87
|
* (it is the standard schema tool); a clear error is thrown if it is not.
|
|
88
88
|
*/
|
|
89
|
-
export async function pushSchema(drizzleInstance) {
|
|
89
|
+
export async function pushSchema(drizzleInstance, config) {
|
|
90
90
|
const require = createRequire(import.meta.url);
|
|
91
91
|
let drizzleKit;
|
|
92
92
|
try {
|
|
@@ -95,7 +95,10 @@ export async function pushSchema(drizzleInstance) {
|
|
|
95
95
|
catch {
|
|
96
96
|
throw new Error("pushSchema() requires `drizzle-kit` to be installed. Add it to your project: bun add -d drizzle-kit");
|
|
97
97
|
}
|
|
98
|
+
// With a config, push the merged core + plugin schema so plugin-declared
|
|
99
|
+
// tables are created too; without one, push core only (backward compatible).
|
|
100
|
+
const schemaToApply = config ? buildSchema(config) : getSchema();
|
|
98
101
|
// drizzle-kit needs the native driver result shape; unwrap a normalized db.
|
|
99
|
-
const { apply } = await drizzleKit.pushSchema(
|
|
102
|
+
const { apply } = await drizzleKit.pushSchema(schemaToApply, unwrapDb(drizzleInstance));
|
|
100
103
|
await apply();
|
|
101
104
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commerce.d.ts","sourceRoot":"","sources":["../../src/runtime/commerce.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"commerce.d.ts","sourceRoot":"","sources":["../../src/runtime/commerce.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAK1C,OAAO,EAAc,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACjE,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAkB,KAAK,gBAAgB,EAAwB,MAAM,wBAAwB,CAAC;AAErG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AACH,MAAM,WAAW,gBAAgB;IAC/B,0EAA0E;IAC1E,GAAG,EAAE,gBAAgB,CAAC;IAEtB,8DAA8D;IAC9D,MAAM,EAAE,MAAM,CAAC;IAEf,mDAAmD;IACnD,EAAE,EAAE,OAAO,CAAC;IAEZ,yDAAyD;IACzD,IAAI,EAAE,YAAY,CAAC;IAEnB,aAAa;IACb,MAAM,EAAE,MAAM,CAAC;IAEf;;;;;;;;;;;;OAYG;IACH,SAAS,CAAC,KAAK,EAAE,KAAK,GAAG,gBAAgB,CAAC;IAE1C;;;;;;;;;;OAUG;IACH,eAAe,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,GAAG,gBAAgB,CAAC;CACtE;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC,gBAAgB,CAAC,CA6C3B"}
|
package/dist/runtime/commerce.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createKernel } from "./kernel.js";
|
|
2
|
+
import { pushSchema } from "../kernel/database/migrate.js";
|
|
2
3
|
import { ensureDefaultOrg, setBootDefaultOrgId } from "../auth/org.js";
|
|
3
4
|
import { setBootStrictOrgResolution } from "../auth/strict-org-resolution.js";
|
|
4
5
|
import { createAuth } from "../auth/setup.js";
|
|
@@ -17,6 +18,14 @@ import { createLocalAPI } from "../kernel/local-api.js";
|
|
|
17
18
|
*/
|
|
18
19
|
export async function createCommerce(config) {
|
|
19
20
|
const kernel = createKernel(config);
|
|
21
|
+
// Zero-migration adapters (PGlite) push core schema at construction, but a
|
|
22
|
+
// plugin's own tables live in `config.customSchemas`, which only exist after
|
|
23
|
+
// plugins run in `defineConfig` — i.e. after the adapter was constructed.
|
|
24
|
+
// Create them now so plugin routes have their tables. Guarded on both signals
|
|
25
|
+
// so plugin-less stores and migration-managed adapters (Postgres) are untouched.
|
|
26
|
+
if (config.databaseAdapter?.autoMigrate && config.customSchemas?.length) {
|
|
27
|
+
await pushSchema(kernel.database.db, config);
|
|
28
|
+
}
|
|
20
29
|
setBootStrictOrgResolution(config.auth?.strictOrgResolution === true);
|
|
21
30
|
// Register the config-driven org ID so resolveOrgId() can use it
|
|
22
31
|
// without requiring every service to have config access.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@porulle/core",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -56,8 +56,8 @@
|
|
|
56
56
|
"eslint": "^9.39.1",
|
|
57
57
|
"typescript": "5.9.2",
|
|
58
58
|
"vitest": "^3.2.4",
|
|
59
|
-
"@porulle/
|
|
60
|
-
"@porulle/
|
|
59
|
+
"@porulle/eslint-config": "0.1.0",
|
|
60
|
+
"@porulle/typescript-config": "0.1.0"
|
|
61
61
|
},
|
|
62
62
|
"publishConfig": {
|
|
63
63
|
"access": "public"
|
|
@@ -61,7 +61,12 @@ export function promotionRoutes(kernel: Kernel) {
|
|
|
61
61
|
const actor = c.get("actor");
|
|
62
62
|
const orgId = resolveOrgId(actor);
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
// Apply (not just validate) so the response carries the authoritative
|
|
65
|
+
// discount the cart would receive — the same computation checkout runs.
|
|
66
|
+
// Returning only the promotion forces callers to re-derive the amount, which
|
|
67
|
+
// drifts from checkout and shows customers a wrong number. apply() still
|
|
68
|
+
// Err's on an invalid / inapplicable code (it validates first).
|
|
69
|
+
const result = await kernel.services.promotions.apply(payload.code, {
|
|
65
70
|
orgId,
|
|
66
71
|
currency: payload.currency,
|
|
67
72
|
subtotal: payload.subtotal,
|
|
@@ -34,6 +34,31 @@ export const PromotionResponseSchema = z.object({
|
|
|
34
34
|
data: z.record(z.string(), z.unknown()),
|
|
35
35
|
}).openapi("PromotionResponse");
|
|
36
36
|
|
|
37
|
+
// The authoritative result of applying a code to a cart — the SAME computation
|
|
38
|
+
// checkout uses, so a storefront can show the exact discount the order will get
|
|
39
|
+
// (no client-side re-derivation, which would drift from checkout).
|
|
40
|
+
export const PromotionValidationResultSchema = z.object({
|
|
41
|
+
data: z.object({
|
|
42
|
+
totalDiscount: z.number().openapi({
|
|
43
|
+
example: 1000,
|
|
44
|
+
description: "Discount this code applies to the cart, in the cart's minor units (e.g. cents).",
|
|
45
|
+
}),
|
|
46
|
+
freeShipping: z.boolean().openapi({ example: false }),
|
|
47
|
+
applied: z.array(z.object({
|
|
48
|
+
promotionId: z.string(),
|
|
49
|
+
code: z.string().optional(),
|
|
50
|
+
type: z.string(),
|
|
51
|
+
discountAmount: z.number(),
|
|
52
|
+
freeShipping: z.boolean(),
|
|
53
|
+
description: z.string(),
|
|
54
|
+
})).openapi({ description: "The promotion(s) applied by this code." }),
|
|
55
|
+
rejectedCodes: z.array(z.object({
|
|
56
|
+
code: z.string(),
|
|
57
|
+
reason: z.string(),
|
|
58
|
+
})),
|
|
59
|
+
}),
|
|
60
|
+
}).openapi("PromotionValidationResult");
|
|
61
|
+
|
|
37
62
|
// ─── Path Params ────────────────────────────────────────────────────────────
|
|
38
63
|
|
|
39
64
|
const PromotionIdParam = z.object({
|
|
@@ -98,8 +123,8 @@ export const validatePromotionRoute = createRoute({
|
|
|
98
123
|
},
|
|
99
124
|
responses: {
|
|
100
125
|
200: {
|
|
101
|
-
content: { "application/json": { schema:
|
|
102
|
-
description: "
|
|
126
|
+
content: { "application/json": { schema: PromotionValidationResultSchema } },
|
|
127
|
+
description: "The code is valid for this cart: the authoritative discount it applies (same computation as checkout).",
|
|
103
128
|
},
|
|
104
129
|
...errorResponses,
|
|
105
130
|
},
|
|
@@ -10,6 +10,14 @@ export interface DatabaseAdapter<TDatabase = unknown, TTransaction = unknown> {
|
|
|
10
10
|
provider: string;
|
|
11
11
|
db: TDatabase;
|
|
12
12
|
transaction<T>(fn: (tx: TTransaction) => Promise<T>): Promise<T>;
|
|
13
|
+
/**
|
|
14
|
+
* Zero-migration adapters (e.g. PGlite) set this so the runtime pushes any
|
|
15
|
+
* plugin-declared tables (`config.customSchemas`) at boot. Plugin schemas
|
|
16
|
+
* only exist after plugins run in `defineConfig` — i.e. after this adapter was
|
|
17
|
+
* constructed — so the adapter cannot create them itself. Omit/false when the
|
|
18
|
+
* consumer manages migrations (e.g. production Postgres via drizzle-kit).
|
|
19
|
+
*/
|
|
20
|
+
autoMigrate?: boolean;
|
|
13
21
|
}
|
|
14
22
|
|
|
15
23
|
export interface DatabaseConnectionFactoryInput {
|
|
@@ -103,7 +103,10 @@ export function getSchemaFiles(): string[] {
|
|
|
103
103
|
* Drizzle instance bound to your database. `drizzle-kit` must be available
|
|
104
104
|
* (it is the standard schema tool); a clear error is thrown if it is not.
|
|
105
105
|
*/
|
|
106
|
-
export async function pushSchema(
|
|
106
|
+
export async function pushSchema(
|
|
107
|
+
drizzleInstance: unknown,
|
|
108
|
+
config?: CommerceConfig,
|
|
109
|
+
): Promise<void> {
|
|
107
110
|
const require = createRequire(import.meta.url);
|
|
108
111
|
let drizzleKit: {
|
|
109
112
|
pushSchema(
|
|
@@ -118,7 +121,10 @@ export async function pushSchema(drizzleInstance: unknown): Promise<void> {
|
|
|
118
121
|
"pushSchema() requires `drizzle-kit` to be installed. Add it to your project: bun add -d drizzle-kit",
|
|
119
122
|
);
|
|
120
123
|
}
|
|
124
|
+
// With a config, push the merged core + plugin schema so plugin-declared
|
|
125
|
+
// tables are created too; without one, push core only (backward compatible).
|
|
126
|
+
const schemaToApply = config ? buildSchema(config) : getSchema();
|
|
121
127
|
// drizzle-kit needs the native driver result shape; unwrap a normalized db.
|
|
122
|
-
const { apply } = await drizzleKit.pushSchema(
|
|
128
|
+
const { apply } = await drizzleKit.pushSchema(schemaToApply, unwrapDb(drizzleInstance));
|
|
123
129
|
await apply();
|
|
124
130
|
}
|
package/src/runtime/commerce.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Actor } from "../auth/types.js";
|
|
|
2
2
|
import type { CommerceConfig } from "../config/types.js";
|
|
3
3
|
import type { Kernel } from "./kernel.js";
|
|
4
4
|
import { createKernel } from "./kernel.js";
|
|
5
|
+
import { pushSchema } from "../kernel/database/migrate.js";
|
|
5
6
|
import { ensureDefaultOrg, setBootDefaultOrgId } from "../auth/org.js";
|
|
6
7
|
import { setBootStrictOrgResolution } from "../auth/strict-org-resolution.js";
|
|
7
8
|
import { createAuth, type AuthInstance } from "../auth/setup.js";
|
|
@@ -122,6 +123,15 @@ export async function createCommerce(
|
|
|
122
123
|
): Promise<CommerceInstance> {
|
|
123
124
|
const kernel = createKernel(config);
|
|
124
125
|
|
|
126
|
+
// Zero-migration adapters (PGlite) push core schema at construction, but a
|
|
127
|
+
// plugin's own tables live in `config.customSchemas`, which only exist after
|
|
128
|
+
// plugins run in `defineConfig` — i.e. after the adapter was constructed.
|
|
129
|
+
// Create them now so plugin routes have their tables. Guarded on both signals
|
|
130
|
+
// so plugin-less stores and migration-managed adapters (Postgres) are untouched.
|
|
131
|
+
if (config.databaseAdapter?.autoMigrate && config.customSchemas?.length) {
|
|
132
|
+
await pushSchema(kernel.database.db, config);
|
|
133
|
+
}
|
|
134
|
+
|
|
125
135
|
setBootStrictOrgResolution(config.auth?.strictOrgResolution === true);
|
|
126
136
|
|
|
127
137
|
// Register the config-driven org ID so resolveOrgId() can use it
|