@porulle/core 0.6.0 → 0.8.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 (301) hide show
  1. package/dist/auth/setup.d.ts.map +1 -1
  2. package/dist/auth/setup.js +1 -0
  3. package/dist/config/defaults.d.ts.map +1 -1
  4. package/dist/config/defaults.js +5 -0
  5. package/dist/config/types.d.ts +17 -1
  6. package/dist/config/types.d.ts.map +1 -1
  7. package/dist/generated/plugin-manifest.d.ts +3 -0
  8. package/dist/generated/plugin-manifest.d.ts.map +1 -1
  9. package/dist/generated/plugin-manifest.js +1 -0
  10. package/dist/hooks/checkout.d.ts.map +1 -1
  11. package/dist/hooks/checkout.js +16 -1
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +1 -0
  15. package/dist/interfaces/rest/index.d.ts.map +1 -1
  16. package/dist/interfaces/rest/index.js +12 -0
  17. package/dist/interfaces/rest/routes/admin/staff.d.ts +12 -0
  18. package/dist/interfaces/rest/routes/admin/staff.d.ts.map +1 -0
  19. package/dist/interfaces/rest/routes/admin/staff.js +172 -0
  20. package/dist/interfaces/rest/routes/analytics.d.ts +5 -0
  21. package/dist/interfaces/rest/routes/analytics.d.ts.map +1 -0
  22. package/dist/interfaces/rest/routes/analytics.js +24 -0
  23. package/dist/interfaces/rest/routes/carts.d.ts.map +1 -1
  24. package/dist/interfaces/rest/routes/carts.js +39 -2
  25. package/dist/interfaces/rest/routes/catalog.d.ts.map +1 -1
  26. package/dist/interfaces/rest/routes/catalog.js +17 -1
  27. package/dist/interfaces/rest/routes/checkout.d.ts.map +1 -1
  28. package/dist/interfaces/rest/routes/checkout.js +17 -0
  29. package/dist/interfaces/rest/routes/documents.d.ts +10 -0
  30. package/dist/interfaces/rest/routes/documents.d.ts.map +1 -0
  31. package/dist/interfaces/rest/routes/documents.js +40 -0
  32. package/dist/interfaces/rest/routes/orders.d.ts.map +1 -1
  33. package/dist/interfaces/rest/routes/orders.js +122 -1
  34. package/dist/interfaces/rest/routes/pricing.d.ts.map +1 -1
  35. package/dist/interfaces/rest/routes/pricing.js +36 -1
  36. package/dist/interfaces/rest/routes/settings.d.ts +5 -0
  37. package/dist/interfaces/rest/routes/settings.d.ts.map +1 -0
  38. package/dist/interfaces/rest/routes/settings.js +50 -0
  39. package/dist/interfaces/rest/routes/shipping.d.ts +5 -0
  40. package/dist/interfaces/rest/routes/shipping.d.ts.map +1 -0
  41. package/dist/interfaces/rest/routes/shipping.js +68 -0
  42. package/dist/interfaces/rest/routes/tax.d.ts +5 -0
  43. package/dist/interfaces/rest/routes/tax.d.ts.map +1 -0
  44. package/dist/interfaces/rest/routes/tax.js +68 -0
  45. package/dist/interfaces/rest/schemas/admin-staff.d.ts +899 -0
  46. package/dist/interfaces/rest/schemas/admin-staff.d.ts.map +1 -0
  47. package/dist/interfaces/rest/schemas/admin-staff.js +98 -0
  48. package/dist/interfaces/rest/schemas/analytics.d.ts +252 -0
  49. package/dist/interfaces/rest/schemas/analytics.d.ts.map +1 -0
  50. package/dist/interfaces/rest/schemas/analytics.js +33 -0
  51. package/dist/interfaces/rest/schemas/carts.d.ts +656 -0
  52. package/dist/interfaces/rest/schemas/carts.d.ts.map +1 -1
  53. package/dist/interfaces/rest/schemas/carts.js +38 -0
  54. package/dist/interfaces/rest/schemas/catalog.d.ts +1230 -2
  55. package/dist/interfaces/rest/schemas/catalog.d.ts.map +1 -1
  56. package/dist/interfaces/rest/schemas/catalog.js +48 -0
  57. package/dist/interfaces/rest/schemas/checkout.d.ts +2 -0
  58. package/dist/interfaces/rest/schemas/checkout.d.ts.map +1 -1
  59. package/dist/interfaces/rest/schemas/checkout.js +4 -0
  60. package/dist/interfaces/rest/schemas/customer-portal.d.ts +35 -0
  61. package/dist/interfaces/rest/schemas/customer-portal.d.ts.map +1 -1
  62. package/dist/interfaces/rest/schemas/documents.d.ts +509 -0
  63. package/dist/interfaces/rest/schemas/documents.d.ts.map +1 -0
  64. package/dist/interfaces/rest/schemas/documents.js +65 -0
  65. package/dist/interfaces/rest/schemas/orders.d.ts +4467 -7
  66. package/dist/interfaces/rest/schemas/orders.d.ts.map +1 -1
  67. package/dist/interfaces/rest/schemas/orders.js +330 -0
  68. package/dist/interfaces/rest/schemas/pricing.d.ts +405 -0
  69. package/dist/interfaces/rest/schemas/pricing.d.ts.map +1 -1
  70. package/dist/interfaces/rest/schemas/pricing.js +70 -0
  71. package/dist/interfaces/rest/schemas/responses.d.ts +156 -0
  72. package/dist/interfaces/rest/schemas/responses.d.ts.map +1 -1
  73. package/dist/interfaces/rest/schemas/search.d.ts +17 -0
  74. package/dist/interfaces/rest/schemas/search.d.ts.map +1 -1
  75. package/dist/interfaces/rest/schemas/settings.d.ts +397 -0
  76. package/dist/interfaces/rest/schemas/settings.d.ts.map +1 -0
  77. package/dist/interfaces/rest/schemas/settings.js +79 -0
  78. package/dist/interfaces/rest/schemas/shipping.d.ts +1093 -0
  79. package/dist/interfaces/rest/schemas/shipping.d.ts.map +1 -0
  80. package/dist/interfaces/rest/schemas/shipping.js +146 -0
  81. package/dist/interfaces/rest/schemas/tax.d.ts +1074 -0
  82. package/dist/interfaces/rest/schemas/tax.d.ts.map +1 -0
  83. package/dist/interfaces/rest/schemas/tax.js +132 -0
  84. package/dist/kernel/database/schema.d.ts +4 -0
  85. package/dist/kernel/database/schema.d.ts.map +1 -1
  86. package/dist/kernel/database/schema.js +8 -0
  87. package/dist/kernel/error-mapper.d.ts.map +1 -1
  88. package/dist/kernel/error-mapper.js +1 -0
  89. package/dist/kernel/errors.d.ts +5 -0
  90. package/dist/kernel/errors.d.ts.map +1 -1
  91. package/dist/kernel/errors.js +9 -0
  92. package/dist/kernel/hooks/types.d.ts +1 -1
  93. package/dist/kernel/hooks/types.d.ts.map +1 -1
  94. package/dist/kernel/plugin/manifest.d.ts +23 -0
  95. package/dist/kernel/plugin/manifest.d.ts.map +1 -1
  96. package/dist/kernel/plugin/manifest.js +17 -2
  97. package/dist/modules/analytics/module.d.ts.map +1 -1
  98. package/dist/modules/analytics/module.js +6 -0
  99. package/dist/modules/analytics/reports.d.ts +58 -0
  100. package/dist/modules/analytics/reports.d.ts.map +1 -0
  101. package/dist/modules/analytics/reports.js +249 -0
  102. package/dist/modules/analytics/service.d.ts +10 -0
  103. package/dist/modules/analytics/service.d.ts.map +1 -1
  104. package/dist/modules/analytics/service.js +14 -0
  105. package/dist/modules/cart/repository/index.d.ts +7 -0
  106. package/dist/modules/cart/repository/index.d.ts.map +1 -1
  107. package/dist/modules/cart/repository/index.js +25 -1
  108. package/dist/modules/cart/schema.d.ts +17 -0
  109. package/dist/modules/cart/schema.d.ts.map +1 -1
  110. package/dist/modules/cart/schema.js +2 -0
  111. package/dist/modules/cart/schemas.d.ts +1 -0
  112. package/dist/modules/cart/schemas.d.ts.map +1 -1
  113. package/dist/modules/cart/schemas.js +1 -0
  114. package/dist/modules/cart/service.d.ts +32 -0
  115. package/dist/modules/cart/service.d.ts.map +1 -1
  116. package/dist/modules/cart/service.js +58 -0
  117. package/dist/modules/catalog/entity-service.d.ts +21 -0
  118. package/dist/modules/catalog/entity-service.d.ts.map +1 -1
  119. package/dist/modules/catalog/entity-service.js +151 -6
  120. package/dist/modules/catalog/schema.d.ts +34 -0
  121. package/dist/modules/catalog/schema.d.ts.map +1 -1
  122. package/dist/modules/catalog/schema.js +5 -0
  123. package/dist/modules/catalog/schemas.d.ts +15 -1
  124. package/dist/modules/catalog/schemas.d.ts.map +1 -1
  125. package/dist/modules/catalog/schemas.js +17 -1
  126. package/dist/modules/catalog/service.d.ts +43 -1
  127. package/dist/modules/catalog/service.d.ts.map +1 -1
  128. package/dist/modules/catalog/service.js +6 -0
  129. package/dist/modules/documents/module.d.ts +7 -0
  130. package/dist/modules/documents/module.d.ts.map +1 -0
  131. package/dist/modules/documents/module.js +12 -0
  132. package/dist/modules/documents/pdf.d.ts +25 -0
  133. package/dist/modules/documents/pdf.d.ts.map +1 -0
  134. package/dist/modules/documents/pdf.js +80 -0
  135. package/dist/modules/documents/render.d.ts +32 -0
  136. package/dist/modules/documents/render.d.ts.map +1 -0
  137. package/dist/modules/documents/render.js +190 -0
  138. package/dist/modules/documents/repository/index.d.ts +25 -0
  139. package/dist/modules/documents/repository/index.d.ts.map +1 -0
  140. package/dist/modules/documents/repository/index.js +47 -0
  141. package/dist/modules/documents/schema.d.ts +214 -0
  142. package/dist/modules/documents/schema.d.ts.map +1 -0
  143. package/dist/modules/documents/schema.js +42 -0
  144. package/dist/modules/documents/service.d.ts +51 -0
  145. package/dist/modules/documents/service.d.ts.map +1 -0
  146. package/dist/modules/documents/service.js +119 -0
  147. package/dist/modules/fulfillment/service.d.ts +18 -0
  148. package/dist/modules/fulfillment/service.d.ts.map +1 -1
  149. package/dist/modules/fulfillment/service.js +85 -1
  150. package/dist/modules/media/service.d.ts +18 -0
  151. package/dist/modules/media/service.d.ts.map +1 -1
  152. package/dist/modules/media/service.js +26 -0
  153. package/dist/modules/orders/repository/index.d.ts +18 -1
  154. package/dist/modules/orders/repository/index.d.ts.map +1 -1
  155. package/dist/modules/orders/repository/index.js +86 -1
  156. package/dist/modules/orders/schema.d.ts +374 -0
  157. package/dist/modules/orders/schema.d.ts.map +1 -1
  158. package/dist/modules/orders/schema.js +52 -1
  159. package/dist/modules/orders/service.d.ts +102 -2
  160. package/dist/modules/orders/service.d.ts.map +1 -1
  161. package/dist/modules/orders/service.js +494 -23
  162. package/dist/modules/pricing/repository/index.d.ts +5 -0
  163. package/dist/modules/pricing/repository/index.d.ts.map +1 -1
  164. package/dist/modules/pricing/repository/index.js +19 -0
  165. package/dist/modules/pricing/service.d.ts +18 -0
  166. package/dist/modules/pricing/service.d.ts.map +1 -1
  167. package/dist/modules/pricing/service.js +57 -1
  168. package/dist/modules/settings/module.d.ts +6 -0
  169. package/dist/modules/settings/module.d.ts.map +1 -0
  170. package/dist/modules/settings/module.js +11 -0
  171. package/dist/modules/settings/repository/index.d.ts +16 -0
  172. package/dist/modules/settings/repository/index.d.ts.map +1 -0
  173. package/dist/modules/settings/repository/index.js +41 -0
  174. package/dist/modules/settings/schema.d.ts +121 -0
  175. package/dist/modules/settings/schema.d.ts.map +1 -0
  176. package/dist/modules/settings/schema.js +24 -0
  177. package/dist/modules/settings/service.d.ts +26 -0
  178. package/dist/modules/settings/service.d.ts.map +1 -0
  179. package/dist/modules/settings/service.js +51 -0
  180. package/dist/modules/shipping/calculator.d.ts +5 -0
  181. package/dist/modules/shipping/calculator.d.ts.map +1 -1
  182. package/dist/modules/shipping/calculator.js +10 -0
  183. package/dist/modules/shipping/module.d.ts +5 -1
  184. package/dist/modules/shipping/module.d.ts.map +1 -1
  185. package/dist/modules/shipping/module.js +4 -1
  186. package/dist/modules/shipping/repository/index.d.ts +27 -2
  187. package/dist/modules/shipping/repository/index.d.ts.map +1 -1
  188. package/dist/modules/shipping/repository/index.js +105 -1
  189. package/dist/modules/shipping/schema.d.ts +419 -0
  190. package/dist/modules/shipping/schema.d.ts.map +1 -0
  191. package/dist/modules/shipping/schema.js +56 -0
  192. package/dist/modules/shipping/service.d.ts +54 -0
  193. package/dist/modules/shipping/service.d.ts.map +1 -1
  194. package/dist/modules/shipping/service.js +161 -2
  195. package/dist/modules/tax/adapter.d.ts +13 -0
  196. package/dist/modules/tax/adapter.d.ts.map +1 -1
  197. package/dist/modules/tax/module.d.ts +5 -1
  198. package/dist/modules/tax/module.d.ts.map +1 -1
  199. package/dist/modules/tax/module.js +4 -1
  200. package/dist/modules/tax/repository/index.d.ts +38 -2
  201. package/dist/modules/tax/repository/index.d.ts.map +1 -1
  202. package/dist/modules/tax/repository/index.js +138 -1
  203. package/dist/modules/tax/schema.d.ts +355 -0
  204. package/dist/modules/tax/schema.d.ts.map +1 -0
  205. package/dist/modules/tax/schema.js +55 -0
  206. package/dist/modules/tax/service.d.ts +60 -1
  207. package/dist/modules/tax/service.d.ts.map +1 -1
  208. package/dist/modules/tax/service.js +227 -2
  209. package/dist/runtime/kernel-modules.d.ts +15 -2
  210. package/dist/runtime/kernel-modules.d.ts.map +1 -1
  211. package/dist/runtime/kernel-modules.js +4 -0
  212. package/dist/runtime/kernel-types.d.ts +5 -1
  213. package/dist/runtime/kernel-types.d.ts.map +1 -1
  214. package/dist/runtime/kernel-types.js +2 -0
  215. package/dist/runtime/server.d.ts.map +1 -1
  216. package/dist/runtime/server.js +35 -3
  217. package/dist/test-utils/create-plugin-test-app.d.ts +3 -0
  218. package/dist/test-utils/create-plugin-test-app.d.ts.map +1 -1
  219. package/dist/test-utils/create-plugin-test-app.js +15 -3
  220. package/dist/test-utils/rest-api-test-utils.d.ts.map +1 -1
  221. package/dist/test-utils/rest-api-test-utils.js +6 -0
  222. package/package.json +3 -3
  223. package/src/auth/setup.ts +1 -0
  224. package/src/config/defaults.ts +5 -0
  225. package/src/config/types.ts +17 -1
  226. package/src/generated/plugin-capabilities.d.ts +1 -0
  227. package/src/generated/plugin-manifest.ts +1 -0
  228. package/src/generated/plugin-repositories.d.ts +1 -0
  229. package/src/hooks/checkout.ts +25 -3
  230. package/src/index.ts +1 -0
  231. package/src/interfaces/rest/index.ts +12 -0
  232. package/src/interfaces/rest/routes/admin/staff.ts +218 -0
  233. package/src/interfaces/rest/routes/analytics.ts +33 -0
  234. package/src/interfaces/rest/routes/carts.ts +52 -2
  235. package/src/interfaces/rest/routes/catalog.ts +34 -0
  236. package/src/interfaces/rest/routes/checkout.ts +22 -0
  237. package/src/interfaces/rest/routes/documents.ts +61 -0
  238. package/src/interfaces/rest/routes/orders.ts +172 -1
  239. package/src/interfaces/rest/routes/pricing.ts +44 -1
  240. package/src/interfaces/rest/routes/settings.ts +61 -0
  241. package/src/interfaces/rest/routes/shipping.ts +92 -0
  242. package/src/interfaces/rest/routes/tax.ts +90 -0
  243. package/src/interfaces/rest/schemas/admin-staff.ts +110 -0
  244. package/src/interfaces/rest/schemas/analytics.ts +39 -0
  245. package/src/interfaces/rest/schemas/carts.ts +40 -0
  246. package/src/interfaces/rest/schemas/catalog.ts +53 -0
  247. package/src/interfaces/rest/schemas/checkout.ts +4 -0
  248. package/src/interfaces/rest/schemas/documents.ts +72 -0
  249. package/src/interfaces/rest/schemas/orders.ts +361 -0
  250. package/src/interfaces/rest/schemas/pricing.ts +75 -0
  251. package/src/interfaces/rest/schemas/settings.ts +93 -0
  252. package/src/interfaces/rest/schemas/shipping.ts +164 -0
  253. package/src/interfaces/rest/schemas/tax.ts +148 -0
  254. package/src/kernel/database/schema.ts +12 -0
  255. package/src/kernel/error-mapper.ts +1 -0
  256. package/src/kernel/errors.ts +11 -0
  257. package/src/kernel/hooks/types.ts +1 -0
  258. package/src/kernel/plugin/manifest.ts +38 -2
  259. package/src/modules/analytics/module.ts +10 -0
  260. package/src/modules/analytics/reports.ts +311 -0
  261. package/src/modules/analytics/service.ts +23 -0
  262. package/src/modules/cart/repository/index.ts +30 -1
  263. package/src/modules/cart/schema.ts +2 -0
  264. package/src/modules/cart/schemas.ts +1 -0
  265. package/src/modules/cart/service.ts +101 -0
  266. package/src/modules/catalog/entity-service.ts +195 -6
  267. package/src/modules/catalog/schema.ts +5 -0
  268. package/src/modules/catalog/schemas.ts +18 -1
  269. package/src/modules/catalog/service.ts +32 -2
  270. package/src/modules/documents/module.ts +19 -0
  271. package/src/modules/documents/pdf.ts +101 -0
  272. package/src/modules/documents/render.ts +231 -0
  273. package/src/modules/documents/repository/index.ts +68 -0
  274. package/src/modules/documents/schema.ts +44 -0
  275. package/src/modules/documents/service.ts +176 -0
  276. package/src/modules/fulfillment/service.ts +139 -1
  277. package/src/modules/media/service.ts +40 -0
  278. package/src/modules/orders/repository/index.ts +127 -1
  279. package/src/modules/orders/schema.ts +54 -1
  280. package/src/modules/orders/service.ts +739 -24
  281. package/src/modules/pricing/repository/index.ts +34 -0
  282. package/src/modules/pricing/service.ts +100 -1
  283. package/src/modules/settings/module.ts +18 -0
  284. package/src/modules/settings/repository/index.ts +59 -0
  285. package/src/modules/settings/schema.ts +25 -0
  286. package/src/modules/settings/service.ts +82 -0
  287. package/src/modules/shipping/calculator.ts +27 -0
  288. package/src/modules/shipping/module.ts +6 -2
  289. package/src/modules/shipping/repository/index.ts +136 -2
  290. package/src/modules/shipping/schema.ts +58 -0
  291. package/src/modules/shipping/service.ts +260 -1
  292. package/src/modules/tax/adapter.ts +9 -0
  293. package/src/modules/tax/module.ts +6 -2
  294. package/src/modules/tax/repository/index.ts +169 -2
  295. package/src/modules/tax/schema.ts +57 -0
  296. package/src/modules/tax/service.ts +313 -2
  297. package/src/runtime/kernel-modules.ts +4 -0
  298. package/src/runtime/kernel-types.ts +6 -0
  299. package/src/runtime/server.ts +36 -3
  300. package/src/test-utils/create-plugin-test-app.ts +17 -4
  301. package/src/test-utils/rest-api-test-utils.ts +6 -0
@@ -181,13 +181,22 @@ export class EntityService {
181
181
  }
182
182
  if (options?.includeCategories) hydrated.categories = await this.repo.findEntityCategories(entity.id, ctx);
183
183
  if (options?.includeBrands) hydrated.brands = await this.repo.findEntityBrands(entity.id, ctx);
184
- if (options?.includeMedia) hydrated.media = [];
184
+ if (options?.includeMedia) {
185
+ const mediaService = this.deps.services.media as {
186
+ listEntityMedia?: (
187
+ entityId: string,
188
+ opts?: { orgId?: string },
189
+ ) => Promise<{ ok: boolean; value?: CatalogEntityHydrated["media"] }>;
190
+ } | undefined;
191
+ const mediaResult = await mediaService?.listEntityMedia?.(entity.id, { orgId: entity.organizationId });
192
+ hydrated.media = mediaResult?.ok && mediaResult.value ? mediaResult.value : [];
193
+ }
185
194
  if (options?.includePricing) {
186
195
  try {
187
- const pricingService = this.deps.services.pricing as { listPrices: (filter: { entityId: string }) => Promise<{ ok: boolean; value?: { prices: Array<{ currency: string; amount: number; compareAtAmount?: number | null }> } }> };
196
+ const pricingService = this.deps.services.pricing as { listPrices: (filter: { entityId: string }) => Promise<{ ok: boolean; value?: { prices: Array<{ id: string; currency: string; amount: number; compareAtAmount?: number | null; createdAt: Date }> } }> };
188
197
  const priceResult = await pricingService.listPrices({ entityId: entity.id });
189
198
  if (priceResult.ok && priceResult.value) {
190
- hydrated.pricing = priceResult.value.prices.map((p) => ({ currency: p.currency, amount: p.amount, compareAtAmount: p.compareAtAmount ?? null }));
199
+ hydrated.pricing = priceResult.value.prices.map((p) => ({ id: p.id, currency: p.currency, amount: p.amount, compareAtAmount: p.compareAtAmount ?? null, createdAt: p.createdAt }));
191
200
  }
192
201
  } catch {}
193
202
  }
@@ -203,7 +212,7 @@ export class EntityService {
203
212
  const afterHooks = this.deps.hooks.resolve("catalog.afterCreate") as CatalogCreateAfterHook[];
204
213
  const context: HookContext = createHookContext({ actor, tx: ctx?.tx ?? null, logger: createLogger("catalog.create"), services: this.deps.services, context: { moduleName: "catalog" }, ...hookDatabaseArg(this.deps.database) });
205
214
  const processedInput = await runBeforeHooks(beforeHooks, input, "create", context);
206
- const entity = await this.repo.createEntity({ organizationId: orgId, type: processedInput.type, slug: processedInput.slug, status: "draft", isVisible: false, metadata: processedInput.metadata ?? {} }, ctx);
215
+ const entity = await this.repo.createEntity({ organizationId: orgId, type: processedInput.type, slug: processedInput.slug, status: "draft", isVisible: false, ...(processedInput.taxClass !== undefined ? { taxClass: processedInput.taxClass } : {}), metadata: processedInput.metadata ?? {} }, ctx);
207
216
  if (processedInput.attributes) {
208
217
  await this.repo.createAttribute({ entityId: entity.id, locale: processedInput.attributes.locale ?? "en", title: processedInput.attributes.title, subtitle: processedInput.attributes.subtitle, description: processedInput.attributes.description, richDescription: processedInput.attributes.richDescription, seoTitle: processedInput.attributes.seoTitle, seoDescription: processedInput.attributes.seoDescription }, ctx);
209
218
  }
@@ -223,7 +232,7 @@ export class EntityService {
223
232
  const afterHooks = this.deps.hooks.resolve("catalog.afterUpdate") as CatalogUpdateAfterHook[];
224
233
  const context: HookContext = createHookContext({ actor, tx: ctx?.tx ?? null, logger: createLogger("catalog.update"), services: this.deps.services, context: { moduleName: "catalog" }, ...hookDatabaseArg(this.deps.database) });
225
234
  const processed = await runBeforeHooks(beforeHooks, input, "update", context);
226
- const updated = await this.repo.updateEntity(id, { ...(processed.slug !== undefined ? { slug: processed.slug } : {}), ...(processed.status !== undefined ? { status: processed.status as SellableEntity["status"] } : {}), ...(processed.metadata !== undefined ? { metadata: processed.metadata } : {}), ...(processed.isVisible !== undefined ? { isVisible: processed.isVisible } : {}) }, ctx);
235
+ 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);
227
236
  if (!updated) return Err(new CommerceNotFoundError("Entity not found."));
228
237
  const hookReport = await runAfterHooks(afterHooks, existing, updated, "update", context);
229
238
  const hydrated = await this.hydrateEntity(updated, undefined, ctx);
@@ -428,13 +437,28 @@ export class EntityService {
428
437
  if (!ov) return Err(new CommerceValidationError(`Option value "${optVal}" does not exist for option type "${optName}".`));
429
438
  optionValueIds.push(ov.id);
430
439
  }
431
- const variant = await this.repo.createVariant({ entityId: input.entityId, status: "active", sortOrder: 0, metadata: {}, ...(input.sku !== undefined ? { sku: input.sku } : {}) }, ctx);
440
+ const variant = await this.repo.createVariant({ entityId: input.entityId, status: "active", sortOrder: 0, metadata: {}, ...(input.sku !== undefined ? { sku: input.sku } : {}), ...(input.taxClass !== undefined ? { taxClass: input.taxClass } : {}) }, ctx);
432
441
  await this.repo.createVariantOptionValues(optionValueIds.map((optionValueId) => ({ variantId: variant.id, optionValueId })), ctx);
433
442
  return Ok(variant);
434
443
  }
435
444
 
436
445
  async generateVariants(entityId: string, strategy: VariantGenerationStrategy, actor: Actor | null, ctx?: TxContext): Promise<Result<Variant[]>> {
437
446
  assertPermission(actor, "catalog:update");
447
+ // Guard the strategy: a missing/malformed body (e.g. a bodyless SDK call that
448
+ // skips JSON validation) must be a 422, not a 500 from dereferencing an
449
+ // undefined strategy.
450
+ const mode = (strategy as { mode?: unknown } | null | undefined)?.mode;
451
+ if (mode !== "all" && mode !== "manual" && mode !== "matrix") {
452
+ return Err(new CommerceValidationError(
453
+ 'A variant generation strategy is required: { "mode": "all" } | { "mode": "manual", "combinations": [...] } | { "mode": "matrix", "matrix": { "include"?, "exclude"? } }.',
454
+ ));
455
+ }
456
+ if (strategy.mode === "manual" && !Array.isArray(strategy.combinations)) {
457
+ return Err(new CommerceValidationError('strategy.combinations (string[][]) is required for mode "manual".'));
458
+ }
459
+ if (strategy.mode === "matrix" && (!strategy.matrix || typeof strategy.matrix !== "object")) {
460
+ return Err(new CommerceValidationError('strategy.matrix is required for mode "matrix".'));
461
+ }
438
462
  const entity = await this.repo.findEntityById(entityId, ctx);
439
463
  if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
440
464
  const entityOptionTypes = await this.repo.findOptionTypesByEntityId(entityId, ctx);
@@ -466,4 +490,169 @@ export class EntityService {
466
490
  }
467
491
  return Ok(created);
468
492
  }
493
+
494
+ // ── One-call variant creation (issue #50) ────────────────────────────────
495
+ // Upserts option axes inline, creates variants, and seeds a zero-stock
496
+ // inventory level per variant so it is sellable immediately. Retry-safe:
497
+ // axes upsert by name/value and existing combinations are skipped.
498
+
499
+ private async upsertAxes(
500
+ entityId: string,
501
+ axes: Array<{ name: string; values: string[] }>,
502
+ ctx?: TxContext,
503
+ ): Promise<Map<string, Map<string, string>>> {
504
+ const existingTypes = await this.repo.findOptionTypesByEntityId(entityId, ctx);
505
+ const result = new Map<string, Map<string, string>>();
506
+ for (const axis of axes) {
507
+ let optionType = existingTypes.find((t) => t.name === axis.name);
508
+ if (!optionType) {
509
+ optionType = await this.repo.createOptionType(
510
+ { entityId, name: axis.name, displayName: axis.name, sortOrder: existingTypes.length },
511
+ ctx,
512
+ );
513
+ existingTypes.push(optionType);
514
+ }
515
+ const existingValues = await this.repo.findOptionValuesByTypeId(optionType.id, ctx);
516
+ const valueIds = new Map<string, string>();
517
+ for (const value of axis.values) {
518
+ let optionValue = existingValues.find((v) => v.value === value);
519
+ if (!optionValue) {
520
+ optionValue = await this.repo.createOptionValue(
521
+ { optionTypeId: optionType.id, value, displayValue: value, sortOrder: existingValues.length, metadata: {} },
522
+ ctx,
523
+ );
524
+ existingValues.push(optionValue);
525
+ }
526
+ valueIds.set(value, optionValue.id);
527
+ }
528
+ result.set(axis.name, valueIds);
529
+ }
530
+ return result;
531
+ }
532
+
533
+ private async existingComboKeys(entityId: string, ctx?: TxContext): Promise<Map<string, string>> {
534
+ const existing = new Map<string, string>();
535
+ for (const variant of await this.repo.findVariantsByEntityId(entityId, ctx)) {
536
+ const rows = await this.repo.findVariantOptionValues(variant.id, ctx);
537
+ const key = rows.map((r) => r.optionValueId).sort().join("|");
538
+ existing.set(key, variant.id);
539
+ }
540
+ return existing;
541
+ }
542
+
543
+ private async seedInventoryLevel(
544
+ entityId: string,
545
+ variantId: string,
546
+ actor: Actor | null,
547
+ ctx?: TxContext,
548
+ ): Promise<Result<unknown>> {
549
+ const inventory = this.deps.services.inventory as {
550
+ adjustDetailed: (
551
+ input: { entityId: string; variantId?: string; adjustment: number; reason: string },
552
+ actor?: Actor | null,
553
+ ctx?: TxContext,
554
+ ) => Promise<Result<unknown>>;
555
+ };
556
+ return inventory.adjustDetailed(
557
+ { entityId, variantId, adjustment: 0, reason: "Variant created — seed zero-stock level" },
558
+ actor,
559
+ ctx,
560
+ );
561
+ }
562
+
563
+ async quickCreateVariant(
564
+ entityId: string,
565
+ input: { options: Record<string, string>; sku?: string | undefined; barcode?: string | undefined },
566
+ actor: Actor | null,
567
+ ctx?: TxContext,
568
+ ): Promise<Result<{ variant: Variant; created: boolean }>> {
569
+ try { assertPermission(actor, "catalog:update"); } catch (error) { return Err(toCommerceError(error)); }
570
+ const entity = await this.repo.findEntityById(entityId, ctx);
571
+ if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
572
+ try { this.assertSameOrg(entity, actor); } catch (error) { return Err(toCommerceError(error)); }
573
+
574
+ const axes = Object.entries(input.options).map(([name, value]) => ({ name, values: [value] }));
575
+ const axisValueIds = await this.upsertAxes(entityId, axes, ctx);
576
+ const optionValueIds = Object.entries(input.options)
577
+ .map(([name, value]) => axisValueIds.get(name)!.get(value)!);
578
+
579
+ const key = [...optionValueIds].sort().join("|");
580
+ const existing = await this.existingComboKeys(entityId, ctx);
581
+ const existingVariantId = existing.get(key);
582
+ if (existingVariantId !== undefined) {
583
+ const variant = await this.repo.findVariantById(existingVariantId, ctx);
584
+ return Ok({ variant: variant!, created: false });
585
+ }
586
+
587
+ const variant = await this.repo.createVariant(
588
+ {
589
+ entityId,
590
+ status: "active",
591
+ sortOrder: 0,
592
+ metadata: { generatedBy: "quick" },
593
+ ...(input.sku !== undefined ? { sku: input.sku } : {}),
594
+ ...(input.barcode !== undefined ? { barcode: input.barcode } : {}),
595
+ },
596
+ ctx,
597
+ );
598
+ await this.repo.createVariantOptionValues(
599
+ optionValueIds.map((optionValueId) => ({ variantId: variant.id, optionValueId })),
600
+ ctx,
601
+ );
602
+ const seeded = await this.seedInventoryLevel(entityId, variant.id, actor, ctx);
603
+ if (!seeded.ok) return seeded;
604
+ return Ok({ variant, created: true });
605
+ }
606
+
607
+ async bulkCreateVariants(
608
+ entityId: string,
609
+ input: { axes: Array<{ name: string; values: string[] }>; skuPrefix?: string | undefined },
610
+ actor: Actor | null,
611
+ ctx?: TxContext,
612
+ ): Promise<Result<{ created: Variant[]; skipped: number }>> {
613
+ try { assertPermission(actor, "catalog:update"); } catch (error) { return Err(toCommerceError(error)); }
614
+ const entity = await this.repo.findEntityById(entityId, ctx);
615
+ if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
616
+ try { this.assertSameOrg(entity, actor); } catch (error) { return Err(toCommerceError(error)); }
617
+
618
+ const axisValueIds = await this.upsertAxes(entityId, input.axes, ctx);
619
+ // Empty axis list → one option-less variant.
620
+ const groups = input.axes.map((axis) =>
621
+ axis.values.map((value) => ({ value, id: axisValueIds.get(axis.name)!.get(value)! })),
622
+ );
623
+ const combinations = cartesian(groups);
624
+
625
+ const existing = await this.existingComboKeys(entityId, ctx);
626
+ const created: Variant[] = [];
627
+ let skipped = 0;
628
+ for (const combo of combinations) {
629
+ const key = combo.map((c) => c.id).sort().join("|");
630
+ if (existing.has(key)) {
631
+ skipped += 1;
632
+ continue;
633
+ }
634
+ const sku = input.skuPrefix !== undefined
635
+ ? [input.skuPrefix, ...combo.map((c) => c.value)].join("-").toUpperCase().replace(/\s+/g, "-")
636
+ : undefined;
637
+ const variant = await this.repo.createVariant(
638
+ {
639
+ entityId,
640
+ status: "active",
641
+ sortOrder: 0,
642
+ metadata: { generatedBy: "bulk" },
643
+ ...(sku !== undefined ? { sku } : {}),
644
+ },
645
+ ctx,
646
+ );
647
+ await this.repo.createVariantOptionValues(
648
+ combo.map((c) => ({ variantId: variant.id, optionValueId: c.id })),
649
+ ctx,
650
+ );
651
+ const seeded = await this.seedInventoryLevel(entityId, variant.id, actor, ctx);
652
+ if (!seeded.ok) return seeded;
653
+ existing.set(key, variant.id);
654
+ created.push(variant);
655
+ }
656
+ return Ok({ created, skipped });
657
+ }
469
658
  }
@@ -25,6 +25,9 @@ export const sellableEntities = pgTable(
25
25
  .notNull()
26
26
  .default("draft"),
27
27
  isVisible: boolean("is_visible").notNull().default(false),
28
+ // Tax class name (issue #57) — maps to tax_classes.name at checkout;
29
+ // a variant-level taxClass overrides this. Null = the org's default class.
30
+ taxClass: text("tax_class"),
28
31
  metadata: jsonb("metadata").$type<Record<string, unknown>>().default({}),
29
32
  createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
30
33
  updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
@@ -183,6 +186,8 @@ export const variants = pgTable("variants", {
183
186
  .notNull(),
184
187
  sku: text("sku").unique(),
185
188
  barcode: text("barcode"),
189
+ // Overrides the entity's taxClass when set (issue #57).
190
+ taxClass: text("tax_class"),
186
191
  status: text("status", { enum: ["active", "discontinued"] })
187
192
  .notNull()
188
193
  .default("active"),
@@ -6,6 +6,7 @@ export const CreateEntityBodySchema = z.object({
6
6
  type: z.string().openapi({ example: "physicalGood" }),
7
7
  slug: z.string().openapi({ example: "my-product" }),
8
8
  status: z.string().optional().openapi({ example: "draft" }),
9
+ taxClass: z.string().optional().openapi({ example: "standard", description: "Product tax class name (issue #57)." }),
9
10
  basePrice: z.number().optional().openapi({ example: 29.99 }),
10
11
  currency: z.string().optional().openapi({ example: "USD" }),
11
12
  metadata: z.record(z.string(), z.unknown()).optional().openapi({ example: { title: "My Product" } }),
@@ -24,6 +25,7 @@ export const CreateEntityBodySchema = z.object({
24
25
  export const UpdateEntityBodySchema = z.object({
25
26
  slug: z.string().optional(),
26
27
  status: z.string().optional(),
28
+ taxClass: z.string().nullable().optional(),
27
29
  metadata: z.record(z.string(), z.unknown()).optional(),
28
30
  isVisible: z.boolean().optional(),
29
31
  }).openapi("UpdateEntityBody");
@@ -69,9 +71,24 @@ export const CreateVariantBodySchema = z.object({
69
71
  sku: z.string().optional().openapi({ example: "SKU-001" }),
70
72
  options: z.record(z.string(), z.string()).openapi({ example: { Color: "Red" } }),
71
73
  price: z.number().optional().openapi({ example: 34.99 }),
74
+ taxClass: z.string().optional().openapi({ example: "zero", description: "Overrides the entity's tax class (issue #57)." }),
72
75
  }).openapi("CreateVariantBody");
73
76
 
74
- export const GenerateVariantsBodySchema = z.object({}).passthrough().openapi("GenerateVariantsBody");
77
+ const VariantMatrixRuleSchema = z.object({
78
+ include: z.array(z.array(z.string())).optional().openapi({ example: [["red", "small"]] }),
79
+ exclude: z.array(z.array(z.string())).optional().openapi({ example: [["red", "large"]] }),
80
+ });
81
+
82
+ export const GenerateVariantsBodySchema = z
83
+ .discriminatedUnion("mode", [
84
+ z.object({ mode: z.literal("all") }),
85
+ z.object({
86
+ mode: z.literal("manual"),
87
+ combinations: z.array(z.array(z.string())).openapi({ example: [["red", "small"]] }),
88
+ }),
89
+ z.object({ mode: z.literal("matrix"), matrix: VariantMatrixRuleSchema }),
90
+ ])
91
+ .openapi("GenerateVariantsBody");
75
92
 
76
93
  // ─── Derived Input Types ─────────────────────────────────────────────────────
77
94
 
@@ -105,8 +105,8 @@ export interface CatalogEntityHydrated extends SellableEntity {
105
105
  optionTypes?: Array<OptionType & { values: OptionValue[] }>;
106
106
  categories?: EntityCategory[];
107
107
  brands?: EntityBrand[];
108
- media?: Array<{ mediaAssetId: string; role: string; variantId?: string }>;
109
- pricing?: Array<{ currency: string; amount: number; compareAtAmount?: number | null }>;
108
+ media?: Array<{ mediaAssetId: string; role: string; sortOrder: number; variantId: string | null; url: string; alt: string | null; contentType: string }>;
109
+ pricing?: Array<{ id: string; currency: string; amount: number; compareAtAmount?: number | null; createdAt: Date }>;
110
110
  }
111
111
 
112
112
  export interface CatalogService {
@@ -259,6 +259,18 @@ export interface CatalogService {
259
259
  actor: Actor | null,
260
260
  ctx?: TxContext,
261
261
  ): Promise<Result<Variant[]>>;
262
+ quickCreateVariant(
263
+ entityId: string,
264
+ input: { options: Record<string, string>; sku?: string | undefined; barcode?: string | undefined },
265
+ actor: Actor | null,
266
+ ctx?: TxContext,
267
+ ): Promise<Result<{ variant: Variant; created: boolean }>>;
268
+ bulkCreateVariants(
269
+ entityId: string,
270
+ input: { axes: Array<{ name: string; values: string[] }>; skuPrefix?: string | undefined },
271
+ actor: Actor | null,
272
+ ctx?: TxContext,
273
+ ): Promise<Result<{ created: Variant[]; skipped: number }>>;
262
274
  }
263
275
 
264
276
  export interface CatalogServiceDeps {
@@ -411,4 +423,22 @@ export class CatalogServiceImpl implements CatalogService {
411
423
  generateVariants(entityId: string, strategy: VariantGenerationStrategy, actor: Actor | null, ctx?: TxContext): Promise<Result<Variant[]>> {
412
424
  return this.entities.generateVariants(entityId, strategy, actor, ctx);
413
425
  }
426
+
427
+ quickCreateVariant(
428
+ entityId: string,
429
+ input: { options: Record<string, string>; sku?: string | undefined; barcode?: string | undefined },
430
+ actor: Actor | null,
431
+ ctx?: TxContext,
432
+ ): Promise<Result<{ variant: Variant; created: boolean }>> {
433
+ return this.entities.quickCreateVariant(entityId, input, actor, ctx);
434
+ }
435
+
436
+ bulkCreateVariants(
437
+ entityId: string,
438
+ input: { axes: Array<{ name: string; values: string[] }>; skuPrefix?: string | undefined },
439
+ actor: Actor | null,
440
+ ctx?: TxContext,
441
+ ): Promise<Result<{ created: Variant[]; skipped: number }>> {
442
+ return this.entities.bulkCreateVariants(entityId, input, actor, ctx);
443
+ }
414
444
  }
@@ -0,0 +1,19 @@
1
+ import type { DrizzleDatabase } from "../../kernel/database/drizzle-db.js";
2
+ import { defineModule } from "../../kernel/module/index.js";
3
+ import { DocumentsRepository } from "./repository/index.js";
4
+ import { invoiceSequences, orderDocuments } from "./schema.js";
5
+ import { DocumentsService } from "./service.js";
6
+
7
+ export const documentsModule = defineModule<
8
+ { invoiceSequences: typeof invoiceSequences; orderDocuments: typeof orderDocuments },
9
+ DocumentsService,
10
+ Record<string, never>
11
+ >({
12
+ id: "documents",
13
+ schema: () => ({ invoiceSequences, orderDocuments }),
14
+ service: (deps) =>
15
+ new DocumentsService({
16
+ repository: new DocumentsRepository(deps.db.db as DrizzleDatabase),
17
+ services: deps.services as Record<string, unknown>,
18
+ }),
19
+ });
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Minimal dependency-free PDF writer (issue #47).
3
+ *
4
+ * Emits PDF 1.4 with Helvetica / Helvetica-Bold (built-in base-14 fonts, no
5
+ * embedding) and uncompressed content streams. Pure string/Uint8Array work —
6
+ * no Node-only APIs, so it runs on Cloudflare Workers and other serverless
7
+ * runtimes. Enough for text documents like invoices; not a general renderer.
8
+ */
9
+
10
+ export interface PdfTextOp {
11
+ x: number;
12
+ y: number;
13
+ size: number;
14
+ bold?: boolean | undefined;
15
+ text: string;
16
+ }
17
+
18
+ /** A4 in PDF points. */
19
+ export const A4 = { width: 595.28, height: 841.89 } as const;
20
+
21
+ // Escape to a PDF literal string: backslash, parens, and non-ASCII bytes as
22
+ // octal escapes (Latin-1 / WinAnsi range; anything beyond becomes "?").
23
+ function escapePdfText(text: string): string {
24
+ let out = "";
25
+ for (const ch of text) {
26
+ const code = ch.codePointAt(0)!;
27
+ if (ch === "\\" || ch === "(" || ch === ")") out += `\\${ch}`;
28
+ else if (code >= 32 && code <= 126) out += ch;
29
+ else if (code > 126 && code <= 255) out += `\\${code.toString(8).padStart(3, "0")}`;
30
+ else out += "?";
31
+ }
32
+ return out;
33
+ }
34
+
35
+ function contentStream(ops: PdfTextOp[]): string {
36
+ const parts: string[] = [];
37
+ for (const op of ops) {
38
+ const font = op.bold ? "/F2" : "/F1";
39
+ parts.push(
40
+ `BT ${font} ${op.size} Tf ${op.x.toFixed(2)} ${op.y.toFixed(2)} Td (${escapePdfText(op.text)}) Tj ET`,
41
+ );
42
+ }
43
+ return parts.join("\n");
44
+ }
45
+
46
+ /**
47
+ * Builds a complete PDF document; one inner array of text ops per page.
48
+ */
49
+ export function buildPdf(pages: PdfTextOp[][]): Uint8Array {
50
+ // Object numbering: 1 Catalog, 2 Pages, 3 F1, 4 F2, then per page: Page, Contents.
51
+ const objects: string[] = [];
52
+ const pageObjectNumbers: number[] = [];
53
+ const firstPageObj = 5;
54
+
55
+ for (let i = 0; i < pages.length; i++) {
56
+ pageObjectNumbers.push(firstPageObj + i * 2);
57
+ }
58
+
59
+ objects.push(`1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n`);
60
+ objects.push(
61
+ `2 0 obj\n<< /Type /Pages /Kids [${pageObjectNumbers.map((n) => `${n} 0 R`).join(" ")}] /Count ${pages.length} >>\nendobj\n`,
62
+ );
63
+ objects.push(`3 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>\nendobj\n`);
64
+ objects.push(`4 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>\nendobj\n`);
65
+
66
+ for (let i = 0; i < pages.length; i++) {
67
+ const pageNum = pageObjectNumbers[i]!;
68
+ const contentNum = pageNum + 1;
69
+ const stream = contentStream(pages[i]!);
70
+ objects.push(
71
+ `${pageNum} 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ${A4.width} ${A4.height}] ` +
72
+ `/Resources << /Font << /F1 3 0 R /F2 4 0 R >> >> /Contents ${contentNum} 0 R >>\nendobj\n`,
73
+ );
74
+ objects.push(
75
+ `${contentNum} 0 obj\n<< /Length ${stream.length} >>\nstream\n${stream}\nendstream\nendobj\n`,
76
+ );
77
+ }
78
+
79
+ const header = "%PDF-1.4\n";
80
+ let body = header;
81
+ const offsets: number[] = [];
82
+ for (const obj of objects) {
83
+ offsets.push(body.length);
84
+ body += obj;
85
+ }
86
+
87
+ const xrefOffset = body.length;
88
+ const count = objects.length + 1;
89
+ let xref = `xref\n0 ${count}\n0000000000 65535 f \n`;
90
+ for (const offset of offsets) {
91
+ xref += `${offset.toString().padStart(10, "0")} 00000 n \n`;
92
+ }
93
+ const trailer = `trailer\n<< /Size ${count} /Root 1 0 R >>\nstartxref\n${xrefOffset}\n%%EOF\n`;
94
+
95
+ const full = body + xref + trailer;
96
+ // All bytes are ASCII/Latin-1 by construction (escapePdfText), so a
97
+ // byte-per-char encoding is exact.
98
+ const bytes = new Uint8Array(full.length);
99
+ for (let i = 0; i < full.length; i++) bytes[i] = full.charCodeAt(i) & 0xff;
100
+ return bytes;
101
+ }