@porulle/core 0.7.0 → 0.9.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/auth/middleware.d.ts.map +1 -1
- package/dist/auth/middleware.js +26 -7
- package/dist/auth/setup.d.ts.map +1 -1
- package/dist/auth/setup.js +8 -5
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +7 -0
- package/dist/config/types.d.ts +26 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/generated/plugin-manifest.d.ts +3 -0
- package/dist/generated/plugin-manifest.d.ts.map +1 -1
- package/dist/generated/plugin-manifest.js +1 -0
- package/dist/hooks/checkout-completion.d.ts.map +1 -1
- package/dist/hooks/checkout-completion.js +2 -2
- package/dist/hooks/checkout.d.ts.map +1 -1
- package/dist/hooks/checkout.js +17 -2
- package/dist/index.d.ts +1 -0
- 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 +12 -0
- package/dist/interfaces/rest/routes/admin/staff.d.ts +12 -0
- package/dist/interfaces/rest/routes/admin/staff.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/admin/staff.js +206 -0
- package/dist/interfaces/rest/routes/analytics.d.ts +5 -0
- package/dist/interfaces/rest/routes/analytics.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/analytics.js +24 -0
- package/dist/interfaces/rest/routes/carts.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/carts.js +39 -2
- package/dist/interfaces/rest/routes/catalog.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/catalog.js +17 -1
- package/dist/interfaces/rest/routes/checkout.d.ts +9 -0
- package/dist/interfaces/rest/routes/checkout.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/checkout.js +97 -21
- package/dist/interfaces/rest/routes/documents.d.ts +10 -0
- package/dist/interfaces/rest/routes/documents.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/documents.js +40 -0
- package/dist/interfaces/rest/routes/inventory.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/inventory.js +13 -0
- package/dist/interfaces/rest/routes/orders.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/orders.js +111 -1
- package/dist/interfaces/rest/routes/pricing.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/pricing.js +36 -1
- package/dist/interfaces/rest/routes/settings.d.ts +5 -0
- package/dist/interfaces/rest/routes/settings.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/settings.js +50 -0
- package/dist/interfaces/rest/routes/shipping.d.ts +5 -0
- package/dist/interfaces/rest/routes/shipping.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/shipping.js +68 -0
- package/dist/interfaces/rest/routes/tax.d.ts +5 -0
- package/dist/interfaces/rest/routes/tax.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/tax.js +68 -0
- package/dist/interfaces/rest/schemas/admin-staff.d.ts +899 -0
- package/dist/interfaces/rest/schemas/admin-staff.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/admin-staff.js +98 -0
- package/dist/interfaces/rest/schemas/analytics.d.ts +252 -0
- package/dist/interfaces/rest/schemas/analytics.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/analytics.js +33 -0
- package/dist/interfaces/rest/schemas/carts.d.ts +656 -0
- package/dist/interfaces/rest/schemas/carts.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/carts.js +38 -0
- package/dist/interfaces/rest/schemas/catalog.d.ts +1218 -1
- package/dist/interfaces/rest/schemas/catalog.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/catalog.js +48 -0
- package/dist/interfaces/rest/schemas/checkout.d.ts +2 -0
- package/dist/interfaces/rest/schemas/checkout.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/checkout.js +4 -0
- package/dist/interfaces/rest/schemas/customer-portal.d.ts +35 -0
- package/dist/interfaces/rest/schemas/customer-portal.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/documents.d.ts +509 -0
- package/dist/interfaces/rest/schemas/documents.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/documents.js +65 -0
- package/dist/interfaces/rest/schemas/orders.d.ts +3110 -43
- package/dist/interfaces/rest/schemas/orders.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/orders.js +234 -1
- package/dist/interfaces/rest/schemas/pricing.d.ts +405 -0
- package/dist/interfaces/rest/schemas/pricing.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/pricing.js +70 -0
- package/dist/interfaces/rest/schemas/responses.d.ts +190 -0
- package/dist/interfaces/rest/schemas/responses.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/search.d.ts +17 -0
- package/dist/interfaces/rest/schemas/search.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/settings.d.ts +397 -0
- package/dist/interfaces/rest/schemas/settings.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/settings.js +79 -0
- package/dist/interfaces/rest/schemas/shipping.d.ts +1093 -0
- package/dist/interfaces/rest/schemas/shipping.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/shipping.js +146 -0
- package/dist/interfaces/rest/schemas/tax.d.ts +1074 -0
- package/dist/interfaces/rest/schemas/tax.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/tax.js +132 -0
- package/dist/kernel/database/schema.d.ts +4 -0
- package/dist/kernel/database/schema.d.ts.map +1 -1
- package/dist/kernel/database/schema.js +8 -0
- package/dist/kernel/database/scoped-db.d.ts +2 -1
- package/dist/kernel/database/scoped-db.d.ts.map +1 -1
- package/dist/kernel/database/scoped-db.js +62 -1
- package/dist/kernel/hooks/types.d.ts +1 -1
- package/dist/kernel/hooks/types.d.ts.map +1 -1
- package/dist/kernel/plugin/manifest.d.ts +26 -0
- package/dist/kernel/plugin/manifest.d.ts.map +1 -1
- package/dist/kernel/plugin/manifest.js +17 -2
- package/dist/modules/analytics/drizzle-adapter.d.ts.map +1 -1
- package/dist/modules/analytics/drizzle-adapter.js +16 -3
- package/dist/modules/analytics/module.d.ts.map +1 -1
- package/dist/modules/analytics/module.js +6 -0
- package/dist/modules/analytics/reports.d.ts +58 -0
- package/dist/modules/analytics/reports.d.ts.map +1 -0
- package/dist/modules/analytics/reports.js +249 -0
- package/dist/modules/analytics/service.d.ts +10 -0
- package/dist/modules/analytics/service.d.ts.map +1 -1
- package/dist/modules/analytics/service.js +14 -0
- package/dist/modules/cart/repository/index.d.ts +7 -0
- package/dist/modules/cart/repository/index.d.ts.map +1 -1
- package/dist/modules/cart/repository/index.js +25 -1
- package/dist/modules/cart/schema.d.ts +17 -0
- package/dist/modules/cart/schema.d.ts.map +1 -1
- package/dist/modules/cart/schema.js +2 -0
- package/dist/modules/cart/schemas.d.ts +1 -0
- package/dist/modules/cart/schemas.d.ts.map +1 -1
- package/dist/modules/cart/schemas.js +1 -0
- package/dist/modules/cart/service.d.ts +32 -0
- package/dist/modules/cart/service.d.ts.map +1 -1
- package/dist/modules/cart/service.js +61 -2
- package/dist/modules/catalog/entity-service.d.ts +21 -0
- package/dist/modules/catalog/entity-service.d.ts.map +1 -1
- package/dist/modules/catalog/entity-service.js +166 -5
- package/dist/modules/catalog/schema.d.ts +34 -0
- package/dist/modules/catalog/schema.d.ts.map +1 -1
- package/dist/modules/catalog/schema.js +5 -0
- package/dist/modules/catalog/schemas.d.ts +3 -0
- package/dist/modules/catalog/schemas.d.ts.map +1 -1
- package/dist/modules/catalog/schemas.js +3 -0
- package/dist/modules/catalog/service.d.ts +36 -0
- package/dist/modules/catalog/service.d.ts.map +1 -1
- package/dist/modules/catalog/service.js +6 -0
- package/dist/modules/customers/service.d.ts.map +1 -1
- package/dist/modules/customers/service.js +6 -0
- package/dist/modules/documents/module.d.ts +7 -0
- package/dist/modules/documents/module.d.ts.map +1 -0
- package/dist/modules/documents/module.js +12 -0
- package/dist/modules/documents/pdf.d.ts +25 -0
- package/dist/modules/documents/pdf.d.ts.map +1 -0
- package/dist/modules/documents/pdf.js +80 -0
- package/dist/modules/documents/render.d.ts +32 -0
- package/dist/modules/documents/render.d.ts.map +1 -0
- package/dist/modules/documents/render.js +190 -0
- package/dist/modules/documents/repository/index.d.ts +25 -0
- package/dist/modules/documents/repository/index.d.ts.map +1 -0
- package/dist/modules/documents/repository/index.js +47 -0
- package/dist/modules/documents/schema.d.ts +214 -0
- package/dist/modules/documents/schema.d.ts.map +1 -0
- package/dist/modules/documents/schema.js +42 -0
- package/dist/modules/documents/service.d.ts +51 -0
- package/dist/modules/documents/service.d.ts.map +1 -0
- package/dist/modules/documents/service.js +119 -0
- package/dist/modules/fulfillment/service.d.ts +18 -0
- package/dist/modules/fulfillment/service.d.ts.map +1 -1
- package/dist/modules/fulfillment/service.js +85 -1
- package/dist/modules/orders/repository/index.d.ts +18 -1
- package/dist/modules/orders/repository/index.d.ts.map +1 -1
- package/dist/modules/orders/repository/index.js +86 -1
- package/dist/modules/orders/schema.d.ts +391 -0
- package/dist/modules/orders/schema.d.ts.map +1 -1
- package/dist/modules/orders/schema.js +56 -1
- package/dist/modules/orders/service.d.ts +102 -3
- package/dist/modules/orders/service.d.ts.map +1 -1
- package/dist/modules/orders/service.js +653 -29
- package/dist/modules/orders/stale-order-cleanup.d.ts.map +1 -1
- package/dist/modules/orders/stale-order-cleanup.js +13 -1
- package/dist/modules/pricing/repository/index.d.ts +5 -0
- package/dist/modules/pricing/repository/index.d.ts.map +1 -1
- package/dist/modules/pricing/repository/index.js +19 -0
- package/dist/modules/pricing/service.d.ts +18 -0
- package/dist/modules/pricing/service.d.ts.map +1 -1
- package/dist/modules/pricing/service.js +43 -5
- package/dist/modules/settings/module.d.ts +6 -0
- package/dist/modules/settings/module.d.ts.map +1 -0
- package/dist/modules/settings/module.js +11 -0
- package/dist/modules/settings/repository/index.d.ts +16 -0
- package/dist/modules/settings/repository/index.d.ts.map +1 -0
- package/dist/modules/settings/repository/index.js +41 -0
- package/dist/modules/settings/schema.d.ts +121 -0
- package/dist/modules/settings/schema.d.ts.map +1 -0
- package/dist/modules/settings/schema.js +24 -0
- package/dist/modules/settings/service.d.ts +26 -0
- package/dist/modules/settings/service.d.ts.map +1 -0
- package/dist/modules/settings/service.js +51 -0
- package/dist/modules/shipping/calculator.d.ts +5 -0
- package/dist/modules/shipping/calculator.d.ts.map +1 -1
- package/dist/modules/shipping/calculator.js +10 -0
- package/dist/modules/shipping/module.d.ts +5 -1
- package/dist/modules/shipping/module.d.ts.map +1 -1
- package/dist/modules/shipping/module.js +4 -1
- package/dist/modules/shipping/repository/index.d.ts +27 -2
- package/dist/modules/shipping/repository/index.d.ts.map +1 -1
- package/dist/modules/shipping/repository/index.js +105 -1
- package/dist/modules/shipping/schema.d.ts +419 -0
- package/dist/modules/shipping/schema.d.ts.map +1 -0
- package/dist/modules/shipping/schema.js +56 -0
- package/dist/modules/shipping/service.d.ts +54 -0
- package/dist/modules/shipping/service.d.ts.map +1 -1
- package/dist/modules/shipping/service.js +161 -2
- package/dist/modules/tax/adapter.d.ts +13 -0
- package/dist/modules/tax/adapter.d.ts.map +1 -1
- package/dist/modules/tax/module.d.ts +5 -1
- package/dist/modules/tax/module.d.ts.map +1 -1
- package/dist/modules/tax/module.js +4 -1
- package/dist/modules/tax/repository/index.d.ts +38 -2
- package/dist/modules/tax/repository/index.d.ts.map +1 -1
- package/dist/modules/tax/repository/index.js +138 -1
- package/dist/modules/tax/schema.d.ts +355 -0
- package/dist/modules/tax/schema.d.ts.map +1 -0
- package/dist/modules/tax/schema.js +55 -0
- package/dist/modules/tax/service.d.ts +60 -1
- package/dist/modules/tax/service.d.ts.map +1 -1
- package/dist/modules/tax/service.js +227 -2
- package/dist/runtime/kernel-modules.d.ts +15 -2
- package/dist/runtime/kernel-modules.d.ts.map +1 -1
- package/dist/runtime/kernel-modules.js +4 -0
- package/dist/runtime/kernel-types.d.ts +5 -1
- package/dist/runtime/kernel-types.d.ts.map +1 -1
- package/dist/runtime/kernel-types.js +2 -0
- package/dist/runtime/server.js +1 -1
- package/dist/test-utils/create-plugin-test-app.d.ts +3 -0
- package/dist/test-utils/create-plugin-test-app.d.ts.map +1 -1
- package/dist/test-utils/create-plugin-test-app.js +15 -3
- package/dist/test-utils/create-test-config.d.ts.map +1 -1
- package/dist/test-utils/create-test-config.js +2 -0
- package/dist/test-utils/order-test-helpers.d.ts +12 -0
- package/dist/test-utils/order-test-helpers.d.ts.map +1 -0
- package/dist/test-utils/order-test-helpers.js +18 -0
- package/dist/test-utils/rest-api-test-utils.d.ts.map +1 -1
- package/dist/test-utils/rest-api-test-utils.js +7 -0
- package/dist/test-utils/test-actors.js +1 -1
- package/dist/testing.d.ts +1 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +1 -0
- package/package.json +3 -3
- package/src/auth/middleware.ts +26 -7
- package/src/auth/setup.ts +11 -4
- package/src/config/defaults.ts +7 -0
- package/src/config/types.ts +26 -1
- package/src/generated/plugin-capabilities.d.ts +1 -0
- package/src/generated/plugin-manifest.ts +1 -0
- package/src/generated/plugin-repositories.d.ts +1 -0
- package/src/hooks/checkout-completion.ts +22 -16
- package/src/hooks/checkout.ts +36 -11
- package/src/index.ts +1 -0
- package/src/interfaces/rest/index.ts +12 -0
- package/src/interfaces/rest/routes/admin/staff.ts +260 -0
- package/src/interfaces/rest/routes/analytics.ts +33 -0
- package/src/interfaces/rest/routes/carts.ts +52 -2
- package/src/interfaces/rest/routes/catalog.ts +34 -0
- package/src/interfaces/rest/routes/checkout.ts +114 -27
- package/src/interfaces/rest/routes/documents.ts +61 -0
- package/src/interfaces/rest/routes/inventory.ts +14 -0
- package/src/interfaces/rest/routes/orders.ts +148 -1
- package/src/interfaces/rest/routes/pricing.ts +44 -1
- package/src/interfaces/rest/routes/settings.ts +61 -0
- package/src/interfaces/rest/routes/shipping.ts +92 -0
- package/src/interfaces/rest/routes/tax.ts +90 -0
- package/src/interfaces/rest/schemas/admin-staff.ts +110 -0
- package/src/interfaces/rest/schemas/analytics.ts +39 -0
- package/src/interfaces/rest/schemas/carts.ts +40 -0
- package/src/interfaces/rest/schemas/catalog.ts +53 -0
- package/src/interfaces/rest/schemas/checkout.ts +4 -0
- package/src/interfaces/rest/schemas/documents.ts +72 -0
- package/src/interfaces/rest/schemas/orders.ts +258 -1
- package/src/interfaces/rest/schemas/pricing.ts +75 -0
- package/src/interfaces/rest/schemas/settings.ts +93 -0
- package/src/interfaces/rest/schemas/shipping.ts +164 -0
- package/src/interfaces/rest/schemas/tax.ts +148 -0
- package/src/kernel/database/schema.ts +12 -0
- package/src/kernel/database/scoped-db.ts +70 -1
- package/src/kernel/hooks/types.ts +1 -0
- package/src/kernel/plugin/manifest.ts +41 -2
- package/src/modules/analytics/drizzle-adapter.ts +19 -3
- package/src/modules/analytics/module.ts +10 -0
- package/src/modules/analytics/reports.ts +311 -0
- package/src/modules/analytics/service.ts +23 -0
- package/src/modules/cart/repository/index.ts +30 -1
- package/src/modules/cart/schema.ts +2 -0
- package/src/modules/cart/schemas.ts +1 -0
- package/src/modules/cart/service.ts +104 -2
- package/src/modules/catalog/entity-service.ts +196 -4
- package/src/modules/catalog/schema.ts +5 -0
- package/src/modules/catalog/schemas.ts +3 -0
- package/src/modules/catalog/service.ts +30 -0
- package/src/modules/customers/service.ts +6 -0
- package/src/modules/documents/module.ts +19 -0
- package/src/modules/documents/pdf.ts +101 -0
- package/src/modules/documents/render.ts +231 -0
- package/src/modules/documents/repository/index.ts +68 -0
- package/src/modules/documents/schema.ts +44 -0
- package/src/modules/documents/service.ts +176 -0
- package/src/modules/fulfillment/service.ts +139 -1
- package/src/modules/orders/repository/index.ts +127 -1
- package/src/modules/orders/schema.ts +58 -1
- package/src/modules/orders/service.ts +1041 -65
- package/src/modules/orders/stale-order-cleanup.ts +18 -4
- package/src/modules/pricing/repository/index.ts +34 -0
- package/src/modules/pricing/service.ts +76 -6
- package/src/modules/settings/module.ts +18 -0
- package/src/modules/settings/repository/index.ts +59 -0
- package/src/modules/settings/schema.ts +25 -0
- package/src/modules/settings/service.ts +82 -0
- package/src/modules/shipping/calculator.ts +27 -0
- package/src/modules/shipping/module.ts +6 -2
- package/src/modules/shipping/repository/index.ts +136 -2
- package/src/modules/shipping/schema.ts +58 -0
- package/src/modules/shipping/service.ts +260 -1
- package/src/modules/tax/adapter.ts +9 -0
- package/src/modules/tax/module.ts +6 -2
- package/src/modules/tax/repository/index.ts +169 -2
- package/src/modules/tax/schema.ts +57 -0
- package/src/modules/tax/service.ts +313 -2
- package/src/runtime/kernel-modules.ts +4 -0
- package/src/runtime/kernel-types.ts +6 -0
- package/src/runtime/server.ts +1 -1
- package/src/test-utils/create-plugin-test-app.ts +17 -4
- package/src/test-utils/create-test-config.ts +2 -0
- package/src/test-utils/order-test-helpers.ts +26 -0
- package/src/test-utils/rest-api-test-utils.ts +7 -0
- package/src/test-utils/test-actors.ts +1 -1
- package/src/testing.ts +1 -0
|
@@ -1,6 +1,20 @@
|
|
|
1
|
-
import { eq, and, lt
|
|
1
|
+
import { eq, and, lt } from "drizzle-orm";
|
|
2
|
+
import type { Actor } from "../../auth/types.js";
|
|
2
3
|
import { orders } from "./schema.js";
|
|
3
|
-
import type { TaskDefinition
|
|
4
|
+
import type { TaskDefinition } from "../../kernel/jobs/types.js";
|
|
5
|
+
|
|
6
|
+
function systemActorForOrg(organizationId: string): Actor {
|
|
7
|
+
return {
|
|
8
|
+
type: "user",
|
|
9
|
+
userId: "system",
|
|
10
|
+
email: null,
|
|
11
|
+
name: "System",
|
|
12
|
+
vendorId: null,
|
|
13
|
+
organizationId,
|
|
14
|
+
role: "admin",
|
|
15
|
+
permissions: ["*:*"],
|
|
16
|
+
};
|
|
17
|
+
}
|
|
4
18
|
|
|
5
19
|
/**
|
|
6
20
|
* Stale Order Cleanup Task
|
|
@@ -46,14 +60,14 @@ export const staleOrderCleanupTask: TaskDefinition<
|
|
|
46
60
|
|
|
47
61
|
// Cancel each stale order via the service (triggers inventory release + payment refund)
|
|
48
62
|
const orderService = ctx.services.orders as {
|
|
49
|
-
cancel(orderId: string, actor: null, reason: string): Promise<unknown>;
|
|
63
|
+
cancel(orderId: string, actor: Actor | null, reason: string): Promise<unknown>;
|
|
50
64
|
};
|
|
51
65
|
|
|
52
66
|
for (const order of staleOrders) {
|
|
53
67
|
try {
|
|
54
68
|
await orderService.cancel(
|
|
55
69
|
order.id,
|
|
56
|
-
|
|
70
|
+
systemActorForOrg(order.organizationId),
|
|
57
71
|
`Auto-cancelled: pending for >${thresholdHours}h`,
|
|
58
72
|
);
|
|
59
73
|
cancelledIds.push(order.id);
|
|
@@ -192,6 +192,40 @@ export class PricingRepository {
|
|
|
192
192
|
);
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
async findModifiers(
|
|
196
|
+
orgId: string,
|
|
197
|
+
filter?: { entityId?: string; currency?: string; activeAt?: Date },
|
|
198
|
+
ctx?: TxContext,
|
|
199
|
+
): Promise<PriceModifier[]> {
|
|
200
|
+
const db = this.getDb(ctx);
|
|
201
|
+
const conditions = [eq(priceModifiers.organizationId, orgId)];
|
|
202
|
+
if (filter?.entityId) {
|
|
203
|
+
conditions.push(eq(priceModifiers.entityId, filter.entityId));
|
|
204
|
+
}
|
|
205
|
+
if (filter?.currency) {
|
|
206
|
+
conditions.push(eq(priceModifiers.currency, filter.currency));
|
|
207
|
+
}
|
|
208
|
+
if (filter?.activeAt) {
|
|
209
|
+
conditions.push(
|
|
210
|
+
or(
|
|
211
|
+
isNull(priceModifiers.validFrom),
|
|
212
|
+
lte(priceModifiers.validFrom, filter.activeAt),
|
|
213
|
+
)!,
|
|
214
|
+
);
|
|
215
|
+
conditions.push(
|
|
216
|
+
or(
|
|
217
|
+
isNull(priceModifiers.validUntil),
|
|
218
|
+
gte(priceModifiers.validUntil, filter.activeAt),
|
|
219
|
+
)!,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
const rows = await db
|
|
223
|
+
.select()
|
|
224
|
+
.from(priceModifiers)
|
|
225
|
+
.where(and(...conditions));
|
|
226
|
+
return rows.sort((a, b) => a.priority - b.priority);
|
|
227
|
+
}
|
|
228
|
+
|
|
195
229
|
async findActiveModifiers(
|
|
196
230
|
orgId: string,
|
|
197
231
|
entityId: string,
|
|
@@ -211,15 +211,14 @@ export class PricingService {
|
|
|
211
211
|
);
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
const
|
|
214
|
+
const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
|
|
215
|
+
const entity = await this.catalogRepo.findEntityById(input.entityId, ctx, orgId);
|
|
215
216
|
if (!entity) {
|
|
216
217
|
return Err(
|
|
217
218
|
new CommerceNotFoundError("Entity not found for price assignment."),
|
|
218
219
|
);
|
|
219
220
|
}
|
|
220
221
|
|
|
221
|
-
const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
|
|
222
|
-
|
|
223
222
|
const priceData: PriceInsert = {
|
|
224
223
|
organizationId: orgId,
|
|
225
224
|
entityId: input.entityId,
|
|
@@ -333,6 +332,77 @@ export class PricingService {
|
|
|
333
332
|
return Ok(modifier);
|
|
334
333
|
}
|
|
335
334
|
|
|
335
|
+
async listModifiers(
|
|
336
|
+
filter?: { entityId?: string; active?: boolean; currency?: string },
|
|
337
|
+
actor?: Actor | null,
|
|
338
|
+
ctx?: TxContext,
|
|
339
|
+
): Promise<Result<PriceModifier[]>> {
|
|
340
|
+
const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
|
|
341
|
+
const rows = await this.repo.findModifiers(
|
|
342
|
+
orgId,
|
|
343
|
+
{
|
|
344
|
+
...(filter?.entityId !== undefined ? { entityId: filter.entityId } : {}),
|
|
345
|
+
...(filter?.currency !== undefined
|
|
346
|
+
? { currency: normalizeCurrency(filter.currency) }
|
|
347
|
+
: {}),
|
|
348
|
+
...(filter?.active ? { activeAt: new Date() } : {}),
|
|
349
|
+
},
|
|
350
|
+
ctx,
|
|
351
|
+
);
|
|
352
|
+
return Ok(rows);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async updateModifier(
|
|
356
|
+
id: string,
|
|
357
|
+
patch: {
|
|
358
|
+
name?: string | undefined;
|
|
359
|
+
value?: number | undefined;
|
|
360
|
+
priority?: number | undefined;
|
|
361
|
+
validFrom?: Date | null | undefined;
|
|
362
|
+
validUntil?: Date | null | undefined;
|
|
363
|
+
minQuantity?: number | null | undefined;
|
|
364
|
+
maxQuantity?: number | null | undefined;
|
|
365
|
+
metadata?: Record<string, unknown> | undefined;
|
|
366
|
+
},
|
|
367
|
+
actor?: Actor | null,
|
|
368
|
+
ctx?: TxContext,
|
|
369
|
+
): Promise<Result<PriceModifier>> {
|
|
370
|
+
const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
|
|
371
|
+
const existing = await this.repo.findModifierById(orgId, id, ctx);
|
|
372
|
+
if (!existing) {
|
|
373
|
+
return Err(new CommerceNotFoundError("Price modifier not found."));
|
|
374
|
+
}
|
|
375
|
+
const updated = await this.repo.updateModifier(
|
|
376
|
+
id,
|
|
377
|
+
{
|
|
378
|
+
...(patch.name !== undefined ? { name: patch.name } : {}),
|
|
379
|
+
...(patch.value !== undefined ? { value: patch.value } : {}),
|
|
380
|
+
...(patch.priority !== undefined ? { priority: patch.priority } : {}),
|
|
381
|
+
...(patch.validFrom !== undefined ? { validFrom: patch.validFrom } : {}),
|
|
382
|
+
...(patch.validUntil !== undefined ? { validUntil: patch.validUntil } : {}),
|
|
383
|
+
...(patch.minQuantity !== undefined ? { minQuantity: patch.minQuantity } : {}),
|
|
384
|
+
...(patch.maxQuantity !== undefined ? { maxQuantity: patch.maxQuantity } : {}),
|
|
385
|
+
...(patch.metadata !== undefined ? { metadata: patch.metadata } : {}),
|
|
386
|
+
},
|
|
387
|
+
ctx,
|
|
388
|
+
);
|
|
389
|
+
return Ok(updated!);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async deleteModifier(
|
|
393
|
+
id: string,
|
|
394
|
+
actor?: Actor | null,
|
|
395
|
+
ctx?: TxContext,
|
|
396
|
+
): Promise<Result<{ deleted: true }>> {
|
|
397
|
+
const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
|
|
398
|
+
const existing = await this.repo.findModifierById(orgId, id, ctx);
|
|
399
|
+
if (!existing) {
|
|
400
|
+
return Err(new CommerceNotFoundError("Price modifier not found."));
|
|
401
|
+
}
|
|
402
|
+
await this.repo.deleteModifier(id, ctx);
|
|
403
|
+
return Ok({ deleted: true });
|
|
404
|
+
}
|
|
405
|
+
|
|
336
406
|
async listPrices(
|
|
337
407
|
filter?: {
|
|
338
408
|
entityId?: string;
|
|
@@ -402,7 +472,8 @@ export class PricingService {
|
|
|
402
472
|
actor?: Actor | null,
|
|
403
473
|
ctx?: TxContext,
|
|
404
474
|
): Promise<Result<ResolvedPrice>> {
|
|
405
|
-
const
|
|
475
|
+
const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
|
|
476
|
+
const entity = await this.catalogRepo.findEntityById(input.entityId, ctx, orgId);
|
|
406
477
|
if (!entity) {
|
|
407
478
|
return Err(
|
|
408
479
|
new CommerceNotFoundError(`Entity ${input.entityId} not found.`),
|
|
@@ -421,8 +492,7 @@ export class PricingService {
|
|
|
421
492
|
const currency = normalizeCurrency(input.currency);
|
|
422
493
|
const groupSet = toGroupSet(input);
|
|
423
494
|
|
|
424
|
-
// Get matching prices from repository
|
|
425
|
-
const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
|
|
495
|
+
// Get matching prices from repository (orgId resolved above)
|
|
426
496
|
const allPrices = await this.repo.findPricesByEntityId(
|
|
427
497
|
orgId,
|
|
428
498
|
input.entityId,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { DrizzleDatabase } from "../../kernel/database/drizzle-db.js";
|
|
2
|
+
import { defineModule } from "../../kernel/module/index.js";
|
|
3
|
+
import { SettingsRepository } from "./repository/index.js";
|
|
4
|
+
import { storeSettings } from "./schema.js";
|
|
5
|
+
import { SettingsService } from "./service.js";
|
|
6
|
+
|
|
7
|
+
export const settingsModule = defineModule<
|
|
8
|
+
{ storeSettings: typeof storeSettings },
|
|
9
|
+
SettingsService,
|
|
10
|
+
Record<string, never>
|
|
11
|
+
>({
|
|
12
|
+
id: "settings",
|
|
13
|
+
schema: () => ({ storeSettings }),
|
|
14
|
+
service: (deps) =>
|
|
15
|
+
new SettingsService({
|
|
16
|
+
repository: new SettingsRepository(deps.db.db as DrizzleDatabase),
|
|
17
|
+
}),
|
|
18
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { and, eq } from "drizzle-orm";
|
|
2
|
+
import type { TxContext } from "../../../kernel/database/tx-context.js";
|
|
3
|
+
import type {
|
|
4
|
+
DrizzleDatabase,
|
|
5
|
+
DbOrTx,
|
|
6
|
+
} from "../../../kernel/database/drizzle-db.js";
|
|
7
|
+
import { storeSettings } from "../schema.js";
|
|
8
|
+
|
|
9
|
+
export type StoreSetting = typeof storeSettings.$inferSelect;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Persistence for org-scoped runtime settings (issue #49).
|
|
13
|
+
*/
|
|
14
|
+
export class SettingsRepository {
|
|
15
|
+
constructor(private readonly db: DrizzleDatabase) {}
|
|
16
|
+
|
|
17
|
+
private getDb(ctx?: TxContext): DbOrTx {
|
|
18
|
+
return (ctx?.tx as DbOrTx | undefined) ?? this.db;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async findByGroup(
|
|
22
|
+
orgId: string,
|
|
23
|
+
group: string,
|
|
24
|
+
ctx?: TxContext,
|
|
25
|
+
): Promise<StoreSetting | undefined> {
|
|
26
|
+
const db = this.getDb(ctx);
|
|
27
|
+
const rows = await db
|
|
28
|
+
.select()
|
|
29
|
+
.from(storeSettings)
|
|
30
|
+
.where(and(eq(storeSettings.organizationId, orgId), eq(storeSettings.group, group)));
|
|
31
|
+
return rows[0];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async findAll(orgId: string, ctx?: TxContext): Promise<StoreSetting[]> {
|
|
35
|
+
const db = this.getDb(ctx);
|
|
36
|
+
return db
|
|
37
|
+
.select()
|
|
38
|
+
.from(storeSettings)
|
|
39
|
+
.where(eq(storeSettings.organizationId, orgId));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async upsert(
|
|
43
|
+
orgId: string,
|
|
44
|
+
group: string,
|
|
45
|
+
value: Record<string, unknown>,
|
|
46
|
+
ctx?: TxContext,
|
|
47
|
+
): Promise<StoreSetting> {
|
|
48
|
+
const db = this.getDb(ctx);
|
|
49
|
+
const rows = await db
|
|
50
|
+
.insert(storeSettings)
|
|
51
|
+
.values({ organizationId: orgId, group, value })
|
|
52
|
+
.onConflictDoUpdate({
|
|
53
|
+
target: [storeSettings.organizationId, storeSettings.group],
|
|
54
|
+
set: { value, updatedAt: new Date() },
|
|
55
|
+
})
|
|
56
|
+
.returning();
|
|
57
|
+
return rows[0]!;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { index, jsonb, pgTable, text, timestamp, uniqueIndex, uuid } from "drizzle-orm/pg-core";
|
|
2
|
+
import { organization } from "../../auth/auth-schema.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Org-scoped runtime settings (issue #49).
|
|
6
|
+
*
|
|
7
|
+
* One row per (organization, group). Groups are shallow JSON objects —
|
|
8
|
+
* `general` (currency, timezone, locale), `branding` (receipt header/footer,
|
|
9
|
+
* store name, logo), `policies` (free-form policy knobs), plus any custom
|
|
10
|
+
* group a plugin namespaces for itself. PATCH semantics are shallow merge
|
|
11
|
+
* with `null` deleting a key.
|
|
12
|
+
*/
|
|
13
|
+
export const storeSettings = pgTable("store_settings", {
|
|
14
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
15
|
+
organizationId: text("organization_id")
|
|
16
|
+
.notNull()
|
|
17
|
+
.references(() => organization.id, { onDelete: "cascade" }),
|
|
18
|
+
group: text("group").notNull(),
|
|
19
|
+
value: jsonb("value").$type<Record<string, unknown>>().notNull().default({}),
|
|
20
|
+
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
21
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
|
22
|
+
}, (table) => ({
|
|
23
|
+
orgGroupUnique: uniqueIndex("store_settings_org_group_unique").on(table.organizationId, table.group),
|
|
24
|
+
orgIdx: index("idx_store_settings_org").on(table.organizationId),
|
|
25
|
+
}));
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { resolveOrgId } from "../../auth/org.js";
|
|
2
|
+
import type { Actor } from "../../auth/types.js";
|
|
3
|
+
import type { TxContext } from "../../kernel/database/tx-context.js";
|
|
4
|
+
import { CommerceValidationError } from "../../kernel/errors.js";
|
|
5
|
+
import { Err, Ok, type Result } from "../../kernel/result.js";
|
|
6
|
+
import type { SettingsRepository } from "./repository/index.js";
|
|
7
|
+
|
|
8
|
+
const GROUP_NAME_RE = /^[a-z][a-z0-9_-]*$/;
|
|
9
|
+
|
|
10
|
+
interface SettingsServiceDeps {
|
|
11
|
+
repository: SettingsRepository;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Org-scoped runtime settings (issue #49).
|
|
16
|
+
*
|
|
17
|
+
* REST-facing methods resolve the org from the actor; `read()` is the
|
|
18
|
+
* plugin/hook-facing API — an orgId-keyed lookup with no actor requirement so
|
|
19
|
+
* policy values can be consumed at request time inside any service or hook.
|
|
20
|
+
*/
|
|
21
|
+
export class SettingsService {
|
|
22
|
+
private repository: SettingsRepository;
|
|
23
|
+
|
|
24
|
+
constructor(deps: SettingsServiceDeps) {
|
|
25
|
+
this.repository = deps.repository;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Runtime read for plugins/hooks. Unset groups read as `{}`. */
|
|
29
|
+
async read(
|
|
30
|
+
orgId: string,
|
|
31
|
+
group: string,
|
|
32
|
+
ctx?: TxContext,
|
|
33
|
+
): Promise<Record<string, unknown>> {
|
|
34
|
+
const row = await this.repository.findByGroup(orgId, group, ctx);
|
|
35
|
+
return row?.value ?? {};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async getGroup(
|
|
39
|
+
group: string,
|
|
40
|
+
actor?: Actor | null,
|
|
41
|
+
ctx?: TxContext,
|
|
42
|
+
): Promise<Result<Record<string, unknown>>> {
|
|
43
|
+
const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
|
|
44
|
+
return Ok(await this.read(orgId, group, ctx));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async getAll(
|
|
48
|
+
actor?: Actor | null,
|
|
49
|
+
ctx?: TxContext,
|
|
50
|
+
): Promise<Result<Record<string, Record<string, unknown>>>> {
|
|
51
|
+
const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
|
|
52
|
+
const rows = await this.repository.findAll(orgId, ctx);
|
|
53
|
+
const all: Record<string, Record<string, unknown>> = {};
|
|
54
|
+
for (const row of rows) all[row.group] = row.value;
|
|
55
|
+
return Ok(all);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Shallow merge: keys in `patch` overwrite, `null` deletes the key. */
|
|
59
|
+
async updateGroup(
|
|
60
|
+
group: string,
|
|
61
|
+
patch: Record<string, unknown>,
|
|
62
|
+
actor?: Actor | null,
|
|
63
|
+
ctx?: TxContext,
|
|
64
|
+
): Promise<Result<Record<string, unknown>>> {
|
|
65
|
+
if (!GROUP_NAME_RE.test(group)) {
|
|
66
|
+
return Err(
|
|
67
|
+
new CommerceValidationError(
|
|
68
|
+
"Settings group must match [a-z][a-z0-9_-]* (e.g. general, branding, policies).",
|
|
69
|
+
),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
|
|
73
|
+
const current = await this.read(orgId, group, ctx);
|
|
74
|
+
const next: Record<string, unknown> = { ...current };
|
|
75
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
76
|
+
if (value === null) delete next[key];
|
|
77
|
+
else next[key] = value;
|
|
78
|
+
}
|
|
79
|
+
const row = await this.repository.upsert(orgId, group, next, ctx);
|
|
80
|
+
return Ok(row.value);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -117,6 +117,33 @@ function resolveStrategy(config: CommerceConfig): ShippingStrategy {
|
|
|
117
117
|
};
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Total weight (grams) of the shippable line items — used by both the
|
|
122
|
+
* code-config strategy and runtime zone rates with weight bands.
|
|
123
|
+
*/
|
|
124
|
+
export async function computeShippableWeightGrams(
|
|
125
|
+
config: CommerceConfig,
|
|
126
|
+
catalogRepo: CatalogRepository,
|
|
127
|
+
lineItems: ShippingLineItem[],
|
|
128
|
+
ctx?: TxContext,
|
|
129
|
+
): Promise<number> {
|
|
130
|
+
const flags = await Promise.all(
|
|
131
|
+
lineItems.map((lineItem) =>
|
|
132
|
+
isShippableEntity(config, catalogRepo, lineItem.entityId, ctx),
|
|
133
|
+
),
|
|
134
|
+
);
|
|
135
|
+
const shippable = lineItems.filter((_, i) => flags[i]);
|
|
136
|
+
const weights = await Promise.all(
|
|
137
|
+
shippable.map((lineItem) =>
|
|
138
|
+
resolveWeightGrams(catalogRepo, lineItem.entityId, lineItem.variantId, ctx),
|
|
139
|
+
),
|
|
140
|
+
);
|
|
141
|
+
return shippable.reduce(
|
|
142
|
+
(sum, lineItem, i) => sum + (weights[i] ?? 0) * lineItem.quantity,
|
|
143
|
+
0,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
120
147
|
export async function calculateShippingCost(
|
|
121
148
|
config: CommerceConfig,
|
|
122
149
|
catalogRepo: CatalogRepository,
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { CommerceConfig } from "../../config/types.js";
|
|
2
|
+
import type { DrizzleDatabase } from "../../kernel/database/drizzle-db.js";
|
|
2
3
|
import { defineModule } from "../../kernel/module/index.js";
|
|
3
4
|
import type { CatalogServiceImpl } from "../catalog/service.js";
|
|
5
|
+
import { ShippingConfigRepository } from "./repository/index.js";
|
|
6
|
+
import { shippingZones, shippingRates } from "./schema.js";
|
|
4
7
|
import { ShippingService } from "./service.js";
|
|
5
8
|
|
|
6
9
|
type ShippingModuleDeps = {
|
|
@@ -8,16 +11,17 @@ type ShippingModuleDeps = {
|
|
|
8
11
|
};
|
|
9
12
|
|
|
10
13
|
export const shippingModule = defineModule<
|
|
11
|
-
|
|
14
|
+
{ shippingZones: typeof shippingZones; shippingRates: typeof shippingRates },
|
|
12
15
|
ShippingService,
|
|
13
16
|
ShippingModuleDeps
|
|
14
17
|
>({
|
|
15
18
|
id: "shipping",
|
|
16
19
|
dependencies: ["catalog"],
|
|
17
|
-
schema: () => ({}),
|
|
20
|
+
schema: () => ({ shippingZones, shippingRates }),
|
|
18
21
|
service: (deps) =>
|
|
19
22
|
new ShippingService({
|
|
20
23
|
config: deps.config as CommerceConfig,
|
|
21
24
|
catalogRepository: deps.services.catalog.repository,
|
|
25
|
+
repository: new ShippingConfigRepository(deps.db.db as DrizzleDatabase),
|
|
22
26
|
}),
|
|
23
27
|
});
|
|
@@ -1,5 +1,139 @@
|
|
|
1
|
+
import { eq, and, asc } from "drizzle-orm";
|
|
1
2
|
import type { TxContext } from "../../../kernel/database/tx-context.js";
|
|
3
|
+
import type {
|
|
4
|
+
DrizzleDatabase,
|
|
5
|
+
DbOrTx,
|
|
6
|
+
} from "../../../kernel/database/drizzle-db.js";
|
|
7
|
+
import { shippingZones, shippingRates } from "../schema.js";
|
|
2
8
|
|
|
3
|
-
export
|
|
4
|
-
|
|
9
|
+
export type ShippingZone = typeof shippingZones.$inferSelect;
|
|
10
|
+
export type ShippingZoneInsert = typeof shippingZones.$inferInsert;
|
|
11
|
+
export type ShippingRate = typeof shippingRates.$inferSelect;
|
|
12
|
+
export type ShippingRateInsert = typeof shippingRates.$inferInsert;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Persistence for runtime shipping zones & rates (issue #45).
|
|
16
|
+
*/
|
|
17
|
+
export class ShippingConfigRepository {
|
|
18
|
+
constructor(private readonly db: DrizzleDatabase) {}
|
|
19
|
+
|
|
20
|
+
private getDb(ctx?: TxContext): DbOrTx {
|
|
21
|
+
return (ctx?.tx as DbOrTx | undefined) ?? this.db;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ── Zones ──────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
async findZoneById(orgId: string, id: string, ctx?: TxContext): Promise<ShippingZone | undefined> {
|
|
27
|
+
const db = this.getDb(ctx);
|
|
28
|
+
const rows = await db
|
|
29
|
+
.select()
|
|
30
|
+
.from(shippingZones)
|
|
31
|
+
.where(and(eq(shippingZones.organizationId, orgId), eq(shippingZones.id, id)));
|
|
32
|
+
return rows[0];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async findZones(orgId: string, ctx?: TxContext): Promise<ShippingZone[]> {
|
|
36
|
+
const db = this.getDb(ctx);
|
|
37
|
+
return db
|
|
38
|
+
.select()
|
|
39
|
+
.from(shippingZones)
|
|
40
|
+
.where(eq(shippingZones.organizationId, orgId))
|
|
41
|
+
.orderBy(asc(shippingZones.priority));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async findActiveZones(orgId: string, ctx?: TxContext): Promise<ShippingZone[]> {
|
|
45
|
+
const db = this.getDb(ctx);
|
|
46
|
+
return db
|
|
47
|
+
.select()
|
|
48
|
+
.from(shippingZones)
|
|
49
|
+
.where(and(eq(shippingZones.organizationId, orgId), eq(shippingZones.isActive, true)))
|
|
50
|
+
.orderBy(asc(shippingZones.priority));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async createZone(data: ShippingZoneInsert, ctx?: TxContext): Promise<ShippingZone> {
|
|
54
|
+
const db = this.getDb(ctx);
|
|
55
|
+
const rows = await db.insert(shippingZones).values(data).returning();
|
|
56
|
+
return rows[0]!;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async updateZone(
|
|
60
|
+
id: string,
|
|
61
|
+
data: Partial<Omit<ShippingZoneInsert, "id" | "organizationId">>,
|
|
62
|
+
ctx?: TxContext,
|
|
63
|
+
): Promise<ShippingZone | undefined> {
|
|
64
|
+
const db = this.getDb(ctx);
|
|
65
|
+
const rows = await db
|
|
66
|
+
.update(shippingZones)
|
|
67
|
+
.set({ ...data, updatedAt: new Date() })
|
|
68
|
+
.where(eq(shippingZones.id, id))
|
|
69
|
+
.returning();
|
|
70
|
+
return rows[0];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async deleteZone(id: string, ctx?: TxContext): Promise<boolean> {
|
|
74
|
+
const db = this.getDb(ctx);
|
|
75
|
+
const rows = await db.delete(shippingZones).where(eq(shippingZones.id, id)).returning();
|
|
76
|
+
return rows.length > 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Rates ──────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
async findRateById(orgId: string, id: string, ctx?: TxContext): Promise<ShippingRate | undefined> {
|
|
82
|
+
const db = this.getDb(ctx);
|
|
83
|
+
const rows = await db
|
|
84
|
+
.select()
|
|
85
|
+
.from(shippingRates)
|
|
86
|
+
.where(and(eq(shippingRates.organizationId, orgId), eq(shippingRates.id, id)));
|
|
87
|
+
return rows[0];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async findRates(
|
|
91
|
+
orgId: string,
|
|
92
|
+
filter?: { zoneId?: string },
|
|
93
|
+
ctx?: TxContext,
|
|
94
|
+
): Promise<ShippingRate[]> {
|
|
95
|
+
const db = this.getDb(ctx);
|
|
96
|
+
const conditions = [eq(shippingRates.organizationId, orgId)];
|
|
97
|
+
if (filter?.zoneId) conditions.push(eq(shippingRates.zoneId, filter.zoneId));
|
|
98
|
+
return db
|
|
99
|
+
.select()
|
|
100
|
+
.from(shippingRates)
|
|
101
|
+
.where(and(...conditions))
|
|
102
|
+
.orderBy(asc(shippingRates.amount));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async findActiveRatesByZoneId(zoneId: string, ctx?: TxContext): Promise<ShippingRate[]> {
|
|
106
|
+
const db = this.getDb(ctx);
|
|
107
|
+
return db
|
|
108
|
+
.select()
|
|
109
|
+
.from(shippingRates)
|
|
110
|
+
.where(and(eq(shippingRates.zoneId, zoneId), eq(shippingRates.isActive, true)))
|
|
111
|
+
.orderBy(asc(shippingRates.amount));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async createRate(data: ShippingRateInsert, ctx?: TxContext): Promise<ShippingRate> {
|
|
115
|
+
const db = this.getDb(ctx);
|
|
116
|
+
const rows = await db.insert(shippingRates).values(data).returning();
|
|
117
|
+
return rows[0]!;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async updateRate(
|
|
121
|
+
id: string,
|
|
122
|
+
data: Partial<Omit<ShippingRateInsert, "id" | "organizationId">>,
|
|
123
|
+
ctx?: TxContext,
|
|
124
|
+
): Promise<ShippingRate | undefined> {
|
|
125
|
+
const db = this.getDb(ctx);
|
|
126
|
+
const rows = await db
|
|
127
|
+
.update(shippingRates)
|
|
128
|
+
.set({ ...data, updatedAt: new Date() })
|
|
129
|
+
.where(eq(shippingRates.id, id))
|
|
130
|
+
.returning();
|
|
131
|
+
return rows[0];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async deleteRate(id: string, ctx?: TxContext): Promise<boolean> {
|
|
135
|
+
const db = this.getDb(ctx);
|
|
136
|
+
const rows = await db.delete(shippingRates).where(eq(shippingRates.id, id)).returning();
|
|
137
|
+
return rows.length > 0;
|
|
138
|
+
}
|
|
5
139
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { boolean, index, integer, jsonb, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
|
|
2
|
+
import { organization } from "../../auth/auth-schema.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Runtime shipping configuration (issue #45).
|
|
6
|
+
*
|
|
7
|
+
* Zones match a destination (country + optional state); rates belong to a
|
|
8
|
+
* zone and carry a flat amount with optional subtotal/weight bands and a
|
|
9
|
+
* free-shipping threshold. When zones exist for an organization they take
|
|
10
|
+
* precedence over the code-config strategy in `defineConfig({ shipping })`,
|
|
11
|
+
* which remains the fallback.
|
|
12
|
+
*/
|
|
13
|
+
export const shippingZones = pgTable("shipping_zones", {
|
|
14
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
15
|
+
organizationId: text("organization_id")
|
|
16
|
+
.notNull()
|
|
17
|
+
.references(() => organization.id, { onDelete: "cascade" }),
|
|
18
|
+
name: text("name").notNull(),
|
|
19
|
+
// ISO 3166-1 alpha-2 codes, uppercase. "*" matches any country.
|
|
20
|
+
countries: jsonb("countries").$type<string[]>().notNull().default([]),
|
|
21
|
+
// Optional state/province codes; empty array = the whole country.
|
|
22
|
+
states: jsonb("states").$type<string[]>().notNull().default([]),
|
|
23
|
+
priority: integer("priority").notNull().default(100),
|
|
24
|
+
isActive: boolean("is_active").notNull().default(true),
|
|
25
|
+
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
26
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
|
27
|
+
}, (table) => ({
|
|
28
|
+
orgIdx: index("idx_shipping_zones_org").on(table.organizationId),
|
|
29
|
+
priorityIdx: index("idx_shipping_zones_priority").on(table.priority),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
export const shippingRates = pgTable("shipping_rates", {
|
|
33
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
34
|
+
organizationId: text("organization_id")
|
|
35
|
+
.notNull()
|
|
36
|
+
.references(() => organization.id, { onDelete: "cascade" }),
|
|
37
|
+
zoneId: uuid("zone_id")
|
|
38
|
+
.notNull()
|
|
39
|
+
.references(() => shippingZones.id, { onDelete: "cascade" }),
|
|
40
|
+
name: text("name").notNull(),
|
|
41
|
+
currency: text("currency").notNull().default("USD"),
|
|
42
|
+
// Flat cost in minor units
|
|
43
|
+
amount: integer("amount").notNull(),
|
|
44
|
+
// Price bands (subtotal after discount, minor units) — nulls are open-ended
|
|
45
|
+
minSubtotal: integer("min_subtotal"),
|
|
46
|
+
maxSubtotal: integer("max_subtotal"),
|
|
47
|
+
// Weight bands (grams) — nulls are open-ended
|
|
48
|
+
minWeightGrams: integer("min_weight_grams"),
|
|
49
|
+
maxWeightGrams: integer("max_weight_grams"),
|
|
50
|
+
// Subtotal at or above which shipping is free for this rate
|
|
51
|
+
freeShippingThreshold: integer("free_shipping_threshold"),
|
|
52
|
+
isActive: boolean("is_active").notNull().default(true),
|
|
53
|
+
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
54
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
|
55
|
+
}, (table) => ({
|
|
56
|
+
zoneIdx: index("idx_shipping_rates_zone").on(table.zoneId),
|
|
57
|
+
orgIdx: index("idx_shipping_rates_org").on(table.organizationId),
|
|
58
|
+
}));
|