@open-mercato/core 0.6.6-develop.6257.1.6d0af84d26 → 0.6.6-develop.6286.1.d985c5c374

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 (208) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/carrier_shipment_idempotency_key/index.js +19 -0
  3. package/dist/generated/entities/carrier_shipment_idempotency_key/index.js.map +7 -0
  4. package/dist/generated/entities.ids.generated.js +2 -1
  5. package/dist/generated/entities.ids.generated.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +10 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/modules/catalog/lib/productResolution.js +45 -0
  9. package/dist/modules/catalog/lib/productResolution.js.map +7 -0
  10. package/dist/modules/communication_channels/workers/channel-import-history.js +1 -1
  11. package/dist/modules/communication_channels/workers/channel-import-history.js.map +2 -2
  12. package/dist/modules/currencies/api/fetch-configs/route.js +34 -0
  13. package/dist/modules/currencies/api/fetch-configs/route.js.map +2 -2
  14. package/dist/modules/currencies/components/CurrencyFetchingConfig.js +17 -15
  15. package/dist/modules/currencies/components/CurrencyFetchingConfig.js.map +2 -2
  16. package/dist/modules/customers/api/companies/[id]/route.js +176 -161
  17. package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
  18. package/dist/modules/customers/api/people/[id]/route.js +75 -59
  19. package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
  20. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js +17 -10
  21. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js.map +2 -2
  22. package/dist/modules/customers/components/detail/ActivitiesAddNewMenu.js +6 -4
  23. package/dist/modules/customers/components/detail/ActivitiesAddNewMenu.js.map +2 -2
  24. package/dist/modules/customers/components/detail/AiActionChips.js +5 -2
  25. package/dist/modules/customers/components/detail/AiActionChips.js.map +2 -2
  26. package/dist/modules/customers/components/detail/schedule/ParticipantsField.js +19 -21
  27. package/dist/modules/customers/components/detail/schedule/ParticipantsField.js.map +2 -2
  28. package/dist/modules/data_sync/api/runs/[id]/cancel.js +1 -1
  29. package/dist/modules/data_sync/api/runs/[id]/cancel.js.map +2 -2
  30. package/dist/modules/data_sync/backend/data-sync/page.js +9 -45
  31. package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
  32. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js +11 -17
  33. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js.map +2 -2
  34. package/dist/modules/data_sync/components/IntegrationScheduleTab.js +12 -10
  35. package/dist/modules/data_sync/components/IntegrationScheduleTab.js.map +2 -2
  36. package/dist/modules/data_sync/lib/id-mapping.js +46 -1
  37. package/dist/modules/data_sync/lib/id-mapping.js.map +2 -2
  38. package/dist/modules/data_sync/lib/sync-engine.js +2 -2
  39. package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
  40. package/dist/modules/data_sync/lib/syncRunStatus.js +30 -0
  41. package/dist/modules/data_sync/lib/syncRunStatus.js.map +7 -0
  42. package/dist/modules/entities/api/records.js +6 -0
  43. package/dist/modules/entities/api/records.js.map +2 -2
  44. package/dist/modules/feature_toggles/api/global/route.js +17 -15
  45. package/dist/modules/feature_toggles/api/global/route.js.map +2 -2
  46. package/dist/modules/feature_toggles/api/openapi.js +2 -2
  47. package/dist/modules/feature_toggles/api/openapi.js.map +2 -2
  48. package/dist/modules/feature_toggles/commands/global.js +14 -0
  49. package/dist/modules/feature_toggles/commands/global.js.map +2 -2
  50. package/dist/modules/inbox_ops/backend/inbox-ops/page.js +6 -0
  51. package/dist/modules/inbox_ops/backend/inbox-ops/page.js.map +2 -2
  52. package/dist/modules/messages/components/message-detail/hooks/useMessageDetails.js +3 -1
  53. package/dist/modules/messages/components/message-detail/hooks/useMessageDetails.js.map +2 -2
  54. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js +12 -5
  55. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js.map +2 -2
  56. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsQueries.js +10 -1
  57. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsQueries.js.map +2 -2
  58. package/dist/modules/payment_gateways/api/cancel/route.js +5 -0
  59. package/dist/modules/payment_gateways/api/cancel/route.js.map +2 -2
  60. package/dist/modules/payment_gateways/api/capture/route.js +5 -0
  61. package/dist/modules/payment_gateways/api/capture/route.js.map +2 -2
  62. package/dist/modules/payment_gateways/api/refund/route.js +5 -0
  63. package/dist/modules/payment_gateways/api/refund/route.js.map +2 -2
  64. package/dist/modules/payment_gateways/lib/gateway-service.js +52 -25
  65. package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
  66. package/dist/modules/payment_gateways/lib/status-machine.js +13 -0
  67. package/dist/modules/payment_gateways/lib/status-machine.js.map +2 -2
  68. package/dist/modules/perspectives/api/[tableId]/route.js +8 -2
  69. package/dist/modules/perspectives/api/[tableId]/route.js.map +2 -2
  70. package/dist/modules/perspectives/data/entities.js +33 -3
  71. package/dist/modules/perspectives/data/entities.js.map +2 -2
  72. package/dist/modules/perspectives/migrations/Migration20260619120000_perspectives_live_row_uniqueness.js +63 -0
  73. package/dist/modules/perspectives/migrations/Migration20260619120000_perspectives_live_row_uniqueness.js.map +7 -0
  74. package/dist/modules/perspectives/services/perspectiveService.js +17 -0
  75. package/dist/modules/perspectives/services/perspectiveService.js.map +2 -2
  76. package/dist/modules/planner/commands/availability-weekly.js +33 -1
  77. package/dist/modules/planner/commands/availability-weekly.js.map +2 -2
  78. package/dist/modules/planner/components/AvailabilityRulesEditor.js +14 -2
  79. package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
  80. package/dist/modules/progress/api/active/route.js +1 -1
  81. package/dist/modules/progress/api/active/route.js.map +2 -2
  82. package/dist/modules/progress/lib/progressService.js.map +1 -1
  83. package/dist/modules/progress/lib/progressServiceImpl.js +33 -8
  84. package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
  85. package/dist/modules/query_index/api/status.js +19 -24
  86. package/dist/modules/query_index/api/status.js.map +3 -3
  87. package/dist/modules/query_index/lib/coverage.js +35 -0
  88. package/dist/modules/query_index/lib/coverage.js.map +2 -2
  89. package/dist/modules/query_index/lib/engine.js +11 -6
  90. package/dist/modules/query_index/lib/engine.js.map +2 -2
  91. package/dist/modules/resources/api/resource-types.js +5 -37
  92. package/dist/modules/resources/api/resource-types.js.map +2 -2
  93. package/dist/modules/resources/api/resourceTypeCounts.js +35 -0
  94. package/dist/modules/resources/api/resourceTypeCounts.js.map +7 -0
  95. package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js +1 -1
  96. package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js.map +1 -1
  97. package/dist/modules/resources/backend/resources/resource-types/page.js +2 -1
  98. package/dist/modules/resources/backend/resources/resource-types/page.js.map +2 -2
  99. package/dist/modules/sales/components/documents/ReturnDialog.js +4 -1
  100. package/dist/modules/sales/components/documents/ReturnDialog.js.map +2 -2
  101. package/dist/modules/sales/components/documents/ShipmentDialog.js +17 -2
  102. package/dist/modules/sales/components/documents/ShipmentDialog.js.map +2 -2
  103. package/dist/modules/sales/di.js +4 -0
  104. package/dist/modules/sales/di.js.map +2 -2
  105. package/dist/modules/sales/services/salesOrderService.js +26 -0
  106. package/dist/modules/sales/services/salesOrderService.js.map +7 -0
  107. package/dist/modules/shipping_carriers/api/shipments/route.js +8 -0
  108. package/dist/modules/shipping_carriers/api/shipments/route.js.map +2 -2
  109. package/dist/modules/shipping_carriers/data/entities.js +38 -0
  110. package/dist/modules/shipping_carriers/data/entities.js.map +2 -2
  111. package/dist/modules/shipping_carriers/data/validators.js +2 -1
  112. package/dist/modules/shipping_carriers/data/validators.js.map +2 -2
  113. package/dist/modules/shipping_carriers/lib/shipment-idempotency.js +76 -0
  114. package/dist/modules/shipping_carriers/lib/shipment-idempotency.js.map +7 -0
  115. package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/shipmentApi.js +2 -1
  116. package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/shipmentApi.js.map +2 -2
  117. package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js +20 -2
  118. package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js.map +2 -2
  119. package/dist/modules/shipping_carriers/lib/shipping-service.js +82 -43
  120. package/dist/modules/shipping_carriers/lib/shipping-service.js.map +2 -2
  121. package/dist/modules/shipping_carriers/migrations/Migration20260618173616_shipping_carriers.js +14 -0
  122. package/dist/modules/shipping_carriers/migrations/Migration20260618173616_shipping_carriers.js.map +7 -0
  123. package/dist/modules/staff/lib/timesheets-ui/ListView.js +15 -6
  124. package/dist/modules/staff/lib/timesheets-ui/ListView.js.map +2 -2
  125. package/dist/modules/translations/components/TranslationManager.js +9 -18
  126. package/dist/modules/translations/components/TranslationManager.js.map +2 -2
  127. package/generated/entities/carrier_shipment_idempotency_key/index.ts +8 -0
  128. package/generated/entities.ids.generated.ts +2 -1
  129. package/generated/entity-fields-registry.ts +10 -0
  130. package/package.json +9 -8
  131. package/src/modules/catalog/lib/productResolution.ts +75 -0
  132. package/src/modules/communication_channels/workers/channel-import-history.ts +1 -1
  133. package/src/modules/currencies/api/fetch-configs/route.ts +36 -0
  134. package/src/modules/currencies/components/CurrencyFetchingConfig.tsx +20 -17
  135. package/src/modules/currencies/i18n/de.json +2 -1
  136. package/src/modules/currencies/i18n/en.json +2 -1
  137. package/src/modules/currencies/i18n/es.json +2 -1
  138. package/src/modules/currencies/i18n/pl.json +2 -1
  139. package/src/modules/customers/api/companies/[id]/route.ts +214 -189
  140. package/src/modules/customers/api/people/[id]/route.ts +106 -80
  141. package/src/modules/customers/backend/config/customers/pipeline-stages/page.tsx +19 -12
  142. package/src/modules/customers/components/detail/ActivitiesAddNewMenu.tsx +8 -6
  143. package/src/modules/customers/components/detail/AiActionChips.tsx +6 -3
  144. package/src/modules/customers/components/detail/schedule/ParticipantsField.tsx +16 -18
  145. package/src/modules/data_sync/api/runs/[id]/cancel.ts +1 -1
  146. package/src/modules/data_sync/backend/data-sync/page.tsx +11 -58
  147. package/src/modules/data_sync/backend/data-sync/runs/[id]/page.tsx +13 -20
  148. package/src/modules/data_sync/components/IntegrationScheduleTab.tsx +10 -8
  149. package/src/modules/data_sync/lib/id-mapping.ts +58 -1
  150. package/src/modules/data_sync/lib/sync-engine.ts +2 -2
  151. package/src/modules/data_sync/lib/syncRunStatus.ts +39 -0
  152. package/src/modules/entities/api/records.ts +14 -0
  153. package/src/modules/feature_toggles/api/global/route.ts +16 -14
  154. package/src/modules/feature_toggles/api/openapi.ts +2 -2
  155. package/src/modules/feature_toggles/commands/global.ts +15 -0
  156. package/src/modules/inbox_ops/backend/inbox-ops/page.tsx +7 -0
  157. package/src/modules/messages/components/message-detail/hooks/useMessageDetails.ts +2 -0
  158. package/src/modules/messages/components/message-detail/hooks/useMessageDetailsActions.ts +14 -4
  159. package/src/modules/messages/components/message-detail/hooks/useMessageDetailsQueries.ts +15 -1
  160. package/src/modules/payment_gateways/api/cancel/route.ts +5 -0
  161. package/src/modules/payment_gateways/api/capture/route.ts +5 -0
  162. package/src/modules/payment_gateways/api/refund/route.ts +5 -0
  163. package/src/modules/payment_gateways/lib/gateway-service.ts +58 -25
  164. package/src/modules/payment_gateways/lib/status-machine.ts +16 -0
  165. package/src/modules/perspectives/api/[tableId]/route.ts +8 -2
  166. package/src/modules/perspectives/data/entities.ts +43 -3
  167. package/src/modules/perspectives/migrations/.snapshot-open-mercato.json +69 -25
  168. package/src/modules/perspectives/migrations/Migration20260619120000_perspectives_live_row_uniqueness.ts +65 -0
  169. package/src/modules/perspectives/services/perspectiveService.ts +17 -0
  170. package/src/modules/planner/commands/availability-weekly.ts +48 -1
  171. package/src/modules/planner/components/AvailabilityRulesEditor.tsx +31 -10
  172. package/src/modules/progress/api/active/route.ts +1 -1
  173. package/src/modules/progress/lib/progressService.ts +2 -2
  174. package/src/modules/progress/lib/progressServiceImpl.ts +33 -8
  175. package/src/modules/query_index/api/status.ts +28 -32
  176. package/src/modules/query_index/lib/coverage.ts +63 -0
  177. package/src/modules/query_index/lib/engine.ts +37 -24
  178. package/src/modules/resources/api/resource-types.ts +4 -44
  179. package/src/modules/resources/api/resourceTypeCounts.ts +74 -0
  180. package/src/modules/resources/backend/resources/resource-types/[id]/edit/page.tsx +1 -1
  181. package/src/modules/resources/backend/resources/resource-types/page.tsx +1 -0
  182. package/src/modules/sales/components/documents/ReturnDialog.tsx +4 -1
  183. package/src/modules/sales/components/documents/ShipmentDialog.tsx +20 -2
  184. package/src/modules/sales/di.ts +6 -0
  185. package/src/modules/sales/services/salesOrderService.ts +38 -0
  186. package/src/modules/shipping_carriers/api/shipments/route.ts +8 -0
  187. package/src/modules/shipping_carriers/data/entities.ts +33 -0
  188. package/src/modules/shipping_carriers/data/validators.ts +1 -0
  189. package/src/modules/shipping_carriers/i18n/de.json +1 -0
  190. package/src/modules/shipping_carriers/i18n/en.json +1 -0
  191. package/src/modules/shipping_carriers/i18n/es.json +1 -0
  192. package/src/modules/shipping_carriers/i18n/pl.json +1 -0
  193. package/src/modules/shipping_carriers/lib/shipment-idempotency.ts +119 -0
  194. package/src/modules/shipping_carriers/lib/shipment-wizard/hooks/shipmentApi.ts +3 -1
  195. package/src/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.ts +24 -2
  196. package/src/modules/shipping_carriers/lib/shipping-service.ts +87 -43
  197. package/src/modules/shipping_carriers/migrations/.snapshot-open-mercato.json +197 -0
  198. package/src/modules/shipping_carriers/migrations/Migration20260618173616_shipping_carriers.ts +14 -0
  199. package/src/modules/staff/i18n/de.json +1 -0
  200. package/src/modules/staff/i18n/en.json +1 -0
  201. package/src/modules/staff/i18n/es.json +1 -0
  202. package/src/modules/staff/i18n/pl.json +1 -0
  203. package/src/modules/staff/lib/timesheets-ui/ListView.tsx +15 -7
  204. package/src/modules/translations/components/TranslationManager.tsx +17 -24
  205. package/src/modules/translations/i18n/de.json +1 -0
  206. package/src/modules/translations/i18n/en.json +1 -0
  207. package/src/modules/translations/i18n/es.json +1 -0
  208. package/src/modules/translations/i18n/pl.json +1 -0
@@ -5,6 +5,7 @@ import { Page, PageBody } from '@open-mercato/ui/backend/Page'
5
5
  import { FormHeader } from '@open-mercato/ui/backend/forms'
6
6
  import { Card, CardHeader, CardTitle, CardContent } from '@open-mercato/ui/primitives/card'
7
7
  import { Badge } from '@open-mercato/ui/primitives/badge'
8
+ import { StatusBadge } from '@open-mercato/ui/primitives/status-badge'
8
9
  import { Button } from '@open-mercato/ui/primitives/button'
9
10
  import { LogList, type LogListEntry } from '@open-mercato/ui/backend/LogList'
10
11
  import { Progress } from '@open-mercato/ui/primitives/progress'
@@ -15,6 +16,7 @@ import { useT } from '@open-mercato/shared/lib/i18n/context'
15
16
  import { LoadingMessage, ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'
16
17
  import { useAppEvent } from '@open-mercato/ui/backend/injection/useAppEvent'
17
18
  import { RotateCcw, XCircle } from 'lucide-react'
19
+ import { getSyncRunStatusVariant } from '../../../../lib/syncRunStatus'
18
20
 
19
21
  type SyncRunDetail = {
20
22
  id: string
@@ -69,15 +71,6 @@ function formatEtaSeconds(seconds: number): string {
69
71
  return `${hours}h ${minutes}m`
70
72
  }
71
73
 
72
- const STATUS_STYLES: Record<string, string> = {
73
- pending: 'bg-gray-100 text-gray-800',
74
- running: 'bg-blue-100 text-blue-800',
75
- completed: 'bg-green-100 text-green-800',
76
- failed: 'bg-red-100 text-red-800',
77
- cancelled: 'bg-yellow-100 text-yellow-800',
78
- paused: 'bg-orange-100 text-orange-800',
79
- }
80
-
81
74
  type SyncRunDetailPageProps = {
82
75
  params?: {
83
76
  id?: string | string[]
@@ -284,9 +277,9 @@ export default function SyncRunDetailPage({ params }: SyncRunDetailPageProps) {
284
277
  statusBadge={(
285
278
  <div className="mt-2 flex flex-wrap gap-2">
286
279
  <Badge variant="outline">{t(`data_sync.dashboard.direction.${run.direction}`)}</Badge>
287
- <Badge variant="secondary" className={STATUS_STYLES[run.status] ?? ''}>
280
+ <StatusBadge variant={getSyncRunStatusVariant(run.status)}>
288
281
  {t(`data_sync.dashboard.status.${run.status}`)}
289
- </Badge>
282
+ </StatusBadge>
290
283
  {run.triggeredBy ? <Badge variant="outline">{run.triggeredBy}</Badge> : null}
291
284
  </div>
292
285
  )}
@@ -312,9 +305,9 @@ export default function SyncRunDetailPage({ params }: SyncRunDetailPageProps) {
312
305
  <CardHeader>
313
306
  <div className="flex items-center justify-between gap-3">
314
307
  <CardTitle>{t('data_sync.runs.detail.progress')}</CardTitle>
315
- <Badge variant="secondary" className={STATUS_STYLES[progressStatus] ?? ''}>
308
+ <StatusBadge variant={getSyncRunStatusVariant(progressStatus)}>
316
309
  {t(`data_sync.dashboard.status.${progressStatus}`)}
317
- </Badge>
310
+ </StatusBadge>
318
311
  </div>
319
312
  </CardHeader>
320
313
  <CardContent className="space-y-3">
@@ -355,37 +348,37 @@ export default function SyncRunDetailPage({ params }: SyncRunDetailPageProps) {
355
348
  <div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
356
349
  <Card>
357
350
  <CardContent className="pt-6 text-center">
358
- <div className="text-2xl font-bold text-green-600">{run.createdCount}</div>
351
+ <div className="text-2xl font-bold text-status-success-text">{run.createdCount}</div>
359
352
  <p className="text-sm text-muted-foreground">{t('data_sync.runs.detail.counters.created')}</p>
360
353
  </CardContent>
361
354
  </Card>
362
355
  <Card>
363
356
  <CardContent className="pt-6 text-center">
364
- <div className="text-2xl font-bold text-blue-600">{run.updatedCount}</div>
357
+ <div className="text-2xl font-bold text-status-info-text">{run.updatedCount}</div>
365
358
  <p className="text-sm text-muted-foreground">{t('data_sync.runs.detail.counters.updated')}</p>
366
359
  </CardContent>
367
360
  </Card>
368
361
  <Card>
369
362
  <CardContent className="pt-6 text-center">
370
- <div className="text-2xl font-bold text-gray-600">{run.skippedCount}</div>
363
+ <div className="text-2xl font-bold text-muted-foreground">{run.skippedCount}</div>
371
364
  <p className="text-sm text-muted-foreground">{t('data_sync.runs.detail.counters.skipped')}</p>
372
365
  </CardContent>
373
366
  </Card>
374
367
  <Card>
375
368
  <CardContent className="pt-6 text-center">
376
- <div className="text-2xl font-bold text-red-600">{run.failedCount}</div>
369
+ <div className="text-2xl font-bold text-status-error-text">{run.failedCount}</div>
377
370
  <p className="text-sm text-muted-foreground">{t('data_sync.runs.detail.counters.failed')}</p>
378
371
  </CardContent>
379
372
  </Card>
380
373
  </div>
381
374
 
382
375
  {run.lastError && (
383
- <Card className="border-red-200 bg-red-50">
376
+ <Card className="border-status-error-border bg-status-error-bg">
384
377
  <CardHeader>
385
- <CardTitle className="text-red-800">{t('data_sync.runs.detail.error')}</CardTitle>
378
+ <CardTitle className="text-status-error-text">{t('data_sync.runs.detail.error')}</CardTitle>
386
379
  </CardHeader>
387
380
  <CardContent>
388
- <pre className="text-sm text-red-700 whitespace-pre-wrap">{run.lastError}</pre>
381
+ <pre className="text-sm text-status-error-text whitespace-pre-wrap">{run.lastError}</pre>
389
382
  </CardContent>
390
383
  </Card>
391
384
  )}
@@ -18,8 +18,10 @@ import {
18
18
  SelectValue,
19
19
  } from '@open-mercato/ui/primitives/select'
20
20
  import { Spinner } from '@open-mercato/ui/primitives/spinner'
21
+ import { Switch } from '@open-mercato/ui/primitives/switch'
21
22
  import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
22
23
  import { useT } from '@open-mercato/shared/lib/i18n/context'
24
+ import { getSyncSummaryVariant } from '../lib/syncRunStatus'
23
25
  import {
24
26
  CalendarClock,
25
27
  Play,
@@ -360,13 +362,13 @@ export function IntegrationScheduleTab(props: IntegrationScheduleTabProps) {
360
362
  <div className="flex flex-wrap items-start justify-between gap-3">
361
363
  <div className="space-y-2">
362
364
  <div className="flex flex-wrap items-center gap-2">
363
- <Badge variant="outline" className={props.isEnabled ? 'border-emerald-300 text-emerald-700' : 'border-amber-300 text-amber-700'}>
365
+ <Badge variant={getSyncSummaryVariant(props.isEnabled ? 'enabled' : 'disabled')}>
364
366
  {props.isEnabled ? <ShieldCheck className="mr-2 h-3.5 w-3.5" /> : <ShieldAlert className="mr-2 h-3.5 w-3.5" />}
365
367
  {props.isEnabled
366
368
  ? t('data_sync.dashboard.start.status.enabled', 'Integration enabled')
367
369
  : t('data_sync.dashboard.start.status.disabled', 'Integration disabled')}
368
370
  </Badge>
369
- <Badge variant="outline" className={props.hasCredentials ? 'border-sky-300 text-sky-700' : 'border-amber-300 text-amber-700'}>
371
+ <Badge variant={getSyncSummaryVariant(props.hasCredentials ? 'ready' : 'missing')}>
370
372
  <CalendarClock className="mr-2 h-3.5 w-3.5" />
371
373
  {props.hasCredentials
372
374
  ? t('data_sync.dashboard.start.status.credentialsReady', 'Credentials ready')
@@ -477,22 +479,22 @@ export function IntegrationScheduleTab(props: IntegrationScheduleTabProps) {
477
479
  </td>
478
480
  <td className="px-3 py-3">
479
481
  <label className="flex min-h-10 items-center gap-2 text-sm">
480
- <input
481
- type="checkbox"
482
+ <Switch
482
483
  checked={scheduleState.fullSync}
483
- onChange={(event) => updateScheduleEditor(row.key, { fullSync: event.target.checked }, row.entityType)}
484
+ onCheckedChange={(checked) => updateScheduleEditor(row.key, { fullSync: checked }, row.entityType)}
484
485
  disabled={controlsDisabled}
486
+ aria-label={t('data_sync.dashboard.start.fullSync', 'Run as full sync')}
485
487
  />
486
488
  <span>{t('data_sync.integrationTab.fullSyncShort', 'Full')}</span>
487
489
  </label>
488
490
  </td>
489
491
  <td className="px-3 py-3">
490
492
  <label className="flex min-h-10 items-center gap-2 text-sm">
491
- <input
492
- type="checkbox"
493
+ <Switch
493
494
  checked={scheduleState.isEnabled}
494
- onChange={(event) => updateScheduleEditor(row.key, { isEnabled: event.target.checked }, row.entityType)}
495
+ onCheckedChange={(checked) => updateScheduleEditor(row.key, { isEnabled: checked }, row.entityType)}
495
496
  disabled={controlsDisabled}
497
+ aria-label={t('data_sync.dashboard.schedule.enabled', 'Schedule enabled')}
496
498
  />
497
499
  <span>{scheduleState.isEnabled ? t('data_sync.dashboard.schedule.status.shortEnabled', 'Scheduled') : t('data_sync.dashboard.schedule.status.shortDisabled', 'Paused')}</span>
498
500
  </label>
@@ -1,5 +1,5 @@
1
1
  import type { EntityManager } from '@mikro-orm/postgresql'
2
- import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
2
+ import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
3
3
  import { SyncExternalIdMapping } from '../../integrations/data/entities'
4
4
 
5
5
  type MappingScope = {
@@ -119,6 +119,63 @@ export function createExternalIdMappingService(em: EntityManager) {
119
119
  await em.persist(created).flush()
120
120
  return created
121
121
  },
122
+
123
+ async deleteExternalIdMapping(
124
+ integrationId: string,
125
+ entityType: string,
126
+ localId: string,
127
+ scope: MappingScope,
128
+ ): Promise<boolean> {
129
+ const row = await findOneWithDecryption(
130
+ em,
131
+ SyncExternalIdMapping,
132
+ {
133
+ integrationId,
134
+ internalEntityType: entityType,
135
+ internalEntityId: localId,
136
+ organizationId: scope.organizationId,
137
+ tenantId: scope.tenantId,
138
+ deletedAt: null,
139
+ },
140
+ undefined,
141
+ scope,
142
+ )
143
+ if (!row) return false
144
+ row.deletedAt = new Date()
145
+ await em.flush()
146
+ return true
147
+ },
148
+
149
+ async deleteExternalIdMappings(
150
+ integrationId: string,
151
+ entityType: string,
152
+ localIds: string[],
153
+ scope: MappingScope,
154
+ ): Promise<number> {
155
+ const uniqueLocalIds = Array.from(new Set(localIds))
156
+ if (uniqueLocalIds.length === 0) return 0
157
+ const rows = await findWithDecryption(
158
+ em,
159
+ SyncExternalIdMapping,
160
+ {
161
+ integrationId,
162
+ internalEntityType: entityType,
163
+ internalEntityId: { $in: uniqueLocalIds },
164
+ organizationId: scope.organizationId,
165
+ tenantId: scope.tenantId,
166
+ deletedAt: null,
167
+ },
168
+ undefined,
169
+ scope,
170
+ )
171
+ if (rows.length === 0) return 0
172
+ const now = new Date()
173
+ for (const row of rows) {
174
+ row.deletedAt = now
175
+ }
176
+ await em.flush()
177
+ return rows.length
178
+ },
122
179
  }
123
180
  }
124
181
 
@@ -435,7 +435,7 @@ export function createSyncEngine(deps: EngineDeps) {
435
435
  scope: { organizationId: scope.organizationId, tenantId: scope.tenantId },
436
436
  runId: run.id,
437
437
  })) {
438
- if (run.progressJobId && await progressService.isCancellationRequested(run.progressJobId, scope.tenantId)) {
438
+ if (run.progressJobId && await progressService.isCancellationRequested(run.progressJobId, scope.tenantId, scope.organizationId)) {
439
439
  await finalizeRun(run.id, 'cancelled', scope, undefined, operationalTelemetry)
440
440
  return
441
441
  }
@@ -579,7 +579,7 @@ export function createSyncEngine(deps: EngineDeps) {
579
579
  scope: { organizationId: scope.organizationId, tenantId: scope.tenantId },
580
580
  runId: run.id,
581
581
  })) {
582
- if (run.progressJobId && await progressService.isCancellationRequested(run.progressJobId, scope.tenantId)) {
582
+ if (run.progressJobId && await progressService.isCancellationRequested(run.progressJobId, scope.tenantId, scope.organizationId)) {
583
583
  await finalizeRun(run.id, 'cancelled', scope, undefined, operationalTelemetry)
584
584
  return
585
585
  }
@@ -0,0 +1,39 @@
1
+ import type { StatusBadgeVariant, StatusMap } from '@open-mercato/ui/primitives/status-badge'
2
+
3
+ export type SyncRunStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled' | 'paused'
4
+
5
+ export const syncRunStatusVariants: StatusMap<SyncRunStatus> = {
6
+ pending: 'neutral',
7
+ running: 'info',
8
+ completed: 'success',
9
+ failed: 'error',
10
+ cancelled: 'warning',
11
+ paused: 'warning',
12
+ }
13
+
14
+ export function getSyncRunStatusVariant(status: string): StatusBadgeVariant {
15
+ return syncRunStatusVariants[status as SyncRunStatus] ?? 'neutral'
16
+ }
17
+
18
+ export type SyncSummaryKind =
19
+ | 'enabled'
20
+ | 'disabled'
21
+ | 'ready'
22
+ | 'missing'
23
+ | 'scheduled'
24
+ | 'paused'
25
+ | 'none'
26
+
27
+ export const syncSummaryVariants: Record<SyncSummaryKind, StatusBadgeVariant> = {
28
+ enabled: 'success',
29
+ ready: 'success',
30
+ disabled: 'neutral',
31
+ missing: 'warning',
32
+ scheduled: 'info',
33
+ paused: 'warning',
34
+ none: 'neutral',
35
+ }
36
+
37
+ export function getSyncSummaryVariant(kind: SyncSummaryKind): StatusBadgeVariant {
38
+ return syncSummaryVariants[kind] ?? 'neutral'
39
+ }
@@ -382,6 +382,13 @@ export async function POST(req: Request) {
382
382
  if (entityKind === 'system') return systemEntityRecordsRejection(entityId)
383
383
  const isCustomEntity = entityKind === 'custom'
384
384
  await assertEntityAclForRequest({ auth, entityId, action: 'manage', isCustomEntity, rbac })
385
+ // Strip reserved record/system columns the edit form echoes back from the loaded record
386
+ // (`id`, plus `updated_at`/`updatedAt` used for optimistic locking). They are not custom
387
+ // fields; without this they validate as cf_id / cf_updated_at / cf_updatedAt and are
388
+ // rejected as "Unknown custom field", which fails EVERY custom-entity edit-form save.
389
+ for (const reservedKey of ['id', 'created_at', 'createdAt', 'updated_at', 'updatedAt', 'deleted_at', 'deletedAt']) {
390
+ delete (values as any)[reservedKey]
391
+ }
385
392
  const norm = normalizeValues(values)
386
393
 
387
394
  // Validate against custom field definitions
@@ -448,6 +455,13 @@ export async function PUT(req: Request) {
448
455
  if (entityKind === 'system') return systemEntityRecordsRejection(entityId)
449
456
  const isCustomEntity = entityKind === 'custom'
450
457
  await assertEntityAclForRequest({ auth, entityId, action: 'manage', isCustomEntity, rbac })
458
+ // Strip reserved record/system columns the edit form echoes back from the loaded record
459
+ // (`id`, plus `updated_at`/`updatedAt` used for optimistic locking). They are not custom
460
+ // fields; without this they validate as cf_id / cf_updated_at / cf_updatedAt and are
461
+ // rejected as "Unknown custom field", which fails EVERY custom-entity edit-form save.
462
+ for (const reservedKey of ['id', 'created_at', 'createdAt', 'updated_at', 'updatedAt', 'deleted_at', 'deletedAt']) {
463
+ delete (values as any)[reservedKey]
464
+ }
451
465
  const norm = normalizeValues(values)
452
466
 
453
467
  // Validate against custom field definitions
@@ -49,6 +49,21 @@ const listFields = [
49
49
  'updated_at',
50
50
  ]
51
51
 
52
+ export const transformFeatureToggleListItem = (item: Record<string, unknown>) => {
53
+ if (!item) return item
54
+ return {
55
+ id: item.id,
56
+ identifier: item.identifier,
57
+ name: item.name,
58
+ description: item.description ?? null,
59
+ category: item.category ?? null,
60
+ type: item.type,
61
+ defaultValue: item.default_value,
62
+ createdAt: item.created_at,
63
+ updatedAt: item.updated_at,
64
+ }
65
+ }
66
+
52
67
  const buildFilters = (query: FeatureToggleListQuery): Record<string, unknown> => {
53
68
  const filters: Record<string, unknown> = {}
54
69
  const search = query.search?.trim()
@@ -105,20 +120,7 @@ const crud = makeCrudRoute({
105
120
  updatedAt: 'updated_at',
106
121
  type: 'type',
107
122
  },
108
- transformItem: (item: Record<string, unknown>) => {
109
- if (!item) return item
110
- return {
111
- id: item.id,
112
- identifier: item.identifier,
113
- name: item.name,
114
- description: item.description ?? null,
115
- category: item.category ?? null,
116
- type: item.type,
117
- defaultValue: item.default_value,
118
- created_at: item.created_at,
119
- updated_at: item.updated_at,
120
- }
121
- },
123
+ transformItem: transformFeatureToggleListItem,
122
124
  buildFilters: async (query) => buildFilters(query),
123
125
  },
124
126
  actions: {
@@ -28,8 +28,8 @@ export const featureToggleSchema = z
28
28
  category: z.string().nullable().optional(),
29
29
  type: toggleTypeSchema,
30
30
  defaultValue: z.any().nullable().optional(),
31
- created_at: z.string().optional(),
32
- updated_at: z.string().optional(),
31
+ createdAt: z.string().nullable().optional(),
32
+ updatedAt: z.string().nullable().optional(),
33
33
  })
34
34
  .passthrough()
35
35
 
@@ -6,6 +6,7 @@ import { ToggleCreateInput, toggleCreateSchema, ToggleUpdateInput, toggleUpdateS
6
6
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
7
7
  import { registerCommand } from '@open-mercato/shared/lib/commands'
8
8
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
9
+ import { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
9
10
  import { buildChanges, emitCrudSideEffects, emitCrudUndoSideEffects, requireId } from '@open-mercato/shared/lib/commands/helpers'
10
11
  import { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'
11
12
  import { resolveRedoSnapshot } from '@open-mercato/shared/lib/commands/redo'
@@ -49,6 +50,8 @@ type ToggleUndoPayload = {
49
50
 
50
51
  const featureToggleCrudIndexer = { entityType: E.feature_toggles.feature_toggle }
51
52
 
53
+ const FEATURE_TOGGLE_LOCK_RESOURCE_KIND = 'feature_toggles.feature_toggle'
54
+
52
55
  function featureToggleIdentifiers(
53
56
  toggle: FeatureToggle | ToggleSnapshot,
54
57
  ctx: { auth?: { tenantId?: string | null } | null },
@@ -217,6 +220,12 @@ const updateToggleCommand: CommandHandler<ToggleUpdateInput, { toggleId: string
217
220
  const em = (ctx.container.resolve('em') as EntityManager).fork()
218
221
  const toggle = await em.findOne(FeatureToggle, { id: parsed.id, deletedAt: null })
219
222
  if (!toggle) throw new CrudHttpError(404, { error: 'Toggle not found' })
223
+ enforceCommandOptimisticLock({
224
+ resourceKind: FEATURE_TOGGLE_LOCK_RESOURCE_KIND,
225
+ resourceId: toggle.id,
226
+ current: toggle.updatedAt ?? null,
227
+ request: ctx.request ?? null,
228
+ })
220
229
  const previousIdentifier = toggle.identifier
221
230
  if (parsed.identifier && parsed.identifier !== toggle.identifier) {
222
231
  const existing = await em.findOne(FeatureToggle, { identifier: parsed.identifier })
@@ -344,6 +353,12 @@ const deleteToggleCommand: CommandHandler<{ body?: Record<string, unknown>; quer
344
353
  const em = (ctx.container.resolve('em') as EntityManager).fork()
345
354
  const toggle = await em.findOne(FeatureToggle, { id, deletedAt: null })
346
355
  if (!toggle) throw new CrudHttpError(404, { error: 'Feature toggle not found' })
356
+ enforceCommandOptimisticLock({
357
+ resourceKind: FEATURE_TOGGLE_LOCK_RESOURCE_KIND,
358
+ resourceId: toggle.id,
359
+ current: toggle.updatedAt ?? null,
360
+ request: ctx.request ?? null,
361
+ })
347
362
  const featureTogglesService = ctx.container.resolve('featureTogglesService') as FeatureTogglesService
348
363
  await featureTogglesService.invalidateIsEnabledCacheByIdentifierTag(toggle.identifier)
349
364
 
@@ -148,6 +148,13 @@ export default function InboxOpsProposalsPage() {
148
148
  if (initialLoadComplete) loadProposals()
149
149
  }, [page, statusFilter, categoryFilter, search, scopeVersion]) // eslint-disable-line react-hooks/exhaustive-deps
150
150
 
151
+ React.useEffect(() => {
152
+ if (initialLoadComplete) {
153
+ loadCounts()
154
+ loadSettings()
155
+ }
156
+ }, [scopeVersion]) // eslint-disable-line react-hooks/exhaustive-deps
157
+
151
158
  const handleCopyAddress = React.useCallback(() => {
152
159
  if (settings?.inboxAddress) {
153
160
  navigator.clipboard.writeText(settings.inboxAddress)
@@ -61,7 +61,9 @@ export function useMessageDetails(id: string) {
61
61
  attachments: queryState.attachments,
62
62
  isArchived,
63
63
  onDeleted: () => router.push('/backend/messages'),
64
+ onMarkedUnread: () => router.push('/backend/messages'),
64
65
  refreshDetailWithoutAutoMarkRead: queryState.refreshDetailWithoutAutoMarkRead,
66
+ suppressAutoMarkRead: queryState.suppressAutoMarkRead,
65
67
  })
66
68
 
67
69
  const conversationState = useMessageDetailsConversation({
@@ -18,6 +18,7 @@ import { isSafeNavigationHref, parseObjectActionId, toErrorMessage } from '../ut
18
18
 
19
19
  type RequestAndRefreshOptions = {
20
20
  skipDetailAutoMarkRead?: boolean
21
+ onSuccess?: () => void
21
22
  }
22
23
 
23
24
  type ConversationActionKind =
@@ -42,7 +43,9 @@ type UseMessageDetailsActionsInput = {
42
43
  attachments: MessageAttachment[] | undefined
43
44
  isArchived: boolean
44
45
  onDeleted: () => void
46
+ onMarkedUnread: () => void
45
47
  refreshDetailWithoutAutoMarkRead: () => Promise<MessageDetail | null>
48
+ suppressAutoMarkRead: () => void
46
49
  }
47
50
 
48
51
  export function useMessageDetailsActions({
@@ -53,7 +56,9 @@ export function useMessageDetailsActions({
53
56
  attachments,
54
57
  isArchived,
55
58
  onDeleted,
59
+ onMarkedUnread,
56
60
  refreshDetailWithoutAutoMarkRead,
61
+ suppressAutoMarkRead,
57
62
  }: UseMessageDetailsActionsInput) {
58
63
  const [replyOpen, setReplyOpen] = React.useState(false)
59
64
  const [forwardOpen, setForwardOpen] = React.useState(false)
@@ -93,6 +98,7 @@ export function useMessageDetailsActions({
93
98
  context: { resourceKind: 'message', messageId: id, action: 'state-change', retryLastMutation },
94
99
  mutationPayload: { messageId: id, action: 'state-change', url, method },
95
100
  })
101
+ options?.onSuccess?.()
96
102
  } catch (err) {
97
103
  flash(
98
104
  err instanceof Error
@@ -203,14 +209,15 @@ export function useMessageDetailsActions({
203
209
  }, [id, runConversationAction, t])
204
210
 
205
211
  const markConversationUnread = React.useCallback(async (messageId?: string) => {
212
+ suppressAutoMarkRead()
206
213
  const targetMessageId = messageId ?? id
207
214
  await runConversationAction('markAllUnread', {
208
215
  url: `/api/messages/${encodeURIComponent(targetMessageId)}/conversation/read`,
209
216
  method: 'DELETE',
210
217
  successMessage: t('messages.flash.conversationMarkedUnread', 'Conversation marked unread.'),
211
- skipDetailAutoMarkRead: true,
218
+ onSuccess: onMarkedUnread,
212
219
  })
213
- }, [id, runConversationAction, t])
220
+ }, [id, onMarkedUnread, runConversationAction, suppressAutoMarkRead, t])
214
221
 
215
222
  const markConversationRead = React.useCallback(async (messageId?: string) => {
216
223
  const targetMessageId = messageId ?? id
@@ -385,12 +392,15 @@ export function useMessageDetailsActions({
385
392
  }, [detail?.actionData?.actions])
386
393
 
387
394
  const toggleRead = React.useCallback(async () => {
395
+ if (detail?.isRead) {
396
+ suppressAutoMarkRead()
397
+ }
388
398
  await requestAndRefresh(
389
399
  `/api/messages/${encodeURIComponent(id)}/read`,
390
400
  detail?.isRead ? 'DELETE' : 'PUT',
391
- detail?.isRead ? { skipDetailAutoMarkRead: true } : undefined,
401
+ detail?.isRead ? { skipDetailAutoMarkRead: true, onSuccess: onMarkedUnread } : undefined,
392
402
  )
393
- }, [detail?.isRead, id, requestAndRefresh])
403
+ }, [detail?.isRead, id, onMarkedUnread, requestAndRefresh, suppressAutoMarkRead])
394
404
 
395
405
  const toggleArchive = React.useCallback(async () => {
396
406
  await requestAndRefresh(
@@ -26,10 +26,19 @@ export function useMessageDetailsQueries({
26
26
  [id, scopeVersion],
27
27
  )
28
28
 
29
+ const suppressAutoMarkReadRef = React.useRef(false)
30
+
31
+ React.useEffect(() => {
32
+ suppressAutoMarkReadRef.current = false
33
+ }, [id])
34
+
29
35
  const detailQuery = useQuery<MessageDetail | null>({
30
36
  queryKey: detailQueryKey,
31
37
  queryFn: async () => {
32
- const call = await apiCall<MessageDetail>(`/api/messages/${encodeURIComponent(id)}`)
38
+ const detailUrl = suppressAutoMarkReadRef.current
39
+ ? `/api/messages/${encodeURIComponent(id)}?skipMarkRead=1`
40
+ : `/api/messages/${encodeURIComponent(id)}`
41
+ const call = await apiCall<MessageDetail>(detailUrl)
33
42
  if (call.status === 404) {
34
43
  return null
35
44
  }
@@ -97,6 +106,10 @@ export function useMessageDetailsQueries({
97
106
  return call.result
98
107
  }, [detailQueryKey, id, queryClient, t])
99
108
 
109
+ const suppressAutoMarkRead = React.useCallback(() => {
110
+ suppressAutoMarkReadRef.current = true
111
+ }, [])
112
+
100
113
  const listItemComponentKey = detail?.typeDefinition.ui?.listItemComponent ?? null
101
114
  const contentComponentKey = detail?.typeDefinition.ui?.contentComponent ?? null
102
115
  const actionsComponentKey = detail?.typeDefinition.ui?.actionsComponent ?? null
@@ -111,6 +124,7 @@ export function useMessageDetailsQueries({
111
124
  attachmentsQuery,
112
125
  attachments,
113
126
  refreshDetailWithoutAutoMarkRead,
127
+ suppressAutoMarkRead,
114
128
  listItemComponentKey,
115
129
  contentComponentKey,
116
130
  actionsComponentKey,
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'
2
2
  import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
3
3
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
4
4
  import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
5
+ import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
5
6
  import { cancelSchema } from '../../data/validators'
6
7
  import type { PaymentGatewayService } from '../../lib/gateway-service'
7
8
  import { paymentGatewaysTag } from '../openapi'
@@ -73,6 +74,9 @@ export async function POST(req: Request) {
73
74
  })
74
75
  return NextResponse.json(result)
75
76
  } catch (err: unknown) {
77
+ if (isCrudHttpError(err)) {
78
+ return NextResponse.json(err.body, { status: err.status })
79
+ }
76
80
  const message = err instanceof Error ? err.message : 'Cancel failed'
77
81
  return NextResponse.json({ error: message }, { status: 502 })
78
82
  }
@@ -87,6 +91,7 @@ export const openApi = {
87
91
  tags: [paymentGatewaysTag],
88
92
  responses: [
89
93
  { status: 200, description: 'Payment cancelled' },
94
+ { status: 409, description: 'Invalid payment status transition' },
90
95
  { status: 422, description: 'Invalid payload' },
91
96
  { status: 502, description: 'Gateway provider error' },
92
97
  ],
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'
2
2
  import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
3
3
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
4
4
  import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
5
+ import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
5
6
  import { captureSchema } from '../../data/validators'
6
7
  import type { PaymentGatewayService } from '../../lib/gateway-service'
7
8
  import { paymentGatewaysTag } from '../openapi'
@@ -73,6 +74,9 @@ export async function POST(req: Request) {
73
74
  })
74
75
  return NextResponse.json(result)
75
76
  } catch (err: unknown) {
77
+ if (isCrudHttpError(err)) {
78
+ return NextResponse.json(err.body, { status: err.status })
79
+ }
76
80
  const message = err instanceof Error ? err.message : 'Capture failed'
77
81
  return NextResponse.json({ error: message }, { status: 502 })
78
82
  }
@@ -87,6 +91,7 @@ export const openApi = {
87
91
  tags: [paymentGatewaysTag],
88
92
  responses: [
89
93
  { status: 200, description: 'Payment captured' },
94
+ { status: 409, description: 'Invalid payment status transition' },
90
95
  { status: 422, description: 'Invalid payload' },
91
96
  { status: 502, description: 'Gateway provider error' },
92
97
  ],
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'
2
2
  import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
3
3
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
4
4
  import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
5
+ import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
5
6
  import { refundSchema } from '../../data/validators'
6
7
  import type { PaymentGatewayService } from '../../lib/gateway-service'
7
8
  import { paymentGatewaysTag } from '../openapi'
@@ -74,6 +75,9 @@ export async function POST(req: Request) {
74
75
  })
75
76
  return NextResponse.json(result)
76
77
  } catch (err: unknown) {
78
+ if (isCrudHttpError(err)) {
79
+ return NextResponse.json(err.body, { status: err.status })
80
+ }
77
81
  const message = err instanceof Error ? err.message : 'Refund failed'
78
82
  return NextResponse.json({ error: message }, { status: 502 })
79
83
  }
@@ -88,6 +92,7 @@ export const openApi = {
88
92
  tags: [paymentGatewaysTag],
89
93
  responses: [
90
94
  { status: 200, description: 'Payment refunded' },
95
+ { status: 409, description: 'Invalid payment status transition' },
91
96
  { status: 422, description: 'Invalid payload' },
92
97
  { status: 502, description: 'Gateway provider error' },
93
98
  ],