@porulle/core 0.7.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 (289) 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 +98 -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 +1218 -1
  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 +3110 -43
  66. package/dist/interfaces/rest/schemas/orders.d.ts.map +1 -1
  67. package/dist/interfaces/rest/schemas/orders.js +233 -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/hooks/types.d.ts +1 -1
  88. package/dist/kernel/hooks/types.d.ts.map +1 -1
  89. package/dist/kernel/plugin/manifest.d.ts +23 -0
  90. package/dist/kernel/plugin/manifest.d.ts.map +1 -1
  91. package/dist/kernel/plugin/manifest.js +17 -2
  92. package/dist/modules/analytics/module.d.ts.map +1 -1
  93. package/dist/modules/analytics/module.js +6 -0
  94. package/dist/modules/analytics/reports.d.ts +58 -0
  95. package/dist/modules/analytics/reports.d.ts.map +1 -0
  96. package/dist/modules/analytics/reports.js +249 -0
  97. package/dist/modules/analytics/service.d.ts +10 -0
  98. package/dist/modules/analytics/service.d.ts.map +1 -1
  99. package/dist/modules/analytics/service.js +14 -0
  100. package/dist/modules/cart/repository/index.d.ts +7 -0
  101. package/dist/modules/cart/repository/index.d.ts.map +1 -1
  102. package/dist/modules/cart/repository/index.js +25 -1
  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 +2 -0
  106. package/dist/modules/cart/schemas.d.ts +1 -0
  107. package/dist/modules/cart/schemas.d.ts.map +1 -1
  108. package/dist/modules/cart/schemas.js +1 -0
  109. package/dist/modules/cart/service.d.ts +32 -0
  110. package/dist/modules/cart/service.d.ts.map +1 -1
  111. package/dist/modules/cart/service.js +58 -0
  112. package/dist/modules/catalog/entity-service.d.ts +21 -0
  113. package/dist/modules/catalog/entity-service.d.ts.map +1 -1
  114. package/dist/modules/catalog/entity-service.js +132 -3
  115. package/dist/modules/catalog/schema.d.ts +34 -0
  116. package/dist/modules/catalog/schema.d.ts.map +1 -1
  117. package/dist/modules/catalog/schema.js +5 -0
  118. package/dist/modules/catalog/schemas.d.ts +3 -0
  119. package/dist/modules/catalog/schemas.d.ts.map +1 -1
  120. package/dist/modules/catalog/schemas.js +3 -0
  121. package/dist/modules/catalog/service.d.ts +36 -0
  122. package/dist/modules/catalog/service.d.ts.map +1 -1
  123. package/dist/modules/catalog/service.js +6 -0
  124. package/dist/modules/documents/module.d.ts +7 -0
  125. package/dist/modules/documents/module.d.ts.map +1 -0
  126. package/dist/modules/documents/module.js +12 -0
  127. package/dist/modules/documents/pdf.d.ts +25 -0
  128. package/dist/modules/documents/pdf.d.ts.map +1 -0
  129. package/dist/modules/documents/pdf.js +80 -0
  130. package/dist/modules/documents/render.d.ts +32 -0
  131. package/dist/modules/documents/render.d.ts.map +1 -0
  132. package/dist/modules/documents/render.js +190 -0
  133. package/dist/modules/documents/repository/index.d.ts +25 -0
  134. package/dist/modules/documents/repository/index.d.ts.map +1 -0
  135. package/dist/modules/documents/repository/index.js +47 -0
  136. package/dist/modules/documents/schema.d.ts +214 -0
  137. package/dist/modules/documents/schema.d.ts.map +1 -0
  138. package/dist/modules/documents/schema.js +42 -0
  139. package/dist/modules/documents/service.d.ts +51 -0
  140. package/dist/modules/documents/service.d.ts.map +1 -0
  141. package/dist/modules/documents/service.js +119 -0
  142. package/dist/modules/fulfillment/service.d.ts +18 -0
  143. package/dist/modules/fulfillment/service.d.ts.map +1 -1
  144. package/dist/modules/fulfillment/service.js +85 -1
  145. package/dist/modules/orders/repository/index.d.ts +18 -1
  146. package/dist/modules/orders/repository/index.d.ts.map +1 -1
  147. package/dist/modules/orders/repository/index.js +86 -1
  148. package/dist/modules/orders/schema.d.ts +374 -0
  149. package/dist/modules/orders/schema.d.ts.map +1 -1
  150. package/dist/modules/orders/schema.js +52 -1
  151. package/dist/modules/orders/service.d.ts +86 -1
  152. package/dist/modules/orders/service.d.ts.map +1 -1
  153. package/dist/modules/orders/service.js +447 -20
  154. package/dist/modules/pricing/repository/index.d.ts +5 -0
  155. package/dist/modules/pricing/repository/index.d.ts.map +1 -1
  156. package/dist/modules/pricing/repository/index.js +19 -0
  157. package/dist/modules/pricing/service.d.ts +18 -0
  158. package/dist/modules/pricing/service.d.ts.map +1 -1
  159. package/dist/modules/pricing/service.js +38 -0
  160. package/dist/modules/settings/module.d.ts +6 -0
  161. package/dist/modules/settings/module.d.ts.map +1 -0
  162. package/dist/modules/settings/module.js +11 -0
  163. package/dist/modules/settings/repository/index.d.ts +16 -0
  164. package/dist/modules/settings/repository/index.d.ts.map +1 -0
  165. package/dist/modules/settings/repository/index.js +41 -0
  166. package/dist/modules/settings/schema.d.ts +121 -0
  167. package/dist/modules/settings/schema.d.ts.map +1 -0
  168. package/dist/modules/settings/schema.js +24 -0
  169. package/dist/modules/settings/service.d.ts +26 -0
  170. package/dist/modules/settings/service.d.ts.map +1 -0
  171. package/dist/modules/settings/service.js +51 -0
  172. package/dist/modules/shipping/calculator.d.ts +5 -0
  173. package/dist/modules/shipping/calculator.d.ts.map +1 -1
  174. package/dist/modules/shipping/calculator.js +10 -0
  175. package/dist/modules/shipping/module.d.ts +5 -1
  176. package/dist/modules/shipping/module.d.ts.map +1 -1
  177. package/dist/modules/shipping/module.js +4 -1
  178. package/dist/modules/shipping/repository/index.d.ts +27 -2
  179. package/dist/modules/shipping/repository/index.d.ts.map +1 -1
  180. package/dist/modules/shipping/repository/index.js +105 -1
  181. package/dist/modules/shipping/schema.d.ts +419 -0
  182. package/dist/modules/shipping/schema.d.ts.map +1 -0
  183. package/dist/modules/shipping/schema.js +56 -0
  184. package/dist/modules/shipping/service.d.ts +54 -0
  185. package/dist/modules/shipping/service.d.ts.map +1 -1
  186. package/dist/modules/shipping/service.js +161 -2
  187. package/dist/modules/tax/adapter.d.ts +13 -0
  188. package/dist/modules/tax/adapter.d.ts.map +1 -1
  189. package/dist/modules/tax/module.d.ts +5 -1
  190. package/dist/modules/tax/module.d.ts.map +1 -1
  191. package/dist/modules/tax/module.js +4 -1
  192. package/dist/modules/tax/repository/index.d.ts +38 -2
  193. package/dist/modules/tax/repository/index.d.ts.map +1 -1
  194. package/dist/modules/tax/repository/index.js +138 -1
  195. package/dist/modules/tax/schema.d.ts +355 -0
  196. package/dist/modules/tax/schema.d.ts.map +1 -0
  197. package/dist/modules/tax/schema.js +55 -0
  198. package/dist/modules/tax/service.d.ts +60 -1
  199. package/dist/modules/tax/service.d.ts.map +1 -1
  200. package/dist/modules/tax/service.js +227 -2
  201. package/dist/runtime/kernel-modules.d.ts +15 -2
  202. package/dist/runtime/kernel-modules.d.ts.map +1 -1
  203. package/dist/runtime/kernel-modules.js +4 -0
  204. package/dist/runtime/kernel-types.d.ts +5 -1
  205. package/dist/runtime/kernel-types.d.ts.map +1 -1
  206. package/dist/runtime/kernel-types.js +2 -0
  207. package/dist/runtime/server.js +1 -1
  208. package/dist/test-utils/create-plugin-test-app.d.ts +3 -0
  209. package/dist/test-utils/create-plugin-test-app.d.ts.map +1 -1
  210. package/dist/test-utils/create-plugin-test-app.js +15 -3
  211. package/dist/test-utils/rest-api-test-utils.d.ts.map +1 -1
  212. package/dist/test-utils/rest-api-test-utils.js +6 -0
  213. package/package.json +3 -3
  214. package/src/auth/setup.ts +1 -0
  215. package/src/config/defaults.ts +5 -0
  216. package/src/config/types.ts +17 -1
  217. package/src/generated/plugin-capabilities.d.ts +1 -0
  218. package/src/generated/plugin-manifest.ts +1 -0
  219. package/src/generated/plugin-repositories.d.ts +1 -0
  220. package/src/hooks/checkout.ts +25 -3
  221. package/src/index.ts +1 -0
  222. package/src/interfaces/rest/index.ts +12 -0
  223. package/src/interfaces/rest/routes/admin/staff.ts +218 -0
  224. package/src/interfaces/rest/routes/analytics.ts +33 -0
  225. package/src/interfaces/rest/routes/carts.ts +52 -2
  226. package/src/interfaces/rest/routes/catalog.ts +34 -0
  227. package/src/interfaces/rest/routes/checkout.ts +22 -0
  228. package/src/interfaces/rest/routes/documents.ts +61 -0
  229. package/src/interfaces/rest/routes/orders.ts +137 -1
  230. package/src/interfaces/rest/routes/pricing.ts +44 -1
  231. package/src/interfaces/rest/routes/settings.ts +61 -0
  232. package/src/interfaces/rest/routes/shipping.ts +92 -0
  233. package/src/interfaces/rest/routes/tax.ts +90 -0
  234. package/src/interfaces/rest/schemas/admin-staff.ts +110 -0
  235. package/src/interfaces/rest/schemas/analytics.ts +39 -0
  236. package/src/interfaces/rest/schemas/carts.ts +40 -0
  237. package/src/interfaces/rest/schemas/catalog.ts +53 -0
  238. package/src/interfaces/rest/schemas/checkout.ts +4 -0
  239. package/src/interfaces/rest/schemas/documents.ts +72 -0
  240. package/src/interfaces/rest/schemas/orders.ts +257 -0
  241. package/src/interfaces/rest/schemas/pricing.ts +75 -0
  242. package/src/interfaces/rest/schemas/settings.ts +93 -0
  243. package/src/interfaces/rest/schemas/shipping.ts +164 -0
  244. package/src/interfaces/rest/schemas/tax.ts +148 -0
  245. package/src/kernel/database/schema.ts +12 -0
  246. package/src/kernel/hooks/types.ts +1 -0
  247. package/src/kernel/plugin/manifest.ts +38 -2
  248. package/src/modules/analytics/module.ts +10 -0
  249. package/src/modules/analytics/reports.ts +311 -0
  250. package/src/modules/analytics/service.ts +23 -0
  251. package/src/modules/cart/repository/index.ts +30 -1
  252. package/src/modules/cart/schema.ts +2 -0
  253. package/src/modules/cart/schemas.ts +1 -0
  254. package/src/modules/cart/service.ts +101 -0
  255. package/src/modules/catalog/entity-service.ts +168 -3
  256. package/src/modules/catalog/schema.ts +5 -0
  257. package/src/modules/catalog/schemas.ts +3 -0
  258. package/src/modules/catalog/service.ts +30 -0
  259. package/src/modules/documents/module.ts +19 -0
  260. package/src/modules/documents/pdf.ts +101 -0
  261. package/src/modules/documents/render.ts +231 -0
  262. package/src/modules/documents/repository/index.ts +68 -0
  263. package/src/modules/documents/schema.ts +44 -0
  264. package/src/modules/documents/service.ts +176 -0
  265. package/src/modules/fulfillment/service.ts +139 -1
  266. package/src/modules/orders/repository/index.ts +127 -1
  267. package/src/modules/orders/schema.ts +54 -1
  268. package/src/modules/orders/service.ts +654 -22
  269. package/src/modules/pricing/repository/index.ts +34 -0
  270. package/src/modules/pricing/service.ts +71 -0
  271. package/src/modules/settings/module.ts +18 -0
  272. package/src/modules/settings/repository/index.ts +59 -0
  273. package/src/modules/settings/schema.ts +25 -0
  274. package/src/modules/settings/service.ts +82 -0
  275. package/src/modules/shipping/calculator.ts +27 -0
  276. package/src/modules/shipping/module.ts +6 -2
  277. package/src/modules/shipping/repository/index.ts +136 -2
  278. package/src/modules/shipping/schema.ts +58 -0
  279. package/src/modules/shipping/service.ts +260 -1
  280. package/src/modules/tax/adapter.ts +9 -0
  281. package/src/modules/tax/module.ts +6 -2
  282. package/src/modules/tax/repository/index.ts +169 -2
  283. package/src/modules/tax/schema.ts +57 -0
  284. package/src/modules/tax/service.ts +313 -2
  285. package/src/runtime/kernel-modules.ts +4 -0
  286. package/src/runtime/kernel-types.ts +6 -0
  287. package/src/runtime/server.ts +1 -1
  288. package/src/test-utils/create-plugin-test-app.ts +17 -4
  289. package/src/test-utils/rest-api-test-utils.ts +6 -0
@@ -0,0 +1,68 @@
1
+ import { and, eq, sql } from "drizzle-orm";
2
+ import type { TxContext } from "../../../kernel/database/tx-context.js";
3
+ import type {
4
+ DrizzleDatabase,
5
+ DbOrTx,
6
+ } from "../../../kernel/database/drizzle-db.js";
7
+ import { invoiceSequences, orderDocuments } from "../schema.js";
8
+
9
+ export type OrderDocument = typeof orderDocuments.$inferSelect;
10
+
11
+ /**
12
+ * Persistence for fiscal sequences and issued documents (issue #47).
13
+ */
14
+ export class DocumentsRepository {
15
+ constructor(private readonly db: DrizzleDatabase) {}
16
+
17
+ private getDb(ctx?: TxContext): DbOrTx {
18
+ return (ctx?.tx as DbOrTx | undefined) ?? this.db;
19
+ }
20
+
21
+ /**
22
+ * Atomically allocates the next sequence value for (org, series).
23
+ * Single upsert-returning statement — safe under concurrency.
24
+ */
25
+ async allocate(orgId: string, series: string, ctx?: TxContext): Promise<number> {
26
+ const db = this.getDb(ctx);
27
+ const rows = await db
28
+ .insert(invoiceSequences)
29
+ .values({ organizationId: orgId, series, nextValue: 2 })
30
+ .onConflictDoUpdate({
31
+ target: [invoiceSequences.organizationId, invoiceSequences.series],
32
+ set: {
33
+ nextValue: sql`${invoiceSequences.nextValue} + 1`,
34
+ updatedAt: new Date(),
35
+ },
36
+ })
37
+ .returning({ nextValue: invoiceSequences.nextValue });
38
+ // nextValue is post-increment; the allocated number is one less.
39
+ return rows[0]!.nextValue - 1;
40
+ }
41
+
42
+ async findDocument(
43
+ orgId: string,
44
+ orderId: string,
45
+ type: string,
46
+ ctx?: TxContext,
47
+ ): Promise<OrderDocument | undefined> {
48
+ const db = this.getDb(ctx);
49
+ const rows = await db
50
+ .select()
51
+ .from(orderDocuments)
52
+ .where(and(
53
+ eq(orderDocuments.organizationId, orgId),
54
+ eq(orderDocuments.orderId, orderId),
55
+ eq(orderDocuments.type, type),
56
+ ));
57
+ return rows[0];
58
+ }
59
+
60
+ async createDocument(
61
+ data: { organizationId: string; orderId: string; type: string; documentNumber: string },
62
+ ctx?: TxContext,
63
+ ): Promise<OrderDocument> {
64
+ const db = this.getDb(ctx);
65
+ const rows = await db.insert(orderDocuments).values(data).returning();
66
+ return rows[0]!;
67
+ }
68
+ }
@@ -0,0 +1,44 @@
1
+ import { index, integer, pgTable, text, timestamp, uniqueIndex, uuid } from "drizzle-orm/pg-core";
2
+ import { organization } from "../../auth/auth-schema.js";
3
+ import { orders } from "../orders/schema.js";
4
+
5
+ /**
6
+ * Fiscal document sequences (issue #47).
7
+ *
8
+ * Per-(org, series) counters for legally sequential invoice numbers.
9
+ * Allocation is a single atomic upsert-returning statement, so concurrent
10
+ * requests never observe the same value.
11
+ */
12
+ export const invoiceSequences = pgTable("invoice_sequences", {
13
+ id: uuid("id").defaultRandom().primaryKey(),
14
+ organizationId: text("organization_id")
15
+ .notNull()
16
+ .references(() => organization.id, { onDelete: "cascade" }),
17
+ series: text("series").notNull().default("default"),
18
+ // The NEXT value to hand out; allocation returns the pre-increment value.
19
+ nextValue: integer("next_value").notNull().default(1),
20
+ updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
21
+ }, (table) => ({
22
+ orgSeriesUnique: uniqueIndex("invoice_sequences_org_series_unique").on(table.organizationId, table.series),
23
+ }));
24
+
25
+ /**
26
+ * Issued documents (issue #47). One row per (org, order, type) — re-rendering
27
+ * an order's invoice returns the number issued the first time, so a fiscal
28
+ * number never changes once handed out.
29
+ */
30
+ export const orderDocuments = pgTable("order_documents", {
31
+ id: uuid("id").defaultRandom().primaryKey(),
32
+ organizationId: text("organization_id")
33
+ .notNull()
34
+ .references(() => organization.id, { onDelete: "cascade" }),
35
+ orderId: uuid("order_id")
36
+ .notNull()
37
+ .references(() => orders.id, { onDelete: "cascade" }),
38
+ type: text("type").notNull(), // "invoice" | "receipt"
39
+ documentNumber: text("document_number").notNull(),
40
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
41
+ }, (table) => ({
42
+ orgOrderTypeUnique: uniqueIndex("order_documents_org_order_type_unique").on(table.organizationId, table.orderId, table.type),
43
+ orgIdx: index("idx_order_documents_org").on(table.organizationId),
44
+ }));
@@ -0,0 +1,176 @@
1
+ import { resolveOrgId } from "../../auth/org.js";
2
+ import type { Actor } from "../../auth/types.js";
3
+ import type { TxContext } from "../../kernel/database/tx-context.js";
4
+ import { CommerceValidationError } from "../../kernel/errors.js";
5
+ import { Err, Ok, type Result } from "../../kernel/result.js";
6
+ import type { HydratedOrder, OrderService } from "../orders/service.js";
7
+ import type { SettingsService } from "../settings/service.js";
8
+ import { buildPdf } from "./pdf.js";
9
+ import {
10
+ invoiceHtml,
11
+ invoicePdfOps,
12
+ receiptHtml,
13
+ type DocumentBranding,
14
+ } from "./render.js";
15
+ import type { DocumentsRepository, OrderDocument } from "./repository/index.js";
16
+
17
+ interface DocumentsServiceDeps {
18
+ repository: DocumentsRepository;
19
+ // The shared kernel service container — orders/settings/email are resolved
20
+ // at call time (they may instantiate after this module).
21
+ services: Record<string, unknown>;
22
+ }
23
+
24
+ interface EmailAdapter {
25
+ send(input: { template: string; to: string; data?: Record<string, unknown> }): Promise<void>;
26
+ }
27
+
28
+ const DEFAULT_INVOICE_PREFIX = "INV-";
29
+
30
+ /**
31
+ * Order document rendering + fiscal numbering (issue #47).
32
+ */
33
+ export class DocumentsService {
34
+ private repository: DocumentsRepository;
35
+ private services: Record<string, unknown>;
36
+
37
+ constructor(deps: DocumentsServiceDeps) {
38
+ this.repository = deps.repository;
39
+ this.services = deps.services;
40
+ }
41
+
42
+ private get orders(): OrderService {
43
+ return this.services.orders as OrderService;
44
+ }
45
+
46
+ private get settings(): SettingsService {
47
+ return this.services.settings as SettingsService;
48
+ }
49
+
50
+ private async brandingFor(orgId: string, ctx?: TxContext): Promise<DocumentBranding> {
51
+ return (await this.settings.read(orgId, "branding", ctx)) as DocumentBranding;
52
+ }
53
+
54
+ /**
55
+ * Issues (or returns the previously issued) fiscal invoice number for an
56
+ * order. Idempotent per (org, order): once handed out, the number never
57
+ * changes. Sequence allocation is atomic; a lost creation race falls back
58
+ * to the winner's document.
59
+ */
60
+ async issueInvoiceNumber(
61
+ order: HydratedOrder,
62
+ ctx?: TxContext,
63
+ ): Promise<OrderDocument> {
64
+ const orgId = order.organizationId;
65
+ const existing = await this.repository.findDocument(orgId, order.id, "invoice", ctx);
66
+ if (existing) return existing;
67
+
68
+ const documentsSettings = await this.settings.read(orgId, "documents", ctx);
69
+ const prefix =
70
+ typeof documentsSettings.invoicePrefix === "string"
71
+ ? documentsSettings.invoicePrefix
72
+ : DEFAULT_INVOICE_PREFIX;
73
+
74
+ const value = await this.repository.allocate(orgId, "invoice", ctx);
75
+ const documentNumber = `${prefix}${String(value).padStart(6, "0")}`;
76
+ try {
77
+ return await this.repository.createDocument(
78
+ { organizationId: orgId, orderId: order.id, type: "invoice", documentNumber },
79
+ ctx,
80
+ );
81
+ } catch (error) {
82
+ // Unique (org, order, type) — a concurrent request won; use its number.
83
+ const winner = await this.repository.findDocument(orgId, order.id, "invoice", ctx);
84
+ if (winner) return winner;
85
+ throw error;
86
+ }
87
+ }
88
+
89
+ private async loadOrder(
90
+ orderId: string,
91
+ actor: Actor | null,
92
+ ctx?: TxContext,
93
+ ): Promise<Result<HydratedOrder>> {
94
+ return this.orders.getById(orderId, actor, ctx);
95
+ }
96
+
97
+ async renderInvoiceHtml(
98
+ orderId: string,
99
+ actor: Actor | null,
100
+ ctx?: TxContext,
101
+ ): Promise<Result<{ html: string; invoiceNumber: string }>> {
102
+ const order = await this.loadOrder(orderId, actor, ctx);
103
+ if (!order.ok) return order;
104
+ const branding = await this.brandingFor(order.value.organizationId, ctx);
105
+ const doc = await this.issueInvoiceNumber(order.value, ctx);
106
+ return Ok({
107
+ html: invoiceHtml({ order: order.value, branding, invoiceNumber: doc.documentNumber }),
108
+ invoiceNumber: doc.documentNumber,
109
+ });
110
+ }
111
+
112
+ async renderInvoicePdf(
113
+ orderId: string,
114
+ actor: Actor | null,
115
+ ctx?: TxContext,
116
+ ): Promise<Result<{ pdf: Uint8Array; invoiceNumber: string }>> {
117
+ const order = await this.loadOrder(orderId, actor, ctx);
118
+ if (!order.ok) return order;
119
+ const branding = await this.brandingFor(order.value.organizationId, ctx);
120
+ const doc = await this.issueInvoiceNumber(order.value, ctx);
121
+ const pages = invoicePdfOps({
122
+ order: order.value,
123
+ branding,
124
+ invoiceNumber: doc.documentNumber,
125
+ });
126
+ return Ok({ pdf: buildPdf(pages), invoiceNumber: doc.documentNumber });
127
+ }
128
+
129
+ async renderReceiptHtml(
130
+ orderId: string,
131
+ actor: Actor | null,
132
+ ctx?: TxContext,
133
+ ): Promise<Result<{ html: string }>> {
134
+ const order = await this.loadOrder(orderId, actor, ctx);
135
+ if (!order.ok) return order;
136
+ const branding = await this.brandingFor(order.value.organizationId, ctx);
137
+ return Ok({ html: receiptHtml({ order: order.value, branding }) });
138
+ }
139
+
140
+ /**
141
+ * Emails the invoice via the configured email adapter. The adapter's
142
+ * template contract is (template, to, data) — the rendered HTML and the
143
+ * fiscal number ride in `data` so template-based adapters can use either.
144
+ */
145
+ async emailInvoice(
146
+ orderId: string,
147
+ to: string,
148
+ actor: Actor | null,
149
+ ctx?: TxContext,
150
+ ): Promise<Result<{ sent: true; invoiceNumber: string; to: string }>> {
151
+ const email = this.services.email as EmailAdapter | undefined;
152
+ if (!email) {
153
+ return Err(new CommerceValidationError("No email adapter is configured."));
154
+ }
155
+ const rendered = await this.renderInvoiceHtml(orderId, actor, ctx);
156
+ if (!rendered.ok) return rendered;
157
+
158
+ const order = await this.loadOrder(orderId, actor, ctx);
159
+ if (!order.ok) return order;
160
+
161
+ await email.send({
162
+ template: "order-invoice",
163
+ to,
164
+ data: {
165
+ orgId: resolveOrgId(actor ?? ctx?.actor ?? null),
166
+ orderId,
167
+ orderNumber: order.value.orderNumber,
168
+ invoiceNumber: rendered.value.invoiceNumber,
169
+ grandTotal: order.value.grandTotal,
170
+ currency: order.value.currency,
171
+ html: rendered.value.html,
172
+ },
173
+ });
174
+ return Ok({ sent: true, invoiceNumber: rendered.value.invoiceNumber, to });
175
+ }
176
+ }
@@ -4,7 +4,7 @@ import type { Actor } from "../../auth/types.js";
4
4
  import type { DatabaseAdapter } from "../../kernel/database/adapter.js";
5
5
  import type { PluginDb } from "../../kernel/database/plugin-types.js";
6
6
  import type { TxContext } from "../../kernel/database/tx-context.js";
7
- import { CommerceNotFoundError } from "../../kernel/errors.js";
7
+ import { CommerceNotFoundError, CommerceValidationError } from "../../kernel/errors.js";
8
8
  import { runAfterHooks } from "../../kernel/hooks/executor.js";
9
9
  import { createHookContext } from "../../kernel/hooks/create-context.js";
10
10
  import type { HookRegistry } from "../../kernel/hooks/registry.js";
@@ -346,6 +346,144 @@ export class FulfillmentService {
346
346
  return Ok(undefined);
347
347
  }
348
348
 
349
+ /**
350
+ * Records a fulfillment (e.g. a shipment) for a subset of an order's line
351
+ * items, with optional carrier/tracking details. Supports partial
352
+ * fulfillment (per-line quantities) and multiple fulfillments per order.
353
+ */
354
+ async createFulfillment(
355
+ input: {
356
+ orderId: string;
357
+ lineItems: Array<{ orderLineItemId: string; quantity: number }>;
358
+ carrier?: string;
359
+ trackingNumber?: string;
360
+ trackingUrl?: string;
361
+ type?: string;
362
+ status?: string;
363
+ metadata?: Record<string, unknown>;
364
+ },
365
+ actor?: Actor | null,
366
+ ctx?: TxContext,
367
+ ): Promise<Result<FulfillmentRecord>> {
368
+ const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
369
+ const order = await this.deps.ordersRepository.findById(orgId, input.orderId, ctx);
370
+ if (!order) return Err(new CommerceNotFoundError("Order not found."));
371
+
372
+ const orderLines = await this.deps.ordersRepository.findLineItemsByOrderId(
373
+ input.orderId,
374
+ ctx,
375
+ );
376
+ const linesById = new Map(orderLines.map((li) => [li.id, li]));
377
+
378
+ // Validate every requested line before writing anything: it must belong
379
+ // to the order, and requested + already-fulfilled must not exceed ordered.
380
+ const alreadyFulfilled = new Map<string, number>();
381
+ for (const req of input.lineItems) {
382
+ const line = linesById.get(req.orderLineItemId);
383
+ if (!line) {
384
+ return Err(
385
+ new CommerceValidationError(
386
+ `Line item ${req.orderLineItemId} does not belong to this order.`,
387
+ ),
388
+ );
389
+ }
390
+ const fulfilled = await this.deps.repository.getFulfilledQuantity(
391
+ req.orderLineItemId,
392
+ ctx,
393
+ );
394
+ alreadyFulfilled.set(req.orderLineItemId, fulfilled);
395
+ if (fulfilled + req.quantity > line.quantity) {
396
+ return Err(
397
+ new CommerceValidationError(
398
+ `Cannot fulfill ${req.quantity} of "${line.title}" — only ${line.quantity - fulfilled} remaining.`,
399
+ ),
400
+ );
401
+ }
402
+ }
403
+
404
+ const status = input.status ?? "shipped";
405
+ const created = await this.deps.repository.create(
406
+ {
407
+ orderId: order.id,
408
+ type: input.type ?? "physical",
409
+ status,
410
+ carrier: input.carrier ?? null,
411
+ trackingNumber: input.trackingNumber ?? null,
412
+ trackingUrl: input.trackingUrl ?? null,
413
+ ...(status === "shipped" ? { shippedAt: new Date() } : {}),
414
+ ...(status === "delivered" ? { deliveredAt: new Date() } : {}),
415
+ ...(order.customerId != null ? { customerId: order.customerId } : {}),
416
+ metadata: input.metadata ?? {},
417
+ },
418
+ ctx,
419
+ );
420
+
421
+ await this.deps.repository.createLineItems(
422
+ input.lineItems.map((li) => ({
423
+ fulfillmentId: created.id,
424
+ orderLineItemId: li.orderLineItemId,
425
+ quantity: li.quantity,
426
+ })),
427
+ ctx,
428
+ );
429
+
430
+ await this.deps.repository.createEvent(
431
+ {
432
+ fulfillmentId: created.id,
433
+ eventType: "created",
434
+ toStatus: status,
435
+ ...(actor?.userId !== undefined ? { actorId: actor.userId } : {}),
436
+ description: input.trackingNumber
437
+ ? `Shipment recorded (${input.trackingNumber})`
438
+ : "Fulfillment recorded",
439
+ },
440
+ ctx,
441
+ );
442
+
443
+ // Reflect per-line fulfillment progress on the order line items
444
+ for (const req of input.lineItems) {
445
+ const line = linesById.get(req.orderLineItemId)!;
446
+ const total = (alreadyFulfilled.get(req.orderLineItemId) ?? 0) + req.quantity;
447
+ await this.deps.ordersRepository.updateLineItem(
448
+ req.orderLineItemId,
449
+ { fulfillmentStatus: total >= line.quantity ? "fulfilled" : "partial" },
450
+ ctx,
451
+ );
452
+ }
453
+
454
+ const record: FulfillmentRecord = {
455
+ ...toServiceRecord(
456
+ created,
457
+ input.lineItems.map((li) => ({
458
+ id: "",
459
+ fulfillmentId: created.id,
460
+ orderLineItemId: li.orderLineItemId,
461
+ quantity: li.quantity,
462
+ })),
463
+ ),
464
+ lineItems: input.lineItems.map((li) => ({
465
+ id: li.orderLineItemId,
466
+ title: linesById.get(li.orderLineItemId)?.title ?? "",
467
+ quantity: li.quantity,
468
+ })),
469
+ };
470
+
471
+ const afterHooks = this.deps.hooks.resolve(
472
+ "fulfillment.afterCreate",
473
+ ) as AfterHook<FulfillmentRecord>[];
474
+ const hookCtx: HookContext = createHookContext({
475
+ actor: actor ?? ctx?.actor ?? null,
476
+ tx: ctx?.tx ?? null,
477
+ logger: createLogger("fulfillment"),
478
+ services: this.deps.services,
479
+ context: { moduleName: "fulfillment" },
480
+ database: { db: this.deps.database.db as PluginDb },
481
+ });
482
+ await runAfterHooks(afterHooks, null, record, "create", hookCtx);
483
+
484
+ return Ok(record);
485
+ }
486
+
349
487
  async getByOrderId(
350
488
  orderId: string,
351
489
  ctx?: TxContext,
@@ -4,7 +4,7 @@ import type {
4
4
  DrizzleDatabase,
5
5
  DbOrTx,
6
6
  } from "../../../kernel/database/drizzle-db.js";
7
- import { orders, orderLineItems, orderStatusHistory } from "../schema.js";
7
+ import { orders, orderLineItems, orderNotes, orderRefunds, orderStatusHistory } from "../schema.js";
8
8
  import { customers } from "../../customers/schema.js";
9
9
 
10
10
  export interface OrderLookupRow {
@@ -26,6 +26,10 @@ export type OrderLineItem = typeof orderLineItems.$inferSelect;
26
26
  export type OrderLineItemInsert = typeof orderLineItems.$inferInsert;
27
27
  export type OrderStatusHistory = typeof orderStatusHistory.$inferSelect;
28
28
  export type OrderStatusHistoryInsert = typeof orderStatusHistory.$inferInsert;
29
+ export type OrderRefund = typeof orderRefunds.$inferSelect;
30
+ export type OrderRefundInsert = typeof orderRefunds.$inferInsert;
31
+ export type OrderNote = typeof orderNotes.$inferSelect;
32
+ export type OrderNoteInsert = typeof orderNotes.$inferInsert;
29
33
 
30
34
  /**
31
35
  * OrdersRepository provides type-safe database operations for orders.
@@ -159,6 +163,24 @@ export class OrdersRepository {
159
163
  return query;
160
164
  }
161
165
 
166
+ async findByIdempotencyKey(
167
+ orgId: string,
168
+ idempotencyKey: string,
169
+ ctx?: TxContext,
170
+ ): Promise<Order | undefined> {
171
+ const db = this.getDb(ctx);
172
+ const rows = await db
173
+ .select()
174
+ .from(orders)
175
+ .where(
176
+ and(
177
+ eq(orders.organizationId, orgId),
178
+ eq(orders.idempotencyKey, idempotencyKey),
179
+ ),
180
+ );
181
+ return rows[0];
182
+ }
183
+
162
184
  async create(data: OrderInsert, ctx?: TxContext): Promise<Order> {
163
185
  const db = this.getDb(ctx);
164
186
  const rows = await db.insert(orders).values(data).returning();
@@ -328,6 +350,110 @@ export class OrdersRepository {
328
350
  return { order, lineItems };
329
351
  }
330
352
 
353
+ // ─────────────────────────────────────────────────────────────────────────────
354
+ // Line-level refunds (issue #52)
355
+ // ─────────────────────────────────────────────────────────────────────────────
356
+
357
+ async createRefund(data: OrderRefundInsert, ctx?: TxContext): Promise<OrderRefund> {
358
+ const db = this.getDb(ctx);
359
+ const rows = await db.insert(orderRefunds).values(data).returning();
360
+ return rows[0]!;
361
+ }
362
+
363
+ async findRefundById(
364
+ orgId: string,
365
+ id: string,
366
+ ctx?: TxContext,
367
+ ): Promise<OrderRefund | undefined> {
368
+ const db = this.getDb(ctx);
369
+ const rows = await db
370
+ .select()
371
+ .from(orderRefunds)
372
+ .where(and(eq(orderRefunds.organizationId, orgId), eq(orderRefunds.id, id)));
373
+ return rows[0];
374
+ }
375
+
376
+ async findRefundsByOrderId(orderId: string, ctx?: TxContext): Promise<OrderRefund[]> {
377
+ const db = this.getDb(ctx);
378
+ return db
379
+ .select()
380
+ .from(orderRefunds)
381
+ .where(eq(orderRefunds.orderId, orderId))
382
+ .orderBy(desc(orderRefunds.createdAt));
383
+ }
384
+
385
+ async markRefundUndone(
386
+ id: string,
387
+ undoneBy: string,
388
+ ctx?: TxContext,
389
+ ): Promise<OrderRefund | undefined> {
390
+ const db = this.getDb(ctx);
391
+ const rows = await db
392
+ .update(orderRefunds)
393
+ .set({ status: "undone", undoneAt: new Date(), undoneBy })
394
+ .where(and(eq(orderRefunds.id, id), eq(orderRefunds.status, "completed")))
395
+ .returning();
396
+ return rows[0];
397
+ }
398
+
399
+ /**
400
+ * Sum of completed refunds performed by an operator for the local calendar
401
+ * day in the given timezone — the daily-cap accounting query.
402
+ */
403
+ async sumRefundsByOperatorToday(
404
+ orgId: string,
405
+ performedBy: string,
406
+ timezone: string,
407
+ ctx?: TxContext,
408
+ ): Promise<number> {
409
+ const db = this.getDb(ctx);
410
+ const result = await db.execute(sql`
411
+ SELECT COALESCE(sum(amount), 0)::int AS "total"
412
+ FROM order_refunds
413
+ WHERE organization_id = ${orgId}
414
+ AND performed_by = ${performedBy}
415
+ AND status = 'completed'
416
+ AND (created_at AT TIME ZONE ${timezone})::date = (now() AT TIME ZONE ${timezone})::date
417
+ `);
418
+ const rows = Array.isArray(result)
419
+ ? (result as Record<string, unknown>[])
420
+ : ((result as { rows?: Record<string, unknown>[] }).rows ?? []);
421
+ const total = rows[0]?.total;
422
+ return typeof total === "bigint" ? Number(total) : ((total as number) ?? 0);
423
+ }
424
+
425
+ // ─────────────────────────────────────────────────────────────────────────────
426
+ // Order notes (issue #56)
427
+ // ─────────────────────────────────────────────────────────────────────────────
428
+
429
+ async createNote(data: OrderNoteInsert, ctx?: TxContext): Promise<OrderNote> {
430
+ const db = this.getDb(ctx);
431
+ const rows = await db.insert(orderNotes).values(data).returning();
432
+ return rows[0]!;
433
+ }
434
+
435
+ async findNotesByOrderId(orderId: string, ctx?: TxContext): Promise<OrderNote[]> {
436
+ const db = this.getDb(ctx);
437
+ return db
438
+ .select()
439
+ .from(orderNotes)
440
+ .where(eq(orderNotes.orderId, orderId))
441
+ .orderBy(desc(orderNotes.pinned), desc(orderNotes.createdAt));
442
+ }
443
+
444
+ async deleteNote(orgId: string, orderId: string, noteId: string, ctx?: TxContext): Promise<boolean> {
445
+ const db = this.getDb(ctx);
446
+ const rows = await db
447
+ .delete(orderNotes)
448
+ .where(and(
449
+ eq(orderNotes.id, noteId),
450
+ eq(orderNotes.orderId, orderId),
451
+ eq(orderNotes.organizationId, orgId),
452
+ ))
453
+ .returning();
454
+ return rows.length > 0;
455
+ }
456
+
331
457
  // ─────────────────────────────────────────────────────────────────────────────
332
458
  // Order Number Generation
333
459
  // ─────────────────────────────────────────────────────────────────────────────
@@ -1,4 +1,5 @@
1
- import { index, integer, jsonb, pgTable, text, timestamp, uniqueIndex, uuid } from "drizzle-orm/pg-core";
1
+ import { sql } from "drizzle-orm";
2
+ import { boolean, index, integer, jsonb, pgTable, text, timestamp, uniqueIndex, uuid } from "drizzle-orm/pg-core";
2
3
  import { organization } from "../../auth/auth-schema.js";
3
4
  import { sellableEntities, variants } from "../catalog/schema.js";
4
5
 
@@ -17,6 +18,9 @@ export const orders = pgTable("orders", {
17
18
  amountCaptured: integer("amount_captured"),
18
19
  paymentIntentId: text("payment_intent_id"),
19
20
  paymentMethodId: text("payment_method_id"),
21
+ // Client-supplied key making order creation safely retryable (offline POS
22
+ // queues, network retries) — replays return the original order.
23
+ idempotencyKey: text("idempotency_key"),
20
24
  metadata: jsonb("metadata").$type<Record<string, unknown>>().default({}),
21
25
  placedAt: timestamp("placed_at", { withTimezone: true }).defaultNow().notNull(),
22
26
  fulfilledAt: timestamp("fulfilled_at", { withTimezone: true }),
@@ -28,6 +32,9 @@ export const orders = pgTable("orders", {
28
32
  customerIdIdx: index("idx_orders_customer_id").on(table.customerId),
29
33
  placedAtIdx: index("idx_orders_placed_at").on(table.placedAt),
30
34
  paymentIntentIdx: index("idx_orders_payment_intent").on(table.paymentIntentId),
35
+ orgIdempotencyKeyUnique: uniqueIndex("orders_org_idempotency_key_unique")
36
+ .on(table.organizationId, table.idempotencyKey)
37
+ .where(sql`idempotency_key IS NOT NULL`),
31
38
  }));
32
39
 
33
40
  export const orderLineItems = pgTable("order_line_items", {
@@ -45,6 +52,9 @@ export const orderLineItems = pgTable("order_line_items", {
45
52
  totalPrice: integer("total_price").notNull(),
46
53
  taxAmount: integer("tax_amount").notNull().default(0),
47
54
  discountAmount: integer("discount_amount").notNull().default(0),
55
+ // Units already refunded on this line (issue #52) — line-level refund REST
56
+ // rejects refunds beyond `quantity - refundedQuantity`.
57
+ refundedQuantity: integer("refunded_quantity").notNull().default(0),
48
58
  fulfillmentStatus: text("fulfillment_status").notNull().default("unfulfilled"),
49
59
  metadata: jsonb("metadata").$type<Record<string, unknown>>().default({}),
50
60
  }, (table) => [
@@ -52,6 +62,49 @@ export const orderLineItems = pgTable("order_line_items", {
52
62
  index("idx_order_line_items_entity_id").on(table.entityId),
53
63
  ]);
54
64
 
65
+ /**
66
+ * Line-level refunds (issue #52). One row per refund operation — the ledger
67
+ * behind the daily refund cap and the undo window. `lines` records which
68
+ * line items and quantities the refund covered so undo can restore them.
69
+ */
70
+ export const orderRefunds = pgTable("order_refunds", {
71
+ id: uuid("id").defaultRandom().primaryKey(),
72
+ organizationId: text("organization_id").notNull().references(() => organization.id, { onDelete: "cascade" }),
73
+ orderId: uuid("order_id")
74
+ .references(() => orders.id, { onDelete: "cascade" })
75
+ .notNull(),
76
+ amount: integer("amount").notNull(),
77
+ reason: text("reason"),
78
+ lines: jsonb("lines").$type<Array<{ lineItemId: string; quantity: number; amount: number }>>().notNull(),
79
+ performedBy: text("performed_by").notNull(),
80
+ status: text("status", { enum: ["completed", "undone"] }).notNull().default("completed"),
81
+ undoneAt: timestamp("undone_at", { withTimezone: true }),
82
+ undoneBy: text("undone_by"),
83
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
84
+ }, (table) => [
85
+ index("idx_order_refunds_order_id").on(table.orderId),
86
+ index("idx_order_refunds_org_performed_by").on(table.organizationId, table.performedBy, table.createdAt),
87
+ ]);
88
+
89
+ /**
90
+ * Operator annotations on orders (issue #56) — "customer will pick up
91
+ * Thursday". Merged into GET /orders/{id}/timeline alongside status history
92
+ * and refund ledger events.
93
+ */
94
+ export const orderNotes = pgTable("order_notes", {
95
+ id: uuid("id").defaultRandom().primaryKey(),
96
+ organizationId: text("organization_id").notNull().references(() => organization.id, { onDelete: "cascade" }),
97
+ orderId: uuid("order_id")
98
+ .references(() => orders.id, { onDelete: "cascade" })
99
+ .notNull(),
100
+ author: text("author").notNull(),
101
+ body: text("body").notNull(),
102
+ pinned: boolean("pinned").notNull().default(false),
103
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
104
+ }, (table) => [
105
+ index("idx_order_notes_order_id").on(table.orderId),
106
+ ]);
107
+
55
108
  export const orderStatusHistory = pgTable("order_status_history", {
56
109
  id: uuid("id").defaultRandom().primaryKey(),
57
110
  orderId: uuid("order_id")