@open-mercato/core 0.6.4-develop.4210.1.d412061cfe → 0.6.4-develop.4236.1.9fa6806b34

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 (370) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/staff_time_entry/index.js +37 -0
  3. package/dist/generated/entities/staff_time_entry/index.js.map +7 -0
  4. package/dist/generated/entities/staff_time_entry_segment/index.js +23 -0
  5. package/dist/generated/entities/staff_time_entry_segment/index.js.map +7 -0
  6. package/dist/generated/entities/staff_time_project/index.js +35 -0
  7. package/dist/generated/entities/staff_time_project/index.js.map +7 -0
  8. package/dist/generated/entities/staff_time_project_member/index.js +29 -0
  9. package/dist/generated/entities/staff_time_project_member/index.js.map +7 -0
  10. package/dist/generated/entities.ids.generated.js +5 -1
  11. package/dist/generated/entities.ids.generated.js.map +2 -2
  12. package/dist/generated/entity-fields-registry.js +64 -0
  13. package/dist/generated/entity-fields-registry.js.map +2 -2
  14. package/dist/helpers/integration/timesheetFixtures.js +50 -0
  15. package/dist/helpers/integration/timesheetFixtures.js.map +7 -0
  16. package/dist/modules/attachments/api/library/[id]/route.js +20 -16
  17. package/dist/modules/attachments/api/library/[id]/route.js.map +2 -2
  18. package/dist/modules/attachments/api/route.js +18 -14
  19. package/dist/modules/attachments/api/route.js.map +2 -2
  20. package/dist/modules/auth/api/roles/acl/route.js +10 -4
  21. package/dist/modules/auth/api/roles/acl/route.js.map +2 -2
  22. package/dist/modules/auth/api/sidebar/preferences/route.js +27 -20
  23. package/dist/modules/auth/api/sidebar/preferences/route.js.map +2 -2
  24. package/dist/modules/auth/api/users/acl/route.js +16 -11
  25. package/dist/modules/auth/api/users/acl/route.js.map +2 -2
  26. package/dist/modules/auth/commands/users.js +87 -71
  27. package/dist/modules/auth/commands/users.js.map +2 -2
  28. package/dist/modules/auth/services/sidebarPreferencesService.js +39 -30
  29. package/dist/modules/auth/services/sidebarPreferencesService.js.map +2 -2
  30. package/dist/modules/catalog/commands/categories.js +61 -12
  31. package/dist/modules/catalog/commands/categories.js.map +2 -2
  32. package/dist/modules/catalog/commands/products.js +79 -54
  33. package/dist/modules/catalog/commands/products.js.map +2 -2
  34. package/dist/modules/catalog/commands/variants.js +29 -16
  35. package/dist/modules/catalog/commands/variants.js.map +2 -2
  36. package/dist/modules/currencies/commands/currencies.js +15 -8
  37. package/dist/modules/currencies/commands/currencies.js.map +2 -2
  38. package/dist/modules/customer_accounts/api/admin/users.js +27 -26
  39. package/dist/modules/customer_accounts/api/admin/users.js.map +2 -2
  40. package/dist/modules/customer_accounts/api/password/reset-confirm.js +5 -5
  41. package/dist/modules/customer_accounts/api/password/reset-confirm.js.map +2 -2
  42. package/dist/modules/customer_accounts/api/portal/users/[id]/roles.js +11 -10
  43. package/dist/modules/customer_accounts/api/portal/users/[id]/roles.js.map +2 -2
  44. package/dist/modules/customers/commands/addresses.js +35 -21
  45. package/dist/modules/customers/commands/addresses.js.map +2 -2
  46. package/dist/modules/customers/commands/companies.js +163 -162
  47. package/dist/modules/customers/commands/companies.js.map +2 -2
  48. package/dist/modules/customers/commands/deals.js +3 -4
  49. package/dist/modules/customers/commands/deals.js.map +2 -2
  50. package/dist/modules/customers/commands/interactions.js +19 -22
  51. package/dist/modules/customers/commands/interactions.js.map +2 -2
  52. package/dist/modules/customers/commands/people.js +18 -15
  53. package/dist/modules/customers/commands/people.js.map +2 -2
  54. package/dist/modules/customers/commands/personCompanyLinks.js +105 -94
  55. package/dist/modules/customers/commands/personCompanyLinks.js.map +2 -2
  56. package/dist/modules/customers/commands/pipeline-stages.js +30 -23
  57. package/dist/modules/customers/commands/pipeline-stages.js.map +2 -2
  58. package/dist/modules/customers/commands/pipelines.js +27 -20
  59. package/dist/modules/customers/commands/pipelines.js.map +2 -2
  60. package/dist/modules/customers/commands/tags.js +13 -5
  61. package/dist/modules/customers/commands/tags.js.map +2 -2
  62. package/dist/modules/dashboards/api/users/widgets/route.js +0 -1
  63. package/dist/modules/dashboards/api/users/widgets/route.js.map +2 -2
  64. package/dist/modules/dashboards/api/widgets/data/route.js +29 -1
  65. package/dist/modules/dashboards/api/widgets/data/route.js.map +2 -2
  66. package/dist/modules/data_sync/lib/sync-engine.js +4 -4
  67. package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
  68. package/dist/modules/data_sync/lib/sync-run-service.js +51 -27
  69. package/dist/modules/data_sync/lib/sync-run-service.js.map +2 -2
  70. package/dist/modules/directory/commands/organizations.js +192 -158
  71. package/dist/modules/directory/commands/organizations.js.map +3 -3
  72. package/dist/modules/inbox_ops/api/emails/[id]/reprocess/route.js +22 -16
  73. package/dist/modules/inbox_ops/api/emails/[id]/reprocess/route.js.map +2 -2
  74. package/dist/modules/messages/commands/messages.js +77 -75
  75. package/dist/modules/messages/commands/messages.js.map +2 -2
  76. package/dist/modules/messages/commands/shared.js +132 -132
  77. package/dist/modules/messages/commands/shared.js.map +2 -2
  78. package/dist/modules/perspectives/api/[tableId]/route.js +37 -26
  79. package/dist/modules/perspectives/api/[tableId]/route.js.map +2 -2
  80. package/dist/modules/resources/commands/resources.js +125 -117
  81. package/dist/modules/resources/commands/resources.js.map +2 -2
  82. package/dist/modules/resources/commands/tags.js +7 -3
  83. package/dist/modules/resources/commands/tags.js.map +2 -2
  84. package/dist/modules/sales/api/quotes/send/route.js +12 -11
  85. package/dist/modules/sales/api/quotes/send/route.js.map +2 -2
  86. package/dist/modules/sales/commands/documents.js +629 -478
  87. package/dist/modules/sales/commands/documents.js.map +2 -2
  88. package/dist/modules/sales/commands/payments.js +146 -146
  89. package/dist/modules/sales/commands/payments.js.map +2 -2
  90. package/dist/modules/sales/commands/returns.js +68 -60
  91. package/dist/modules/sales/commands/returns.js.map +2 -2
  92. package/dist/modules/staff/acl.js +10 -1
  93. package/dist/modules/staff/acl.js.map +2 -2
  94. package/dist/modules/staff/analytics.js +33 -0
  95. package/dist/modules/staff/analytics.js.map +7 -0
  96. package/dist/modules/staff/api/guards.js +31 -0
  97. package/dist/modules/staff/api/guards.js.map +7 -0
  98. package/dist/modules/staff/api/interceptors.js +96 -0
  99. package/dist/modules/staff/api/interceptors.js.map +7 -0
  100. package/dist/modules/staff/api/timesheets/my-projects/[projectId]/route.js +170 -0
  101. package/dist/modules/staff/api/timesheets/my-projects/[projectId]/route.js.map +7 -0
  102. package/dist/modules/staff/api/timesheets/my-projects/route.js +103 -0
  103. package/dist/modules/staff/api/timesheets/my-projects/route.js.map +7 -0
  104. package/dist/modules/staff/api/timesheets/projects/kpis/route.js +147 -0
  105. package/dist/modules/staff/api/timesheets/projects/kpis/route.js.map +7 -0
  106. package/dist/modules/staff/api/timesheets/time-entries/[id]/segments/[segmentId]/route.js +171 -0
  107. package/dist/modules/staff/api/timesheets/time-entries/[id]/segments/[segmentId]/route.js.map +7 -0
  108. package/dist/modules/staff/api/timesheets/time-entries/[id]/segments/route.js +180 -0
  109. package/dist/modules/staff/api/timesheets/time-entries/[id]/segments/route.js.map +7 -0
  110. package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.js +155 -0
  111. package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.js.map +7 -0
  112. package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-stop/route.js +173 -0
  113. package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-stop/route.js.map +7 -0
  114. package/dist/modules/staff/api/timesheets/time-entries/bulk/route.js +260 -0
  115. package/dist/modules/staff/api/timesheets/time-entries/bulk/route.js.map +7 -0
  116. package/dist/modules/staff/api/timesheets/time-entries/route.js +188 -0
  117. package/dist/modules/staff/api/timesheets/time-entries/route.js.map +7 -0
  118. package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js +159 -0
  119. package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js.map +7 -0
  120. package/dist/modules/staff/api/timesheets/time-projects/route.js +230 -0
  121. package/dist/modules/staff/api/timesheets/time-projects/route.js.map +7 -0
  122. package/dist/modules/staff/backend/staff/timesheets/page.js +710 -0
  123. package/dist/modules/staff/backend/staff/timesheets/page.js.map +7 -0
  124. package/dist/modules/staff/backend/staff/timesheets/page.meta.js +22 -0
  125. package/dist/modules/staff/backend/staff/timesheets/page.meta.js.map +7 -0
  126. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.js +125 -0
  127. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.js.map +7 -0
  128. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.meta.js +16 -0
  129. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.meta.js.map +7 -0
  130. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/page.js +418 -0
  131. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/page.js.map +7 -0
  132. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/page.meta.js +16 -0
  133. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/page.meta.js.map +7 -0
  134. package/dist/modules/staff/backend/staff/timesheets/projects/create/page.js +79 -0
  135. package/dist/modules/staff/backend/staff/timesheets/projects/create/page.js.map +7 -0
  136. package/dist/modules/staff/backend/staff/timesheets/projects/create/page.meta.js +16 -0
  137. package/dist/modules/staff/backend/staff/timesheets/projects/create/page.meta.js.map +7 -0
  138. package/dist/modules/staff/backend/staff/timesheets/projects/page.js +602 -0
  139. package/dist/modules/staff/backend/staff/timesheets/projects/page.js.map +7 -0
  140. package/dist/modules/staff/backend/staff/timesheets/projects/page.meta.js +25 -0
  141. package/dist/modules/staff/backend/staff/timesheets/projects/page.meta.js.map +7 -0
  142. package/dist/modules/staff/backend/staff/timesheets/projects/projectFormConfig.js +123 -0
  143. package/dist/modules/staff/backend/staff/timesheets/projects/projectFormConfig.js.map +7 -0
  144. package/dist/modules/staff/cli.js +38 -1
  145. package/dist/modules/staff/cli.js.map +2 -2
  146. package/dist/modules/staff/commands/index.js +2 -0
  147. package/dist/modules/staff/commands/index.js.map +2 -2
  148. package/dist/modules/staff/commands/leave-requests.js +30 -28
  149. package/dist/modules/staff/commands/leave-requests.js.map +3 -3
  150. package/dist/modules/staff/commands/team-members.js +21 -20
  151. package/dist/modules/staff/commands/team-members.js.map +2 -2
  152. package/dist/modules/staff/commands/timesheets-entries.js +409 -0
  153. package/dist/modules/staff/commands/timesheets-entries.js.map +7 -0
  154. package/dist/modules/staff/commands/timesheets-projects.js +618 -0
  155. package/dist/modules/staff/commands/timesheets-projects.js.map +7 -0
  156. package/dist/modules/staff/data/enrichers.js +104 -0
  157. package/dist/modules/staff/data/enrichers.js.map +7 -0
  158. package/dist/modules/staff/data/entities.js +226 -1
  159. package/dist/modules/staff/data/entities.js.map +2 -2
  160. package/dist/modules/staff/data/validators.js +113 -1
  161. package/dist/modules/staff/data/validators.js.map +2 -2
  162. package/dist/modules/staff/events.js +13 -1
  163. package/dist/modules/staff/events.js.map +2 -2
  164. package/dist/modules/staff/lib/crud.js +7 -1
  165. package/dist/modules/staff/lib/crud.js.map +2 -2
  166. package/dist/modules/staff/lib/staffMemberResolver.js +15 -0
  167. package/dist/modules/staff/lib/staffMemberResolver.js.map +7 -0
  168. package/dist/modules/staff/lib/timesheets-projects/computeProjectHoursTrend.js +60 -0
  169. package/dist/modules/staff/lib/timesheets-projects/computeProjectHoursTrend.js.map +7 -0
  170. package/dist/modules/staff/lib/timesheets-projects/computeProjectsKpis.js +260 -0
  171. package/dist/modules/staff/lib/timesheets-projects/computeProjectsKpis.js.map +7 -0
  172. package/dist/modules/staff/lib/timesheets-projects/dateBuckets.js +41 -0
  173. package/dist/modules/staff/lib/timesheets-projects/dateBuckets.js.map +7 -0
  174. package/dist/modules/staff/lib/timesheets-projects/initials.js +10 -0
  175. package/dist/modules/staff/lib/timesheets-projects/initials.js.map +7 -0
  176. package/dist/modules/staff/lib/timesheets-projects/kpiMath.js +12 -0
  177. package/dist/modules/staff/lib/timesheets-projects/kpiMath.js.map +7 -0
  178. package/dist/modules/staff/lib/timesheets-projects/listProjectMembersPreview.js +55 -0
  179. package/dist/modules/staff/lib/timesheets-projects/listProjectMembersPreview.js.map +7 -0
  180. package/dist/modules/staff/lib/timesheets-projects-ui/HoursSparkline.js +66 -0
  181. package/dist/modules/staff/lib/timesheets-projects-ui/HoursSparkline.js.map +7 -0
  182. package/dist/modules/staff/lib/timesheets-projects-ui/ProjectCard.js +81 -0
  183. package/dist/modules/staff/lib/timesheets-projects-ui/ProjectCard.js.map +7 -0
  184. package/dist/modules/staff/lib/timesheets-projects-ui/ProjectMembersAvatarStack.js +58 -0
  185. package/dist/modules/staff/lib/timesheets-projects-ui/ProjectMembersAvatarStack.js.map +7 -0
  186. package/dist/modules/staff/lib/timesheets-projects-ui/ProjectsKpiStrip.js +152 -0
  187. package/dist/modules/staff/lib/timesheets-projects-ui/ProjectsKpiStrip.js.map +7 -0
  188. package/dist/modules/staff/lib/timesheets-projects-ui/SavedViewTabs.js +37 -0
  189. package/dist/modules/staff/lib/timesheets-projects-ui/SavedViewTabs.js.map +7 -0
  190. package/dist/modules/staff/lib/timesheets-projects-ui/ViewModeToggle.js +57 -0
  191. package/dist/modules/staff/lib/timesheets-projects-ui/ViewModeToggle.js.map +7 -0
  192. package/dist/modules/staff/lib/timesheets-projects-ui/useProjectsViewMode.js +50 -0
  193. package/dist/modules/staff/lib/timesheets-projects-ui/useProjectsViewMode.js.map +7 -0
  194. package/dist/modules/staff/lib/timesheets-ui/AddRowDropdown.js +163 -0
  195. package/dist/modules/staff/lib/timesheets-ui/AddRowDropdown.js.map +7 -0
  196. package/dist/modules/staff/lib/timesheets-ui/CalendarPicker.js +209 -0
  197. package/dist/modules/staff/lib/timesheets-ui/CalendarPicker.js.map +7 -0
  198. package/dist/modules/staff/lib/timesheets-ui/ColorPicker.js +52 -0
  199. package/dist/modules/staff/lib/timesheets-ui/ColorPicker.js.map +7 -0
  200. package/dist/modules/staff/lib/timesheets-ui/CreateProjectDialog.js +77 -0
  201. package/dist/modules/staff/lib/timesheets-ui/CreateProjectDialog.js.map +7 -0
  202. package/dist/modules/staff/lib/timesheets-ui/ListView.js +173 -0
  203. package/dist/modules/staff/lib/timesheets-ui/ListView.js.map +7 -0
  204. package/dist/modules/staff/lib/timesheets-ui/ProjectColorDot.js +32 -0
  205. package/dist/modules/staff/lib/timesheets-ui/ProjectColorDot.js.map +7 -0
  206. package/dist/modules/staff/lib/timesheets-ui/TimerBar.js +270 -0
  207. package/dist/modules/staff/lib/timesheets-ui/TimerBar.js.map +7 -0
  208. package/dist/modules/staff/lib/timesheets-ui/ViewSwitcher.js +57 -0
  209. package/dist/modules/staff/lib/timesheets-ui/ViewSwitcher.js.map +7 -0
  210. package/dist/modules/staff/lib/timesheets-ui/colors.js +43 -0
  211. package/dist/modules/staff/lib/timesheets-ui/colors.js.map +7 -0
  212. package/dist/modules/staff/migrations/Migration20260326135612.js +24 -0
  213. package/dist/modules/staff/migrations/Migration20260326135612.js.map +7 -0
  214. package/dist/modules/staff/migrations/Migration20260413102715.js +23 -0
  215. package/dist/modules/staff/migrations/Migration20260413102715.js.map +7 -0
  216. package/dist/modules/staff/migrations/Migration20260413111602.js +13 -0
  217. package/dist/modules/staff/migrations/Migration20260413111602.js.map +7 -0
  218. package/dist/modules/staff/migrations/Migration20260511112759.js +19 -0
  219. package/dist/modules/staff/migrations/Migration20260511112759.js.map +7 -0
  220. package/dist/modules/staff/search.js +35 -0
  221. package/dist/modules/staff/search.js.map +2 -2
  222. package/dist/modules/staff/setup.js +15 -1
  223. package/dist/modules/staff/setup.js.map +2 -2
  224. package/dist/modules/staff/widgets/dashboard/timesheets-hours-by-project/config.js +16 -0
  225. package/dist/modules/staff/widgets/dashboard/timesheets-hours-by-project/config.js.map +7 -0
  226. package/dist/modules/staff/widgets/dashboard/timesheets-hours-by-project/widget.client.js +126 -0
  227. package/dist/modules/staff/widgets/dashboard/timesheets-hours-by-project/widget.client.js.map +7 -0
  228. package/dist/modules/staff/widgets/dashboard/timesheets-hours-by-project/widget.js +26 -0
  229. package/dist/modules/staff/widgets/dashboard/timesheets-hours-by-project/widget.js.map +7 -0
  230. package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/config.js +15 -0
  231. package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/config.js.map +7 -0
  232. package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js +238 -0
  233. package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js.map +7 -0
  234. package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.js +26 -0
  235. package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.js.map +7 -0
  236. package/dist/modules/staff/widgets/injection/timer-sidebar-indicator/widget.js +145 -0
  237. package/dist/modules/staff/widgets/injection/timer-sidebar-indicator/widget.js.map +7 -0
  238. package/dist/modules/staff/widgets/injection-table.js +12 -0
  239. package/dist/modules/staff/widgets/injection-table.js.map +7 -0
  240. package/dist/modules/sync_excel/api/import/route.js +19 -17
  241. package/dist/modules/sync_excel/api/import/route.js.map +2 -2
  242. package/dist/modules/translations/commands/translations.js +22 -19
  243. package/dist/modules/translations/commands/translations.js.map +2 -2
  244. package/generated/entities/staff_time_entry/index.ts +17 -0
  245. package/generated/entities/staff_time_entry_segment/index.ts +10 -0
  246. package/generated/entities/staff_time_project/index.ts +16 -0
  247. package/generated/entities/staff_time_project_member/index.ts +13 -0
  248. package/generated/entities.ids.generated.ts +5 -1
  249. package/generated/entity-fields-registry.ts +64 -0
  250. package/package.json +7 -7
  251. package/src/helpers/integration/timesheetFixtures.ts +61 -0
  252. package/src/modules/attachments/api/library/[id]/route.ts +24 -17
  253. package/src/modules/attachments/api/route.ts +20 -14
  254. package/src/modules/auth/api/roles/acl/route.ts +11 -5
  255. package/src/modules/auth/api/sidebar/preferences/route.ts +33 -24
  256. package/src/modules/auth/api/users/acl/route.ts +17 -12
  257. package/src/modules/auth/commands/users.ts +96 -80
  258. package/src/modules/auth/services/sidebarPreferencesService.ts +40 -32
  259. package/src/modules/catalog/commands/categories.ts +61 -12
  260. package/src/modules/catalog/commands/products.ts +93 -60
  261. package/src/modules/catalog/commands/variants.ts +29 -16
  262. package/src/modules/currencies/commands/currencies.ts +27 -14
  263. package/src/modules/customer_accounts/api/admin/users.ts +31 -26
  264. package/src/modules/customer_accounts/api/password/reset-confirm.ts +5 -6
  265. package/src/modules/customer_accounts/api/portal/users/[id]/roles.ts +14 -13
  266. package/src/modules/customers/commands/addresses.ts +35 -23
  267. package/src/modules/customers/commands/companies.ts +166 -165
  268. package/src/modules/customers/commands/deals.ts +2 -4
  269. package/src/modules/customers/commands/interactions.ts +20 -26
  270. package/src/modules/customers/commands/people.ts +18 -15
  271. package/src/modules/customers/commands/personCompanyLinks.ts +109 -100
  272. package/src/modules/customers/commands/pipeline-stages.ts +31 -27
  273. package/src/modules/customers/commands/pipelines.ts +29 -23
  274. package/src/modules/customers/commands/tags.ts +13 -5
  275. package/src/modules/dashboards/api/users/widgets/route.ts +0 -1
  276. package/src/modules/dashboards/api/widgets/data/route.ts +36 -1
  277. package/src/modules/data_sync/lib/sync-engine.ts +4 -5
  278. package/src/modules/data_sync/lib/sync-run-service.ts +57 -28
  279. package/src/modules/directory/commands/organizations.ts +203 -166
  280. package/src/modules/inbox_ops/api/emails/[id]/reprocess/route.ts +26 -18
  281. package/src/modules/messages/commands/messages.ts +82 -80
  282. package/src/modules/messages/commands/shared.ts +138 -133
  283. package/src/modules/perspectives/api/[tableId]/route.ts +38 -27
  284. package/src/modules/resources/commands/resources.ts +127 -117
  285. package/src/modules/resources/commands/tags.ts +7 -3
  286. package/src/modules/sales/api/quotes/send/route.ts +17 -12
  287. package/src/modules/sales/commands/documents.ts +673 -481
  288. package/src/modules/sales/commands/payments.ts +158 -152
  289. package/src/modules/sales/commands/returns.ts +74 -63
  290. package/src/modules/staff/acl.ts +11 -0
  291. package/src/modules/staff/analytics.ts +30 -0
  292. package/src/modules/staff/api/guards.ts +59 -0
  293. package/src/modules/staff/api/interceptors.ts +122 -0
  294. package/src/modules/staff/api/timesheets/my-projects/[projectId]/route.ts +191 -0
  295. package/src/modules/staff/api/timesheets/my-projects/route.ts +115 -0
  296. package/src/modules/staff/api/timesheets/projects/kpis/route.ts +159 -0
  297. package/src/modules/staff/api/timesheets/time-entries/[id]/segments/[segmentId]/route.ts +187 -0
  298. package/src/modules/staff/api/timesheets/time-entries/[id]/segments/route.ts +191 -0
  299. package/src/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.ts +168 -0
  300. package/src/modules/staff/api/timesheets/time-entries/[id]/timer-stop/route.ts +191 -0
  301. package/src/modules/staff/api/timesheets/time-entries/bulk/route.ts +292 -0
  302. package/src/modules/staff/api/timesheets/time-entries/route.ts +193 -0
  303. package/src/modules/staff/api/timesheets/time-projects/[id]/employees/route.ts +167 -0
  304. package/src/modules/staff/api/timesheets/time-projects/route.ts +244 -0
  305. package/src/modules/staff/backend/staff/timesheets/page.meta.ts +20 -0
  306. package/src/modules/staff/backend/staff/timesheets/page.tsx +899 -0
  307. package/src/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.meta.ts +12 -0
  308. package/src/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.tsx +141 -0
  309. package/src/modules/staff/backend/staff/timesheets/projects/[id]/page.meta.ts +12 -0
  310. package/src/modules/staff/backend/staff/timesheets/projects/[id]/page.tsx +579 -0
  311. package/src/modules/staff/backend/staff/timesheets/projects/create/page.meta.ts +12 -0
  312. package/src/modules/staff/backend/staff/timesheets/projects/create/page.tsx +90 -0
  313. package/src/modules/staff/backend/staff/timesheets/projects/page.meta.ts +23 -0
  314. package/src/modules/staff/backend/staff/timesheets/projects/page.tsx +765 -0
  315. package/src/modules/staff/backend/staff/timesheets/projects/projectFormConfig.ts +138 -0
  316. package/src/modules/staff/cli.ts +40 -1
  317. package/src/modules/staff/commands/index.ts +2 -0
  318. package/src/modules/staff/commands/leave-requests.ts +37 -29
  319. package/src/modules/staff/commands/team-members.ts +25 -20
  320. package/src/modules/staff/commands/timesheets-entries.ts +504 -0
  321. package/src/modules/staff/commands/timesheets-projects.ts +699 -0
  322. package/src/modules/staff/data/enrichers.ts +134 -0
  323. package/src/modules/staff/data/entities.ts +198 -0
  324. package/src/modules/staff/data/validators.ts +129 -0
  325. package/src/modules/staff/events.ts +13 -0
  326. package/src/modules/staff/i18n/de.json +209 -1
  327. package/src/modules/staff/i18n/en.json +209 -1
  328. package/src/modules/staff/i18n/es.json +209 -1
  329. package/src/modules/staff/i18n/pl.json +209 -1
  330. package/src/modules/staff/lib/crud.ts +8 -0
  331. package/src/modules/staff/lib/staffMemberResolver.ts +22 -0
  332. package/src/modules/staff/lib/timesheets-projects/computeProjectHoursTrend.ts +89 -0
  333. package/src/modules/staff/lib/timesheets-projects/computeProjectsKpis.ts +311 -0
  334. package/src/modules/staff/lib/timesheets-projects/dateBuckets.ts +37 -0
  335. package/src/modules/staff/lib/timesheets-projects/initials.ts +6 -0
  336. package/src/modules/staff/lib/timesheets-projects/kpiMath.ts +8 -0
  337. package/src/modules/staff/lib/timesheets-projects/listProjectMembersPreview.ts +83 -0
  338. package/src/modules/staff/lib/timesheets-projects-ui/HoursSparkline.tsx +75 -0
  339. package/src/modules/staff/lib/timesheets-projects-ui/ProjectCard.tsx +110 -0
  340. package/src/modules/staff/lib/timesheets-projects-ui/ProjectMembersAvatarStack.tsx +73 -0
  341. package/src/modules/staff/lib/timesheets-projects-ui/ProjectsKpiStrip.tsx +185 -0
  342. package/src/modules/staff/lib/timesheets-projects-ui/SavedViewTabs.tsx +53 -0
  343. package/src/modules/staff/lib/timesheets-projects-ui/ViewModeToggle.tsx +63 -0
  344. package/src/modules/staff/lib/timesheets-projects-ui/useProjectsViewMode.ts +63 -0
  345. package/src/modules/staff/lib/timesheets-ui/AddRowDropdown.tsx +188 -0
  346. package/src/modules/staff/lib/timesheets-ui/CalendarPicker.tsx +229 -0
  347. package/src/modules/staff/lib/timesheets-ui/ColorPicker.tsx +65 -0
  348. package/src/modules/staff/lib/timesheets-ui/CreateProjectDialog.tsx +99 -0
  349. package/src/modules/staff/lib/timesheets-ui/ListView.tsx +230 -0
  350. package/src/modules/staff/lib/timesheets-ui/ProjectColorDot.tsx +40 -0
  351. package/src/modules/staff/lib/timesheets-ui/TimerBar.tsx +327 -0
  352. package/src/modules/staff/lib/timesheets-ui/ViewSwitcher.tsx +60 -0
  353. package/src/modules/staff/lib/timesheets-ui/colors.ts +58 -0
  354. package/src/modules/staff/migrations/.snapshot-open-mercato.json +1148 -0
  355. package/src/modules/staff/migrations/Migration20260326135612.ts +26 -0
  356. package/src/modules/staff/migrations/Migration20260413102715.ts +25 -0
  357. package/src/modules/staff/migrations/Migration20260413111602.ts +13 -0
  358. package/src/modules/staff/migrations/Migration20260511112759.ts +21 -0
  359. package/src/modules/staff/search.ts +35 -0
  360. package/src/modules/staff/setup.ts +15 -0
  361. package/src/modules/staff/widgets/dashboard/timesheets-hours-by-project/config.ts +17 -0
  362. package/src/modules/staff/widgets/dashboard/timesheets-hours-by-project/widget.client.tsx +158 -0
  363. package/src/modules/staff/widgets/dashboard/timesheets-hours-by-project/widget.ts +25 -0
  364. package/src/modules/staff/widgets/dashboard/timesheets-time-reporting/config.ts +15 -0
  365. package/src/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.tsx +297 -0
  366. package/src/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.ts +25 -0
  367. package/src/modules/staff/widgets/injection/timer-sidebar-indicator/widget.tsx +161 -0
  368. package/src/modules/staff/widgets/injection-table.ts +10 -0
  369. package/src/modules/sync_excel/api/import/route.ts +23 -18
  370. package/src/modules/translations/commands/translations.ts +49 -41
@@ -4,6 +4,7 @@ import { randomUUID } from "crypto";
4
4
  import { z } from "zod";
5
5
  import { registerCommand } from "@open-mercato/shared/lib/commands";
6
6
  import type { CommandHandler } from "@open-mercato/shared/lib/commands";
7
+ import { withAtomicFlush } from "@open-mercato/shared/lib/commands/flush";
7
8
  import {
8
9
  buildChanges,
9
10
  emitCrudSideEffects,
@@ -4561,7 +4562,6 @@ const createQuoteCommand: CommandHandler<
4561
4562
  createdAt: new Date(),
4562
4563
  updatedAt: new Date(),
4563
4564
  });
4564
- em.persist(quote);
4565
4565
 
4566
4566
  const lineInputs = (parsed.lines ?? []).map((line, index) =>
4567
4567
  quoteLineCreateSchema.parse({
@@ -4631,32 +4631,48 @@ const createQuoteCommand: CommandHandler<
4631
4631
  context: calculationContext,
4632
4632
  });
4633
4633
 
4634
- await replaceQuoteLines(em, quote, calculation, normalizedLineInputs);
4635
- await replaceQuoteAdjustments(em, quote, calculation, adjustmentInputs);
4636
- applyQuoteTotals(quote, calculation.totals, calculation.lines.length);
4637
4634
  let eventBus: EventBus | null = null;
4638
4635
  try {
4639
4636
  eventBus = ctx.container.resolve("eventBus") as EventBus;
4640
4637
  } catch {
4641
4638
  eventBus = null;
4642
4639
  }
4643
- await emitTotalsCalculated(eventBus, {
4644
- documentKind: "quote",
4645
- documentId: quote.id,
4646
- organizationId: quote.organizationId,
4647
- tenantId: quote.tenantId,
4648
- customerId: quote.customerEntityId ?? null,
4649
- totals: calculation.totals,
4650
- lineCount: calculation.lines.length,
4651
- });
4652
- await syncSalesDocumentTags(em, {
4653
- documentId: quote.id,
4654
- kind: "quote",
4655
- organizationId: quote.organizationId,
4656
- tenantId: quote.tenantId,
4657
- tagIds: parsed.tags,
4658
- });
4659
- await em.flush();
4640
+ // Persist the quote header, lines, adjustments, totals and tags atomically.
4641
+ // replace*/setRecordCustomFields flush mid-build, so without a transaction a
4642
+ // later failure (e.g. tag sync) would leave a half-built quote committed (#2336).
4643
+ await withAtomicFlush(
4644
+ em,
4645
+ [
4646
+ async () => {
4647
+ em.persist(quote);
4648
+ await replaceQuoteLines(em, quote, calculation, normalizedLineInputs);
4649
+ await replaceQuoteAdjustments(
4650
+ em,
4651
+ quote,
4652
+ calculation,
4653
+ adjustmentInputs,
4654
+ );
4655
+ applyQuoteTotals(quote, calculation.totals, calculation.lines.length);
4656
+ await emitTotalsCalculated(eventBus, {
4657
+ documentKind: "quote",
4658
+ documentId: quote.id,
4659
+ organizationId: quote.organizationId,
4660
+ tenantId: quote.tenantId,
4661
+ customerId: quote.customerEntityId ?? null,
4662
+ totals: calculation.totals,
4663
+ lineCount: calculation.lines.length,
4664
+ });
4665
+ await syncSalesDocumentTags(em, {
4666
+ documentId: quote.id,
4667
+ kind: "quote",
4668
+ organizationId: quote.organizationId,
4669
+ tenantId: quote.tenantId,
4670
+ tagIds: parsed.tags,
4671
+ });
4672
+ },
4673
+ ],
4674
+ { transaction: true },
4675
+ );
4660
4676
 
4661
4677
  // Create notification for users with sales.quotes.manage feature
4662
4678
  try {
@@ -4814,22 +4830,31 @@ const deleteQuoteCommand: CommandHandler<
4814
4830
  em.find(SalesQuoteAdjustment, { quote: quote.id }),
4815
4831
  em.find(SalesQuoteLine, { quote: quote.id }),
4816
4832
  ]);
4817
- await em.nativeDelete(SalesDocumentAddress, {
4818
- documentId: quote.id,
4819
- documentKind: "quote",
4820
- });
4821
- await em.nativeDelete(SalesNote, {
4822
- contextType: "quote",
4823
- contextId: quote.id,
4824
- });
4825
- await em.nativeDelete(SalesDocumentTagAssignment, {
4826
- documentId: quote.id,
4827
- documentKind: "quote",
4828
- });
4829
- await em.nativeDelete(SalesQuoteAdjustment, { quote: quote.id });
4830
- await em.nativeDelete(SalesQuoteLine, { quote: quote.id });
4831
- em.remove(quote);
4832
- await em.flush();
4833
+ // Delete the quote and its cascade atomically so a mid-cascade failure
4834
+ // cannot leave a partially-deleted quote committed (#2336).
4835
+ await withAtomicFlush(
4836
+ em,
4837
+ [
4838
+ async () => {
4839
+ await em.nativeDelete(SalesDocumentAddress, {
4840
+ documentId: quote.id,
4841
+ documentKind: "quote",
4842
+ });
4843
+ await em.nativeDelete(SalesNote, {
4844
+ contextType: "quote",
4845
+ contextId: quote.id,
4846
+ });
4847
+ await em.nativeDelete(SalesDocumentTagAssignment, {
4848
+ documentId: quote.id,
4849
+ documentKind: "quote",
4850
+ });
4851
+ await em.nativeDelete(SalesQuoteAdjustment, { quote: quote.id });
4852
+ await em.nativeDelete(SalesQuoteLine, { quote: quote.id });
4853
+ em.remove(quote);
4854
+ },
4855
+ ],
4856
+ { transaction: true },
4857
+ );
4833
4858
  const dataEngine = ctx.container.resolve<DataEngine>("dataEngine");
4834
4859
  await Promise.all([
4835
4860
  queueDeletionSideEffects(dataEngine, quote, E.sales.sales_quote),
@@ -4923,107 +4948,125 @@ const updateQuoteCommand: CommandHandler<
4923
4948
  parsed.paymentMethodSnapshot !== undefined ||
4924
4949
  parsed.paymentMethodCode !== undefined ||
4925
4950
  parsed.currencyCode !== undefined;
4926
- await applyDocumentUpdate({
4927
- kind: "quote",
4928
- entity: quote,
4929
- input: parsed,
4951
+ // Apply the scalar update, optional sent-token reset, and totals recalc
4952
+ // atomically. applyDocumentUpdate/replace*/setRecordCustomFields flush
4953
+ // mid-build, so without a transaction a later failure would leave a
4954
+ // half-updated quote committed (#2336).
4955
+ await withAtomicFlush(
4930
4956
  em,
4931
- });
4932
- await em.flush();
4933
- if (shouldInvalidateSentToken) {
4934
- quote.status = "draft";
4935
- quote.statusEntryId = await resolveStatusEntryIdByValue(em, {
4936
- tenantId: quote.tenantId,
4937
- organizationId: quote.organizationId,
4938
- value: "draft",
4939
- });
4940
- }
4941
- if (shouldRecalculateTotals) {
4942
- const [existingLines, adjustments] = await Promise.all([
4943
- em.find(SalesQuoteLine, { quote }, { orderBy: { lineNumber: "asc" } }),
4944
- em.find(
4945
- SalesQuoteAdjustment,
4946
- { quote },
4947
- { orderBy: { position: "asc" } },
4948
- ),
4949
- ]);
4950
- const lineSnapshots = existingLines.map(mapQuoteLineEntityToSnapshot);
4951
- const calcLines = lineSnapshots.map((line, index) =>
4952
- createLineSnapshotFromInput(
4953
- {
4954
- ...line,
4955
- organizationId: quote.organizationId,
4956
- tenantId: quote.tenantId,
4957
- quoteId: quote.id,
4958
- lineNumber: line.lineNumber ?? index + 1,
4959
- statusEntryId: (line as any).statusEntryId ?? null,
4960
- catalogSnapshot: (line as any).catalogSnapshot ?? null,
4961
- promotionSnapshot: (line as any).promotionSnapshot ?? null,
4962
- },
4963
- line.lineNumber ?? index + 1,
4964
- ),
4965
- );
4966
- const adjustmentDrafts = adjustments.map(mapQuoteAdjustmentToDraft);
4967
- const salesCalculationService =
4968
- ctx.container.resolve<SalesCalculationService>(
4969
- "salesCalculationService",
4970
- );
4971
- const calculationContext = buildCalculationContext({
4972
- tenantId: quote.tenantId,
4973
- organizationId: quote.organizationId,
4974
- currencyCode: quote.currencyCode,
4975
- shippingSnapshot: quote.shippingMethodSnapshot,
4976
- paymentSnapshot: quote.paymentMethodSnapshot,
4977
- shippingMethodId: quote.shippingMethodId ?? null,
4978
- paymentMethodId: quote.paymentMethodId ?? null,
4979
- shippingMethodCode: quote.shippingMethodCode ?? null,
4980
- paymentMethodCode: quote.paymentMethodCode ?? null,
4981
- });
4982
- const calculation = await salesCalculationService.calculateDocumentTotals(
4983
- {
4984
- documentKind: "quote",
4985
- lines: calcLines,
4986
- adjustments: adjustmentDrafts,
4987
- context: calculationContext,
4957
+ [
4958
+ async () => {
4959
+ await applyDocumentUpdate({
4960
+ kind: "quote",
4961
+ entity: quote,
4962
+ input: parsed,
4963
+ em,
4964
+ });
4965
+ if (shouldInvalidateSentToken) {
4966
+ quote.status = "draft";
4967
+ quote.statusEntryId = await resolveStatusEntryIdByValue(em, {
4968
+ tenantId: quote.tenantId,
4969
+ organizationId: quote.organizationId,
4970
+ value: "draft",
4971
+ });
4972
+ }
4973
+ if (shouldRecalculateTotals) {
4974
+ const [existingLines, adjustments] = await Promise.all([
4975
+ em.find(
4976
+ SalesQuoteLine,
4977
+ { quote },
4978
+ { orderBy: { lineNumber: "asc" } },
4979
+ ),
4980
+ em.find(
4981
+ SalesQuoteAdjustment,
4982
+ { quote },
4983
+ { orderBy: { position: "asc" } },
4984
+ ),
4985
+ ]);
4986
+ const lineSnapshots = existingLines.map(mapQuoteLineEntityToSnapshot);
4987
+ const calcLines = lineSnapshots.map((line, index) =>
4988
+ createLineSnapshotFromInput(
4989
+ {
4990
+ ...line,
4991
+ organizationId: quote.organizationId,
4992
+ tenantId: quote.tenantId,
4993
+ quoteId: quote.id,
4994
+ lineNumber: line.lineNumber ?? index + 1,
4995
+ statusEntryId: (line as any).statusEntryId ?? null,
4996
+ catalogSnapshot: (line as any).catalogSnapshot ?? null,
4997
+ promotionSnapshot: (line as any).promotionSnapshot ?? null,
4998
+ },
4999
+ line.lineNumber ?? index + 1,
5000
+ ),
5001
+ );
5002
+ const adjustmentDrafts = adjustments.map(mapQuoteAdjustmentToDraft);
5003
+ const salesCalculationService =
5004
+ ctx.container.resolve<SalesCalculationService>(
5005
+ "salesCalculationService",
5006
+ );
5007
+ const calculationContext = buildCalculationContext({
5008
+ tenantId: quote.tenantId,
5009
+ organizationId: quote.organizationId,
5010
+ currencyCode: quote.currencyCode,
5011
+ shippingSnapshot: quote.shippingMethodSnapshot,
5012
+ paymentSnapshot: quote.paymentMethodSnapshot,
5013
+ shippingMethodId: quote.shippingMethodId ?? null,
5014
+ paymentMethodId: quote.paymentMethodId ?? null,
5015
+ shippingMethodCode: quote.shippingMethodCode ?? null,
5016
+ paymentMethodCode: quote.paymentMethodCode ?? null,
5017
+ });
5018
+ const calculation =
5019
+ await salesCalculationService.calculateDocumentTotals({
5020
+ documentKind: "quote",
5021
+ lines: calcLines,
5022
+ adjustments: adjustmentDrafts,
5023
+ context: calculationContext,
5024
+ });
5025
+ const adjustmentInputs = adjustmentDrafts.map((adj, index) => ({
5026
+ organizationId: quote.organizationId,
5027
+ tenantId: quote.tenantId,
5028
+ quoteId: quote.id,
5029
+ scope: adj.scope ?? "order",
5030
+ kind: adj.kind ?? "custom",
5031
+ code: adj.code ?? undefined,
5032
+ label: adj.label ?? undefined,
5033
+ calculatorKey: adj.calculatorKey ?? undefined,
5034
+ promotionId: adj.promotionId ?? undefined,
5035
+ rate: adj.rate ?? undefined,
5036
+ amountNet: adj.amountNet ?? undefined,
5037
+ amountGross: adj.amountGross ?? undefined,
5038
+ currencyCode: adj.currencyCode ?? quote.currencyCode,
5039
+ metadata: adj.metadata ?? undefined,
5040
+ position: adj.position ?? index,
5041
+ }));
5042
+ await replaceQuoteAdjustments(
5043
+ em,
5044
+ quote,
5045
+ calculation,
5046
+ adjustmentInputs,
5047
+ );
5048
+ applyQuoteTotals(quote, calculation.totals, calculation.lines.length);
5049
+ let eventBus: EventBus | null = null;
5050
+ try {
5051
+ eventBus = ctx.container.resolve("eventBus") as EventBus;
5052
+ } catch {
5053
+ eventBus = null;
5054
+ }
5055
+ await emitTotalsCalculated(eventBus, {
5056
+ documentKind: "quote",
5057
+ documentId: quote.id,
5058
+ organizationId: quote.organizationId,
5059
+ tenantId: quote.tenantId,
5060
+ customerId: quote.customerEntityId ?? null,
5061
+ totals: calculation.totals,
5062
+ lineCount: calculation.lines.length,
5063
+ });
5064
+ }
5065
+ quote.updatedAt = new Date();
4988
5066
  },
4989
- );
4990
- const adjustmentInputs = adjustmentDrafts.map((adj, index) => ({
4991
- organizationId: quote.organizationId,
4992
- tenantId: quote.tenantId,
4993
- quoteId: quote.id,
4994
- scope: adj.scope ?? "order",
4995
- kind: adj.kind ?? "custom",
4996
- code: adj.code ?? undefined,
4997
- label: adj.label ?? undefined,
4998
- calculatorKey: adj.calculatorKey ?? undefined,
4999
- promotionId: adj.promotionId ?? undefined,
5000
- rate: adj.rate ?? undefined,
5001
- amountNet: adj.amountNet ?? undefined,
5002
- amountGross: adj.amountGross ?? undefined,
5003
- currencyCode: adj.currencyCode ?? quote.currencyCode,
5004
- metadata: adj.metadata ?? undefined,
5005
- position: adj.position ?? index,
5006
- }));
5007
- await replaceQuoteAdjustments(em, quote, calculation, adjustmentInputs);
5008
- applyQuoteTotals(quote, calculation.totals, calculation.lines.length);
5009
- let eventBus: EventBus | null = null;
5010
- try {
5011
- eventBus = ctx.container.resolve("eventBus") as EventBus;
5012
- } catch {
5013
- eventBus = null;
5014
- }
5015
- await emitTotalsCalculated(eventBus, {
5016
- documentKind: "quote",
5017
- documentId: quote.id,
5018
- organizationId: quote.organizationId,
5019
- tenantId: quote.tenantId,
5020
- customerId: quote.customerEntityId ?? null,
5021
- totals: calculation.totals,
5022
- lineCount: calculation.lines.length,
5023
- });
5024
- }
5025
- quote.updatedAt = new Date();
5026
- await em.flush();
5067
+ ],
5068
+ { transaction: true },
5069
+ );
5027
5070
  const resourceKind =
5028
5071
  deriveResourceFromCommandId(updateQuoteCommand.id) ?? "sales.quote";
5029
5072
  await invalidateCrudCache(
@@ -5126,106 +5169,124 @@ const updateOrderCommand: CommandHandler<
5126
5169
  parsed.paymentMethodSnapshot !== undefined ||
5127
5170
  parsed.paymentMethodCode !== undefined ||
5128
5171
  parsed.currencyCode !== undefined;
5129
- await applyDocumentUpdate({
5130
- kind: "order",
5131
- entity: order,
5132
- input: parsed,
5172
+ // Apply the scalar update, totals recalc, and status-change note atomically.
5173
+ // applyDocumentUpdate/replace*/setRecordCustomFields flush mid-build, so
5174
+ // without a transaction a later failure would leave a half-updated order
5175
+ // committed (#2336).
5176
+ await withAtomicFlush(
5133
5177
  em,
5134
- });
5135
- await em.flush();
5136
- if (shouldRecalculateTotals) {
5137
- const [existingLines, adjustments] = await Promise.all([
5138
- em.find(SalesOrderLine, { order }, { orderBy: { lineNumber: "asc" } }),
5139
- em.find(
5140
- SalesOrderAdjustment,
5141
- { order },
5142
- { orderBy: { position: "asc" } },
5143
- ),
5144
- ]);
5145
- const lineSnapshots = existingLines.map(mapOrderLineEntityToSnapshot);
5146
- const calcLines = lineSnapshots.map((line, index) =>
5147
- createLineSnapshotFromInput(
5148
- {
5149
- ...line,
5150
- organizationId: order.organizationId,
5151
- tenantId: order.tenantId,
5152
- orderId: order.id,
5153
- lineNumber: line.lineNumber ?? index + 1,
5154
- statusEntryId: (line as any).statusEntryId ?? null,
5155
- catalogSnapshot: (line as any).catalogSnapshot ?? null,
5156
- promotionSnapshot: (line as any).promotionSnapshot ?? null,
5157
- },
5158
- line.lineNumber ?? index + 1,
5159
- ),
5160
- );
5161
- const adjustmentDrafts = adjustments.map(mapOrderAdjustmentToDraft);
5162
- const salesCalculationService =
5163
- ctx.container.resolve<SalesCalculationService>(
5164
- "salesCalculationService",
5165
- );
5166
- const calculationContext = buildCalculationContext({
5167
- tenantId: order.tenantId,
5168
- organizationId: order.organizationId,
5169
- currencyCode: order.currencyCode,
5170
- shippingSnapshot: order.shippingMethodSnapshot,
5171
- paymentSnapshot: order.paymentMethodSnapshot,
5172
- shippingMethodId: order.shippingMethodId ?? null,
5173
- paymentMethodId: order.paymentMethodId ?? null,
5174
- shippingMethodCode: order.shippingMethodCode ?? null,
5175
- paymentMethodCode: order.paymentMethodCode ?? null,
5176
- });
5177
- const calculation = await salesCalculationService.calculateDocumentTotals(
5178
- {
5179
- documentKind: "order",
5180
- lines: calcLines,
5181
- adjustments: adjustmentDrafts,
5182
- context: calculationContext,
5183
- existingTotals: resolveExistingPaymentTotals(order),
5178
+ [
5179
+ async () => {
5180
+ await applyDocumentUpdate({
5181
+ kind: "order",
5182
+ entity: order,
5183
+ input: parsed,
5184
+ em,
5185
+ });
5186
+ if (shouldRecalculateTotals) {
5187
+ const [existingLines, adjustments] = await Promise.all([
5188
+ em.find(
5189
+ SalesOrderLine,
5190
+ { order },
5191
+ { orderBy: { lineNumber: "asc" } },
5192
+ ),
5193
+ em.find(
5194
+ SalesOrderAdjustment,
5195
+ { order },
5196
+ { orderBy: { position: "asc" } },
5197
+ ),
5198
+ ]);
5199
+ const lineSnapshots = existingLines.map(mapOrderLineEntityToSnapshot);
5200
+ const calcLines = lineSnapshots.map((line, index) =>
5201
+ createLineSnapshotFromInput(
5202
+ {
5203
+ ...line,
5204
+ organizationId: order.organizationId,
5205
+ tenantId: order.tenantId,
5206
+ orderId: order.id,
5207
+ lineNumber: line.lineNumber ?? index + 1,
5208
+ statusEntryId: (line as any).statusEntryId ?? null,
5209
+ catalogSnapshot: (line as any).catalogSnapshot ?? null,
5210
+ promotionSnapshot: (line as any).promotionSnapshot ?? null,
5211
+ },
5212
+ line.lineNumber ?? index + 1,
5213
+ ),
5214
+ );
5215
+ const adjustmentDrafts = adjustments.map(mapOrderAdjustmentToDraft);
5216
+ const salesCalculationService =
5217
+ ctx.container.resolve<SalesCalculationService>(
5218
+ "salesCalculationService",
5219
+ );
5220
+ const calculationContext = buildCalculationContext({
5221
+ tenantId: order.tenantId,
5222
+ organizationId: order.organizationId,
5223
+ currencyCode: order.currencyCode,
5224
+ shippingSnapshot: order.shippingMethodSnapshot,
5225
+ paymentSnapshot: order.paymentMethodSnapshot,
5226
+ shippingMethodId: order.shippingMethodId ?? null,
5227
+ paymentMethodId: order.paymentMethodId ?? null,
5228
+ shippingMethodCode: order.shippingMethodCode ?? null,
5229
+ paymentMethodCode: order.paymentMethodCode ?? null,
5230
+ });
5231
+ const calculation =
5232
+ await salesCalculationService.calculateDocumentTotals({
5233
+ documentKind: "order",
5234
+ lines: calcLines,
5235
+ adjustments: adjustmentDrafts,
5236
+ context: calculationContext,
5237
+ existingTotals: resolveExistingPaymentTotals(order),
5238
+ });
5239
+ const adjustmentInputs = adjustmentDrafts.map((adj, index) => ({
5240
+ organizationId: order.organizationId,
5241
+ tenantId: order.tenantId,
5242
+ orderId: order.id,
5243
+ scope: adj.scope ?? "order",
5244
+ kind: adj.kind ?? "custom",
5245
+ code: adj.code ?? undefined,
5246
+ label: adj.label ?? undefined,
5247
+ calculatorKey: adj.calculatorKey ?? undefined,
5248
+ promotionId: adj.promotionId ?? undefined,
5249
+ rate: adj.rate ?? undefined,
5250
+ amountNet: adj.amountNet ?? undefined,
5251
+ amountGross: adj.amountGross ?? undefined,
5252
+ currencyCode: adj.currencyCode ?? order.currencyCode,
5253
+ metadata: adj.metadata ?? undefined,
5254
+ position: adj.position ?? index,
5255
+ }));
5256
+ await replaceOrderAdjustments(
5257
+ em,
5258
+ order,
5259
+ calculation,
5260
+ adjustmentInputs,
5261
+ );
5262
+ applyOrderTotals(order, calculation.totals, calculation.lines.length);
5263
+ let eventBus: EventBus | null = null;
5264
+ try {
5265
+ eventBus = ctx.container.resolve("eventBus") as EventBus;
5266
+ } catch {
5267
+ eventBus = null;
5268
+ }
5269
+ await emitTotalsCalculated(eventBus, {
5270
+ documentKind: "order",
5271
+ documentId: order.id,
5272
+ organizationId: order.organizationId,
5273
+ tenantId: order.tenantId,
5274
+ customerId: order.customerEntityId ?? null,
5275
+ totals: calculation.totals,
5276
+ lineCount: calculation.lines.length,
5277
+ });
5278
+ }
5279
+ statusChangeNote = await appendOrderStatusChangeNote({
5280
+ em,
5281
+ order,
5282
+ previousStatus,
5283
+ auth: ctx.auth ?? null,
5284
+ });
5285
+ order.updatedAt = new Date();
5184
5286
  },
5185
- );
5186
- const adjustmentInputs = adjustmentDrafts.map((adj, index) => ({
5187
- organizationId: order.organizationId,
5188
- tenantId: order.tenantId,
5189
- orderId: order.id,
5190
- scope: adj.scope ?? "order",
5191
- kind: adj.kind ?? "custom",
5192
- code: adj.code ?? undefined,
5193
- label: adj.label ?? undefined,
5194
- calculatorKey: adj.calculatorKey ?? undefined,
5195
- promotionId: adj.promotionId ?? undefined,
5196
- rate: adj.rate ?? undefined,
5197
- amountNet: adj.amountNet ?? undefined,
5198
- amountGross: adj.amountGross ?? undefined,
5199
- currencyCode: adj.currencyCode ?? order.currencyCode,
5200
- metadata: adj.metadata ?? undefined,
5201
- position: adj.position ?? index,
5202
- }));
5203
- await replaceOrderAdjustments(em, order, calculation, adjustmentInputs);
5204
- applyOrderTotals(order, calculation.totals, calculation.lines.length);
5205
- let eventBus: EventBus | null = null;
5206
- try {
5207
- eventBus = ctx.container.resolve("eventBus") as EventBus;
5208
- } catch {
5209
- eventBus = null;
5210
- }
5211
- await emitTotalsCalculated(eventBus, {
5212
- documentKind: "order",
5213
- documentId: order.id,
5214
- organizationId: order.organizationId,
5215
- tenantId: order.tenantId,
5216
- customerId: order.customerEntityId ?? null,
5217
- totals: calculation.totals,
5218
- lineCount: calculation.lines.length,
5219
- });
5220
- }
5221
- statusChangeNote = await appendOrderStatusChangeNote({
5222
- em,
5223
- order,
5224
- previousStatus,
5225
- auth: ctx.auth ?? null,
5226
- });
5227
- order.updatedAt = new Date();
5228
- await em.flush();
5287
+ ],
5288
+ { transaction: true },
5289
+ );
5229
5290
  if (statusChangeNote) {
5230
5291
  const dataEngine = ctx.container.resolve("dataEngine");
5231
5292
  await emitCrudSideEffects({
@@ -5481,8 +5542,6 @@ const createOrderCommand: CommandHandler<
5481
5542
  createdAt: new Date(),
5482
5543
  updatedAt: new Date(),
5483
5544
  });
5484
- em.persist(order);
5485
-
5486
5545
  const lineInputs = (parsed.lines ?? []).map((line, index) =>
5487
5546
  orderLineCreateSchema.parse({
5488
5547
  ...line,
@@ -5552,32 +5611,48 @@ const createOrderCommand: CommandHandler<
5552
5611
  existingTotals: resolveExistingPaymentTotals(order),
5553
5612
  });
5554
5613
 
5555
- await replaceOrderLines(em, order, calculation, normalizedLineInputs);
5556
- await replaceOrderAdjustments(em, order, calculation, adjustmentInputs);
5557
- applyOrderTotals(order, calculation.totals, calculation.lines.length);
5558
5614
  let eventBus: EventBus | null = null;
5559
5615
  try {
5560
5616
  eventBus = ctx.container.resolve("eventBus") as EventBus;
5561
5617
  } catch {
5562
5618
  eventBus = null;
5563
5619
  }
5564
- await emitTotalsCalculated(eventBus, {
5565
- documentKind: "order",
5566
- documentId: order.id,
5567
- organizationId: order.organizationId,
5568
- tenantId: order.tenantId,
5569
- customerId: order.customerEntityId ?? null,
5570
- totals: calculation.totals,
5571
- lineCount: calculation.lines.length,
5572
- });
5573
- await syncSalesDocumentTags(em, {
5574
- documentId: order.id,
5575
- kind: "order",
5576
- organizationId: order.organizationId,
5577
- tenantId: order.tenantId,
5578
- tagIds: parsed.tags,
5579
- });
5580
- await em.flush();
5620
+ // Persist the order header, lines, adjustments, totals and tags atomically.
5621
+ // replace*/setRecordCustomFields flush mid-build, so without a transaction a
5622
+ // later failure (e.g. tag sync) would leave a half-built order committed (#2336).
5623
+ await withAtomicFlush(
5624
+ em,
5625
+ [
5626
+ async () => {
5627
+ em.persist(order);
5628
+ await replaceOrderLines(em, order, calculation, normalizedLineInputs);
5629
+ await replaceOrderAdjustments(
5630
+ em,
5631
+ order,
5632
+ calculation,
5633
+ adjustmentInputs,
5634
+ );
5635
+ applyOrderTotals(order, calculation.totals, calculation.lines.length);
5636
+ await emitTotalsCalculated(eventBus, {
5637
+ documentKind: "order",
5638
+ documentId: order.id,
5639
+ organizationId: order.organizationId,
5640
+ tenantId: order.tenantId,
5641
+ customerId: order.customerEntityId ?? null,
5642
+ totals: calculation.totals,
5643
+ lineCount: calculation.lines.length,
5644
+ });
5645
+ await syncSalesDocumentTags(em, {
5646
+ documentId: order.id,
5647
+ kind: "order",
5648
+ organizationId: order.organizationId,
5649
+ tenantId: order.tenantId,
5650
+ tagIds: parsed.tags,
5651
+ });
5652
+ },
5653
+ ],
5654
+ { transaction: true },
5655
+ );
5581
5656
 
5582
5657
  // Create notification for users with sales.orders.manage feature
5583
5658
  try {
@@ -5751,30 +5826,39 @@ const deleteOrderCommand: CommandHandler<
5751
5826
  em.find(SalesOrderAdjustment, { order: order.id }),
5752
5827
  em.find(SalesOrderLine, { order: order.id }),
5753
5828
  ]);
5754
- if (shipmentIds.length) {
5755
- await em.nativeDelete(SalesShipmentItem, {
5756
- shipment: { $in: shipmentIds },
5757
- });
5758
- await em.nativeDelete(SalesShipment, { id: { $in: shipmentIds } });
5759
- }
5760
- await em.nativeDelete(SalesPaymentAllocation, { order: order.id });
5761
- await em.nativeDelete(SalesPayment, { order: order.id });
5762
- await em.nativeDelete(SalesDocumentAddress, {
5763
- documentId: order.id,
5764
- documentKind: "order",
5765
- });
5766
- await em.nativeDelete(SalesNote, {
5767
- contextType: "order",
5768
- contextId: order.id,
5769
- });
5770
- await em.nativeDelete(SalesDocumentTagAssignment, {
5771
- documentId: order.id,
5772
- documentKind: "order",
5773
- });
5774
- await em.nativeDelete(SalesOrderAdjustment, { order: order.id });
5775
- await em.nativeDelete(SalesOrderLine, { order: order.id });
5776
- em.remove(order);
5777
- await em.flush();
5829
+ // Delete the order and its cascade atomically so a mid-cascade failure
5830
+ // cannot leave a partially-deleted order committed (#2336).
5831
+ await withAtomicFlush(
5832
+ em,
5833
+ [
5834
+ async () => {
5835
+ if (shipmentIds.length) {
5836
+ await em.nativeDelete(SalesShipmentItem, {
5837
+ shipment: { $in: shipmentIds },
5838
+ });
5839
+ await em.nativeDelete(SalesShipment, { id: { $in: shipmentIds } });
5840
+ }
5841
+ await em.nativeDelete(SalesPaymentAllocation, { order: order.id });
5842
+ await em.nativeDelete(SalesPayment, { order: order.id });
5843
+ await em.nativeDelete(SalesDocumentAddress, {
5844
+ documentId: order.id,
5845
+ documentKind: "order",
5846
+ });
5847
+ await em.nativeDelete(SalesNote, {
5848
+ contextType: "order",
5849
+ contextId: order.id,
5850
+ });
5851
+ await em.nativeDelete(SalesDocumentTagAssignment, {
5852
+ documentId: order.id,
5853
+ documentKind: "order",
5854
+ });
5855
+ await em.nativeDelete(SalesOrderAdjustment, { order: order.id });
5856
+ await em.nativeDelete(SalesOrderLine, { order: order.id });
5857
+ em.remove(order);
5858
+ },
5859
+ ],
5860
+ { transaction: true },
5861
+ );
5778
5862
  const dataEngine = ctx.container.resolve<DataEngine>("dataEngine");
5779
5863
  await Promise.all([
5780
5864
  queueDeletionSideEffects(dataEngine, order, E.sales.sales_order),
@@ -6581,30 +6665,39 @@ const orderLineUpsertCommand: CommandHandler<
6581
6665
  context: calculationContext,
6582
6666
  existingTotals: resolveExistingPaymentTotals(order),
6583
6667
  });
6584
- await applyOrderLineResults({
6585
- em,
6586
- order,
6587
- calculation,
6588
- sourceLines: sourceInputs,
6589
- existingLines,
6590
- });
6591
- applyOrderTotals(order, calculation.totals, calculation.lines.length);
6592
6668
  let eventBus: EventBus | null = null;
6593
6669
  try {
6594
6670
  eventBus = ctx.container.resolve("eventBus") as EventBus;
6595
6671
  } catch {
6596
6672
  eventBus = null;
6597
6673
  }
6598
- await emitTotalsCalculated(eventBus, {
6599
- documentKind: "order",
6600
- documentId: order.id,
6601
- organizationId: order.organizationId,
6602
- tenantId: order.tenantId,
6603
- customerId: order.customerEntityId ?? null,
6604
- totals: calculation.totals,
6605
- lineCount: calculation.lines.length,
6606
- });
6607
- await em.flush();
6674
+ // Persist the line changes and recalculated totals atomically so a
6675
+ // mid-build failure cannot leave a half-updated order committed (#2336).
6676
+ await withAtomicFlush(
6677
+ em,
6678
+ [
6679
+ async () => {
6680
+ await applyOrderLineResults({
6681
+ em,
6682
+ order,
6683
+ calculation,
6684
+ sourceLines: sourceInputs,
6685
+ existingLines,
6686
+ });
6687
+ applyOrderTotals(order, calculation.totals, calculation.lines.length);
6688
+ await emitTotalsCalculated(eventBus, {
6689
+ documentKind: "order",
6690
+ documentId: order.id,
6691
+ organizationId: order.organizationId,
6692
+ tenantId: order.tenantId,
6693
+ customerId: order.customerEntityId ?? null,
6694
+ totals: calculation.totals,
6695
+ lineCount: calculation.lines.length,
6696
+ });
6697
+ },
6698
+ ],
6699
+ { transaction: true },
6700
+ );
6608
6701
  return { orderId: order.id, lineId };
6609
6702
  },
6610
6703
  captureAfter: async (_input, result, ctx) => {
@@ -6749,30 +6842,39 @@ const orderLineDeleteCommand: CommandHandler<
6749
6842
  context: calculationContext,
6750
6843
  existingTotals: resolveExistingPaymentTotals(order),
6751
6844
  });
6752
- await applyOrderLineResults({
6753
- em,
6754
- order,
6755
- calculation,
6756
- sourceLines: sourceInputs,
6757
- existingLines,
6758
- });
6759
- applyOrderTotals(order, calculation.totals, calculation.lines.length);
6760
6845
  let eventBus: EventBus | null = null;
6761
6846
  try {
6762
6847
  eventBus = ctx.container.resolve("eventBus") as EventBus;
6763
6848
  } catch {
6764
6849
  eventBus = null;
6765
6850
  }
6766
- await emitTotalsCalculated(eventBus, {
6767
- documentKind: "order",
6768
- documentId: order.id,
6769
- organizationId: order.organizationId,
6770
- tenantId: order.tenantId,
6771
- customerId: order.customerEntityId ?? null,
6772
- totals: calculation.totals,
6773
- lineCount: calculation.lines.length,
6774
- });
6775
- await em.flush();
6851
+ // Persist the line removal and recalculated totals atomically so a
6852
+ // mid-build failure cannot leave a half-updated order committed (#2336).
6853
+ await withAtomicFlush(
6854
+ em,
6855
+ [
6856
+ async () => {
6857
+ await applyOrderLineResults({
6858
+ em,
6859
+ order,
6860
+ calculation,
6861
+ sourceLines: sourceInputs,
6862
+ existingLines,
6863
+ });
6864
+ applyOrderTotals(order, calculation.totals, calculation.lines.length);
6865
+ await emitTotalsCalculated(eventBus, {
6866
+ documentKind: "order",
6867
+ documentId: order.id,
6868
+ organizationId: order.organizationId,
6869
+ tenantId: order.tenantId,
6870
+ customerId: order.customerEntityId ?? null,
6871
+ totals: calculation.totals,
6872
+ lineCount: calculation.lines.length,
6873
+ });
6874
+ },
6875
+ ],
6876
+ { transaction: true },
6877
+ );
6776
6878
  return { orderId: order.id, lineId: parsed.id };
6777
6879
  },
6778
6880
  captureAfter: async (_input, result, ctx) => {
@@ -7050,30 +7152,39 @@ const quoteLineUpsertCommand: CommandHandler<
7050
7152
  adjustments: adjustmentDrafts,
7051
7153
  context: calculationContext,
7052
7154
  });
7053
- await applyQuoteLineResults({
7054
- em,
7055
- quote,
7056
- calculation,
7057
- sourceLines: sourceInputs,
7058
- existingLines,
7059
- });
7060
- applyQuoteTotals(quote, calculation.totals, calculation.lines.length);
7061
7155
  let eventBus: EventBus | null = null;
7062
7156
  try {
7063
7157
  eventBus = ctx.container.resolve("eventBus") as EventBus;
7064
7158
  } catch {
7065
7159
  eventBus = null;
7066
7160
  }
7067
- await emitTotalsCalculated(eventBus, {
7068
- documentKind: "quote",
7069
- documentId: quote.id,
7070
- organizationId: quote.organizationId,
7071
- tenantId: quote.tenantId,
7072
- customerId: quote.customerEntityId ?? null,
7073
- totals: calculation.totals,
7074
- lineCount: calculation.lines.length,
7075
- });
7076
- await em.flush();
7161
+ // Persist the line changes and recalculated totals atomically so a
7162
+ // mid-build failure cannot leave a half-updated quote committed (#2336).
7163
+ await withAtomicFlush(
7164
+ em,
7165
+ [
7166
+ async () => {
7167
+ await applyQuoteLineResults({
7168
+ em,
7169
+ quote,
7170
+ calculation,
7171
+ sourceLines: sourceInputs,
7172
+ existingLines,
7173
+ });
7174
+ applyQuoteTotals(quote, calculation.totals, calculation.lines.length);
7175
+ await emitTotalsCalculated(eventBus, {
7176
+ documentKind: "quote",
7177
+ documentId: quote.id,
7178
+ organizationId: quote.organizationId,
7179
+ tenantId: quote.tenantId,
7180
+ customerId: quote.customerEntityId ?? null,
7181
+ totals: calculation.totals,
7182
+ lineCount: calculation.lines.length,
7183
+ });
7184
+ },
7185
+ ],
7186
+ { transaction: true },
7187
+ );
7077
7188
  return { quoteId: quote.id, lineId };
7078
7189
  },
7079
7190
  captureAfter: async (_input, result, ctx) => {
@@ -7194,30 +7305,39 @@ const quoteLineDeleteCommand: CommandHandler<
7194
7305
  adjustments: adjustmentDrafts,
7195
7306
  context: calculationContext,
7196
7307
  });
7197
- await applyQuoteLineResults({
7198
- em,
7199
- quote,
7200
- calculation,
7201
- sourceLines: sourceInputs,
7202
- existingLines,
7203
- });
7204
- applyQuoteTotals(quote, calculation.totals, calculation.lines.length);
7205
7308
  let eventBus: EventBus | null = null;
7206
7309
  try {
7207
7310
  eventBus = ctx.container.resolve("eventBus") as EventBus;
7208
7311
  } catch {
7209
7312
  eventBus = null;
7210
7313
  }
7211
- await emitTotalsCalculated(eventBus, {
7212
- documentKind: "quote",
7213
- documentId: quote.id,
7214
- organizationId: quote.organizationId,
7215
- tenantId: quote.tenantId,
7216
- customerId: quote.customerEntityId ?? null,
7217
- totals: calculation.totals,
7218
- lineCount: calculation.lines.length,
7219
- });
7220
- await em.flush();
7314
+ // Persist the line removal and recalculated totals atomically so a
7315
+ // mid-build failure cannot leave a half-updated quote committed (#2336).
7316
+ await withAtomicFlush(
7317
+ em,
7318
+ [
7319
+ async () => {
7320
+ await applyQuoteLineResults({
7321
+ em,
7322
+ quote,
7323
+ calculation,
7324
+ sourceLines: sourceInputs,
7325
+ existingLines,
7326
+ });
7327
+ applyQuoteTotals(quote, calculation.totals, calculation.lines.length);
7328
+ await emitTotalsCalculated(eventBus, {
7329
+ documentKind: "quote",
7330
+ documentId: quote.id,
7331
+ organizationId: quote.organizationId,
7332
+ tenantId: quote.tenantId,
7333
+ customerId: quote.customerEntityId ?? null,
7334
+ totals: calculation.totals,
7335
+ lineCount: calculation.lines.length,
7336
+ });
7337
+ },
7338
+ ],
7339
+ { transaction: true },
7340
+ );
7221
7341
  return { quoteId: quote.id, lineId: parsed.id };
7222
7342
  },
7223
7343
  captureAfter: async (_input, result, ctx) => {
@@ -7483,25 +7603,34 @@ const orderAdjustmentUpsertCommand: CommandHandler<
7483
7603
  customFields: (adj as any).customFields ?? undefined,
7484
7604
  position: adj.position ?? index,
7485
7605
  }));
7486
- await replaceOrderAdjustments(em, order, calculation, adjustmentInputs);
7487
- applyOrderTotals(order, calculation.totals, calculation.lines.length);
7488
- order.updatedAt = new Date();
7489
7606
  let eventBus: EventBus | null = null;
7490
7607
  try {
7491
7608
  eventBus = ctx.container.resolve("eventBus") as EventBus;
7492
7609
  } catch {
7493
7610
  eventBus = null;
7494
7611
  }
7495
- await emitTotalsCalculated(eventBus, {
7496
- documentKind: "order",
7497
- documentId: order.id,
7498
- organizationId: order.organizationId,
7499
- tenantId: order.tenantId,
7500
- customerId: order.customerEntityId ?? null,
7501
- totals: calculation.totals,
7502
- lineCount: calculation.lines.length,
7503
- });
7504
- await em.flush();
7612
+ // Persist the adjustment change and recalculated totals atomically so a
7613
+ // mid-build failure cannot leave a half-updated order committed (#2336).
7614
+ await withAtomicFlush(
7615
+ em,
7616
+ [
7617
+ async () => {
7618
+ await replaceOrderAdjustments(em, order, calculation, adjustmentInputs);
7619
+ applyOrderTotals(order, calculation.totals, calculation.lines.length);
7620
+ order.updatedAt = new Date();
7621
+ await emitTotalsCalculated(eventBus, {
7622
+ documentKind: "order",
7623
+ documentId: order.id,
7624
+ organizationId: order.organizationId,
7625
+ tenantId: order.tenantId,
7626
+ customerId: order.customerEntityId ?? null,
7627
+ totals: calculation.totals,
7628
+ lineCount: calculation.lines.length,
7629
+ });
7630
+ },
7631
+ ],
7632
+ { transaction: true },
7633
+ );
7505
7634
  return { orderId: order.id, adjustmentId };
7506
7635
  },
7507
7636
  captureAfter: async (_input, result, ctx) => {
@@ -7638,25 +7767,34 @@ const orderAdjustmentDeleteCommand: CommandHandler<
7638
7767
  metadata: adj.metadata ?? undefined,
7639
7768
  position: adj.position ?? index,
7640
7769
  }));
7641
- await replaceOrderAdjustments(em, order, calculation, adjustmentInputs);
7642
- applyOrderTotals(order, calculation.totals, calculation.lines.length);
7643
- order.updatedAt = new Date();
7644
7770
  let eventBus: EventBus | null = null;
7645
7771
  try {
7646
7772
  eventBus = ctx.container.resolve("eventBus") as EventBus;
7647
7773
  } catch {
7648
7774
  eventBus = null;
7649
7775
  }
7650
- await emitTotalsCalculated(eventBus, {
7651
- documentKind: "order",
7652
- documentId: order.id,
7653
- organizationId: order.organizationId,
7654
- tenantId: order.tenantId,
7655
- customerId: order.customerEntityId ?? null,
7656
- totals: calculation.totals,
7657
- lineCount: calculation.lines.length,
7658
- });
7659
- await em.flush();
7776
+ // Persist the adjustment removal and recalculated totals atomically so a
7777
+ // mid-build failure cannot leave a half-updated order committed (#2336).
7778
+ await withAtomicFlush(
7779
+ em,
7780
+ [
7781
+ async () => {
7782
+ await replaceOrderAdjustments(em, order, calculation, adjustmentInputs);
7783
+ applyOrderTotals(order, calculation.totals, calculation.lines.length);
7784
+ order.updatedAt = new Date();
7785
+ await emitTotalsCalculated(eventBus, {
7786
+ documentKind: "order",
7787
+ documentId: order.id,
7788
+ organizationId: order.organizationId,
7789
+ tenantId: order.tenantId,
7790
+ customerId: order.customerEntityId ?? null,
7791
+ totals: calculation.totals,
7792
+ lineCount: calculation.lines.length,
7793
+ });
7794
+ },
7795
+ ],
7796
+ { transaction: true },
7797
+ );
7660
7798
  return { orderId: order.id, adjustmentId: parsed.id };
7661
7799
  },
7662
7800
  captureAfter: async (_input, result, ctx) => {
@@ -7920,25 +8058,34 @@ const quoteAdjustmentUpsertCommand: CommandHandler<
7920
8058
  customFields: (adj as any).customFields ?? undefined,
7921
8059
  position: adj.position ?? index,
7922
8060
  }));
7923
- await replaceQuoteAdjustments(em, quote, calculation, adjustmentInputs);
7924
- applyQuoteTotals(quote, calculation.totals, calculation.lines.length);
7925
- quote.updatedAt = new Date();
7926
8061
  let eventBus: EventBus | null = null;
7927
8062
  try {
7928
8063
  eventBus = ctx.container.resolve("eventBus") as EventBus;
7929
8064
  } catch {
7930
8065
  eventBus = null;
7931
8066
  }
7932
- await emitTotalsCalculated(eventBus, {
7933
- documentKind: "quote",
7934
- documentId: quote.id,
7935
- organizationId: quote.organizationId,
7936
- tenantId: quote.tenantId,
7937
- customerId: quote.customerEntityId ?? null,
7938
- totals: calculation.totals,
7939
- lineCount: calculation.lines.length,
7940
- });
7941
- await em.flush();
8067
+ // Persist the adjustment change and recalculated totals atomically so a
8068
+ // mid-build failure cannot leave a half-updated quote committed (#2336).
8069
+ await withAtomicFlush(
8070
+ em,
8071
+ [
8072
+ async () => {
8073
+ await replaceQuoteAdjustments(em, quote, calculation, adjustmentInputs);
8074
+ applyQuoteTotals(quote, calculation.totals, calculation.lines.length);
8075
+ quote.updatedAt = new Date();
8076
+ await emitTotalsCalculated(eventBus, {
8077
+ documentKind: "quote",
8078
+ documentId: quote.id,
8079
+ organizationId: quote.organizationId,
8080
+ tenantId: quote.tenantId,
8081
+ customerId: quote.customerEntityId ?? null,
8082
+ totals: calculation.totals,
8083
+ lineCount: calculation.lines.length,
8084
+ });
8085
+ },
8086
+ ],
8087
+ { transaction: true },
8088
+ );
7942
8089
  return { quoteId: quote.id, adjustmentId };
7943
8090
  },
7944
8091
  captureAfter: async (_input, result, ctx) => {
@@ -8074,25 +8221,34 @@ const quoteAdjustmentDeleteCommand: CommandHandler<
8074
8221
  metadata: adj.metadata ?? undefined,
8075
8222
  position: adj.position ?? index,
8076
8223
  }));
8077
- await replaceQuoteAdjustments(em, quote, calculation, adjustmentInputs);
8078
- applyQuoteTotals(quote, calculation.totals, calculation.lines.length);
8079
- quote.updatedAt = new Date();
8080
8224
  let eventBus: EventBus | null = null;
8081
8225
  try {
8082
8226
  eventBus = ctx.container.resolve("eventBus") as EventBus;
8083
8227
  } catch {
8084
8228
  eventBus = null;
8085
8229
  }
8086
- await emitTotalsCalculated(eventBus, {
8087
- documentKind: "quote",
8088
- documentId: quote.id,
8089
- organizationId: quote.organizationId,
8090
- tenantId: quote.tenantId,
8091
- customerId: quote.customerEntityId ?? null,
8092
- totals: calculation.totals,
8093
- lineCount: calculation.lines.length,
8094
- });
8095
- await em.flush();
8230
+ // Persist the adjustment removal and recalculated totals atomically so a
8231
+ // mid-build failure cannot leave a half-updated quote committed (#2336).
8232
+ await withAtomicFlush(
8233
+ em,
8234
+ [
8235
+ async () => {
8236
+ await replaceQuoteAdjustments(em, quote, calculation, adjustmentInputs);
8237
+ applyQuoteTotals(quote, calculation.totals, calculation.lines.length);
8238
+ quote.updatedAt = new Date();
8239
+ await emitTotalsCalculated(eventBus, {
8240
+ documentKind: "quote",
8241
+ documentId: quote.id,
8242
+ organizationId: quote.organizationId,
8243
+ tenantId: quote.tenantId,
8244
+ customerId: quote.customerEntityId ?? null,
8245
+ totals: calculation.totals,
8246
+ lineCount: calculation.lines.length,
8247
+ });
8248
+ },
8249
+ ],
8250
+ { transaction: true },
8251
+ );
8096
8252
  return { quoteId: quote.id, adjustmentId: parsed.id };
8097
8253
  },
8098
8254
  captureAfter: async (_input, result, ctx) => {
@@ -8218,48 +8374,66 @@ const createInvoiceCommand: CommandHandler<
8218
8374
  createdAt: new Date(),
8219
8375
  updatedAt: new Date(),
8220
8376
  });
8221
- em.persist(invoice);
8222
-
8223
- if (parsed.lines?.length) {
8224
- for (let i = 0; i < parsed.lines.length; i++) {
8225
- const line = parsed.lines[i];
8226
- em.persist(
8227
- em.create(SalesInvoiceLine, {
8228
- id: randomUUID(),
8229
- invoice,
8230
- orderLineId: line.orderLineId ?? null,
8231
- organizationId: parsed.organizationId,
8232
- tenantId: parsed.tenantId,
8233
- lineNumber: line.lineNumber ?? i + 1,
8234
- kind: line.kind ?? "product",
8235
- name: line.name ?? null,
8236
- sku: line.sku ?? null,
8237
- description: line.description ?? null,
8238
- quantity: toNumericString(line.quantity ?? 0),
8239
- quantityUnit: line.quantityUnit ?? null,
8240
- normalizedQuantity: toNumericString(line.normalizedQuantity ?? 0),
8241
- normalizedUnit: line.normalizedUnit ?? null,
8242
- uomSnapshot: line.uomSnapshot ?? null,
8243
- currencyCode: line.currencyCode ?? parsed.currencyCode,
8244
- unitPriceNet: toNumericString(line.unitPriceNet ?? 0),
8245
- unitPriceGross: toNumericString(line.unitPriceGross ?? 0),
8246
- discountAmount: toNumericString(line.discountAmount ?? 0),
8247
- discountPercent: toNumericString(line.discountPercent ?? 0),
8248
- taxRate: toNumericString(line.taxRate ?? 0),
8249
- taxAmount: toNumericString(line.taxAmount ?? 0),
8250
- totalNetAmount: toNumericString(line.totalNetAmount ?? 0),
8251
- totalGrossAmount: toNumericString(line.totalGrossAmount ?? 0),
8252
- metadata: line.metadata ?? null,
8253
- }),
8254
- );
8255
- }
8256
- }
8257
8377
 
8258
- if (parsed.customFieldSetId) {
8259
- await setRecordCustomFields(em, invoiceId, parsed.customFieldSetId, parsed);
8260
- }
8378
+ // Header + lines + custom-field writes must commit atomically.
8379
+ // setRecordCustomFields flushes mid-build, so without a transaction a
8380
+ // partial write could persist the header without its lines/custom fields.
8381
+ await withAtomicFlush(
8382
+ em,
8383
+ [
8384
+ async () => {
8385
+ em.persist(invoice);
8386
+
8387
+ if (parsed.lines?.length) {
8388
+ for (let i = 0; i < parsed.lines.length; i++) {
8389
+ const line = parsed.lines[i];
8390
+ em.persist(
8391
+ em.create(SalesInvoiceLine, {
8392
+ id: randomUUID(),
8393
+ invoice,
8394
+ orderLineId: line.orderLineId ?? null,
8395
+ organizationId: parsed.organizationId,
8396
+ tenantId: parsed.tenantId,
8397
+ lineNumber: line.lineNumber ?? i + 1,
8398
+ kind: line.kind ?? "product",
8399
+ name: line.name ?? null,
8400
+ sku: line.sku ?? null,
8401
+ description: line.description ?? null,
8402
+ quantity: toNumericString(line.quantity ?? 0),
8403
+ quantityUnit: line.quantityUnit ?? null,
8404
+ normalizedQuantity: toNumericString(line.normalizedQuantity ?? 0),
8405
+ normalizedUnit: line.normalizedUnit ?? null,
8406
+ uomSnapshot: line.uomSnapshot ?? null,
8407
+ currencyCode: line.currencyCode ?? parsed.currencyCode,
8408
+ unitPriceNet: toNumericString(line.unitPriceNet ?? 0),
8409
+ unitPriceGross: toNumericString(line.unitPriceGross ?? 0),
8410
+ discountAmount: toNumericString(line.discountAmount ?? 0),
8411
+ discountPercent: toNumericString(line.discountPercent ?? 0),
8412
+ taxRate: toNumericString(line.taxRate ?? 0),
8413
+ taxAmount: toNumericString(line.taxAmount ?? 0),
8414
+ totalNetAmount: toNumericString(line.totalNetAmount ?? 0),
8415
+ totalGrossAmount: toNumericString(line.totalGrossAmount ?? 0),
8416
+ metadata: line.metadata ?? null,
8417
+ }),
8418
+ );
8419
+ }
8420
+ }
8261
8421
 
8262
- await em.flush();
8422
+ if (parsed.customFieldSetId) {
8423
+ await setRecordCustomFields(em, {
8424
+ entityId: E.sales.sales_invoice,
8425
+ recordId: invoiceId,
8426
+ organizationId: parsed.organizationId,
8427
+ tenantId: parsed.tenantId,
8428
+ values: normalizeCustomFieldValues(
8429
+ ((parsed as Record<string, unknown>).customFields as Record<string, unknown>) ?? {},
8430
+ ),
8431
+ });
8432
+ }
8433
+ },
8434
+ ],
8435
+ { transaction: true },
8436
+ );
8263
8437
 
8264
8438
  const dataEngine = ctx.container.resolve("dataEngine") as DataEngine;
8265
8439
  await emitCrudSideEffects({
@@ -8696,45 +8870,63 @@ const createCreditMemoCommand: CommandHandler<
8696
8870
  createdAt: new Date(),
8697
8871
  updatedAt: new Date(),
8698
8872
  });
8699
- em.persist(creditMemo);
8700
-
8701
- if (parsed.lines?.length) {
8702
- for (let i = 0; i < parsed.lines.length; i++) {
8703
- const line = parsed.lines[i];
8704
- em.persist(
8705
- em.create(SalesCreditMemoLine, {
8706
- id: randomUUID(),
8707
- creditMemo,
8708
- orderLineId: line.orderLineId ?? null,
8709
- organizationId: parsed.organizationId,
8710
- tenantId: parsed.tenantId,
8711
- lineNumber: line.lineNumber ?? i + 1,
8712
- name: line.name ?? null,
8713
- sku: line.sku ?? null,
8714
- description: line.description ?? null,
8715
- quantity: toNumericString(line.quantity ?? 0),
8716
- quantityUnit: line.quantityUnit ?? null,
8717
- normalizedQuantity: toNumericString(line.normalizedQuantity ?? 0),
8718
- normalizedUnit: line.normalizedUnit ?? null,
8719
- uomSnapshot: line.uomSnapshot ?? null,
8720
- currencyCode: line.currencyCode ?? parsed.currencyCode,
8721
- unitPriceNet: toNumericString(line.unitPriceNet ?? 0),
8722
- unitPriceGross: toNumericString(line.unitPriceGross ?? 0),
8723
- taxRate: toNumericString(line.taxRate ?? 0),
8724
- taxAmount: toNumericString(line.taxAmount ?? 0),
8725
- totalNetAmount: toNumericString(line.totalNetAmount ?? 0),
8726
- totalGrossAmount: toNumericString(line.totalGrossAmount ?? 0),
8727
- metadata: line.metadata ?? null,
8728
- }),
8729
- );
8730
- }
8731
- }
8732
8873
 
8733
- if (parsed.customFieldSetId) {
8734
- await setRecordCustomFields(em, creditMemoId, parsed.customFieldSetId, parsed);
8735
- }
8874
+ // Header + lines + custom-field writes must commit atomically.
8875
+ // setRecordCustomFields flushes mid-build, so without a transaction a
8876
+ // partial write could persist the header without its lines/custom fields.
8877
+ await withAtomicFlush(
8878
+ em,
8879
+ [
8880
+ async () => {
8881
+ em.persist(creditMemo);
8882
+
8883
+ if (parsed.lines?.length) {
8884
+ for (let i = 0; i < parsed.lines.length; i++) {
8885
+ const line = parsed.lines[i];
8886
+ em.persist(
8887
+ em.create(SalesCreditMemoLine, {
8888
+ id: randomUUID(),
8889
+ creditMemo,
8890
+ orderLineId: line.orderLineId ?? null,
8891
+ organizationId: parsed.organizationId,
8892
+ tenantId: parsed.tenantId,
8893
+ lineNumber: line.lineNumber ?? i + 1,
8894
+ name: line.name ?? null,
8895
+ sku: line.sku ?? null,
8896
+ description: line.description ?? null,
8897
+ quantity: toNumericString(line.quantity ?? 0),
8898
+ quantityUnit: line.quantityUnit ?? null,
8899
+ normalizedQuantity: toNumericString(line.normalizedQuantity ?? 0),
8900
+ normalizedUnit: line.normalizedUnit ?? null,
8901
+ uomSnapshot: line.uomSnapshot ?? null,
8902
+ currencyCode: line.currencyCode ?? parsed.currencyCode,
8903
+ unitPriceNet: toNumericString(line.unitPriceNet ?? 0),
8904
+ unitPriceGross: toNumericString(line.unitPriceGross ?? 0),
8905
+ taxRate: toNumericString(line.taxRate ?? 0),
8906
+ taxAmount: toNumericString(line.taxAmount ?? 0),
8907
+ totalNetAmount: toNumericString(line.totalNetAmount ?? 0),
8908
+ totalGrossAmount: toNumericString(line.totalGrossAmount ?? 0),
8909
+ metadata: line.metadata ?? null,
8910
+ }),
8911
+ );
8912
+ }
8913
+ }
8736
8914
 
8737
- await em.flush();
8915
+ if (parsed.customFieldSetId) {
8916
+ await setRecordCustomFields(em, {
8917
+ entityId: E.sales.sales_credit_memo,
8918
+ recordId: creditMemoId,
8919
+ organizationId: parsed.organizationId,
8920
+ tenantId: parsed.tenantId,
8921
+ values: normalizeCustomFieldValues(
8922
+ ((parsed as Record<string, unknown>).customFields as Record<string, unknown>) ?? {},
8923
+ ),
8924
+ });
8925
+ }
8926
+ },
8927
+ ],
8928
+ { transaction: true },
8929
+ );
8738
8930
 
8739
8931
  const dataEngine = ctx.container.resolve("dataEngine") as DataEngine;
8740
8932
  await emitCrudSideEffects({