@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
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
import { CommerceValidationError } from "../../kernel/errors.js";
|
|
3
|
+
import { Err, Ok } from "../../kernel/result.js";
|
|
4
|
+
export const RETAIL_REPORTS = [
|
|
5
|
+
{ name: "daily-journal", description: "Per-day sales journal: order rows + summary with prior-day deltas." },
|
|
6
|
+
{ name: "tax-summary", description: "Tax collected over a range, bucketed per local day, with prior-period deltas." },
|
|
7
|
+
{ name: "inventory-aging", description: "On-hand stock bucketed by days since last restock (0-30/31-60/61-90/90+)." },
|
|
8
|
+
{ name: "sell-through", description: "Units sold in the range vs current on-hand, per entity." },
|
|
9
|
+
{ name: "reorder-needed", description: "Inventory rows at or below their reorder threshold." },
|
|
10
|
+
{ name: "staff-sales", description: "Orders and revenue grouped by orders.metadata.staffId." },
|
|
11
|
+
];
|
|
12
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
13
|
+
// Orders excluded from financial reports.
|
|
14
|
+
const EXCLUDED_STATUSES = ["cancelled", "voided"];
|
|
15
|
+
export class RetailReportsEngine {
|
|
16
|
+
db;
|
|
17
|
+
readSettings;
|
|
18
|
+
constructor(db, readSettings) {
|
|
19
|
+
this.db = db;
|
|
20
|
+
this.readSettings = readSettings;
|
|
21
|
+
}
|
|
22
|
+
async run(name, params, orgId) {
|
|
23
|
+
for (const value of [params.date, params.from, params.to]) {
|
|
24
|
+
if (value !== undefined && !DATE_RE.test(value)) {
|
|
25
|
+
return Err(new CommerceValidationError("Dates must be YYYY-MM-DD."));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const tz = await this.timezoneFor(orgId);
|
|
29
|
+
switch (name) {
|
|
30
|
+
case "daily-journal":
|
|
31
|
+
return Ok(await this.dailyJournal(orgId, tz, params.date ?? localToday(tz)));
|
|
32
|
+
case "tax-summary": {
|
|
33
|
+
const [from, to] = this.rangeOrDefault(params, tz);
|
|
34
|
+
return Ok(await this.taxSummary(orgId, tz, from, to));
|
|
35
|
+
}
|
|
36
|
+
case "inventory-aging":
|
|
37
|
+
return Ok(await this.inventoryAging(orgId));
|
|
38
|
+
case "sell-through": {
|
|
39
|
+
const [from, to] = this.rangeOrDefault(params, tz);
|
|
40
|
+
return Ok(await this.sellThrough(orgId, tz, from, to));
|
|
41
|
+
}
|
|
42
|
+
case "reorder-needed":
|
|
43
|
+
return Ok(await this.reorderNeeded(orgId));
|
|
44
|
+
case "staff-sales": {
|
|
45
|
+
const [from, to] = this.rangeOrDefault(params, tz);
|
|
46
|
+
return Ok(await this.staffSales(orgId, tz, from, to));
|
|
47
|
+
}
|
|
48
|
+
default:
|
|
49
|
+
return Err(new CommerceValidationError(`Unknown report "${name}". Available: ${RETAIL_REPORTS.map((r) => r.name).join(", ")}.`));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async timezoneFor(orgId) {
|
|
53
|
+
const general = await this.readSettings(orgId, "general");
|
|
54
|
+
return typeof general.timezone === "string" ? general.timezone : "UTC";
|
|
55
|
+
}
|
|
56
|
+
rangeOrDefault(params, tz) {
|
|
57
|
+
const today = localToday(tz);
|
|
58
|
+
return [params.from ?? `${today.slice(0, 8)}01`, params.to ?? today];
|
|
59
|
+
}
|
|
60
|
+
async rows(query) {
|
|
61
|
+
const result = await this.db.execute(query);
|
|
62
|
+
const raw = Array.isArray(result)
|
|
63
|
+
? result
|
|
64
|
+
: (result.rows ?? []);
|
|
65
|
+
return raw.map((row) => {
|
|
66
|
+
const mapped = {};
|
|
67
|
+
for (const [key, value] of Object.entries(row)) {
|
|
68
|
+
mapped[key] = typeof value === "bigint" ? Number(value) : value;
|
|
69
|
+
}
|
|
70
|
+
return mapped;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
async journalSummary(orgId, tz, from, to) {
|
|
74
|
+
const rows = await this.rows(sql `
|
|
75
|
+
SELECT count(*)::int AS "orderCount",
|
|
76
|
+
COALESCE(sum(grand_total), 0)::int AS "grossSales",
|
|
77
|
+
COALESCE(sum(subtotal), 0)::int AS "subtotal",
|
|
78
|
+
COALESCE(sum(discount_total), 0)::int AS "discounts",
|
|
79
|
+
COALESCE(sum(tax_total), 0)::int AS "tax",
|
|
80
|
+
COALESCE(sum(shipping_total), 0)::int AS "shipping"
|
|
81
|
+
FROM orders
|
|
82
|
+
WHERE organization_id = ${orgId}
|
|
83
|
+
AND status NOT IN (${sql.join(EXCLUDED_STATUSES.map((s) => sql `${s}`), sql `, `)})
|
|
84
|
+
AND (placed_at AT TIME ZONE ${tz})::date BETWEEN ${from}::date AND ${to}::date
|
|
85
|
+
`);
|
|
86
|
+
return rows[0];
|
|
87
|
+
}
|
|
88
|
+
async dailyJournal(orgId, tz, date) {
|
|
89
|
+
const summary = await this.journalSummary(orgId, tz, date, date);
|
|
90
|
+
const previousDate = shiftDate(date, -1);
|
|
91
|
+
const previous = await this.journalSummary(orgId, tz, previousDate, previousDate);
|
|
92
|
+
const orders = await this.rows(sql `
|
|
93
|
+
SELECT order_number AS "orderNumber", status, currency,
|
|
94
|
+
grand_total AS "grandTotal", tax_total AS "tax",
|
|
95
|
+
discount_total AS "discounts", placed_at AS "placedAt"
|
|
96
|
+
FROM orders
|
|
97
|
+
WHERE organization_id = ${orgId}
|
|
98
|
+
AND status NOT IN (${sql.join(EXCLUDED_STATUSES.map((s) => sql `${s}`), sql `, `)})
|
|
99
|
+
AND (placed_at AT TIME ZONE ${tz})::date = ${date}::date
|
|
100
|
+
ORDER BY placed_at ASC
|
|
101
|
+
`);
|
|
102
|
+
return {
|
|
103
|
+
report: "daily-journal",
|
|
104
|
+
timezone: tz,
|
|
105
|
+
date,
|
|
106
|
+
summary,
|
|
107
|
+
previous,
|
|
108
|
+
delta: deltas(summary, previous),
|
|
109
|
+
orders,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
async taxSummary(orgId, tz, from, to) {
|
|
113
|
+
const days = await this.rows(sql `
|
|
114
|
+
SELECT to_char((placed_at AT TIME ZONE ${tz})::date, 'YYYY-MM-DD') AS "day",
|
|
115
|
+
count(*)::int AS "orderCount",
|
|
116
|
+
COALESCE(sum(tax_total), 0)::int AS "tax",
|
|
117
|
+
COALESCE(sum(grand_total), 0)::int AS "gross"
|
|
118
|
+
FROM orders
|
|
119
|
+
WHERE organization_id = ${orgId}
|
|
120
|
+
AND status NOT IN (${sql.join(EXCLUDED_STATUSES.map((s) => sql `${s}`), sql `, `)})
|
|
121
|
+
AND (placed_at AT TIME ZONE ${tz})::date BETWEEN ${from}::date AND ${to}::date
|
|
122
|
+
GROUP BY 1
|
|
123
|
+
ORDER BY 1
|
|
124
|
+
`);
|
|
125
|
+
const totals = await this.journalSummary(orgId, tz, from, to);
|
|
126
|
+
const lengthDays = daysBetween(from, to) + 1;
|
|
127
|
+
const prevFrom = shiftDate(from, -lengthDays);
|
|
128
|
+
const prevTo = shiftDate(to, -lengthDays);
|
|
129
|
+
const previous = await this.journalSummary(orgId, tz, prevFrom, prevTo);
|
|
130
|
+
return {
|
|
131
|
+
report: "tax-summary",
|
|
132
|
+
timezone: tz,
|
|
133
|
+
from,
|
|
134
|
+
to,
|
|
135
|
+
totals,
|
|
136
|
+
previous: { from: prevFrom, to: prevTo, ...previous },
|
|
137
|
+
delta: deltas(totals, previous),
|
|
138
|
+
days,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
async inventoryAging(orgId) {
|
|
142
|
+
const rows = await this.rows(sql `
|
|
143
|
+
SELECT il.entity_id AS "entityId", il.variant_id AS "variantId",
|
|
144
|
+
e.slug AS "slug",
|
|
145
|
+
il.quantity_on_hand AS "quantityOnHand",
|
|
146
|
+
il.quantity_reserved AS "quantityReserved",
|
|
147
|
+
GREATEST(0, EXTRACT(day FROM now() - COALESCE(il.last_restocked_at, il.updated_at)))::int AS "ageDays"
|
|
148
|
+
FROM inventory_levels il
|
|
149
|
+
LEFT JOIN sellable_entities e ON e.id = il.entity_id
|
|
150
|
+
WHERE il.organization_id = ${orgId} AND il.quantity_on_hand > 0
|
|
151
|
+
ORDER BY "ageDays" DESC
|
|
152
|
+
`);
|
|
153
|
+
const buckets = [
|
|
154
|
+
{ bucket: "0-30", min: 0, max: 30 },
|
|
155
|
+
{ bucket: "31-60", min: 31, max: 60 },
|
|
156
|
+
{ bucket: "61-90", min: 61, max: 90 },
|
|
157
|
+
{ bucket: "90+", min: 91, max: Infinity },
|
|
158
|
+
].map(({ bucket, min, max }) => {
|
|
159
|
+
const inBucket = rows.filter((r) => r.ageDays >= min && r.ageDays <= max);
|
|
160
|
+
return {
|
|
161
|
+
bucket,
|
|
162
|
+
skuCount: inBucket.length,
|
|
163
|
+
quantityOnHand: inBucket.reduce((sum, r) => sum + r.quantityOnHand, 0),
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
return { report: "inventory-aging", buckets, rows };
|
|
167
|
+
}
|
|
168
|
+
async sellThrough(orgId, tz, from, to) {
|
|
169
|
+
const rows = await this.rows(sql `
|
|
170
|
+
SELECT li.entity_id AS "entityId",
|
|
171
|
+
min(li.title) AS "title",
|
|
172
|
+
sum(li.quantity)::int AS "unitsSold",
|
|
173
|
+
COALESCE((SELECT sum(quantity_on_hand)::int FROM inventory_levels il
|
|
174
|
+
WHERE il.entity_id = li.entity_id AND il.organization_id = ${orgId}), 0) AS "onHand"
|
|
175
|
+
FROM order_line_items li
|
|
176
|
+
JOIN orders o ON o.id = li.order_id
|
|
177
|
+
WHERE o.organization_id = ${orgId}
|
|
178
|
+
AND o.status NOT IN (${sql.join(EXCLUDED_STATUSES.map((s) => sql `${s}`), sql `, `)})
|
|
179
|
+
AND (o.placed_at AT TIME ZONE ${tz})::date BETWEEN ${from}::date AND ${to}::date
|
|
180
|
+
GROUP BY li.entity_id
|
|
181
|
+
ORDER BY "unitsSold" DESC
|
|
182
|
+
`);
|
|
183
|
+
return {
|
|
184
|
+
report: "sell-through",
|
|
185
|
+
timezone: tz,
|
|
186
|
+
from,
|
|
187
|
+
to,
|
|
188
|
+
rows: rows.map((r) => {
|
|
189
|
+
const sold = r.unitsSold;
|
|
190
|
+
const onHand = r.onHand;
|
|
191
|
+
return { ...r, sellThroughRate: sold + onHand > 0 ? sold / (sold + onHand) : 0 };
|
|
192
|
+
}),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
async reorderNeeded(orgId) {
|
|
196
|
+
const rows = await this.rows(sql `
|
|
197
|
+
SELECT il.entity_id AS "entityId", il.variant_id AS "variantId",
|
|
198
|
+
il.warehouse_id AS "warehouseId", e.slug AS "slug",
|
|
199
|
+
il.quantity_on_hand AS "quantityOnHand",
|
|
200
|
+
il.quantity_reserved AS "quantityReserved",
|
|
201
|
+
(il.quantity_on_hand - il.quantity_reserved) AS "available",
|
|
202
|
+
il.reorder_threshold AS "reorderThreshold",
|
|
203
|
+
il.reorder_quantity AS "reorderQuantity"
|
|
204
|
+
FROM inventory_levels il
|
|
205
|
+
LEFT JOIN sellable_entities e ON e.id = il.entity_id
|
|
206
|
+
WHERE il.organization_id = ${orgId}
|
|
207
|
+
AND il.reorder_threshold IS NOT NULL
|
|
208
|
+
AND (il.quantity_on_hand - il.quantity_reserved) <= il.reorder_threshold
|
|
209
|
+
ORDER BY "available" ASC
|
|
210
|
+
`);
|
|
211
|
+
return { report: "reorder-needed", rows };
|
|
212
|
+
}
|
|
213
|
+
async staffSales(orgId, tz, from, to) {
|
|
214
|
+
const rows = await this.rows(sql `
|
|
215
|
+
SELECT COALESCE(metadata->>'staffId', 'unattributed') AS "staffId",
|
|
216
|
+
count(*)::int AS "orderCount",
|
|
217
|
+
COALESCE(sum(grand_total), 0)::int AS "revenue",
|
|
218
|
+
COALESCE(sum(discount_total), 0)::int AS "discounts"
|
|
219
|
+
FROM orders
|
|
220
|
+
WHERE organization_id = ${orgId}
|
|
221
|
+
AND status NOT IN (${sql.join(EXCLUDED_STATUSES.map((s) => sql `${s}`), sql `, `)})
|
|
222
|
+
AND (placed_at AT TIME ZONE ${tz})::date BETWEEN ${from}::date AND ${to}::date
|
|
223
|
+
GROUP BY 1
|
|
224
|
+
ORDER BY "revenue" DESC
|
|
225
|
+
`);
|
|
226
|
+
return { report: "staff-sales", timezone: tz, from, to, rows };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// ── Pure date helpers (calendar dates, no time component) ──────────────────
|
|
230
|
+
/** Today's local calendar date (YYYY-MM-DD) in an IANA timezone. */
|
|
231
|
+
export function localToday(tz) {
|
|
232
|
+
return new Intl.DateTimeFormat("en-CA", { timeZone: tz }).format(new Date());
|
|
233
|
+
}
|
|
234
|
+
function shiftDate(date, days) {
|
|
235
|
+
const d = new Date(`${date}T00:00:00Z`);
|
|
236
|
+
d.setUTCDate(d.getUTCDate() + days);
|
|
237
|
+
return d.toISOString().slice(0, 10);
|
|
238
|
+
}
|
|
239
|
+
function daysBetween(from, to) {
|
|
240
|
+
const ms = new Date(`${to}T00:00:00Z`).getTime() - new Date(`${from}T00:00:00Z`).getTime();
|
|
241
|
+
return Math.round(ms / 86_400_000);
|
|
242
|
+
}
|
|
243
|
+
function deltas(current, previous) {
|
|
244
|
+
const out = {};
|
|
245
|
+
for (const key of Object.keys(current)) {
|
|
246
|
+
out[key] = (current[key] ?? 0) - (previous[key] ?? 0);
|
|
247
|
+
}
|
|
248
|
+
return out;
|
|
249
|
+
}
|
|
@@ -7,11 +7,14 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { CommerceConfig } from "../../config/types.js";
|
|
9
9
|
import { type Result } from "../../kernel/result.js";
|
|
10
|
+
import type { Actor } from "../../auth/types.js";
|
|
11
|
+
import type { ReportParams, RetailReportsEngine } from "./reports.js";
|
|
10
12
|
import type { AnalyticsAdapter, AnalyticsMeta, AnalyticsQueryParams, AnalyticsQueryResult, AnalyticsScope } from "./types.js";
|
|
11
13
|
export type { AnalyticsTimeDimension, AnalyticsFilter, AnalyticsQueryParams, AnalyticsModelDefinition, AnalyticsMeta, } from "./types.js";
|
|
12
14
|
export interface AnalyticsServiceDeps {
|
|
13
15
|
adapter: AnalyticsAdapter;
|
|
14
16
|
config: CommerceConfig;
|
|
17
|
+
reports?: RetailReportsEngine;
|
|
15
18
|
}
|
|
16
19
|
export declare class AnalyticsService {
|
|
17
20
|
private deps;
|
|
@@ -33,6 +36,13 @@ export declare class AnalyticsService {
|
|
|
33
36
|
*/
|
|
34
37
|
query(params: AnalyticsQueryParams, scope: AnalyticsScope): Promise<Result<AnalyticsQueryResult>>;
|
|
35
38
|
getDashboard(name: string, scope: AnalyticsScope): Promise<Result<AnalyticsQueryResult>>;
|
|
39
|
+
/** The canned retail reports pack (issue #48). */
|
|
40
|
+
listReports(): Array<{
|
|
41
|
+
name: string;
|
|
42
|
+
description: string;
|
|
43
|
+
}>;
|
|
44
|
+
/** Runs a canned retail report (issue #48), org-scoped to the actor. */
|
|
45
|
+
getReport(name: string, params: ReportParams, actor?: Actor | null): Promise<Result<Record<string, unknown>>>;
|
|
36
46
|
getMeta(): Promise<Result<AnalyticsMeta>>;
|
|
37
47
|
meta(): Promise<Result<{
|
|
38
48
|
measures: string[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../../src/modules/analytics/service.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../../src/modules/analytics/service.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAE9D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAEtE,OAAO,KAAK,EACV,gBAAgB,EAChB,aAAa,EAGb,oBAAoB,EACpB,oBAAoB,EACpB,cAAc,EACf,MAAM,YAAY,CAAC;AAGpB,YAAY,EACV,sBAAsB,EACtB,eAAe,EACf,oBAAoB,EACpB,wBAAwB,EACxB,aAAa,GACd,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,gBAAgB,CAAC;IAC1B,MAAM,EAAE,cAAc,CAAC;IACvB,OAAO,CAAC,EAAE,mBAAmB,CAAC;CAC/B;AAED,qBAAa,gBAAgB;IAKf,OAAO,CAAC,IAAI;IAJxB,OAAO,CAAC,YAAY,CAAkC;IACtD,OAAO,CAAC,kBAAkB,CAAkC;IAC5D,OAAO,CAAC,kBAAkB,CAAS;gBAEf,IAAI,EAAE,oBAAoB;IAE9C;;;;;;OAMG;IACH,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAiCnC;;;OAGG;IACG,KAAK,CAAC,MAAM,EAAE,oBAAoB,EAAE,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;IAIjG,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;IA4B9F,kDAAkD;IAClD,WAAW,IAAI,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;IAI3D,wEAAwE;IAClE,SAAS,CACb,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,YAAY,EACpB,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,GACnB,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAQrC,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;IAsBzC,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC;QAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;QAAC,UAAU,EAAE,MAAM,EAAE,CAAC;QAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IAY/F,OAAO,CAAC,gBAAgB;YA0CV,8BAA8B;CAkD7C"}
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { CommerceValidationError } from "../../kernel/errors.js";
|
|
9
9
|
import { Err, Ok } from "../../kernel/result.js";
|
|
10
|
+
import { resolveOrgId } from "../../auth/org.js";
|
|
11
|
+
import { RETAIL_REPORTS } from "./reports.js";
|
|
10
12
|
export class AnalyticsService {
|
|
11
13
|
deps;
|
|
12
14
|
pluginModels = [];
|
|
@@ -80,6 +82,18 @@ export class AnalyticsService {
|
|
|
80
82
|
}
|
|
81
83
|
return Err(new CommerceValidationError(`Unknown analytics dashboard: ${name}`));
|
|
82
84
|
}
|
|
85
|
+
/** The canned retail reports pack (issue #48). */
|
|
86
|
+
listReports() {
|
|
87
|
+
return RETAIL_REPORTS.map((r) => ({ name: r.name, description: r.description }));
|
|
88
|
+
}
|
|
89
|
+
/** Runs a canned retail report (issue #48), org-scoped to the actor. */
|
|
90
|
+
async getReport(name, params, actor) {
|
|
91
|
+
if (!this.deps.reports) {
|
|
92
|
+
return Err(new CommerceValidationError("Retail reports are not available."));
|
|
93
|
+
}
|
|
94
|
+
const orgId = resolveOrgId(actor ?? null);
|
|
95
|
+
return this.deps.reports.run(name, params, orgId);
|
|
96
|
+
}
|
|
83
97
|
async getMeta() {
|
|
84
98
|
await this.ensureCustomSchemaModelsLoaded();
|
|
85
99
|
// Meta returns model definitions, not data — always use admin scope
|
|
@@ -18,6 +18,13 @@ export declare class CartRepository {
|
|
|
18
18
|
findById(orgId: string, id: string, ctx?: TxContext): Promise<Cart | undefined>;
|
|
19
19
|
findByCustomerId(orgId: string, customerId: string, ctx?: TxContext): Promise<Cart | undefined>;
|
|
20
20
|
findActiveByCustomerId(orgId: string, customerId: string, ctx?: TxContext): Promise<Cart | undefined>;
|
|
21
|
+
list(orgId: string, filter?: {
|
|
22
|
+
status?: string;
|
|
23
|
+
olderThan?: Date;
|
|
24
|
+
hasCustomer?: boolean;
|
|
25
|
+
}, ctx?: TxContext): Promise<Array<Cart & {
|
|
26
|
+
customerEmail: string | null;
|
|
27
|
+
}>>;
|
|
21
28
|
findExpiredCarts(ctx?: TxContext): Promise<Cart[]>;
|
|
22
29
|
create(data: CartInsert, ctx?: TxContext): Promise<Cart>;
|
|
23
30
|
update(id: string, data: Partial<Omit<CartInsert, "id">>, ctx?: TxContext): Promise<Cart | undefined>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/modules/cart/repository/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wCAAwC,CAAC;AACxE,OAAO,KAAK,EACV,eAAe,EAEhB,MAAM,wCAAwC,CAAC;AAChD,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/modules/cart/repository/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { eq, and, lt } from "drizzle-orm";
|
|
1
|
+
import { eq, and, lt, desc, isNull, isNotNull } from "drizzle-orm";
|
|
2
2
|
import { carts, cartLineItems } from "../schema.js";
|
|
3
|
+
import { customers } from "../../customers/schema.js";
|
|
3
4
|
/**
|
|
4
5
|
* CartRepository provides type-safe database operations for shopping carts.
|
|
5
6
|
*
|
|
@@ -41,6 +42,29 @@ export class CartRepository {
|
|
|
41
42
|
.where(and(eq(carts.organizationId, orgId), eq(carts.customerId, customerId), eq(carts.status, "active")));
|
|
42
43
|
return rows[0];
|
|
43
44
|
}
|
|
45
|
+
async list(orgId, filter, ctx) {
|
|
46
|
+
const db = this.getDb(ctx);
|
|
47
|
+
const conditions = [eq(carts.organizationId, orgId)];
|
|
48
|
+
if (filter?.status) {
|
|
49
|
+
conditions.push(eq(carts.status, filter.status));
|
|
50
|
+
}
|
|
51
|
+
if (filter?.olderThan) {
|
|
52
|
+
conditions.push(lt(carts.updatedAt, filter.olderThan));
|
|
53
|
+
}
|
|
54
|
+
if (filter?.hasCustomer === true) {
|
|
55
|
+
conditions.push(isNotNull(carts.customerId));
|
|
56
|
+
}
|
|
57
|
+
if (filter?.hasCustomer === false) {
|
|
58
|
+
conditions.push(isNull(carts.customerId));
|
|
59
|
+
}
|
|
60
|
+
const rows = await db
|
|
61
|
+
.select({ cart: carts, customerEmail: customers.email })
|
|
62
|
+
.from(carts)
|
|
63
|
+
.leftJoin(customers, eq(carts.customerId, customers.id))
|
|
64
|
+
.where(and(...conditions))
|
|
65
|
+
.orderBy(desc(carts.updatedAt));
|
|
66
|
+
return rows.map((r) => ({ ...r.cart, customerEmail: r.customerEmail ?? null }));
|
|
67
|
+
}
|
|
44
68
|
async findExpiredCarts(ctx) {
|
|
45
69
|
const db = this.getDb(ctx);
|
|
46
70
|
return db
|
|
@@ -87,6 +87,23 @@ export declare const carts: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
|
87
87
|
identity: undefined;
|
|
88
88
|
generated: undefined;
|
|
89
89
|
}, {}, {}>;
|
|
90
|
+
email: import("drizzle-orm/pg-core").PgColumn<{
|
|
91
|
+
name: "email";
|
|
92
|
+
tableName: "carts";
|
|
93
|
+
dataType: "string";
|
|
94
|
+
columnType: "PgText";
|
|
95
|
+
data: string;
|
|
96
|
+
driverParam: string;
|
|
97
|
+
notNull: false;
|
|
98
|
+
hasDefault: false;
|
|
99
|
+
isPrimaryKey: false;
|
|
100
|
+
isAutoincrement: false;
|
|
101
|
+
hasRuntimeDefault: false;
|
|
102
|
+
enumValues: [string, ...string[]];
|
|
103
|
+
baseColumn: never;
|
|
104
|
+
identity: undefined;
|
|
105
|
+
generated: undefined;
|
|
106
|
+
}, {}, {}>;
|
|
90
107
|
secret: import("drizzle-orm/pg-core").PgColumn<{
|
|
91
108
|
name: "secret";
|
|
92
109
|
tableName: "carts";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../src/modules/cart/schema.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,KAAK
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../src/modules/cart/schema.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAsBf,CAAC;AAEJ,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAexB,CAAC"}
|
|
@@ -11,6 +11,8 @@ export const carts = pgTable("carts", {
|
|
|
11
11
|
.notNull()
|
|
12
12
|
.default("active"),
|
|
13
13
|
currency: text("currency").notNull().default("USD"),
|
|
14
|
+
// Shopper contact for guest carts — enables abandoned-checkout recovery
|
|
15
|
+
email: text("email"),
|
|
14
16
|
secret: text("secret"),
|
|
15
17
|
metadata: jsonb("metadata").$type().default({}),
|
|
16
18
|
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
|
|
@@ -2,6 +2,7 @@ import { z } from "@hono/zod-openapi";
|
|
|
2
2
|
export declare const CreateCartBodySchema: z.ZodObject<{
|
|
3
3
|
customerId: z.ZodOptional<z.ZodString>;
|
|
4
4
|
currency: z.ZodOptional<z.ZodString>;
|
|
5
|
+
email: z.ZodOptional<z.ZodEmail>;
|
|
5
6
|
}, z.core.$strip>;
|
|
6
7
|
export declare const AddCartItemBodySchema: z.ZodObject<{
|
|
7
8
|
entityId: z.ZodString;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../../../src/modules/cart/schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,mBAAmB,CAAC;AAItC,eAAO,MAAM,oBAAoB
|
|
1
|
+
{"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../../../src/modules/cart/schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,mBAAmB,CAAC;AAItC,eAAO,MAAM,oBAAoB;;;;iBAIF,CAAC;AAEhC,eAAO,MAAM,qBAAqB;;;;iBAIF,CAAC;AAEjC,eAAO,MAAM,gCAAgC;;iBAEF,CAAC;AAI5C,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,GAAG;IACnE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,GAAG;IACrE,MAAM,EAAE,MAAM,CAAC;IACf,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC,CAAC;AAEF,uFAAuF;AACvF,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB"}
|
|
@@ -3,6 +3,7 @@ import { z } from "@hono/zod-openapi";
|
|
|
3
3
|
export const CreateCartBodySchema = z.object({
|
|
4
4
|
customerId: z.string().optional().openapi({ example: "customer-uuid" }),
|
|
5
5
|
currency: z.string().length(3).optional().openapi({ example: "USD" }),
|
|
6
|
+
email: z.email().optional().openapi({ example: "shopper@example.com" }),
|
|
6
7
|
}).openapi("CreateCartRequest");
|
|
7
8
|
export const AddCartItemBodySchema = z.object({
|
|
8
9
|
entityId: z.string().openapi({ example: "product-uuid" }),
|
|
@@ -2,6 +2,7 @@ import type { Actor } from "../../auth/types.js";
|
|
|
2
2
|
import type { CommerceConfig } from "../../config/types.js";
|
|
3
3
|
import type { HookRegistry } from "../../kernel/hooks/registry.js";
|
|
4
4
|
import { type Result } from "../../kernel/result.js";
|
|
5
|
+
import { type Pagination } from "../../utils/pagination.js";
|
|
5
6
|
import type { DatabaseAdapter } from "../../kernel/database/adapter.js";
|
|
6
7
|
import type { TxContext } from "../../kernel/database/tx-context.js";
|
|
7
8
|
import { CartRepository, type Cart, type CartLineItem } from "./repository/index.js";
|
|
@@ -31,6 +32,37 @@ export declare class CartService {
|
|
|
31
32
|
removeItem(cartId: string, itemId: string, actor?: Actor | null, ctx?: TxContext): Promise<Result<void>>;
|
|
32
33
|
updateQuantity(input: UpdateCartItemInput, actor?: Actor | null, ctx?: TxContext): Promise<Result<CartLineItem>>;
|
|
33
34
|
merge(sourceCartId: string, targetCartId: string, actor?: Actor | null, ctx?: TxContext): Promise<Result<void>>;
|
|
35
|
+
/**
|
|
36
|
+
* Admin listing of carts with abandoned-checkout recovery filters:
|
|
37
|
+
* status, olderThan (not touched since), hasCustomer, plus pagination.
|
|
38
|
+
* Returns shopper identity (cart email + linked customer email).
|
|
39
|
+
*/
|
|
40
|
+
list(filter?: {
|
|
41
|
+
status?: string;
|
|
42
|
+
olderThan?: Date;
|
|
43
|
+
hasCustomer?: boolean;
|
|
44
|
+
page?: number;
|
|
45
|
+
limit?: number;
|
|
46
|
+
}, actor?: Actor | null, ctx?: TxContext): Promise<Result<{
|
|
47
|
+
items: Array<Cart & {
|
|
48
|
+
customerEmail: string | null;
|
|
49
|
+
}>;
|
|
50
|
+
pagination: Pagination;
|
|
51
|
+
}>>;
|
|
52
|
+
/**
|
|
53
|
+
* Abandoned-checkout recovery primitive: reactivates the cart, extends its
|
|
54
|
+
* expiry, and returns a resume secret that gates guest access to the cart —
|
|
55
|
+
* enough to build a recovery email with a resume/checkout link.
|
|
56
|
+
* Fires the `cart.afterRecover` hook so plugins/webhooks can react.
|
|
57
|
+
*/
|
|
58
|
+
recover(cartId: string, actor?: Actor | null, ctx?: TxContext): Promise<Result<{
|
|
59
|
+
cartId: string;
|
|
60
|
+
secret: string;
|
|
61
|
+
status: string;
|
|
62
|
+
expiresAt: string;
|
|
63
|
+
email: string | null;
|
|
64
|
+
customerId: string | null;
|
|
65
|
+
}>>;
|
|
34
66
|
abandon(cartId: string, actor?: Actor | null, ctx?: TxContext): Promise<Result<void>>;
|
|
35
67
|
markAsCheckedOut(cartId: string, actor?: Actor | null, ctx?: TxContext): Promise<Result<void>>;
|
|
36
68
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../../src/modules/cart/service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAc5D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAE9D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAC;AAExE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qCAAqC,CAAC;AACrE,OAAO,EAAE,cAAc,EAAE,KAAK,IAAI,EAAE,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AAExE,YAAY,EACV,eAAe,EACf,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,cAAc,CAAC;AAEtB,OAAO,KAAK,EACV,eAAe,EACf,gBAAgB,EAChB,mBAAmB,EACpB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAA0B,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAE5E,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,cAAc,CAAC;IAC3B,iBAAiB,EAAE,iBAAiB,CAAC;IACrC,KAAK,EAAE,YAAY,CAAC;IACpB,MAAM,EAAE,cAAc,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,QAAQ,EAAE,eAAe,CAAC;IAC1B,eAAe,CAAC,EAAE,eAAe,CAAC;CACnC;AA6BD,qBAAa,WAAW;IAIV,OAAO,CAAC,IAAI;IAHxB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAiB;IACtC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAoB;gBAE5B,IAAI,EAAE,eAAe;IAKnC,MAAM,CACV,KAAK,EAAE,eAAe,EACtB,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,EACpB,GAAG,CAAC,EAAE,SAAS,GACd,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../../src/modules/cart/service.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,6 +5,7 @@ import { runAfterHooks, runBeforeHooks } from "../../kernel/hooks/executor.js";
|
|
|
5
5
|
import { createHookContext } from "../../kernel/hooks/create-context.js";
|
|
6
6
|
import { Err, Ok } from "../../kernel/result.js";
|
|
7
7
|
import { createLogger } from "../../utils/logger.js";
|
|
8
|
+
import { paginate } from "../../utils/pagination.js";
|
|
8
9
|
import { defaultCartItemMatcher } from "./matcher.js";
|
|
9
10
|
function makeContext(actor, services, database, tx = null) {
|
|
10
11
|
return createHookContext({
|
|
@@ -80,6 +81,7 @@ export class CartService {
|
|
|
80
81
|
metadata: input.metadata ?? {},
|
|
81
82
|
expiresAt: new Date(now.getTime() + ttlMinutes * 60 * 1000),
|
|
82
83
|
...(resolvedCustomerId !== null ? { customerId: resolvedCustomerId } : {}),
|
|
84
|
+
...(input.email !== undefined ? { email: input.email } : {}),
|
|
83
85
|
}, ctx);
|
|
84
86
|
return Ok(cart);
|
|
85
87
|
}
|
|
@@ -134,8 +136,9 @@ export class CartService {
|
|
|
134
136
|
if (quantity <= 0) {
|
|
135
137
|
return Err(new CommerceValidationError("Quantity must be greater than zero."));
|
|
136
138
|
}
|
|
137
|
-
// Validate entity exists
|
|
138
|
-
|
|
139
|
+
// Validate entity exists AND belongs to the actor's org (org-scoped lookup
|
|
140
|
+
// so another tenant's entity id cannot be added to this cart).
|
|
141
|
+
const entity = await this.catalogRepo.findEntityById(input.entityId, ctx, orgId);
|
|
139
142
|
if (!entity)
|
|
140
143
|
return Err(new CommerceNotFoundError("Entity not found."));
|
|
141
144
|
// Check if entity has variants
|
|
@@ -279,6 +282,62 @@ export class CartService {
|
|
|
279
282
|
await this.repo.updateStatus(sourceCartId, "merged", ctx);
|
|
280
283
|
return Ok(undefined);
|
|
281
284
|
}
|
|
285
|
+
/**
|
|
286
|
+
* Admin listing of carts with abandoned-checkout recovery filters:
|
|
287
|
+
* status, olderThan (not touched since), hasCustomer, plus pagination.
|
|
288
|
+
* Returns shopper identity (cart email + linked customer email).
|
|
289
|
+
*/
|
|
290
|
+
async list(filter, actor, ctx) {
|
|
291
|
+
try {
|
|
292
|
+
assertPermission(actor ?? null, "cart:manage");
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
return Err(toCommerceError(error));
|
|
296
|
+
}
|
|
297
|
+
const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
|
|
298
|
+
const rows = await this.repo.list(orgId, {
|
|
299
|
+
...(filter?.status !== undefined ? { status: filter.status } : {}),
|
|
300
|
+
...(filter?.olderThan !== undefined ? { olderThan: filter.olderThan } : {}),
|
|
301
|
+
...(filter?.hasCustomer !== undefined ? { hasCustomer: filter.hasCustomer } : {}),
|
|
302
|
+
}, ctx);
|
|
303
|
+
const paged = paginate(rows, filter?.page ?? 1, filter?.limit ?? 20);
|
|
304
|
+
return Ok({ items: paged.items, pagination: paged.pagination });
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Abandoned-checkout recovery primitive: reactivates the cart, extends its
|
|
308
|
+
* expiry, and returns a resume secret that gates guest access to the cart —
|
|
309
|
+
* enough to build a recovery email with a resume/checkout link.
|
|
310
|
+
* Fires the `cart.afterRecover` hook so plugins/webhooks can react.
|
|
311
|
+
*/
|
|
312
|
+
async recover(cartId, actor, ctx) {
|
|
313
|
+
try {
|
|
314
|
+
assertPermission(actor ?? null, "cart:manage");
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
return Err(toCommerceError(error));
|
|
318
|
+
}
|
|
319
|
+
const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
|
|
320
|
+
const cart = await this.repo.findById(orgId, cartId, ctx);
|
|
321
|
+
if (!cart)
|
|
322
|
+
return Err(new CommerceNotFoundError("Cart not found."));
|
|
323
|
+
if (cart.status === "checked_out" || cart.status === "merged" || cart.status === "checking_out") {
|
|
324
|
+
return Err(new CommerceValidationError(`Cart with status "${cart.status}" cannot be recovered.`));
|
|
325
|
+
}
|
|
326
|
+
const secret = cart.secret ?? crypto.randomUUID();
|
|
327
|
+
const ttlMinutes = this.deps.config.cart?.ttlMinutes ?? 60 * 24 * 7;
|
|
328
|
+
const expiresAt = new Date(Date.now() + ttlMinutes * 60 * 1000);
|
|
329
|
+
const updated = await this.repo.update(cart.id, { status: "active", secret, expiresAt }, ctx);
|
|
330
|
+
const afterHooks = this.deps.hooks.resolve("cart.afterRecover");
|
|
331
|
+
await runAfterHooks(afterHooks, null, updated ?? cart, "recover", makeContext(actor ?? null, this.deps.services, this.deps.database, ctx?.tx ?? null));
|
|
332
|
+
return Ok({
|
|
333
|
+
cartId: cart.id,
|
|
334
|
+
secret,
|
|
335
|
+
status: "active",
|
|
336
|
+
expiresAt: expiresAt.toISOString(),
|
|
337
|
+
email: cart.email ?? null,
|
|
338
|
+
customerId: cart.customerId ?? null,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
282
341
|
async abandon(cartId, actor, ctx) {
|
|
283
342
|
const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
|
|
284
343
|
const cart = await this.repo.findById(orgId, cartId, ctx);
|
|
@@ -26,5 +26,26 @@ export declare class EntityService {
|
|
|
26
26
|
createOptionValue(input: CreateOptionValueInput, actor: Actor | null, ctx?: TxContext): Promise<Result<OptionValue>>;
|
|
27
27
|
createVariant(input: CreateVariantInput, actor: Actor | null, ctx?: TxContext): Promise<Result<Variant>>;
|
|
28
28
|
generateVariants(entityId: string, strategy: VariantGenerationStrategy, actor: Actor | null, ctx?: TxContext): Promise<Result<Variant[]>>;
|
|
29
|
+
private upsertAxes;
|
|
30
|
+
private existingComboKeys;
|
|
31
|
+
private seedInventoryLevel;
|
|
32
|
+
quickCreateVariant(entityId: string, input: {
|
|
33
|
+
options: Record<string, string>;
|
|
34
|
+
sku?: string | undefined;
|
|
35
|
+
barcode?: string | undefined;
|
|
36
|
+
}, actor: Actor | null, ctx?: TxContext): Promise<Result<{
|
|
37
|
+
variant: Variant;
|
|
38
|
+
created: boolean;
|
|
39
|
+
}>>;
|
|
40
|
+
bulkCreateVariants(entityId: string, input: {
|
|
41
|
+
axes: Array<{
|
|
42
|
+
name: string;
|
|
43
|
+
values: string[];
|
|
44
|
+
}>;
|
|
45
|
+
skuPrefix?: string | undefined;
|
|
46
|
+
}, actor: Actor | null, ctx?: TxContext): Promise<Result<{
|
|
47
|
+
created: Variant[];
|
|
48
|
+
skipped: number;
|
|
49
|
+
}>>;
|
|
29
50
|
}
|
|
30
51
|
//# sourceMappingURL=entity-service.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entity-service.d.ts","sourceRoot":"","sources":["../../../src/modules/catalog/entity-service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAkBjD,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAI9D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qCAAqC,CAAC;AACrE,OAAO,KAAK,EACV,kBAAkB,EAClB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EACjB,UAAU,EACV,UAAU,EACV,kBAAkB,EAClB,iBAAiB,EACjB,yBAAyB,EACzB,qBAAqB,EACrB,sBAAsB,EACtB,kBAAkB,EACnB,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAEV,iBAAiB,EAGjB,UAAU,EACV,WAAW,EACX,OAAO,EACR,MAAM,uBAAuB,CAAC;AAsC/B,qBAAa,aAAa;IACZ,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,kBAAkB;IAErD,OAAO,KAAK,IAAI,GAEf;IAED,OAAO,CAAC,aAAa;YAQP,6BAA6B;YAyD7B,aAAa;IAgDrB,MAAM,CAAC,KAAK,EAAE,iBAAiB,EAAE,KAAK,EAAE,KAAK,GAAG,IAAI,EAAE,GAAG,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,qBAAqB,CAAC,CAAC;IAoB9G,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,EAAE,KAAK,EAAE,KAAK,GAAG,IAAI,EAAE,GAAG,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,qBAAqB,CAAC,CAAC;IAgB1H,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,GAAG,IAAI,EAAE,GAAG,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAe/E,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,UAAU,EAAE,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,EAAE,GAAG,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,qBAAqB,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"entity-service.d.ts","sourceRoot":"","sources":["../../../src/modules/catalog/entity-service.ts"],"names":[],"mappings":"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"}
|