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