@porulle/core 0.11.0 → 0.13.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.
Files changed (289) hide show
  1. package/dist/auth/auth-schema-guard.d.ts +2444 -0
  2. package/dist/auth/auth-schema-guard.d.ts.map +1 -0
  3. package/dist/auth/auth-schema-guard.js +113 -0
  4. package/dist/auth/auth-schema.d.ts +228 -0
  5. package/dist/auth/auth-schema.d.ts.map +1 -1
  6. package/dist/auth/auth-schema.js +24 -2
  7. package/dist/auth/middleware.d.ts +1 -0
  8. package/dist/auth/middleware.d.ts.map +1 -1
  9. package/dist/auth/middleware.js +13 -5
  10. package/dist/auth/org.d.ts +8 -2
  11. package/dist/auth/org.d.ts.map +1 -1
  12. package/dist/auth/org.js +12 -4
  13. package/dist/auth/organization-guard.d.ts +5 -0
  14. package/dist/auth/organization-guard.d.ts.map +1 -0
  15. package/dist/auth/organization-guard.js +124 -0
  16. package/dist/auth/permissions.d.ts +10 -0
  17. package/dist/auth/permissions.d.ts.map +1 -1
  18. package/dist/auth/permissions.js +31 -7
  19. package/dist/auth/role-authority.d.ts +44 -0
  20. package/dist/auth/role-authority.d.ts.map +1 -0
  21. package/dist/auth/role-authority.js +85 -0
  22. package/dist/auth/setup.d.ts.map +1 -1
  23. package/dist/auth/setup.js +17 -0
  24. package/dist/auth/strict-org-resolution.d.ts.map +1 -1
  25. package/dist/auth/strict-org-resolution.js +9 -1
  26. package/dist/auth/types.d.ts +1 -1
  27. package/dist/auth/types.d.ts.map +1 -1
  28. package/dist/config/defaults.d.ts.map +1 -1
  29. package/dist/config/defaults.js +2 -0
  30. package/dist/config/types.d.ts +30 -4
  31. package/dist/config/types.d.ts.map +1 -1
  32. package/dist/hooks/checkout.d.ts.map +1 -1
  33. package/dist/hooks/checkout.js +10 -9
  34. package/dist/index.d.ts +6 -5
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +4 -3
  37. package/dist/interfaces/rest/audit-middleware.d.ts.map +1 -1
  38. package/dist/interfaces/rest/audit-middleware.js +2 -0
  39. package/dist/interfaces/rest/customer-portal.d.ts.map +1 -1
  40. package/dist/interfaces/rest/customer-portal.js +9 -8
  41. package/dist/interfaces/rest/route-coverage.d.ts +14 -0
  42. package/dist/interfaces/rest/route-coverage.d.ts.map +1 -0
  43. package/dist/interfaces/rest/route-coverage.js +81 -0
  44. package/dist/interfaces/rest/router.d.ts +7 -2
  45. package/dist/interfaces/rest/router.d.ts.map +1 -1
  46. package/dist/interfaces/rest/router.js +8 -4
  47. package/dist/interfaces/rest/routes/admin/compensation-failures.d.ts.map +1 -1
  48. package/dist/interfaces/rest/routes/admin/compensation-failures.js +7 -5
  49. package/dist/interfaces/rest/routes/admin/custom-field-proposals.js +2 -2
  50. package/dist/interfaces/rest/routes/admin/permissions.d.ts +1 -1
  51. package/dist/interfaces/rest/routes/admin/permissions.d.ts.map +1 -1
  52. package/dist/interfaces/rest/routes/admin/permissions.js +2 -0
  53. package/dist/interfaces/rest/routes/admin/staff.d.ts.map +1 -1
  54. package/dist/interfaces/rest/routes/admin/staff.js +126 -66
  55. package/dist/interfaces/rest/routes/admin-jobs.js +3 -3
  56. package/dist/interfaces/rest/routes/audit.js +3 -3
  57. package/dist/interfaces/rest/routes/carts.d.ts.map +1 -1
  58. package/dist/interfaces/rest/routes/carts.js +13 -10
  59. package/dist/interfaces/rest/routes/catalog.d.ts.map +1 -1
  60. package/dist/interfaces/rest/routes/catalog.js +10 -2
  61. package/dist/interfaces/rest/routes/checkout.d.ts.map +1 -1
  62. package/dist/interfaces/rest/routes/checkout.js +30 -35
  63. package/dist/interfaces/rest/routes/documents.d.ts.map +1 -1
  64. package/dist/interfaces/rest/routes/documents.js +10 -5
  65. package/dist/interfaces/rest/routes/inventory.d.ts.map +1 -1
  66. package/dist/interfaces/rest/routes/inventory.js +2 -0
  67. package/dist/interfaces/rest/routes/media.d.ts.map +1 -1
  68. package/dist/interfaces/rest/routes/media.js +7 -3
  69. package/dist/interfaces/rest/routes/orders.d.ts.map +1 -1
  70. package/dist/interfaces/rest/routes/orders.js +26 -8
  71. package/dist/interfaces/rest/routes/pricing.js +2 -2
  72. package/dist/interfaces/rest/routes/promotions.d.ts.map +1 -1
  73. package/dist/interfaces/rest/routes/promotions.js +13 -10
  74. package/dist/interfaces/rest/routes/search.d.ts.map +1 -1
  75. package/dist/interfaces/rest/routes/search.js +2 -1
  76. package/dist/interfaces/rest/schemas/carts.d.ts +7 -0
  77. package/dist/interfaces/rest/schemas/carts.d.ts.map +1 -1
  78. package/dist/interfaces/rest/schemas/customer-portal.d.ts +35 -0
  79. package/dist/interfaces/rest/schemas/customer-portal.d.ts.map +1 -1
  80. package/dist/interfaces/rest/schemas/orders.d.ts +204 -0
  81. package/dist/interfaces/rest/schemas/orders.d.ts.map +1 -1
  82. package/dist/interfaces/rest/schemas/responses.d.ts +71 -0
  83. package/dist/interfaces/rest/schemas/responses.d.ts.map +1 -1
  84. package/dist/interfaces/rest/utils.d.ts +15 -0
  85. package/dist/interfaces/rest/utils.d.ts.map +1 -1
  86. package/dist/interfaces/rest/utils.js +45 -2
  87. package/dist/kernel/compensation/executor.js +2 -2
  88. package/dist/kernel/database/tx-context.d.ts +8 -0
  89. package/dist/kernel/database/tx-context.d.ts.map +1 -1
  90. package/dist/kernel/database/tx-context.js +13 -0
  91. package/dist/kernel/hooks/create-context.d.ts +2 -0
  92. package/dist/kernel/hooks/create-context.d.ts.map +1 -1
  93. package/dist/kernel/hooks/create-context.js +1 -0
  94. package/dist/kernel/hooks/types.d.ts +2 -0
  95. package/dist/kernel/hooks/types.d.ts.map +1 -1
  96. package/dist/kernel/plugin/manifest.d.ts.map +1 -1
  97. package/dist/kernel/plugin/manifest.js +9 -5
  98. package/dist/modules/analytics/service.js +2 -2
  99. package/dist/modules/audit/service.js +3 -3
  100. package/dist/modules/cart/repository/index.d.ts +13 -7
  101. package/dist/modules/cart/repository/index.d.ts.map +1 -1
  102. package/dist/modules/cart/repository/index.js +16 -10
  103. package/dist/modules/cart/schema.d.ts +17 -0
  104. package/dist/modules/cart/schema.d.ts.map +1 -1
  105. package/dist/modules/cart/schema.js +4 -0
  106. package/dist/modules/cart/service.d.ts +22 -9
  107. package/dist/modules/cart/service.d.ts.map +1 -1
  108. package/dist/modules/cart/service.js +57 -33
  109. package/dist/modules/catalog/brand-service.js +7 -7
  110. package/dist/modules/catalog/category-service.js +7 -7
  111. package/dist/modules/catalog/entity-service.d.ts +14 -7
  112. package/dist/modules/catalog/entity-service.d.ts.map +1 -1
  113. package/dist/modules/catalog/entity-service.js +109 -31
  114. package/dist/modules/catalog/read-policy.d.ts +9 -0
  115. package/dist/modules/catalog/read-policy.d.ts.map +1 -0
  116. package/dist/modules/catalog/read-policy.js +8 -0
  117. package/dist/modules/catalog/repository/index.d.ts +3 -2
  118. package/dist/modules/catalog/repository/index.d.ts.map +1 -1
  119. package/dist/modules/catalog/repository/index.js +14 -4
  120. package/dist/modules/catalog/service.d.ts +15 -13
  121. package/dist/modules/catalog/service.d.ts.map +1 -1
  122. package/dist/modules/catalog/service.js +25 -18
  123. package/dist/modules/customers/module.d.ts.map +1 -1
  124. package/dist/modules/customers/module.js +1 -0
  125. package/dist/modules/customers/service.d.ts +2 -0
  126. package/dist/modules/customers/service.d.ts.map +1 -1
  127. package/dist/modules/customers/service.js +21 -20
  128. package/dist/modules/documents/module.d.ts.map +1 -1
  129. package/dist/modules/documents/module.js +1 -0
  130. package/dist/modules/documents/service.d.ts +18 -4
  131. package/dist/modules/documents/service.d.ts.map +1 -1
  132. package/dist/modules/documents/service.js +54 -16
  133. package/dist/modules/fulfillment/module.d.ts.map +1 -1
  134. package/dist/modules/fulfillment/module.js +1 -0
  135. package/dist/modules/fulfillment/service.d.ts +2 -0
  136. package/dist/modules/fulfillment/service.d.ts.map +1 -1
  137. package/dist/modules/fulfillment/service.js +15 -6
  138. package/dist/modules/inventory/service.d.ts.map +1 -1
  139. package/dist/modules/inventory/service.js +15 -14
  140. package/dist/modules/media/service.js +6 -6
  141. package/dist/modules/orders/guest-access.d.ts +27 -0
  142. package/dist/modules/orders/guest-access.d.ts.map +1 -0
  143. package/dist/modules/orders/guest-access.js +46 -0
  144. package/dist/modules/orders/module.d.ts.map +1 -1
  145. package/dist/modules/orders/module.js +1 -0
  146. package/dist/modules/orders/quote.d.ts.map +1 -1
  147. package/dist/modules/orders/quote.js +1 -0
  148. package/dist/modules/orders/repository/index.d.ts +1 -1
  149. package/dist/modules/orders/repository/index.d.ts.map +1 -1
  150. package/dist/modules/orders/repository/index.js +2 -2
  151. package/dist/modules/orders/schema.d.ts +17 -0
  152. package/dist/modules/orders/schema.d.ts.map +1 -1
  153. package/dist/modules/orders/schema.js +5 -3
  154. package/dist/modules/orders/service.d.ts +16 -7
  155. package/dist/modules/orders/service.d.ts.map +1 -1
  156. package/dist/modules/orders/service.js +184 -55
  157. package/dist/modules/pricing/module.d.ts.map +1 -1
  158. package/dist/modules/pricing/module.js +1 -0
  159. package/dist/modules/pricing/service.d.ts +2 -0
  160. package/dist/modules/pricing/service.d.ts.map +1 -1
  161. package/dist/modules/pricing/service.js +12 -11
  162. package/dist/modules/promotions/module.d.ts.map +1 -1
  163. package/dist/modules/promotions/module.js +1 -0
  164. package/dist/modules/promotions/service.d.ts +3 -0
  165. package/dist/modules/promotions/service.d.ts.map +1 -1
  166. package/dist/modules/promotions/service.js +19 -11
  167. package/dist/modules/search/adapter.d.ts +6 -0
  168. package/dist/modules/search/adapter.d.ts.map +1 -1
  169. package/dist/modules/search/module.d.ts.map +1 -1
  170. package/dist/modules/search/module.js +1 -0
  171. package/dist/modules/search/service.d.ts +1 -0
  172. package/dist/modules/search/service.d.ts.map +1 -1
  173. package/dist/modules/search/service.js +57 -6
  174. package/dist/modules/settings/module.d.ts.map +1 -1
  175. package/dist/modules/settings/module.js +1 -0
  176. package/dist/modules/settings/service.d.ts +3 -0
  177. package/dist/modules/settings/service.d.ts.map +1 -1
  178. package/dist/modules/settings/service.js +6 -4
  179. package/dist/modules/shipping/service.js +9 -9
  180. package/dist/modules/tax/module.d.ts.map +1 -1
  181. package/dist/modules/tax/module.js +1 -0
  182. package/dist/modules/tax/service.d.ts +3 -0
  183. package/dist/modules/tax/service.d.ts.map +1 -1
  184. package/dist/modules/tax/service.js +11 -9
  185. package/dist/modules/webhooks/hook.js +2 -2
  186. package/dist/modules/webhooks/module.d.ts.map +1 -1
  187. package/dist/modules/webhooks/module.js +1 -0
  188. package/dist/modules/webhooks/service.d.ts +2 -0
  189. package/dist/modules/webhooks/service.d.ts.map +1 -1
  190. package/dist/modules/webhooks/service.js +4 -4
  191. package/dist/runtime/server.d.ts.map +1 -1
  192. package/dist/runtime/server.js +10 -2
  193. package/dist/test-utils/create-pglite-adapter.d.ts.map +1 -1
  194. package/dist/test-utils/create-pglite-adapter.js +35 -13
  195. package/dist/test-utils/create-test-config.d.ts.map +1 -1
  196. package/dist/test-utils/create-test-config.js +4 -0
  197. package/dist/test-utils/rest-api-test-utils.d.ts.map +1 -1
  198. package/dist/test-utils/rest-api-test-utils.js +2 -0
  199. package/dist/test-utils/test-actors.js +2 -2
  200. package/dist/utils/id.d.ts +2 -0
  201. package/dist/utils/id.d.ts.map +1 -1
  202. package/dist/utils/id.js +11 -0
  203. package/package.json +6 -5
  204. package/src/auth/auth-schema-guard.ts +143 -0
  205. package/src/auth/auth-schema.ts +50 -19
  206. package/src/auth/middleware.ts +17 -5
  207. package/src/auth/org.ts +15 -3
  208. package/src/auth/organization-guard.ts +158 -0
  209. package/src/auth/permissions.ts +31 -8
  210. package/src/auth/role-authority.ts +107 -0
  211. package/src/auth/setup.ts +17 -0
  212. package/src/auth/strict-org-resolution.ts +7 -1
  213. package/src/auth/types.ts +1 -1
  214. package/src/config/defaults.ts +2 -0
  215. package/src/config/types.ts +29 -4
  216. package/src/hooks/checkout.ts +19 -12
  217. package/src/index.ts +16 -6
  218. package/src/interfaces/rest/audit-middleware.ts +2 -0
  219. package/src/interfaces/rest/customer-portal.ts +31 -24
  220. package/src/interfaces/rest/route-coverage.ts +103 -0
  221. package/src/interfaces/rest/router.ts +16 -10
  222. package/src/interfaces/rest/routes/admin/compensation-failures.ts +8 -5
  223. package/src/interfaces/rest/routes/admin/custom-field-proposals.ts +2 -2
  224. package/src/interfaces/rest/routes/admin/permissions.ts +3 -1
  225. package/src/interfaces/rest/routes/admin/staff.ts +161 -72
  226. package/src/interfaces/rest/routes/admin-jobs.ts +3 -3
  227. package/src/interfaces/rest/routes/audit.ts +3 -3
  228. package/src/interfaces/rest/routes/carts.ts +17 -7
  229. package/src/interfaces/rest/routes/catalog.ts +11 -0
  230. package/src/interfaces/rest/routes/checkout.ts +41 -38
  231. package/src/interfaces/rest/routes/documents.ts +15 -1
  232. package/src/interfaces/rest/routes/inventory.ts +2 -0
  233. package/src/interfaces/rest/routes/media.ts +8 -3
  234. package/src/interfaces/rest/routes/orders.ts +27 -8
  235. package/src/interfaces/rest/routes/pricing.ts +2 -2
  236. package/src/interfaces/rest/routes/promotions.ts +12 -9
  237. package/src/interfaces/rest/routes/search.ts +3 -1
  238. package/src/interfaces/rest/utils.ts +61 -2
  239. package/src/kernel/compensation/executor.ts +2 -2
  240. package/src/kernel/database/tx-context.ts +25 -0
  241. package/src/kernel/hooks/create-context.ts +3 -0
  242. package/src/kernel/hooks/types.ts +2 -0
  243. package/src/kernel/plugin/manifest.ts +9 -5
  244. package/src/modules/analytics/service.ts +2 -2
  245. package/src/modules/audit/service.ts +3 -3
  246. package/src/modules/cart/repository/index.ts +22 -8
  247. package/src/modules/cart/schema.ts +4 -0
  248. package/src/modules/cart/service.ts +62 -27
  249. package/src/modules/catalog/brand-service.ts +7 -7
  250. package/src/modules/catalog/category-service.ts +7 -7
  251. package/src/modules/catalog/entity-service.ts +117 -35
  252. package/src/modules/catalog/read-policy.ts +16 -0
  253. package/src/modules/catalog/repository/index.ts +19 -3
  254. package/src/modules/catalog/service.ts +46 -31
  255. package/src/modules/customers/module.ts +2 -0
  256. package/src/modules/customers/service.ts +23 -22
  257. package/src/modules/documents/module.ts +2 -0
  258. package/src/modules/documents/service.ts +74 -11
  259. package/src/modules/fulfillment/module.ts +2 -0
  260. package/src/modules/fulfillment/service.ts +17 -6
  261. package/src/modules/inventory/service.ts +15 -24
  262. package/src/modules/media/service.ts +6 -6
  263. package/src/modules/orders/guest-access.ts +55 -0
  264. package/src/modules/orders/module.ts +1 -0
  265. package/src/modules/orders/quote.ts +1 -0
  266. package/src/modules/orders/repository/index.ts +2 -0
  267. package/src/modules/orders/schema.ts +5 -3
  268. package/src/modules/orders/service.ts +302 -52
  269. package/src/modules/pricing/module.ts +2 -0
  270. package/src/modules/pricing/service.ts +14 -16
  271. package/src/modules/promotions/module.ts +2 -0
  272. package/src/modules/promotions/service.ts +25 -16
  273. package/src/modules/search/adapter.ts +6 -0
  274. package/src/modules/search/module.ts +1 -0
  275. package/src/modules/search/service.ts +55 -6
  276. package/src/modules/settings/module.ts +2 -0
  277. package/src/modules/settings/service.ts +8 -4
  278. package/src/modules/shipping/service.ts +9 -9
  279. package/src/modules/tax/module.ts +1 -0
  280. package/src/modules/tax/service.ts +13 -9
  281. package/src/modules/webhooks/hook.ts +2 -2
  282. package/src/modules/webhooks/module.ts +2 -0
  283. package/src/modules/webhooks/service.ts +6 -4
  284. package/src/runtime/server.ts +16 -2
  285. package/src/test-utils/create-pglite-adapter.ts +34 -12
  286. package/src/test-utils/create-test-config.ts +10 -6
  287. package/src/test-utils/rest-api-test-utils.ts +2 -0
  288. package/src/test-utils/test-actors.ts +2 -2
  289. package/src/utils/id.ts +20 -0
@@ -1,4 +1,4 @@
1
- import { resolveOrgId } from "../../auth/org.js";
1
+ import { resolveOrgIdForCommerce } from "../../auth/org.js";
2
2
  import { assertPermission } from "../../auth/permissions.js";
3
3
  import type { Actor } from "../../auth/types.js";
4
4
  import {
@@ -21,8 +21,12 @@ import type {
21
21
  import { Err, Ok, type Result } from "../../kernel/result.js";
22
22
  import { createLogger } from "../../utils/logger.js";
23
23
  import { paginate } from "../../utils/pagination.js";
24
+ import type { JobsAdapter } from "../../kernel/jobs/adapter.js";
24
25
  import type { PluginDb } from "../../kernel/database/plugin-types.js";
25
- import type { TxContext } from "../../kernel/database/tx-context.js";
26
+ import type { CatalogWriteContext, TxContext } from "../../kernel/database/tx-context.js";
27
+ import { isWriteContextTransactional } from "../../kernel/database/tx-context.js";
28
+ import { canReadUnpublishedCatalog, isCatalogEntityVisible } from "./read-policy.js";
29
+ import { isValidFieldPath } from "./ownership.js";
26
30
  import type {
27
31
  CatalogServiceDeps,
28
32
  CatalogEntityHydrated,
@@ -52,6 +56,26 @@ function hookDatabaseArg(database: CatalogServiceDeps["database"]): { database:
52
56
  return { database: { db: database.db as PluginDb } };
53
57
  }
54
58
 
59
+ const attributeFields = ["title", "subtitle", "description", "richDescription", "seoTitle", "seoDescription"] as const;
60
+
61
+ function catalogHookContext(
62
+ deps: CatalogServiceDeps,
63
+ actor: Actor | null,
64
+ ctx: CatalogWriteContext | undefined,
65
+ operation: string,
66
+ ): HookContext {
67
+ return createHookContext({
68
+ actor,
69
+ tx: isWriteContextTransactional(ctx) ? ctx.tx : null,
70
+ logger: createLogger(`catalog.${operation}`),
71
+ services: deps.services,
72
+ ...(deps.services.jobs ? { jobs: deps.services.jobs as JobsAdapter } : {}),
73
+ context: { moduleName: "catalog", ...(ctx?.hookContext ?? {}) },
74
+ ...hookDatabaseArg(deps.database),
75
+ commerceConfig: deps.config,
76
+ });
77
+ }
78
+
55
79
  type CatalogCreateBeforeHook = BeforeHook<CreateEntityInput>;
56
80
  type CatalogCreateAfterHook = AfterHook<SellableEntity>;
57
81
  type CatalogUpdateBeforeHook = BeforeHook<UpdateEntityInput>;
@@ -100,7 +124,7 @@ export class EntityService {
100
124
 
101
125
  private assertSameOrg(resource: { organizationId?: string | null } | undefined, actor: Actor | null): void {
102
126
  if (!resource) return;
103
- const orgId = resolveOrgId(actor);
127
+ const orgId = resolveOrgIdForCommerce(actor, this.deps.config);
104
128
  if (resource.organizationId && resource.organizationId !== orgId) {
105
129
  throw new CommerceNotFoundError("Entity not found.");
106
130
  }
@@ -256,12 +280,12 @@ export class EntityService {
256
280
 
257
281
  async create(input: CreateEntityInput, actor: Actor | null, ctx?: TxContext): Promise<Result<CatalogEntityHydrated>> { try {
258
282
  assertPermission(actor, "catalog:create");
259
- const orgId = resolveOrgId(actor);
283
+ const orgId = resolveOrgIdForCommerce(actor, this.deps.config);
260
284
  const existingBySlug = await this.repo.findEntityBySlug(orgId, input.slug, ctx);
261
285
  if (existingBySlug) return Err(new CommerceConflictError(`Entity with slug ${input.slug} already exists.`));
262
286
  const beforeHooks = this.deps.hooks.resolve("catalog.beforeCreate") as CatalogCreateBeforeHook[];
263
287
  const afterHooks = this.deps.hooks.resolve("catalog.afterCreate") as CatalogCreateAfterHook[];
264
- const context: HookContext = createHookContext({ actor, tx: ctx?.tx ?? null, logger: createLogger("catalog.create"), services: this.deps.services, context: { moduleName: "catalog" }, ...hookDatabaseArg(this.deps.database) });
288
+ const context: HookContext = createHookContext({ actor, tx: ctx?.tx ?? null, logger: createLogger("catalog.create"), services: this.deps.services, context: { moduleName: "catalog" }, ...hookDatabaseArg(this.deps.database), commerceConfig: this.deps.config });
265
289
  const processedInput = await runBeforeHooks(beforeHooks, input, "create", context);
266
290
  if (processedInput.sourceStoreId != null) assertPermission(actor, "catalog:sync");
267
291
  const customFieldsResult = await this.validateCustomFields(processedInput.type, processedInput.customFields, actor, ctx);
@@ -278,23 +302,24 @@ export class EntityService {
278
302
  return Ok(hydrated, hookReport.hasErrors ? { hookErrors: hookReport.errors } : undefined);
279
303
  } catch (error) { return Err(toCommerceError(error)); } }
280
304
 
281
- async update(id: string, input: UpdateEntityInput, actor: Actor | null, ctx?: TxContext): Promise<Result<CatalogEntityHydrated>> { try {
305
+ async update(id: string, input: UpdateEntityInput, actor: Actor | null, ctx?: CatalogWriteContext): Promise<Result<CatalogEntityHydrated>> { try {
306
+ const txCtx = isWriteContextTransactional(ctx) ? ctx : undefined;
282
307
  assertPermission(actor, "catalog:update");
283
- const existing = await this.repo.findEntityById(id, ctx);
308
+ const existing = await this.repo.findEntityById(id, txCtx);
284
309
  if (!existing) return Err(new CommerceNotFoundError("Entity not found."));
285
310
  this.assertSameOrg(existing, actor);
286
311
  this.assertEntityWritable(existing, actor);
287
312
  const beforeHooks = this.deps.hooks.resolve("catalog.beforeUpdate") as CatalogUpdateBeforeHook[];
288
313
  const afterHooks = this.deps.hooks.resolve("catalog.afterUpdate") as CatalogUpdateAfterHook[];
289
- const context: HookContext = createHookContext({ actor, tx: ctx?.tx ?? null, logger: createLogger("catalog.update"), services: this.deps.services, context: { moduleName: "catalog" }, ...hookDatabaseArg(this.deps.database) });
314
+ const context: HookContext = catalogHookContext(this.deps, actor, ctx, "update");
290
315
  const processed = await runBeforeHooks(beforeHooks, input, "update", context);
291
- const customFieldsResult = await this.validateCustomFields(existing.type, processed.customFields, actor, ctx);
316
+ const customFieldsResult = await this.validateCustomFields(existing.type, processed.customFields, actor, txCtx);
292
317
  if (!customFieldsResult.ok) return customFieldsResult;
293
- const updated = await this.repo.updateEntity(id, { ...(processed.slug !== undefined ? { slug: processed.slug } : {}), ...(processed.status !== undefined ? { status: processed.status as SellableEntity["status"] } : {}), ...(processed.taxClass !== undefined ? { taxClass: processed.taxClass } : {}), ...(processed.metadata !== undefined ? { metadata: processed.metadata } : {}), ...(processed.isVisible !== undefined ? { isVisible: processed.isVisible } : {}) }, ctx);
318
+ const updated = await this.repo.updateEntity(id, { ...(processed.slug !== undefined ? { slug: processed.slug } : {}), ...(processed.status !== undefined ? { status: processed.status as SellableEntity["status"] } : {}), ...(processed.taxClass !== undefined ? { taxClass: processed.taxClass } : {}), ...(processed.metadata !== undefined ? { metadata: processed.metadata } : {}), ...(processed.isVisible !== undefined ? { isVisible: processed.isVisible } : {}) }, txCtx);
294
319
  if (!updated) return Err(new CommerceNotFoundError("Entity not found."));
295
- await this.writeCustomFields(existing.id, customFieldsResult.value, ctx, true);
320
+ await this.writeCustomFields(existing.id, customFieldsResult.value, txCtx, true);
296
321
  const hookReport = await runAfterHooks(afterHooks, existing, updated, "update", context);
297
- const hydrated = await this.hydrateEntity(updated, undefined, ctx);
322
+ const hydrated = await this.hydrateEntity(updated, undefined, txCtx);
298
323
  return Ok(hydrated, hookReport.hasErrors ? { hookErrors: hookReport.errors } : undefined);
299
324
  } catch (error) { return Err(toCommerceError(error)); } }
300
325
 
@@ -314,24 +339,27 @@ export class EntityService {
314
339
  return Ok(undefined);
315
340
  } catch (error) { return Err(toCommerceError(error)); } }
316
341
 
317
- async getById(id: string, options?: GetOptions, actor?: Actor | null, ctx?: TxContext): Promise<Result<CatalogEntityHydrated>> {
318
- const context: HookContext = createHookContext({ actor: actor ?? null, tx: ctx?.tx ?? null, logger: createLogger("catalog.read"), services: this.deps.services, context: { moduleName: "catalog" }, ...hookDatabaseArg(this.deps.database) });
342
+ async getById(id: string, options: GetOptions | undefined, actor: Actor | null, ctx?: TxContext): Promise<Result<CatalogEntityHydrated>> {
343
+ const resolvedActor = actor ?? ctx?.actor ?? null;
344
+ const orgId = resolveOrgIdForCommerce(resolvedActor, this.deps.config);
345
+ const context: HookContext = createHookContext({ actor: resolvedActor, tx: ctx?.tx ?? null, logger: createLogger("catalog.read"), services: this.deps.services, context: { moduleName: "catalog" }, ...hookDatabaseArg(this.deps.database), commerceConfig: this.deps.config });
319
346
  const globalBeforeHooks = this.deps.hooks.resolve("catalog.beforeRead") as CatalogReadBeforeHook[];
320
347
  const globalAfterHooks = this.deps.hooks.resolve("catalog.afterRead") as CatalogReadAfterHook[];
321
348
  let processed = await runBeforeHooks(globalBeforeHooks, { id, ...(options !== undefined ? { options } : {}) }, "read", context);
322
- let entity = await this.repo.findEntityById(processed.id ?? id, ctx);
349
+ let entity = await this.repo.findEntityById(processed.id ?? id, ctx, orgId);
323
350
  if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
351
+ if (!isCatalogEntityVisible(entity, resolvedActor)) return Err(new CommerceNotFoundError("Entity not found."));
324
352
  const entityBeforeHooks = this.deps.hooks.resolve(`catalog.${entity.type}.beforeRead`) as CatalogReadBeforeHook[];
325
353
  if (entityBeforeHooks.length > 0) {
326
354
  processed = await runBeforeHooks(entityBeforeHooks, { ...processed, id: entity.id }, "read", context);
327
355
  const resolvedId = processed.id ?? entity.id;
328
356
  if (resolvedId !== entity.id) {
329
- const refetched = await this.repo.findEntityById(resolvedId, ctx);
357
+ const refetched = await this.repo.findEntityById(resolvedId, ctx, orgId);
330
358
  if (!refetched) return Err(new CommerceNotFoundError("Entity not found."));
359
+ if (!isCatalogEntityVisible(refetched, resolvedActor)) return Err(new CommerceNotFoundError("Entity not found."));
331
360
  entity = refetched;
332
361
  }
333
362
  }
334
- const resolvedActor = actor ?? ctx?.actor ?? null;
335
363
  try {
336
364
  this.assertSameOrg(entity, resolvedActor);
337
365
  } catch (error) {
@@ -343,28 +371,48 @@ export class EntityService {
343
371
  return Ok(result, report.hasErrors ? { hookErrors: report.errors } : undefined);
344
372
  }
345
373
 
346
- async getBySlug(slug: string, options?: GetOptions, actor?: Actor | null, ctx?: TxContext): Promise<Result<CatalogEntityHydrated>> {
347
- const context: HookContext = createHookContext({ actor: actor ?? null, tx: ctx?.tx ?? null, logger: createLogger("catalog.read"), services: this.deps.services, context: { moduleName: "catalog" }, ...hookDatabaseArg(this.deps.database) });
374
+ /**
375
+ * Reads an entity for a trusted server pipeline after the request boundary
376
+ * has already authorized the caller. Organization scope is explicit; the
377
+ * storefront visibility policy is intentionally not applied, so an internal
378
+ * invariant check never depends on the caller's catalog permissions.
379
+ */
380
+ async getByIdForInternalUse(
381
+ id: string,
382
+ organizationId: string,
383
+ options?: GetOptions,
384
+ ctx?: TxContext,
385
+ ): Promise<Result<CatalogEntityHydrated>> {
386
+ const entity = await this.repo.findEntityById(id, ctx, organizationId);
387
+ if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
388
+ return Ok(await this.hydrateEntity(entity, options, ctx));
389
+ }
390
+
391
+ async getBySlug(slug: string, options: GetOptions | undefined, actor: Actor | null, ctx?: TxContext): Promise<Result<CatalogEntityHydrated>> {
392
+ const context: HookContext = createHookContext({ actor: actor ?? null, tx: ctx?.tx ?? null, logger: createLogger("catalog.read"), services: this.deps.services, context: { moduleName: "catalog" }, ...hookDatabaseArg(this.deps.database), commerceConfig: this.deps.config });
348
393
  const globalBeforeHooks = this.deps.hooks.resolve("catalog.beforeRead") as CatalogReadBeforeHook[];
349
394
  const globalAfterHooks = this.deps.hooks.resolve("catalog.afterRead") as CatalogReadAfterHook[];
350
395
  let processed = await runBeforeHooks(globalBeforeHooks, { slug, ...(options !== undefined ? { options } : {}) }, "read", context);
351
396
  let resolvedSlug = processed.slug ?? slug;
352
- const orgId = resolveOrgId(actor ?? null);
397
+ const orgId = resolveOrgIdForCommerce(actor ?? null, this.deps.config);
353
398
  let entity = await this.repo.findEntityBySlug(orgId, resolvedSlug, ctx);
354
399
  if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
400
+ if (!isCatalogEntityVisible(entity, actor ?? null)) return Err(new CommerceNotFoundError("Entity not found."));
355
401
  const entityBeforeHooks = this.deps.hooks.resolve(`catalog.${entity.type}.beforeRead`) as CatalogReadBeforeHook[];
356
402
  if (entityBeforeHooks.length > 0) {
357
403
  processed = await runBeforeHooks(entityBeforeHooks, { ...processed, id: entity.id }, "read", context);
358
404
  const nextId = processed.id ?? entity.id;
359
405
  const nextSlug = processed.slug ?? resolvedSlug;
360
406
  if (nextId !== entity.id) {
361
- const refetched = await this.repo.findEntityById(nextId, ctx);
407
+ const refetched = await this.repo.findEntityById(nextId, ctx, orgId);
362
408
  if (!refetched) return Err(new CommerceNotFoundError("Entity not found."));
409
+ if (!isCatalogEntityVisible(refetched, actor ?? null)) return Err(new CommerceNotFoundError("Entity not found."));
363
410
  entity = refetched;
364
411
  resolvedSlug = nextSlug;
365
412
  } else if (nextSlug !== resolvedSlug) {
366
413
  const refetched = await this.repo.findEntityBySlug(orgId, nextSlug, ctx);
367
414
  if (!refetched) return Err(new CommerceNotFoundError("Entity not found."));
415
+ if (!isCatalogEntityVisible(refetched, actor ?? null)) return Err(new CommerceNotFoundError("Entity not found."));
368
416
  entity = refetched;
369
417
  }
370
418
  }
@@ -374,9 +422,9 @@ export class EntityService {
374
422
  return Ok(result, report.hasErrors ? { hookErrors: report.errors } : undefined);
375
423
  }
376
424
 
377
- async list(params: ListParams, actor?: Actor | null, ctx?: TxContext): Promise<Result<CatalogListResult>> {
425
+ async list(params: ListParams, actor: Actor | null, ctx?: TxContext): Promise<Result<CatalogListResult>> {
378
426
  const resolvedActor = actor ?? ctx?.actor ?? null;
379
- const context: HookContext = createHookContext({ actor: resolvedActor, tx: ctx?.tx ?? null, logger: createLogger("catalog.list"), services: this.deps.services, context: { moduleName: "catalog" }, ...hookDatabaseArg(this.deps.database) });
427
+ const context: HookContext = createHookContext({ actor: resolvedActor, tx: ctx?.tx ?? null, logger: createLogger("catalog.list"), services: this.deps.services, context: { moduleName: "catalog" }, ...hookDatabaseArg(this.deps.database), commerceConfig: this.deps.config });
380
428
  const globalBeforeHooks = this.deps.hooks.resolve("catalog.beforeList") as CatalogListBeforeHook[];
381
429
  const globalAfterHooks = this.deps.hooks.resolve("catalog.afterList") as CatalogListAfterHook[];
382
430
  let processed = await runBeforeHooks(globalBeforeHooks, params, "list", context);
@@ -385,20 +433,28 @@ export class EntityService {
385
433
  const entityBeforeHooks = this.deps.hooks.resolve(`catalog.${initialTypeFilter}.beforeList`) as CatalogListBeforeHook[];
386
434
  if (entityBeforeHooks.length > 0) processed = await runBeforeHooks(entityBeforeHooks, processed, "list", context);
387
435
  }
388
- const listOrgId = resolveOrgId(resolvedActor);
389
- let entities = await this.repo.findEntities(listOrgId, { ...(processed.filter?.type ? { type: processed.filter.type } : {}), ...(processed.filter?.status ? { status: processed.filter.status } : {}) }, ctx);
436
+ const listOrgId = resolveOrgIdForCommerce(resolvedActor, this.deps.config);
437
+ const canReadUnpublished = canReadUnpublishedCatalog(resolvedActor);
438
+ let entities = await this.repo.findEntities(listOrgId, {
439
+ ...(processed.filter?.type ? { type: processed.filter.type } : {}),
440
+ ...(canReadUnpublished
441
+ ? (processed.filter?.status ? { status: processed.filter.status } : {})
442
+ : { status: "active", isVisible: true }),
443
+ }, ctx);
444
+ const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
390
445
  if (processed.filter?.category) {
391
446
  const catInput = processed.filter.category;
392
- const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
393
447
  let category = await this.repo.findCategoryBySlug(listOrgId, catInput, ctx);
394
- if (!category && isUUID.test(catInput)) category = await this.repo.findCategoryById(catInput, ctx);
448
+ if (!category && isUUID.test(catInput)) category = await this.repo.findCategoryById(catInput, ctx, listOrgId);
395
449
  if (!category) return Err(new CommerceValidationError(`Category not found: "${catInput}".`));
396
450
  const entityIds = await this.repo.findEntitiesByCategory(category.id, ctx);
397
451
  entities = entities.filter((e) => new Set(entityIds).has(e.id));
398
452
  }
399
453
  if (processed.filter?.brand) {
400
454
  let brand = await this.repo.findBrandBySlug(listOrgId, processed.filter.brand, ctx);
401
- if (!brand) brand = await this.repo.findBrandById(processed.filter.brand, ctx);
455
+ if (!brand && isUUID.test(processed.filter.brand)) {
456
+ brand = await this.repo.findBrandById(processed.filter.brand, ctx, listOrgId);
457
+ }
402
458
  if (brand) {
403
459
  const brandEntityIds: string[] = [];
404
460
  for (const entity of entities) {
@@ -454,19 +510,45 @@ export class EntityService {
454
510
  archive(id: string, actor: Actor | null, ctx?: TxContext): Promise<Result<CatalogEntityHydrated>> { return this.changeStatus(id, "archived", actor, ctx); }
455
511
  discontinue(id: string, actor: Actor | null, ctx?: TxContext): Promise<Result<CatalogEntityHydrated>> { return this.changeStatus(id, "discontinued", actor, ctx); }
456
512
 
457
- async setAttributes(entityId: string, locale: string, attrs: SetAttributesInput, actor: Actor | null, ctx?: TxContext): Promise<Result<void>> {
513
+ async setAttributes(entityId: string, locale: string, attrs: SetAttributesInput, actor: Actor | null, ctx?: CatalogWriteContext): Promise<Result<void>> {
458
514
  try { assertPermission(actor, "catalog:update"); } catch (error) { return Err(toCommerceError(error)); }
459
- const entity = await this.repo.findEntityById(entityId, ctx);
515
+ const txCtx = isWriteContextTransactional(ctx) ? ctx : undefined;
516
+ const entity = await this.repo.findEntityById(entityId, txCtx);
460
517
  if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
461
518
  try { this.assertSameOrg(entity, actor); } catch (error) { return Err(toCommerceError(error)); }
462
- await this.repo.upsertAttribute(entityId, locale, { title: attrs.title, subtitle: attrs.subtitle, description: attrs.description, richDescription: attrs.richDescription, seoTitle: attrs.seoTitle, seoDescription: attrs.seoDescription }, ctx);
519
+ const beforeAttr = await this.repo.findAttributeByLocale(entityId, locale, txCtx);
520
+ const changedFieldPaths: string[] = [];
521
+ for (const field of attributeFields) {
522
+ if (attrs[field] === undefined) continue;
523
+ const before = beforeAttr?.[field] ?? null;
524
+ const after = attrs[field] ?? null;
525
+ if (before !== after) {
526
+ const path = `attributes.${locale}.${field}`;
527
+ if (isValidFieldPath(path)) changedFieldPaths.push(path);
528
+ }
529
+ }
530
+ await this.repo.upsertAttribute(entityId, locale, { title: attrs.title, subtitle: attrs.subtitle, description: attrs.description, richDescription: attrs.richDescription, seoTitle: attrs.seoTitle, seoDescription: attrs.seoDescription }, txCtx);
531
+ if (changedFieldPaths.length > 0) {
532
+ const afterHooks = this.deps.hooks.resolve("catalog.afterUpdate") as CatalogUpdateAfterHook[];
533
+ const context = catalogHookContext(this.deps, actor, ctx, "update");
534
+ context.context.changedFieldPaths = changedFieldPaths;
535
+ await runAfterHooks(afterHooks, entity, entity, "update", context);
536
+ }
463
537
  return Ok(undefined);
464
538
  }
465
539
 
466
- async getAttributes(entityId: string, locale: string, ctx?: TxContext): Promise<Result<SellableAttribute>> {
467
- const attr = await this.repo.findAttributeByLocale(entityId, locale, ctx);
468
- if (!attr) return Err(new CommerceNotFoundError(`Attributes for locale ${locale} not found.`));
469
- return Ok(attr);
540
+ async getAttributes(entityId: string, locale: string, actor: Actor | null, ctx?: TxContext): Promise<Result<SellableAttribute>> {
541
+ try {
542
+ assertPermission(actor, "catalog:read");
543
+ const entity = await this.repo.findEntityById(entityId, ctx, resolveOrgIdForCommerce(actor, this.deps.config));
544
+ if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
545
+ if (!isCatalogEntityVisible(entity, actor)) return Err(new CommerceNotFoundError("Entity not found."));
546
+ const attr = await this.repo.findAttributeByLocale(entity.id, locale, ctx);
547
+ if (!attr) return Err(new CommerceNotFoundError(`Attributes for locale ${locale} not found.`));
548
+ return Ok(attr);
549
+ } catch (error) {
550
+ return Err(toCommerceError(error));
551
+ }
470
552
  }
471
553
 
472
554
  async createOptionType(input: CreateOptionTypeInput, actor: Actor | null, ctx?: TxContext): Promise<Result<OptionType>> {
@@ -0,0 +1,16 @@
1
+ import { hasPermission } from "../../auth/permissions.js";
2
+ import type { Actor } from "../../auth/types.js";
3
+
4
+ type CatalogVisibility = {
5
+ status: string;
6
+ isVisible: boolean;
7
+ };
8
+
9
+ export function canReadUnpublishedCatalog(actor: Actor | null): boolean {
10
+ return hasPermission(actor, "catalog:read:unpublished");
11
+ }
12
+
13
+ export function isCatalogEntityVisible(entity: CatalogVisibility, actor: Actor | null): boolean {
14
+ return canReadUnpublishedCatalog(actor) ||
15
+ (entity.status === "active" && entity.isVisible);
16
+ }
@@ -124,6 +124,7 @@ export class CatalogRepository {
124
124
  filter?: {
125
125
  type?: string;
126
126
  status?: string;
127
+ isVisible?: boolean;
127
128
  ids?: string[];
128
129
  },
129
130
  ctx?: TxContext,
@@ -139,6 +140,9 @@ export class CatalogRepository {
139
140
  eq(sellableEntities.status, filter.status as SellableEntity["status"]),
140
141
  );
141
142
  }
143
+ if (filter?.isVisible !== undefined) {
144
+ conditions.push(eq(sellableEntities.isVisible, filter.isVisible));
145
+ }
142
146
  if (filter?.ids && filter.ids.length > 0) {
143
147
  conditions.push(inArray(sellableEntities.id, filter.ids));
144
148
  }
@@ -811,12 +815,17 @@ export class CatalogRepository {
811
815
  async findCategoryById(
812
816
  id: string,
813
817
  ctx?: TxContext,
818
+ orgId?: string,
814
819
  ): Promise<Category | undefined> {
815
820
  const db = this.getDb(ctx);
816
821
  const rows = await db
817
822
  .select()
818
823
  .from(categories)
819
- .where(eq(categories.id, id));
824
+ .where(
825
+ orgId
826
+ ? and(eq(categories.id, id), eq(categories.organizationId, orgId))
827
+ : eq(categories.id, id),
828
+ );
820
829
  return rows[0];
821
830
  }
822
831
 
@@ -963,9 +972,16 @@ export class CatalogRepository {
963
972
  // Brands
964
973
  // ─────────────────────────────────────────────────────────────────────────────
965
974
 
966
- async findBrandById(id: string, ctx?: TxContext): Promise<Brand | undefined> {
975
+ async findBrandById(id: string, ctx?: TxContext, orgId?: string): Promise<Brand | undefined> {
967
976
  const db = this.getDb(ctx);
968
- const rows = await db.select().from(brands).where(eq(brands.id, id));
977
+ const rows = await db
978
+ .select()
979
+ .from(brands)
980
+ .where(
981
+ orgId
982
+ ? and(eq(brands.id, id), eq(brands.organizationId, orgId))
983
+ : eq(brands.id, id),
984
+ );
969
985
  return rows[0];
970
986
  }
971
987
 
@@ -1,5 +1,5 @@
1
1
  import type { Actor } from "../../auth/types.js";
2
- import { resolveOrgId } from "../../auth/org.js";
2
+ import { resolveOrgIdForCommerce } from "../../auth/org.js";
3
3
  import { assertPermission } from "../../auth/permissions.js";
4
4
  import type { CommerceConfig } from "../../config/types.js";
5
5
  import type { EntityFieldDefinition, FieldType } from "../../config/types.js";
@@ -8,7 +8,7 @@ import { Err, Ok, type Result } from "../../kernel/result.js";
8
8
  import { CommerceConflictError, CommerceNotFoundError, CommerceValidationError, toCommerceError } from "../../kernel/errors.js";
9
9
  import type { Pagination } from "../../utils/pagination.js";
10
10
  import type { DatabaseAdapter } from "../../kernel/database/adapter.js";
11
- import { createTxContext, type TxContext } from "../../kernel/database/tx-context.js";
11
+ import { createTxContext, type CatalogWriteContext, isWriteContextTransactional, resolveWriteContextHookContext, type TxContext } from "../../kernel/database/tx-context.js";
12
12
  import {
13
13
  CatalogRepository,
14
14
  type SellableEntity,
@@ -159,7 +159,7 @@ export interface CatalogService {
159
159
  id: string,
160
160
  input: UpdateEntityInput,
161
161
  actor: Actor | null,
162
- ctx?: TxContext,
162
+ ctx?: CatalogWriteContext,
163
163
  ): Promise<Result<CatalogEntityHydrated>>;
164
164
  delete(
165
165
  id: string,
@@ -168,17 +168,23 @@ export interface CatalogService {
168
168
  ): Promise<Result<void>>;
169
169
  getById(
170
170
  id: string,
171
+ options: GetOptions | undefined,
172
+ actor: Actor | null,
173
+ ctx?: TxContext,
174
+ ): Promise<Result<CatalogEntityHydrated>>;
175
+ getByIdForInternalUse(
176
+ id: string,
177
+ organizationId: string,
171
178
  options?: GetOptions,
172
- actor?: Actor | null,
173
179
  ctx?: TxContext,
174
180
  ): Promise<Result<CatalogEntityHydrated>>;
175
181
  getBySlug(
176
182
  slug: string,
177
- options?: GetOptions,
178
- actor?: Actor | null,
183
+ options: GetOptions | undefined,
184
+ actor: Actor | null,
179
185
  ctx?: TxContext,
180
186
  ): Promise<Result<CatalogEntityHydrated>>;
181
- list(params: ListParams, actor?: Actor | null, ctx?: TxContext): Promise<Result<CatalogListResult>>;
187
+ list(params: ListParams, actor: Actor | null, ctx?: TxContext): Promise<Result<CatalogListResult>>;
182
188
  /** Returns undefined when no concrete ownership row applies to the field. */
183
189
  resolveFieldOwner(
184
190
  entityId: string,
@@ -234,7 +240,7 @@ export interface CatalogService {
234
240
  locale: string,
235
241
  attrs: SetAttributesInput,
236
242
  actor: Actor | null,
237
- ctx?: TxContext,
243
+ ctx?: CatalogWriteContext,
238
244
  ): Promise<Result<void>>;
239
245
  approveCustomField(
240
246
  entityId: string,
@@ -253,6 +259,7 @@ export interface CatalogService {
253
259
  getAttributes(
254
260
  entityId: string,
255
261
  locale: string,
262
+ actor: Actor | null,
256
263
  ctx?: TxContext,
257
264
  ): Promise<Result<SellableAttribute>>;
258
265
  listCategories(
@@ -470,7 +477,7 @@ export class CatalogServiceImpl implements CatalogService {
470
477
  ): Promise<EntityFieldDefinition[]> {
471
478
  const orgId = typeof actorOrOrg === "string"
472
479
  ? actorOrOrg
473
- : resolveOrgId(actorOrOrg ?? ctx?.actor ?? null);
480
+ : resolveOrgIdForCommerce(actorOrOrg ?? ctx?.actor ?? null, this.config);
474
481
  const codeFields = this.config.entities?.[entityType]?.fields ?? [];
475
482
  const merged = new Map(codeFields.map((field) => [field.name, { ...field }]));
476
483
  const runtimeFields = await this.repository.findActiveEntityFieldDefinitions(orgId, entityType, ctx);
@@ -500,7 +507,7 @@ export class CatalogServiceImpl implements CatalogService {
500
507
  entityType?: string,
501
508
  ctx?: TxContext,
502
509
  ): Promise<Result<EntityFieldDefinitionRecord[]>> {
503
- const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
510
+ const orgId = resolveOrgIdForCommerce(actor ?? ctx?.actor ?? null, this.config);
504
511
  return this.withMutationResult(actor ?? ctx?.actor ?? null, ctx, async (txCtx) =>
505
512
  Ok(await this.repository.findEntityFieldDefinitions(orgId, entityType, txCtx)),
506
513
  );
@@ -515,7 +522,7 @@ export class CatalogServiceImpl implements CatalogService {
515
522
  return Promise.resolve(Err(new CommerceValidationError("Entity type and field name are required.")));
516
523
  }
517
524
  assertPermission(actor, "catalog:update");
518
- const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
525
+ const orgId = resolveOrgIdForCommerce(actor ?? ctx?.actor ?? null, this.config);
519
526
  return this.withMutationResult(actor, ctx, async (txCtx) => {
520
527
  const codeField = this.codeFieldDefinition(input.entityType, input.name);
521
528
  if (codeField && input.type !== codeField.type) {
@@ -553,7 +560,7 @@ export class CatalogServiceImpl implements CatalogService {
553
560
  ctx?: TxContext,
554
561
  ): Promise<Result<EntityFieldDefinitionRecord>> {
555
562
  assertPermission(actor, "catalog:update");
556
- const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
563
+ const orgId = resolveOrgIdForCommerce(actor ?? ctx?.actor ?? null, this.config);
557
564
  return this.withMutationResult(actor, ctx, async (txCtx) => {
558
565
  const existing = await this.repository.findEntityFieldDefinitionById(orgId, id, txCtx);
559
566
  if (!existing) return Err(new CommerceNotFoundError("Entity field definition not found."));
@@ -584,7 +591,7 @@ export class CatalogServiceImpl implements CatalogService {
584
591
  ctx?: TxContext,
585
592
  ): Promise<Result<EntityFieldDefinitionRecord>> {
586
593
  assertPermission(actor, "catalog:update");
587
- const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
594
+ const orgId = resolveOrgIdForCommerce(actor ?? ctx?.actor ?? null, this.config);
588
595
  return this.withMutationResult(actor, ctx, async (txCtx) => {
589
596
  const existing = await this.repository.findEntityFieldDefinitionById(orgId, id, txCtx);
590
597
  if (!existing) return Err(new CommerceNotFoundError("Entity field definition not found."));
@@ -600,13 +607,17 @@ export class CatalogServiceImpl implements CatalogService {
600
607
 
601
608
  private async withMutation<T>(
602
609
  actor: Actor | null,
603
- ctx: TxContext | undefined,
610
+ ctx: CatalogWriteContext | undefined,
604
611
  fn: (txCtx: TxContext) => Promise<T>,
605
612
  ): Promise<T> {
606
- if (ctx?.tx != null) return fn(ctx);
613
+ if (isWriteContextTransactional(ctx)) return fn(ctx);
614
+ const hookContext = resolveWriteContextHookContext(ctx);
607
615
  for (let attempt = 0; attempt < 3; attempt += 1) {
608
616
  try {
609
- return await this.database.transaction(async (tx) => fn(createTxContext(tx, { actor })));
617
+ return await this.database.transaction(async (tx) => fn(createTxContext(tx, {
618
+ actor,
619
+ ...(hookContext ? { hookContext } : {}),
620
+ })));
610
621
  } catch (error) {
611
622
  if (!isUniqueViolation(error) || attempt === 2) throw error;
612
623
  }
@@ -616,7 +627,7 @@ export class CatalogServiceImpl implements CatalogService {
616
627
 
617
628
  private async withMutationResult<T>(
618
629
  actor: Actor | null,
619
- ctx: TxContext | undefined,
630
+ ctx: CatalogWriteContext | undefined,
620
631
  fn: (txCtx: TxContext) => Promise<Result<T>>,
621
632
  ): Promise<Result<T>> {
622
633
  try {
@@ -640,7 +651,7 @@ export class CatalogServiceImpl implements CatalogService {
640
651
  const revisionActor = actor ?? ctx.actor;
641
652
  const organizationId = typeof snapshot.entity.organizationId === "string"
642
653
  ? snapshot.entity.organizationId
643
- : resolveOrgId(revisionActor);
654
+ : resolveOrgIdForCommerce(revisionActor, this.config);
644
655
  return this.repository.createRevision({
645
656
  organizationId,
646
657
  entityId,
@@ -664,7 +675,7 @@ export class CatalogServiceImpl implements CatalogService {
664
675
  assertPermission(revisionActor, "catalog:update");
665
676
  const entity = await this.repository.findEntityById(entityId, txCtx);
666
677
  if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
667
- if (entity.organizationId !== resolveOrgId(revisionActor)) {
678
+ if (entity.organizationId !== resolveOrgIdForCommerce(revisionActor, this.config)) {
668
679
  return Err(new CommerceNotFoundError("Entity not found."));
669
680
  }
670
681
  return Ok(await this.captureRevision(entityId, revisionActor, reason, txCtx));
@@ -784,7 +795,7 @@ export class CatalogServiceImpl implements CatalogService {
784
795
  assertPermission(revisionActor, "catalog:update");
785
796
  const entity = await this.repository.findEntityById(entityId, txCtx);
786
797
  if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
787
- if (entity.organizationId !== resolveOrgId(revisionActor)) {
798
+ if (entity.organizationId !== resolveOrgIdForCommerce(revisionActor, this.config)) {
788
799
  return Err(new CommerceNotFoundError("Entity not found."));
789
800
  }
790
801
  if (entity.sourceStoreId != null) assertPermission(revisionActor, "catalog:sync");
@@ -816,7 +827,7 @@ export class CatalogServiceImpl implements CatalogService {
816
827
  const revisionActor = actor ?? txCtx.actor;
817
828
  assertPermission(revisionActor, "catalog:update");
818
829
  const cutoff = new Date(Date.now() - olderThanDays * 24 * 60 * 60 * 1000);
819
- return Ok(await this.repository.deleteRevisionsOlderThan(resolveOrgId(revisionActor), cutoff, txCtx));
830
+ return Ok(await this.repository.deleteRevisionsOlderThan(resolveOrgIdForCommerce(revisionActor, this.config), cutoff, txCtx));
820
831
  });
821
832
  } catch (error) {
822
833
  return Err(toCommerceError(error));
@@ -831,7 +842,7 @@ export class CatalogServiceImpl implements CatalogService {
831
842
  });
832
843
  }
833
844
 
834
- update(id: string, input: UpdateEntityInput, actor: Actor | null, ctx?: TxContext): Promise<Result<CatalogEntityHydrated>> {
845
+ update(id: string, input: UpdateEntityInput, actor: Actor | null, ctx?: CatalogWriteContext): Promise<Result<CatalogEntityHydrated>> {
835
846
  return this.withMutationResult(actor, ctx, async (txCtx) => {
836
847
  const result = await this.entities.update(id, input, actor, txCtx);
837
848
  if (result.ok) await this.captureRevision(id, actor, "update", txCtx);
@@ -843,15 +854,19 @@ export class CatalogServiceImpl implements CatalogService {
843
854
  return this.entities.delete(id, actor, ctx);
844
855
  }
845
856
 
846
- getById(id: string, options?: GetOptions, actor?: Actor | null, ctx?: TxContext): Promise<Result<CatalogEntityHydrated>> {
857
+ getById(id: string, options: GetOptions | undefined, actor: Actor | null, ctx?: TxContext): Promise<Result<CatalogEntityHydrated>> {
847
858
  return this.entities.getById(id, options, actor, ctx);
848
859
  }
849
860
 
850
- getBySlug(slug: string, options?: GetOptions, actor?: Actor | null, ctx?: TxContext): Promise<Result<CatalogEntityHydrated>> {
861
+ getByIdForInternalUse(id: string, organizationId: string, options?: GetOptions, ctx?: TxContext): Promise<Result<CatalogEntityHydrated>> {
862
+ return this.entities.getByIdForInternalUse(id, organizationId, options, ctx);
863
+ }
864
+
865
+ getBySlug(slug: string, options: GetOptions | undefined, actor: Actor | null, ctx?: TxContext): Promise<Result<CatalogEntityHydrated>> {
851
866
  return this.entities.getBySlug(slug, options, actor, ctx);
852
867
  }
853
868
 
854
- list(params: ListParams, actor?: Actor | null, ctx?: TxContext): Promise<Result<CatalogListResult>> {
869
+ list(params: ListParams, actor: Actor | null, ctx?: TxContext): Promise<Result<CatalogListResult>> {
855
870
  return this.entities.list(params, actor, ctx);
856
871
  }
857
872
 
@@ -910,7 +925,7 @@ export class CatalogServiceImpl implements CatalogService {
910
925
  validateFieldPath(fieldPath);
911
926
  const entity = await this.repository.findEntityById(entityId, txCtx);
912
927
  if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
913
- if (entity.organizationId !== resolveOrgId(setter)) {
928
+ if (entity.organizationId !== resolveOrgIdForCommerce(setter, this.config)) {
914
929
  return Err(new CommerceNotFoundError("Entity not found."));
915
930
  }
916
931
  if (entity.sourceStoreId != null) assertPermission(setter, "catalog:sync");
@@ -937,7 +952,7 @@ export class CatalogServiceImpl implements CatalogService {
937
952
  assertPermission(reader, "catalog:read");
938
953
  const entity = await this.repository.findEntityById(entityId, txCtx);
939
954
  if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
940
- if (entity.organizationId !== resolveOrgId(reader)) {
955
+ if (entity.organizationId !== resolveOrgIdForCommerce(reader, this.config)) {
941
956
  return Err(new CommerceNotFoundError("Entity not found."));
942
957
  }
943
958
  return Ok(await this.repository.findFieldOwnership(entityId, txCtx, storeId));
@@ -989,7 +1004,7 @@ export class CatalogServiceImpl implements CatalogService {
989
1004
  });
990
1005
  }
991
1006
 
992
- setAttributes(entityId: string, locale: string, attrs: SetAttributesInput, actor: Actor | null, ctx?: TxContext): Promise<Result<void>> {
1007
+ setAttributes(entityId: string, locale: string, attrs: SetAttributesInput, actor: Actor | null, ctx?: CatalogWriteContext): Promise<Result<void>> {
993
1008
  return this.withMutationResult(actor, ctx, async (txCtx) => {
994
1009
  const result = await this.entities.setAttributes(entityId, locale, attrs, actor, txCtx);
995
1010
  if (result.ok) await this.captureRevision(entityId, actor, "update", txCtx);
@@ -1010,7 +1025,7 @@ export class CatalogServiceImpl implements CatalogService {
1010
1025
  assertPermission(reviewer, "catalog:update");
1011
1026
  const entity = await this.repository.findEntityById(entityId, txCtx);
1012
1027
  if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
1013
- if (entity.organizationId !== resolveOrgId(reviewer)) {
1028
+ if (entity.organizationId !== resolveOrgIdForCommerce(reviewer, this.config)) {
1014
1029
  return Err(new CommerceNotFoundError("Entity not found."));
1015
1030
  }
1016
1031
  if (entity.sourceStoreId != null) assertPermission(reviewer, "catalog:sync");
@@ -1057,8 +1072,8 @@ export class CatalogServiceImpl implements CatalogService {
1057
1072
  return this.reviewCustomField("rejected", entityId, fieldName, locale, actor, ctx);
1058
1073
  }
1059
1074
 
1060
- getAttributes(entityId: string, locale: string, ctx?: TxContext): Promise<Result<SellableAttribute>> {
1061
- return this.entities.getAttributes(entityId, locale, ctx);
1075
+ getAttributes(entityId: string, locale: string, actor: Actor | null, ctx?: TxContext): Promise<Result<SellableAttribute>> {
1076
+ return this.entities.getAttributes(entityId, locale, actor, ctx);
1062
1077
  }
1063
1078
 
1064
1079
  listCategories(ctx?: TxContext, opts?: { includeArchived?: boolean }): Promise<Result<CategorySummary[]>> {
@@ -1,3 +1,4 @@
1
+ import type { CommerceConfig } from "../../config/types.js";
1
2
  import type { DrizzleDatabase } from "../../kernel/database/drizzle-db.js";
2
3
  import { defineModule } from "../../kernel/module/index.js";
3
4
  import { CustomersRepository } from "./repository/index.js";
@@ -21,6 +22,7 @@ export const customersModule = defineModule({
21
22
  new CustomerService({
22
23
  repository: new CustomersRepository(deps.db.db as DrizzleDatabase),
23
24
  hooks: deps.hooks,
25
+ config: deps.config as CommerceConfig,
24
26
  services: deps.services,
25
27
  database: deps.db,
26
28
  }),