@unifiedcommerce/core 0.1.0 → 0.2.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/setup.d.ts.map +1 -1
- package/dist/auth/setup.js +8 -3
- package/dist/config/types.d.ts +3 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/interfaces/mcp/server.d.ts +3 -5
- package/dist/interfaces/mcp/server.d.ts.map +1 -1
- package/dist/interfaces/mcp/server.js +25 -510
- package/dist/interfaces/mcp/tool-builder.d.ts +120 -0
- package/dist/interfaces/mcp/tool-builder.d.ts.map +1 -0
- package/dist/interfaces/mcp/tool-builder.js +224 -0
- package/dist/interfaces/mcp/tools/analytics.d.ts +42 -0
- package/dist/interfaces/mcp/tools/analytics.d.ts.map +1 -0
- package/dist/interfaces/mcp/tools/analytics.js +70 -0
- package/dist/interfaces/mcp/tools/cart.d.ts +14 -0
- package/dist/interfaces/mcp/tools/cart.d.ts.map +1 -0
- package/dist/interfaces/mcp/tools/cart.js +47 -0
- package/dist/interfaces/mcp/tools/catalog.d.ts +53 -0
- package/dist/interfaces/mcp/tools/catalog.d.ts.map +1 -0
- package/dist/interfaces/mcp/tools/catalog.js +284 -0
- package/dist/interfaces/mcp/tools/index.d.ts +3 -0
- package/dist/interfaces/mcp/tools/index.d.ts.map +1 -0
- package/dist/interfaces/mcp/tools/index.js +20 -0
- package/dist/interfaces/mcp/tools/inventory.d.ts +27 -0
- package/dist/interfaces/mcp/tools/inventory.d.ts.map +1 -0
- package/dist/interfaces/mcp/tools/inventory.js +143 -0
- package/dist/interfaces/mcp/tools/orders.d.ts +18 -0
- package/dist/interfaces/mcp/tools/orders.d.ts.map +1 -0
- package/dist/interfaces/mcp/tools/orders.js +82 -0
- package/dist/interfaces/mcp/tools/pricing.d.ts +29 -0
- package/dist/interfaces/mcp/tools/pricing.d.ts.map +1 -0
- package/dist/interfaces/mcp/tools/pricing.js +90 -0
- package/dist/interfaces/mcp/tools/promotions.d.ts +44 -0
- package/dist/interfaces/mcp/tools/promotions.d.ts.map +1 -0
- package/dist/interfaces/mcp/tools/promotions.js +109 -0
- package/dist/interfaces/mcp/tools/registry.d.ts +32 -0
- package/dist/interfaces/mcp/tools/registry.d.ts.map +1 -0
- package/dist/interfaces/mcp/tools/registry.js +55 -0
- package/dist/interfaces/mcp/tools/search.d.ts +14 -0
- package/dist/interfaces/mcp/tools/search.d.ts.map +1 -0
- package/dist/interfaces/mcp/tools/search.js +39 -0
- package/dist/interfaces/mcp/tools/webhooks.d.ts +15 -0
- package/dist/interfaces/mcp/tools/webhooks.d.ts.map +1 -0
- package/dist/interfaces/mcp/tools/webhooks.js +48 -0
- package/dist/interfaces/mcp/transport.d.ts +17 -2
- package/dist/interfaces/mcp/transport.d.ts.map +1 -1
- package/dist/interfaces/mcp/transport.js +91 -44
- package/dist/interfaces/rest/router.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/checkout.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/checkout.js +1 -1
- package/dist/interfaces/rest/routes/promotions.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/promotions.js +3 -2
- package/dist/kernel/database/adapter.d.ts +8 -0
- package/dist/kernel/database/adapter.d.ts.map +1 -1
- package/dist/kernel/factory/repository-factory.d.ts.map +1 -1
- package/dist/kernel/factory/repository-factory.js +3 -1
- package/dist/kernel/local-api.d.ts.map +1 -1
- package/dist/kernel/local-api.js +2 -0
- package/dist/kernel/plugin/manifest.d.ts +3 -3
- package/dist/kernel/plugin/manifest.d.ts.map +1 -1
- package/dist/kernel/plugin/manifest.js +36 -7
- package/dist/runtime/kernel.d.ts +1 -2
- package/dist/runtime/kernel.d.ts.map +1 -1
- package/dist/runtime/kernel.js +16 -8
- package/dist/runtime/server.d.ts.map +1 -1
- package/dist/runtime/server.js +8 -3
- package/dist/test-utils/create-pglite-adapter.d.ts.map +1 -1
- package/dist/test-utils/create-pglite-adapter.js +7 -6
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +2 -2
- package/src/adapters/console-email.ts +0 -43
- package/src/auth/access.ts +0 -187
- package/src/auth/auth-schema.ts +0 -139
- package/src/auth/middleware.ts +0 -161
- package/src/auth/org.ts +0 -41
- package/src/auth/permissions.ts +0 -28
- package/src/auth/setup.ts +0 -169
- package/src/auth/system-actor.ts +0 -19
- package/src/auth/types.ts +0 -10
- package/src/config/defaults.ts +0 -82
- package/src/config/define-config.ts +0 -53
- package/src/config/types.ts +0 -299
- package/src/generated/plugin-capabilities.d.ts +0 -20
- package/src/generated/plugin-manifest.ts +0 -23
- package/src/generated/plugin-repositories.d.ts +0 -20
- package/src/hooks/checkout-completion.ts +0 -262
- package/src/hooks/checkout.ts +0 -677
- package/src/hooks/order-emails.ts +0 -62
- package/src/index.ts +0 -214
- package/src/interfaces/mcp/agent-prompt.ts +0 -174
- package/src/interfaces/mcp/context-enrichment.ts +0 -177
- package/src/interfaces/mcp/server.ts +0 -617
- package/src/interfaces/mcp/transport.ts +0 -68
- package/src/interfaces/rest/customer-portal.ts +0 -299
- package/src/interfaces/rest/index.ts +0 -74
- package/src/interfaces/rest/router.ts +0 -334
- package/src/interfaces/rest/routes/admin-jobs.ts +0 -58
- package/src/interfaces/rest/routes/audit.ts +0 -50
- package/src/interfaces/rest/routes/carts.ts +0 -89
- package/src/interfaces/rest/routes/catalog.ts +0 -493
- package/src/interfaces/rest/routes/checkout.ts +0 -283
- package/src/interfaces/rest/routes/inventory.ts +0 -70
- package/src/interfaces/rest/routes/media.ts +0 -86
- package/src/interfaces/rest/routes/orders.ts +0 -78
- package/src/interfaces/rest/routes/payments.ts +0 -60
- package/src/interfaces/rest/routes/pricing.ts +0 -57
- package/src/interfaces/rest/routes/promotions.ts +0 -92
- package/src/interfaces/rest/routes/search.ts +0 -71
- package/src/interfaces/rest/routes/webhooks.ts +0 -46
- package/src/interfaces/rest/schemas/admin-jobs.ts +0 -40
- package/src/interfaces/rest/schemas/audit.ts +0 -46
- package/src/interfaces/rest/schemas/carts.ts +0 -125
- package/src/interfaces/rest/schemas/catalog.ts +0 -450
- package/src/interfaces/rest/schemas/checkout.ts +0 -66
- package/src/interfaces/rest/schemas/customer-portal.ts +0 -195
- package/src/interfaces/rest/schemas/inventory.ts +0 -138
- package/src/interfaces/rest/schemas/media.ts +0 -75
- package/src/interfaces/rest/schemas/orders.ts +0 -104
- package/src/interfaces/rest/schemas/pricing.ts +0 -80
- package/src/interfaces/rest/schemas/promotions.ts +0 -110
- package/src/interfaces/rest/schemas/responses.ts +0 -85
- package/src/interfaces/rest/schemas/search.ts +0 -58
- package/src/interfaces/rest/schemas/shared.ts +0 -62
- package/src/interfaces/rest/schemas/webhooks.ts +0 -68
- package/src/interfaces/rest/utils.ts +0 -104
- package/src/interfaces/rest/webhook-router.ts +0 -50
- package/src/kernel/compensation/executor.ts +0 -61
- package/src/kernel/compensation/types.ts +0 -26
- package/src/kernel/database/adapter.ts +0 -13
- package/src/kernel/database/drizzle-db.ts +0 -56
- package/src/kernel/database/migrate.ts +0 -76
- package/src/kernel/database/plugin-types.ts +0 -34
- package/src/kernel/database/schema.ts +0 -49
- package/src/kernel/database/scoped-db.ts +0 -68
- package/src/kernel/database/tx-context.ts +0 -46
- package/src/kernel/error-mapper.ts +0 -15
- package/src/kernel/errors.ts +0 -89
- package/src/kernel/factory/repository-factory.ts +0 -242
- package/src/kernel/hooks/create-context.ts +0 -43
- package/src/kernel/hooks/executor.ts +0 -88
- package/src/kernel/hooks/registry.ts +0 -74
- package/src/kernel/hooks/types.ts +0 -52
- package/src/kernel/http-error.ts +0 -44
- package/src/kernel/jobs/adapter.ts +0 -36
- package/src/kernel/jobs/drizzle-adapter.ts +0 -58
- package/src/kernel/jobs/runner.ts +0 -153
- package/src/kernel/jobs/schema.ts +0 -46
- package/src/kernel/jobs/types.ts +0 -30
- package/src/kernel/local-api.ts +0 -185
- package/src/kernel/plugin/manifest.ts +0 -253
- package/src/kernel/query/executor.ts +0 -184
- package/src/kernel/query/registry.ts +0 -46
- package/src/kernel/result.ts +0 -33
- package/src/kernel/schema/extra-columns.ts +0 -37
- package/src/kernel/service-registry.ts +0 -76
- package/src/kernel/service-timing.ts +0 -89
- package/src/kernel/state-machine/machine.ts +0 -101
- package/src/modules/analytics/drizzle-adapter.ts +0 -426
- package/src/modules/analytics/hooks.ts +0 -11
- package/src/modules/analytics/models.ts +0 -125
- package/src/modules/analytics/repository/index.ts +0 -6
- package/src/modules/analytics/service.ts +0 -245
- package/src/modules/analytics/types.ts +0 -180
- package/src/modules/audit/hooks.ts +0 -78
- package/src/modules/audit/schema.ts +0 -33
- package/src/modules/audit/service.ts +0 -151
- package/src/modules/cart/access.ts +0 -27
- package/src/modules/cart/matcher.ts +0 -26
- package/src/modules/cart/repository/index.ts +0 -234
- package/src/modules/cart/schema.ts +0 -42
- package/src/modules/cart/schemas.ts +0 -38
- package/src/modules/cart/service.ts +0 -541
- package/src/modules/catalog/repository/index.ts +0 -772
- package/src/modules/catalog/schema.ts +0 -203
- package/src/modules/catalog/schemas.ts +0 -104
- package/src/modules/catalog/service.ts +0 -1544
- package/src/modules/customers/repository/index.ts +0 -327
- package/src/modules/customers/schema.ts +0 -64
- package/src/modules/customers/service.ts +0 -171
- package/src/modules/fulfillment/repository/index.ts +0 -426
- package/src/modules/fulfillment/schema.ts +0 -101
- package/src/modules/fulfillment/service.ts +0 -555
- package/src/modules/fulfillment/types.ts +0 -59
- package/src/modules/inventory/repository/index.ts +0 -509
- package/src/modules/inventory/schema.ts +0 -94
- package/src/modules/inventory/schemas.ts +0 -38
- package/src/modules/inventory/service.ts +0 -490
- package/src/modules/media/adapter.ts +0 -17
- package/src/modules/media/repository/index.ts +0 -274
- package/src/modules/media/schema.ts +0 -41
- package/src/modules/media/service.ts +0 -151
- package/src/modules/orders/repository/index.ts +0 -287
- package/src/modules/orders/schema.ts +0 -66
- package/src/modules/orders/service.ts +0 -619
- package/src/modules/orders/stale-order-cleanup.ts +0 -76
- package/src/modules/organization/service.ts +0 -191
- package/src/modules/payments/adapter.ts +0 -47
- package/src/modules/payments/repository/index.ts +0 -6
- package/src/modules/payments/service.ts +0 -107
- package/src/modules/pricing/repository/index.ts +0 -291
- package/src/modules/pricing/schema.ts +0 -71
- package/src/modules/pricing/schemas.ts +0 -38
- package/src/modules/pricing/service.ts +0 -494
- package/src/modules/promotions/repository/index.ts +0 -325
- package/src/modules/promotions/schema.ts +0 -62
- package/src/modules/promotions/schemas.ts +0 -38
- package/src/modules/promotions/service.ts +0 -598
- package/src/modules/search/adapter.ts +0 -57
- package/src/modules/search/hooks.ts +0 -12
- package/src/modules/search/repository/index.ts +0 -6
- package/src/modules/search/service.ts +0 -315
- package/src/modules/shipping/calculator.ts +0 -188
- package/src/modules/shipping/repository/index.ts +0 -6
- package/src/modules/shipping/service.ts +0 -51
- package/src/modules/tax/adapter.ts +0 -60
- package/src/modules/tax/repository/index.ts +0 -6
- package/src/modules/tax/service.ts +0 -53
- package/src/modules/webhooks/hook.ts +0 -34
- package/src/modules/webhooks/repository/index.ts +0 -278
- package/src/modules/webhooks/schema.ts +0 -56
- package/src/modules/webhooks/service.ts +0 -117
- package/src/modules/webhooks/signing.ts +0 -6
- package/src/modules/webhooks/ssrf-guard.ts +0 -71
- package/src/modules/webhooks/tasks.ts +0 -52
- package/src/modules/webhooks/worker.ts +0 -134
- package/src/runtime/commerce.ts +0 -145
- package/src/runtime/kernel.ts +0 -419
- package/src/runtime/logger.ts +0 -36
- package/src/runtime/server.ts +0 -349
- package/src/runtime/shutdown.ts +0 -43
- package/src/test-utils/create-pglite-adapter.ts +0 -129
- package/src/test-utils/create-plugin-test-app.ts +0 -128
- package/src/test-utils/create-repository-test-harness.ts +0 -16
- package/src/test-utils/create-test-config.ts +0 -190
- package/src/test-utils/create-test-kernel.ts +0 -7
- package/src/test-utils/create-test-plugin-context.ts +0 -75
- package/src/test-utils/rest-api-test-utils.ts +0 -265
- package/src/test-utils/test-actors.ts +0 -62
- package/src/test-utils/typed-hooks.ts +0 -54
- package/src/types/commerce-types.ts +0 -34
- package/src/utils/id.ts +0 -3
- package/src/utils/logger.ts +0 -18
- package/src/utils/pagination.ts +0 -22
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { index, integer, jsonb, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
|
|
2
|
-
import { organization } from "../../auth/auth-schema.js";
|
|
3
|
-
import { sellableEntities, variants } from "../catalog/schema.js";
|
|
4
|
-
|
|
5
|
-
export const prices = pgTable(
|
|
6
|
-
"prices",
|
|
7
|
-
{
|
|
8
|
-
id: uuid("id").defaultRandom().primaryKey(),
|
|
9
|
-
organizationId: text("organization_id")
|
|
10
|
-
.notNull()
|
|
11
|
-
.references(() => organization.id, { onDelete: "cascade" }),
|
|
12
|
-
entityId: uuid("entity_id")
|
|
13
|
-
.references(() => sellableEntities.id, { onDelete: "cascade" })
|
|
14
|
-
.notNull(),
|
|
15
|
-
variantId: uuid("variant_id").references(() => variants.id, { onDelete: "cascade" }),
|
|
16
|
-
currency: text("currency").notNull(),
|
|
17
|
-
amount: integer("amount").notNull(),
|
|
18
|
-
customerGroupId: text("customer_group_id"),
|
|
19
|
-
minQuantity: integer("min_quantity"),
|
|
20
|
-
maxQuantity: integer("max_quantity"),
|
|
21
|
-
validFrom: timestamp("valid_from", { withTimezone: true }),
|
|
22
|
-
validUntil: timestamp("valid_until", { withTimezone: true }),
|
|
23
|
-
metadata: jsonb("metadata").$type<Record<string, unknown>>().default({}),
|
|
24
|
-
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
25
|
-
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
|
26
|
-
},
|
|
27
|
-
(table) => ({
|
|
28
|
-
entityVariantCurrencyIdx: index("idx_prices_entity_variant_currency").on(
|
|
29
|
-
table.entityId,
|
|
30
|
-
table.variantId,
|
|
31
|
-
table.currency,
|
|
32
|
-
),
|
|
33
|
-
customerGroupIdx: index("idx_prices_customer_group").on(table.customerGroupId),
|
|
34
|
-
quantityIdx: index("idx_prices_quantity").on(table.minQuantity, table.maxQuantity),
|
|
35
|
-
validityIdx: index("idx_prices_validity").on(table.validFrom, table.validUntil),
|
|
36
|
-
orgIdx: index("idx_prices_org").on(table.organizationId),
|
|
37
|
-
}),
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
export const priceModifiers = pgTable(
|
|
41
|
-
"price_modifiers",
|
|
42
|
-
{
|
|
43
|
-
id: uuid("id").defaultRandom().primaryKey(),
|
|
44
|
-
organizationId: text("organization_id")
|
|
45
|
-
.notNull()
|
|
46
|
-
.references(() => organization.id, { onDelete: "cascade" }),
|
|
47
|
-
name: text("name").notNull(),
|
|
48
|
-
type: text("type", {
|
|
49
|
-
enum: ["percentage_discount", "fixed_discount", "markup", "override"],
|
|
50
|
-
}).notNull(),
|
|
51
|
-
value: integer("value").notNull(),
|
|
52
|
-
priority: integer("priority").notNull().default(100),
|
|
53
|
-
entityId: uuid("entity_id").references(() => sellableEntities.id, { onDelete: "cascade" }),
|
|
54
|
-
variantId: uuid("variant_id").references(() => variants.id, { onDelete: "cascade" }),
|
|
55
|
-
customerGroupId: text("customer_group_id"),
|
|
56
|
-
currency: text("currency").default("USD"),
|
|
57
|
-
minQuantity: integer("min_quantity"),
|
|
58
|
-
maxQuantity: integer("max_quantity"),
|
|
59
|
-
conditions: jsonb("conditions").$type<Record<string, unknown>>().default({}),
|
|
60
|
-
validFrom: timestamp("valid_from", { withTimezone: true }),
|
|
61
|
-
validUntil: timestamp("valid_until", { withTimezone: true }),
|
|
62
|
-
metadata: jsonb("metadata").$type<Record<string, unknown>>().default({}),
|
|
63
|
-
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
64
|
-
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
|
65
|
-
},
|
|
66
|
-
(table) => ({
|
|
67
|
-
entityVariantIdx: index("idx_price_modifiers_entity_variant").on(table.entityId, table.variantId),
|
|
68
|
-
priorityIdx: index("idx_price_modifiers_priority").on(table.priority),
|
|
69
|
-
orgIdx: index("idx_price_modifiers_org").on(table.organizationId),
|
|
70
|
-
}),
|
|
71
|
-
);
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { z } from "@hono/zod-openapi";
|
|
2
|
-
|
|
3
|
-
// ─── Zod Body Schemas (single source of truth) ─────────────────────────────
|
|
4
|
-
|
|
5
|
-
export const SetBasePriceBodySchema = z.object({
|
|
6
|
-
entityId: z.string().openapi({ example: "product-uuid" }),
|
|
7
|
-
variantId: z.string().optional().openapi({ example: "variant-uuid" }),
|
|
8
|
-
currency: z.string().length(3).openapi({ example: "USD" }),
|
|
9
|
-
amount: z.number().openapi({ example: 29.99 }),
|
|
10
|
-
customerGroupId: z.string().optional(),
|
|
11
|
-
minQuantity: z.number().int().optional(),
|
|
12
|
-
maxQuantity: z.number().int().optional(),
|
|
13
|
-
validFrom: z.coerce.date().optional(),
|
|
14
|
-
validUntil: z.coerce.date().optional(),
|
|
15
|
-
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
16
|
-
}).openapi("SetBasePriceRequest");
|
|
17
|
-
|
|
18
|
-
export const CreateModifierBodySchema = z.object({
|
|
19
|
-
name: z.string().openapi({ example: "Summer Sale" }),
|
|
20
|
-
type: z.enum(["percentage_discount", "fixed_discount", "markup", "override"]).openapi({ example: "percentage_discount" }),
|
|
21
|
-
value: z.number().openapi({ example: 10 }),
|
|
22
|
-
priority: z.number().int().optional(),
|
|
23
|
-
entityId: z.string().optional(),
|
|
24
|
-
variantId: z.string().optional(),
|
|
25
|
-
customerGroupId: z.string().optional(),
|
|
26
|
-
currency: z.string().length(3).optional().openapi({ example: "USD" }),
|
|
27
|
-
minQuantity: z.number().int().optional(),
|
|
28
|
-
maxQuantity: z.number().int().optional(),
|
|
29
|
-
conditions: z.record(z.string(), z.unknown()).optional(),
|
|
30
|
-
validFrom: z.coerce.date().optional(),
|
|
31
|
-
validUntil: z.coerce.date().optional(),
|
|
32
|
-
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
33
|
-
}).openapi("CreateModifierRequest");
|
|
34
|
-
|
|
35
|
-
// ─── Derived Input Types ────────────────────────────────────────────────────
|
|
36
|
-
|
|
37
|
-
export type SetBasePriceInput = z.infer<typeof SetBasePriceBodySchema>;
|
|
38
|
-
export type CreatePriceModifierInput = z.infer<typeof CreateModifierBodySchema>;
|
|
@@ -1,494 +0,0 @@
|
|
|
1
|
-
import { resolveOrgId } from "../../auth/org.js";
|
|
2
|
-
import type { Actor } from "../../auth/types.js";
|
|
3
|
-
import {
|
|
4
|
-
CommerceNotFoundError,
|
|
5
|
-
CommerceValidationError,
|
|
6
|
-
} from "../../kernel/errors.js";
|
|
7
|
-
import { Err, Ok, type Result } from "../../kernel/result.js";
|
|
8
|
-
import type { TxContext } from "../../kernel/database/tx-context.js";
|
|
9
|
-
import type {
|
|
10
|
-
PricingRepository,
|
|
11
|
-
Price,
|
|
12
|
-
PriceModifier,
|
|
13
|
-
PriceInsert,
|
|
14
|
-
PriceModifierInsert,
|
|
15
|
-
} from "./repository/index.js";
|
|
16
|
-
import type { CatalogRepository } from "../catalog/repository/index.js";
|
|
17
|
-
|
|
18
|
-
// Re-export PriceModifierType from schema for external use
|
|
19
|
-
export type PriceModifierType =
|
|
20
|
-
| "percentage_discount"
|
|
21
|
-
| "fixed_discount"
|
|
22
|
-
| "markup"
|
|
23
|
-
| "override";
|
|
24
|
-
|
|
25
|
-
interface PricingServiceDeps {
|
|
26
|
-
repository: PricingRepository;
|
|
27
|
-
catalogRepository: CatalogRepository;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface PriceResolutionContext {
|
|
31
|
-
entityId: string;
|
|
32
|
-
variantId?: string;
|
|
33
|
-
currency: string;
|
|
34
|
-
quantity: number;
|
|
35
|
-
customerId?: string;
|
|
36
|
-
customerGroupIds?: string[];
|
|
37
|
-
timestamp?: Date;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface PriceBreakdownStep {
|
|
41
|
-
label: string;
|
|
42
|
-
amountBefore: number;
|
|
43
|
-
delta: number;
|
|
44
|
-
amountAfter: number;
|
|
45
|
-
metadata?: Record<string, unknown>;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface ResolvedPrice {
|
|
49
|
-
baseAmount: number;
|
|
50
|
-
finalAmount: number;
|
|
51
|
-
currency: string;
|
|
52
|
-
appliedModifiers: Array<{
|
|
53
|
-
id: string;
|
|
54
|
-
name: string;
|
|
55
|
-
type: PriceModifierType;
|
|
56
|
-
delta: number;
|
|
57
|
-
value: number;
|
|
58
|
-
priority: number;
|
|
59
|
-
}>;
|
|
60
|
-
breakdown: PriceBreakdownStep[];
|
|
61
|
-
basePriceId: string;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export type { SetBasePriceInput, CreatePriceModifierInput } from "./schemas.js";
|
|
65
|
-
import type { SetBasePriceInput, CreatePriceModifierInput } from "./schemas.js";
|
|
66
|
-
|
|
67
|
-
function matchesQuantity(
|
|
68
|
-
min: number | null | undefined,
|
|
69
|
-
max: number | null | undefined,
|
|
70
|
-
quantity: number,
|
|
71
|
-
): boolean {
|
|
72
|
-
if (min != null && quantity < min) return false;
|
|
73
|
-
if (max != null && quantity > max) return false;
|
|
74
|
-
return true;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function matchesWindow(
|
|
78
|
-
validFrom: Date | null | undefined,
|
|
79
|
-
validUntil: Date | null | undefined,
|
|
80
|
-
timestamp: Date,
|
|
81
|
-
): boolean {
|
|
82
|
-
if (validFrom && timestamp < validFrom) return false;
|
|
83
|
-
if (validUntil && timestamp > validUntil) return false;
|
|
84
|
-
return true;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function durationScore(
|
|
88
|
-
validFrom: Date | null | undefined,
|
|
89
|
-
validUntil: Date | null | undefined,
|
|
90
|
-
): number {
|
|
91
|
-
if (validFrom && validUntil)
|
|
92
|
-
return validUntil.getTime() - validFrom.getTime();
|
|
93
|
-
if (validFrom || validUntil) return Number.MAX_SAFE_INTEGER - 1;
|
|
94
|
-
return Number.MAX_SAFE_INTEGER;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function quantityRangeWidth(
|
|
98
|
-
min: number | null | undefined,
|
|
99
|
-
max: number | null | undefined,
|
|
100
|
-
): number {
|
|
101
|
-
if (min != null && max != null) return Math.max(0, max - min);
|
|
102
|
-
if (min != null || max != null) return Number.MAX_SAFE_INTEGER - 1;
|
|
103
|
-
return Number.MAX_SAFE_INTEGER;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function compareBasePriceSpecificity(
|
|
107
|
-
a: Price,
|
|
108
|
-
b: Price,
|
|
109
|
-
context: PriceResolutionContext,
|
|
110
|
-
): number {
|
|
111
|
-
const aVariant =
|
|
112
|
-
context.variantId !== undefined && a.variantId === context.variantId
|
|
113
|
-
? 1
|
|
114
|
-
: 0;
|
|
115
|
-
const bVariant =
|
|
116
|
-
context.variantId !== undefined && b.variantId === context.variantId
|
|
117
|
-
? 1
|
|
118
|
-
: 0;
|
|
119
|
-
if (aVariant !== bVariant) return bVariant - aVariant;
|
|
120
|
-
|
|
121
|
-
const hasGroupA = a.customerGroupId ? 1 : 0;
|
|
122
|
-
const hasGroupB = b.customerGroupId ? 1 : 0;
|
|
123
|
-
if (hasGroupA !== hasGroupB) return hasGroupB - hasGroupA;
|
|
124
|
-
|
|
125
|
-
const aRange = quantityRangeWidth(a.minQuantity, a.maxQuantity);
|
|
126
|
-
const bRange = quantityRangeWidth(b.minQuantity, b.maxQuantity);
|
|
127
|
-
if (aRange !== bRange) return aRange - bRange;
|
|
128
|
-
|
|
129
|
-
const aDuration = durationScore(a.validFrom, a.validUntil);
|
|
130
|
-
const bDuration = durationScore(b.validFrom, b.validUntil);
|
|
131
|
-
if (aDuration !== bDuration) return aDuration - bDuration;
|
|
132
|
-
|
|
133
|
-
return b.createdAt.getTime() - a.createdAt.getTime();
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function resolveModifierDelta(
|
|
137
|
-
type: PriceModifierType,
|
|
138
|
-
value: number,
|
|
139
|
-
amountBefore: number,
|
|
140
|
-
): number {
|
|
141
|
-
switch (type) {
|
|
142
|
-
case "percentage_discount":
|
|
143
|
-
return -Math.round((amountBefore * value) / 100);
|
|
144
|
-
case "fixed_discount":
|
|
145
|
-
return -value;
|
|
146
|
-
case "markup":
|
|
147
|
-
return value;
|
|
148
|
-
case "override":
|
|
149
|
-
return value - amountBefore;
|
|
150
|
-
default:
|
|
151
|
-
return 0;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function toGroupSet(context: PriceResolutionContext): Set<string> {
|
|
156
|
-
return new Set(context.customerGroupIds ?? []);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function normalizeCurrency(currency: string): string {
|
|
160
|
-
return currency.trim().toUpperCase();
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export class PricingService {
|
|
164
|
-
private readonly repo: PricingRepository;
|
|
165
|
-
private readonly catalogRepo: CatalogRepository;
|
|
166
|
-
|
|
167
|
-
constructor(private deps: PricingServiceDeps) {
|
|
168
|
-
this.repo = deps.repository;
|
|
169
|
-
this.catalogRepo = deps.catalogRepository;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
async setBasePrice(
|
|
173
|
-
input: SetBasePriceInput,
|
|
174
|
-
actor?: Actor | null,
|
|
175
|
-
ctx?: TxContext,
|
|
176
|
-
): Promise<Result<Price>> {
|
|
177
|
-
if (input.amount < 0) {
|
|
178
|
-
return Err(
|
|
179
|
-
new CommerceValidationError("Base price amount cannot be negative."),
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const entity = await this.catalogRepo.findEntityById(input.entityId, ctx);
|
|
184
|
-
if (!entity) {
|
|
185
|
-
return Err(
|
|
186
|
-
new CommerceNotFoundError("Entity not found for price assignment."),
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
|
|
191
|
-
|
|
192
|
-
const priceData: PriceInsert = {
|
|
193
|
-
organizationId: orgId,
|
|
194
|
-
entityId: input.entityId,
|
|
195
|
-
currency: normalizeCurrency(input.currency),
|
|
196
|
-
amount: input.amount,
|
|
197
|
-
metadata: input.metadata ?? {},
|
|
198
|
-
variantId: input.variantId ?? null,
|
|
199
|
-
customerGroupId: input.customerGroupId ?? null,
|
|
200
|
-
minQuantity: input.minQuantity ?? null,
|
|
201
|
-
maxQuantity: input.maxQuantity ?? null,
|
|
202
|
-
validFrom: input.validFrom ?? null,
|
|
203
|
-
validUntil: input.validUntil ?? null,
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
const record = await this.repo.createPrice(priceData, ctx);
|
|
207
|
-
return Ok(record);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
async createModifier(
|
|
211
|
-
input: CreatePriceModifierInput,
|
|
212
|
-
actor?: Actor | null,
|
|
213
|
-
ctx?: TxContext,
|
|
214
|
-
): Promise<Result<PriceModifier>> {
|
|
215
|
-
if (!input.name) {
|
|
216
|
-
return Err(new CommerceValidationError("Modifier name is required."));
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Validate modifier type
|
|
220
|
-
const validTypes: PriceModifierType[] = [
|
|
221
|
-
"percentage_discount",
|
|
222
|
-
"fixed_discount",
|
|
223
|
-
"markup",
|
|
224
|
-
"override",
|
|
225
|
-
];
|
|
226
|
-
if (!validTypes.includes(input.type)) {
|
|
227
|
-
return Err(
|
|
228
|
-
new CommerceValidationError(
|
|
229
|
-
`Invalid modifier type "${input.type}". Must be one of: ${validTypes.join(", ")}`,
|
|
230
|
-
),
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
|
|
235
|
-
|
|
236
|
-
const modifierData: PriceModifierInsert = {
|
|
237
|
-
organizationId: orgId,
|
|
238
|
-
name: input.name,
|
|
239
|
-
type: input.type,
|
|
240
|
-
value: input.value,
|
|
241
|
-
priority: input.priority ?? 100,
|
|
242
|
-
conditions: input.conditions ?? {},
|
|
243
|
-
metadata: input.metadata ?? {},
|
|
244
|
-
entityId: input.entityId ?? null,
|
|
245
|
-
variantId: input.variantId ?? null,
|
|
246
|
-
customerGroupId: input.customerGroupId ?? null,
|
|
247
|
-
currency: input.currency ? normalizeCurrency(input.currency) : null,
|
|
248
|
-
minQuantity: input.minQuantity ?? null,
|
|
249
|
-
maxQuantity: input.maxQuantity ?? null,
|
|
250
|
-
validFrom: input.validFrom ?? null,
|
|
251
|
-
validUntil: input.validUntil ?? null,
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
const modifier = await this.repo.createModifier(modifierData, ctx);
|
|
255
|
-
return Ok(modifier);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
async listPrices(
|
|
259
|
-
filter?: {
|
|
260
|
-
entityId?: string;
|
|
261
|
-
variantId?: string;
|
|
262
|
-
currency?: string;
|
|
263
|
-
customerGroupId?: string;
|
|
264
|
-
},
|
|
265
|
-
ctx?: TxContext,
|
|
266
|
-
): Promise<Result<{ prices: Price[]; modifiers: PriceModifier[] }>> {
|
|
267
|
-
// Get all prices for the entity if specified, or filter after retrieval
|
|
268
|
-
let prices: Price[] = [];
|
|
269
|
-
if (filter?.entityId) {
|
|
270
|
-
prices = await this.repo.findPricesByEntityId(filter.entityId, ctx);
|
|
271
|
-
}
|
|
272
|
-
// Note: For full list without entityId, we'd need a findAll method
|
|
273
|
-
// For now, entityId is typically required for practical use
|
|
274
|
-
|
|
275
|
-
// Apply additional filters
|
|
276
|
-
if (filter?.variantId !== undefined) {
|
|
277
|
-
prices = prices.filter((p) => p.variantId === filter.variantId);
|
|
278
|
-
}
|
|
279
|
-
if (filter?.currency !== undefined) {
|
|
280
|
-
const normalizedCurrency = normalizeCurrency(filter.currency);
|
|
281
|
-
prices = prices.filter((p) => p.currency === normalizedCurrency);
|
|
282
|
-
}
|
|
283
|
-
if (filter?.customerGroupId !== undefined) {
|
|
284
|
-
prices = prices.filter(
|
|
285
|
-
(p) => p.customerGroupId === filter.customerGroupId,
|
|
286
|
-
);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Get modifiers for the entity
|
|
290
|
-
let modifiers: PriceModifier[] = [];
|
|
291
|
-
if (filter?.entityId) {
|
|
292
|
-
modifiers = await this.repo.findModifiersByEntityId(filter.entityId, ctx);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Apply additional filters
|
|
296
|
-
if (filter?.variantId !== undefined) {
|
|
297
|
-
modifiers = modifiers.filter((m) => m.variantId === filter.variantId);
|
|
298
|
-
}
|
|
299
|
-
if (filter?.currency !== undefined) {
|
|
300
|
-
const normalizedCurrency = normalizeCurrency(filter.currency);
|
|
301
|
-
modifiers = modifiers.filter(
|
|
302
|
-
(m) => m.currency === null || m.currency === normalizedCurrency,
|
|
303
|
-
);
|
|
304
|
-
}
|
|
305
|
-
if (filter?.customerGroupId !== undefined) {
|
|
306
|
-
modifiers = modifiers.filter(
|
|
307
|
-
(m) =>
|
|
308
|
-
m.customerGroupId === null ||
|
|
309
|
-
m.customerGroupId === filter.customerGroupId,
|
|
310
|
-
);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
return Ok({ prices, modifiers });
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
async resolve(
|
|
317
|
-
input: PriceResolutionContext,
|
|
318
|
-
ctx?: TxContext,
|
|
319
|
-
): Promise<Result<ResolvedPrice>> {
|
|
320
|
-
const entity = await this.catalogRepo.findEntityById(input.entityId, ctx);
|
|
321
|
-
if (!entity) {
|
|
322
|
-
return Err(
|
|
323
|
-
new CommerceNotFoundError(`Entity ${input.entityId} not found.`),
|
|
324
|
-
);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
if (input.quantity <= 0) {
|
|
328
|
-
return Err(
|
|
329
|
-
new CommerceValidationError(
|
|
330
|
-
"Quantity must be greater than zero for price resolution.",
|
|
331
|
-
),
|
|
332
|
-
);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
const timestamp = input.timestamp ?? new Date();
|
|
336
|
-
const currency = normalizeCurrency(input.currency);
|
|
337
|
-
const groupSet = toGroupSet(input);
|
|
338
|
-
|
|
339
|
-
// Get matching prices from repository
|
|
340
|
-
const allPrices = await this.repo.findPricesByEntityId(input.entityId, ctx);
|
|
341
|
-
|
|
342
|
-
const matchingPrices = allPrices.filter((price) => {
|
|
343
|
-
if (input.variantId !== undefined) {
|
|
344
|
-
if (price.variantId !== null && price.variantId !== input.variantId)
|
|
345
|
-
return false;
|
|
346
|
-
} else if (price.variantId !== null) {
|
|
347
|
-
return false;
|
|
348
|
-
}
|
|
349
|
-
if (price.currency !== currency) return false;
|
|
350
|
-
if (
|
|
351
|
-
!matchesQuantity(price.minQuantity, price.maxQuantity, input.quantity)
|
|
352
|
-
)
|
|
353
|
-
return false;
|
|
354
|
-
if (!matchesWindow(price.validFrom, price.validUntil, timestamp))
|
|
355
|
-
return false;
|
|
356
|
-
if (
|
|
357
|
-
price.customerGroupId !== null &&
|
|
358
|
-
!groupSet.has(price.customerGroupId)
|
|
359
|
-
)
|
|
360
|
-
return false;
|
|
361
|
-
return true;
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
if (matchingPrices.length === 0) {
|
|
365
|
-
const metadataPrice =
|
|
366
|
-
typeof entity.metadata?.basePrice === "number"
|
|
367
|
-
? Math.round(entity.metadata.basePrice)
|
|
368
|
-
: undefined;
|
|
369
|
-
if (metadataPrice === undefined) {
|
|
370
|
-
return Err(
|
|
371
|
-
new CommerceNotFoundError(
|
|
372
|
-
`No base price configured for ${entity.slug} (${currency}).`,
|
|
373
|
-
),
|
|
374
|
-
);
|
|
375
|
-
}
|
|
376
|
-
const fallback: ResolvedPrice = {
|
|
377
|
-
baseAmount: metadataPrice,
|
|
378
|
-
finalAmount: metadataPrice,
|
|
379
|
-
currency,
|
|
380
|
-
basePriceId: "metadata:basePrice",
|
|
381
|
-
appliedModifiers: [],
|
|
382
|
-
breakdown: [
|
|
383
|
-
{
|
|
384
|
-
label: "Base price (entity metadata)",
|
|
385
|
-
amountBefore: metadataPrice,
|
|
386
|
-
delta: 0,
|
|
387
|
-
amountAfter: metadataPrice,
|
|
388
|
-
},
|
|
389
|
-
],
|
|
390
|
-
};
|
|
391
|
-
return Ok(fallback);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
const selectedBase = [...matchingPrices].sort((a, b) =>
|
|
395
|
-
compareBasePriceSpecificity(a, b, input),
|
|
396
|
-
)[0];
|
|
397
|
-
if (!selectedBase) {
|
|
398
|
-
return Err(
|
|
399
|
-
new CommerceNotFoundError("No matching base price could be selected."),
|
|
400
|
-
);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
let runningAmount = selectedBase.amount;
|
|
404
|
-
const breakdown: PriceBreakdownStep[] = [
|
|
405
|
-
{
|
|
406
|
-
label: "Base price",
|
|
407
|
-
amountBefore: selectedBase.amount,
|
|
408
|
-
delta: 0,
|
|
409
|
-
amountAfter: selectedBase.amount,
|
|
410
|
-
metadata: {
|
|
411
|
-
priceId: selectedBase.id,
|
|
412
|
-
customerGroupId: selectedBase.customerGroupId,
|
|
413
|
-
minQuantity: selectedBase.minQuantity,
|
|
414
|
-
maxQuantity: selectedBase.maxQuantity,
|
|
415
|
-
},
|
|
416
|
-
},
|
|
417
|
-
];
|
|
418
|
-
|
|
419
|
-
const appliedModifiers: ResolvedPrice["appliedModifiers"] = [];
|
|
420
|
-
|
|
421
|
-
// Get matching modifiers (includes both entity-specific and global)
|
|
422
|
-
const firstGroupId = groupSet.size > 0 ? [...groupSet][0] : undefined;
|
|
423
|
-
const activeModifiers = await this.repo.findActiveModifiers(
|
|
424
|
-
input.entityId,
|
|
425
|
-
input.variantId,
|
|
426
|
-
firstGroupId,
|
|
427
|
-
currency,
|
|
428
|
-
input.quantity,
|
|
429
|
-
ctx,
|
|
430
|
-
);
|
|
431
|
-
|
|
432
|
-
// Additional filtering for multiple customer groups and conditions
|
|
433
|
-
const modifiers = activeModifiers
|
|
434
|
-
.filter((modifier) => {
|
|
435
|
-
// Re-check customer group for multi-group support
|
|
436
|
-
if (
|
|
437
|
-
modifier.customerGroupId !== null &&
|
|
438
|
-
!groupSet.has(modifier.customerGroupId)
|
|
439
|
-
)
|
|
440
|
-
return false;
|
|
441
|
-
|
|
442
|
-
const conditions = modifier.conditions as Record<
|
|
443
|
-
string,
|
|
444
|
-
unknown
|
|
445
|
-
> | null;
|
|
446
|
-
const minSubtotal = conditions?.minSubtotal;
|
|
447
|
-
if (typeof minSubtotal === "number" && runningAmount < minSubtotal)
|
|
448
|
-
return false;
|
|
449
|
-
|
|
450
|
-
return true;
|
|
451
|
-
})
|
|
452
|
-
.sort((a, b) => a.priority - b.priority);
|
|
453
|
-
|
|
454
|
-
for (const modifier of modifiers) {
|
|
455
|
-
const amountBefore = runningAmount;
|
|
456
|
-
const rawDelta = resolveModifierDelta(
|
|
457
|
-
modifier.type as PriceModifierType,
|
|
458
|
-
modifier.value,
|
|
459
|
-
amountBefore,
|
|
460
|
-
);
|
|
461
|
-
const delta = Math.max(-amountBefore, rawDelta);
|
|
462
|
-
runningAmount = Math.max(0, amountBefore + delta);
|
|
463
|
-
|
|
464
|
-
appliedModifiers.push({
|
|
465
|
-
id: modifier.id,
|
|
466
|
-
name: modifier.name,
|
|
467
|
-
type: modifier.type as PriceModifierType,
|
|
468
|
-
delta,
|
|
469
|
-
value: modifier.value,
|
|
470
|
-
priority: modifier.priority,
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
breakdown.push({
|
|
474
|
-
label: `Modifier: ${modifier.name}`,
|
|
475
|
-
amountBefore,
|
|
476
|
-
delta,
|
|
477
|
-
amountAfter: runningAmount,
|
|
478
|
-
metadata: {
|
|
479
|
-
type: modifier.type,
|
|
480
|
-
priority: modifier.priority,
|
|
481
|
-
},
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
return Ok({
|
|
486
|
-
baseAmount: selectedBase.amount,
|
|
487
|
-
finalAmount: runningAmount,
|
|
488
|
-
currency,
|
|
489
|
-
basePriceId: selectedBase.id,
|
|
490
|
-
appliedModifiers,
|
|
491
|
-
breakdown,
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
}
|