@unifiedcommerce/core 0.1.0 → 0.1.1

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.
Files changed (174) hide show
  1. package/package.json +1 -2
  2. package/src/adapters/console-email.ts +0 -43
  3. package/src/auth/access.ts +0 -187
  4. package/src/auth/auth-schema.ts +0 -139
  5. package/src/auth/middleware.ts +0 -161
  6. package/src/auth/org.ts +0 -41
  7. package/src/auth/permissions.ts +0 -28
  8. package/src/auth/setup.ts +0 -169
  9. package/src/auth/system-actor.ts +0 -19
  10. package/src/auth/types.ts +0 -10
  11. package/src/config/defaults.ts +0 -82
  12. package/src/config/define-config.ts +0 -53
  13. package/src/config/types.ts +0 -299
  14. package/src/generated/plugin-capabilities.d.ts +0 -20
  15. package/src/generated/plugin-manifest.ts +0 -23
  16. package/src/generated/plugin-repositories.d.ts +0 -20
  17. package/src/hooks/checkout-completion.ts +0 -262
  18. package/src/hooks/checkout.ts +0 -677
  19. package/src/hooks/order-emails.ts +0 -62
  20. package/src/index.ts +0 -214
  21. package/src/interfaces/mcp/agent-prompt.ts +0 -174
  22. package/src/interfaces/mcp/context-enrichment.ts +0 -177
  23. package/src/interfaces/mcp/server.ts +0 -617
  24. package/src/interfaces/mcp/transport.ts +0 -68
  25. package/src/interfaces/rest/customer-portal.ts +0 -299
  26. package/src/interfaces/rest/index.ts +0 -74
  27. package/src/interfaces/rest/router.ts +0 -334
  28. package/src/interfaces/rest/routes/admin-jobs.ts +0 -58
  29. package/src/interfaces/rest/routes/audit.ts +0 -50
  30. package/src/interfaces/rest/routes/carts.ts +0 -89
  31. package/src/interfaces/rest/routes/catalog.ts +0 -493
  32. package/src/interfaces/rest/routes/checkout.ts +0 -283
  33. package/src/interfaces/rest/routes/inventory.ts +0 -70
  34. package/src/interfaces/rest/routes/media.ts +0 -86
  35. package/src/interfaces/rest/routes/orders.ts +0 -78
  36. package/src/interfaces/rest/routes/payments.ts +0 -60
  37. package/src/interfaces/rest/routes/pricing.ts +0 -57
  38. package/src/interfaces/rest/routes/promotions.ts +0 -92
  39. package/src/interfaces/rest/routes/search.ts +0 -71
  40. package/src/interfaces/rest/routes/webhooks.ts +0 -46
  41. package/src/interfaces/rest/schemas/admin-jobs.ts +0 -40
  42. package/src/interfaces/rest/schemas/audit.ts +0 -46
  43. package/src/interfaces/rest/schemas/carts.ts +0 -125
  44. package/src/interfaces/rest/schemas/catalog.ts +0 -450
  45. package/src/interfaces/rest/schemas/checkout.ts +0 -66
  46. package/src/interfaces/rest/schemas/customer-portal.ts +0 -195
  47. package/src/interfaces/rest/schemas/inventory.ts +0 -138
  48. package/src/interfaces/rest/schemas/media.ts +0 -75
  49. package/src/interfaces/rest/schemas/orders.ts +0 -104
  50. package/src/interfaces/rest/schemas/pricing.ts +0 -80
  51. package/src/interfaces/rest/schemas/promotions.ts +0 -110
  52. package/src/interfaces/rest/schemas/responses.ts +0 -85
  53. package/src/interfaces/rest/schemas/search.ts +0 -58
  54. package/src/interfaces/rest/schemas/shared.ts +0 -62
  55. package/src/interfaces/rest/schemas/webhooks.ts +0 -68
  56. package/src/interfaces/rest/utils.ts +0 -104
  57. package/src/interfaces/rest/webhook-router.ts +0 -50
  58. package/src/kernel/compensation/executor.ts +0 -61
  59. package/src/kernel/compensation/types.ts +0 -26
  60. package/src/kernel/database/adapter.ts +0 -13
  61. package/src/kernel/database/drizzle-db.ts +0 -56
  62. package/src/kernel/database/migrate.ts +0 -76
  63. package/src/kernel/database/plugin-types.ts +0 -34
  64. package/src/kernel/database/schema.ts +0 -49
  65. package/src/kernel/database/scoped-db.ts +0 -68
  66. package/src/kernel/database/tx-context.ts +0 -46
  67. package/src/kernel/error-mapper.ts +0 -15
  68. package/src/kernel/errors.ts +0 -89
  69. package/src/kernel/factory/repository-factory.ts +0 -242
  70. package/src/kernel/hooks/create-context.ts +0 -43
  71. package/src/kernel/hooks/executor.ts +0 -88
  72. package/src/kernel/hooks/registry.ts +0 -74
  73. package/src/kernel/hooks/types.ts +0 -52
  74. package/src/kernel/http-error.ts +0 -44
  75. package/src/kernel/jobs/adapter.ts +0 -36
  76. package/src/kernel/jobs/drizzle-adapter.ts +0 -58
  77. package/src/kernel/jobs/runner.ts +0 -153
  78. package/src/kernel/jobs/schema.ts +0 -46
  79. package/src/kernel/jobs/types.ts +0 -30
  80. package/src/kernel/local-api.ts +0 -185
  81. package/src/kernel/plugin/manifest.ts +0 -253
  82. package/src/kernel/query/executor.ts +0 -184
  83. package/src/kernel/query/registry.ts +0 -46
  84. package/src/kernel/result.ts +0 -33
  85. package/src/kernel/schema/extra-columns.ts +0 -37
  86. package/src/kernel/service-registry.ts +0 -76
  87. package/src/kernel/service-timing.ts +0 -89
  88. package/src/kernel/state-machine/machine.ts +0 -101
  89. package/src/modules/analytics/drizzle-adapter.ts +0 -426
  90. package/src/modules/analytics/hooks.ts +0 -11
  91. package/src/modules/analytics/models.ts +0 -125
  92. package/src/modules/analytics/repository/index.ts +0 -6
  93. package/src/modules/analytics/service.ts +0 -245
  94. package/src/modules/analytics/types.ts +0 -180
  95. package/src/modules/audit/hooks.ts +0 -78
  96. package/src/modules/audit/schema.ts +0 -33
  97. package/src/modules/audit/service.ts +0 -151
  98. package/src/modules/cart/access.ts +0 -27
  99. package/src/modules/cart/matcher.ts +0 -26
  100. package/src/modules/cart/repository/index.ts +0 -234
  101. package/src/modules/cart/schema.ts +0 -42
  102. package/src/modules/cart/schemas.ts +0 -38
  103. package/src/modules/cart/service.ts +0 -541
  104. package/src/modules/catalog/repository/index.ts +0 -772
  105. package/src/modules/catalog/schema.ts +0 -203
  106. package/src/modules/catalog/schemas.ts +0 -104
  107. package/src/modules/catalog/service.ts +0 -1544
  108. package/src/modules/customers/repository/index.ts +0 -327
  109. package/src/modules/customers/schema.ts +0 -64
  110. package/src/modules/customers/service.ts +0 -171
  111. package/src/modules/fulfillment/repository/index.ts +0 -426
  112. package/src/modules/fulfillment/schema.ts +0 -101
  113. package/src/modules/fulfillment/service.ts +0 -555
  114. package/src/modules/fulfillment/types.ts +0 -59
  115. package/src/modules/inventory/repository/index.ts +0 -509
  116. package/src/modules/inventory/schema.ts +0 -94
  117. package/src/modules/inventory/schemas.ts +0 -38
  118. package/src/modules/inventory/service.ts +0 -490
  119. package/src/modules/media/adapter.ts +0 -17
  120. package/src/modules/media/repository/index.ts +0 -274
  121. package/src/modules/media/schema.ts +0 -41
  122. package/src/modules/media/service.ts +0 -151
  123. package/src/modules/orders/repository/index.ts +0 -287
  124. package/src/modules/orders/schema.ts +0 -66
  125. package/src/modules/orders/service.ts +0 -619
  126. package/src/modules/orders/stale-order-cleanup.ts +0 -76
  127. package/src/modules/organization/service.ts +0 -191
  128. package/src/modules/payments/adapter.ts +0 -47
  129. package/src/modules/payments/repository/index.ts +0 -6
  130. package/src/modules/payments/service.ts +0 -107
  131. package/src/modules/pricing/repository/index.ts +0 -291
  132. package/src/modules/pricing/schema.ts +0 -71
  133. package/src/modules/pricing/schemas.ts +0 -38
  134. package/src/modules/pricing/service.ts +0 -494
  135. package/src/modules/promotions/repository/index.ts +0 -325
  136. package/src/modules/promotions/schema.ts +0 -62
  137. package/src/modules/promotions/schemas.ts +0 -38
  138. package/src/modules/promotions/service.ts +0 -598
  139. package/src/modules/search/adapter.ts +0 -57
  140. package/src/modules/search/hooks.ts +0 -12
  141. package/src/modules/search/repository/index.ts +0 -6
  142. package/src/modules/search/service.ts +0 -315
  143. package/src/modules/shipping/calculator.ts +0 -188
  144. package/src/modules/shipping/repository/index.ts +0 -6
  145. package/src/modules/shipping/service.ts +0 -51
  146. package/src/modules/tax/adapter.ts +0 -60
  147. package/src/modules/tax/repository/index.ts +0 -6
  148. package/src/modules/tax/service.ts +0 -53
  149. package/src/modules/webhooks/hook.ts +0 -34
  150. package/src/modules/webhooks/repository/index.ts +0 -278
  151. package/src/modules/webhooks/schema.ts +0 -56
  152. package/src/modules/webhooks/service.ts +0 -117
  153. package/src/modules/webhooks/signing.ts +0 -6
  154. package/src/modules/webhooks/ssrf-guard.ts +0 -71
  155. package/src/modules/webhooks/tasks.ts +0 -52
  156. package/src/modules/webhooks/worker.ts +0 -134
  157. package/src/runtime/commerce.ts +0 -145
  158. package/src/runtime/kernel.ts +0 -419
  159. package/src/runtime/logger.ts +0 -36
  160. package/src/runtime/server.ts +0 -349
  161. package/src/runtime/shutdown.ts +0 -43
  162. package/src/test-utils/create-pglite-adapter.ts +0 -129
  163. package/src/test-utils/create-plugin-test-app.ts +0 -128
  164. package/src/test-utils/create-repository-test-harness.ts +0 -16
  165. package/src/test-utils/create-test-config.ts +0 -190
  166. package/src/test-utils/create-test-kernel.ts +0 -7
  167. package/src/test-utils/create-test-plugin-context.ts +0 -75
  168. package/src/test-utils/rest-api-test-utils.ts +0 -265
  169. package/src/test-utils/test-actors.ts +0 -62
  170. package/src/test-utils/typed-hooks.ts +0 -54
  171. package/src/types/commerce-types.ts +0 -34
  172. package/src/utils/id.ts +0 -3
  173. package/src/utils/logger.ts +0 -18
  174. 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
- }