@open-mercato/core 0.4.7-develop-0a657b411f → 0.4.7-develop-e249d3e7d0

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 (278) hide show
  1. package/dist/generated/entities/carrier_shipment/index.js +37 -0
  2. package/dist/generated/entities/carrier_shipment/index.js.map +7 -0
  3. package/dist/generated/entities/gateway_transaction/index.js +47 -0
  4. package/dist/generated/entities/gateway_transaction/index.js.map +7 -0
  5. package/dist/generated/entities/webhook_processed_event/index.js +17 -0
  6. package/dist/generated/entities/webhook_processed_event/index.js.map +7 -0
  7. package/dist/generated/entities.ids.generated.js +10 -1
  8. package/dist/generated/entities.ids.generated.js.map +2 -2
  9. package/dist/generated/entity-fields-registry.js +6 -0
  10. package/dist/generated/entity-fields-registry.js.map +2 -2
  11. package/dist/modules/data_sync/api/runs/[id]/cancel.js +14 -5
  12. package/dist/modules/data_sync/api/runs/[id]/cancel.js.map +2 -2
  13. package/dist/modules/data_sync/backend/data-sync/page.meta.js +2 -2
  14. package/dist/modules/data_sync/backend/data-sync/page.meta.js.map +1 -1
  15. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js +37 -12
  16. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js.map +2 -2
  17. package/dist/modules/directory/api/get/tenants/lookup.js +1 -0
  18. package/dist/modules/directory/api/get/tenants/lookup.js.map +2 -2
  19. package/dist/modules/integrations/api/[id]/route.js +38 -11
  20. package/dist/modules/integrations/api/[id]/route.js.map +2 -2
  21. package/dist/modules/integrations/api/logs/route.js +52 -26
  22. package/dist/modules/integrations/api/logs/route.js.map +2 -2
  23. package/dist/modules/integrations/api/route.js +37 -7
  24. package/dist/modules/integrations/api/route.js.map +2 -2
  25. package/dist/modules/integrations/api/umes-read.js +121 -0
  26. package/dist/modules/integrations/api/umes-read.js.map +7 -0
  27. package/dist/modules/integrations/backend/integrations/[id]/page.js +715 -183
  28. package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
  29. package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js +30 -9
  30. package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js.map +2 -2
  31. package/dist/modules/integrations/backend/integrations/detail-page-widgets.js +46 -0
  32. package/dist/modules/integrations/backend/integrations/detail-page-widgets.js.map +7 -0
  33. package/dist/modules/integrations/backend/integrations/page.js +78 -62
  34. package/dist/modules/integrations/backend/integrations/page.js.map +2 -2
  35. package/dist/modules/integrations/backend/integrations/page.meta.js +2 -2
  36. package/dist/modules/integrations/backend/integrations/page.meta.js.map +1 -1
  37. package/dist/modules/integrations/setup.js +2 -2
  38. package/dist/modules/integrations/setup.js.map +2 -2
  39. package/dist/modules/payment_gateways/acl.js +12 -0
  40. package/dist/modules/payment_gateways/acl.js.map +7 -0
  41. package/dist/modules/payment_gateways/api/cancel/route.js +55 -0
  42. package/dist/modules/payment_gateways/api/cancel/route.js.map +7 -0
  43. package/dist/modules/payment_gateways/api/capture/route.js +55 -0
  44. package/dist/modules/payment_gateways/api/capture/route.js.map +7 -0
  45. package/dist/modules/payment_gateways/api/interceptors.js +24 -0
  46. package/dist/modules/payment_gateways/api/interceptors.js.map +7 -0
  47. package/dist/modules/payment_gateways/api/openapi.js +5 -0
  48. package/dist/modules/payment_gateways/api/openapi.js.map +7 -0
  49. package/dist/modules/payment_gateways/api/refund/route.js +56 -0
  50. package/dist/modules/payment_gateways/api/refund/route.js.map +7 -0
  51. package/dist/modules/payment_gateways/api/sessions/route.js +74 -0
  52. package/dist/modules/payment_gateways/api/sessions/route.js.map +7 -0
  53. package/dist/modules/payment_gateways/api/status/route.js +66 -0
  54. package/dist/modules/payment_gateways/api/status/route.js.map +7 -0
  55. package/dist/modules/payment_gateways/api/transactions/[id]/route.js +118 -0
  56. package/dist/modules/payment_gateways/api/transactions/[id]/route.js.map +7 -0
  57. package/dist/modules/payment_gateways/api/transactions/route.js +113 -0
  58. package/dist/modules/payment_gateways/api/transactions/route.js.map +7 -0
  59. package/dist/modules/payment_gateways/api/webhook/[provider]/route.js +136 -0
  60. package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +7 -0
  61. package/dist/modules/payment_gateways/backend/payment-gateways/page.js +496 -0
  62. package/dist/modules/payment_gateways/backend/payment-gateways/page.js.map +7 -0
  63. package/dist/modules/payment_gateways/backend/payment-gateways/page.meta.js +23 -0
  64. package/dist/modules/payment_gateways/backend/payment-gateways/page.meta.js.map +7 -0
  65. package/dist/modules/payment_gateways/data/enrichers.js +5 -0
  66. package/dist/modules/payment_gateways/data/enrichers.js.map +7 -0
  67. package/dist/modules/payment_gateways/data/entities.js +131 -0
  68. package/dist/modules/payment_gateways/data/entities.js.map +7 -0
  69. package/dist/modules/payment_gateways/data/validators.js +57 -0
  70. package/dist/modules/payment_gateways/data/validators.js.map +7 -0
  71. package/dist/modules/payment_gateways/di.js +16 -0
  72. package/dist/modules/payment_gateways/di.js.map +7 -0
  73. package/dist/modules/payment_gateways/events.js +21 -0
  74. package/dist/modules/payment_gateways/events.js.map +7 -0
  75. package/dist/modules/payment_gateways/i18n/en.js +6 -0
  76. package/dist/modules/payment_gateways/i18n/en.js.map +7 -0
  77. package/dist/modules/payment_gateways/i18n/pl.js +6 -0
  78. package/dist/modules/payment_gateways/i18n/pl.js.map +7 -0
  79. package/dist/modules/payment_gateways/index.js +9 -0
  80. package/dist/modules/payment_gateways/index.js.map +7 -0
  81. package/dist/modules/payment_gateways/lib/gateway-service.js +378 -0
  82. package/dist/modules/payment_gateways/lib/gateway-service.js.map +7 -0
  83. package/dist/modules/payment_gateways/lib/queue.js +17 -0
  84. package/dist/modules/payment_gateways/lib/queue.js.map +7 -0
  85. package/dist/modules/payment_gateways/lib/status-machine.js +29 -0
  86. package/dist/modules/payment_gateways/lib/status-machine.js.map +7 -0
  87. package/dist/modules/payment_gateways/lib/webhook-processor.js +88 -0
  88. package/dist/modules/payment_gateways/lib/webhook-processor.js.map +7 -0
  89. package/dist/modules/payment_gateways/lib/webhook-utils.js +42 -0
  90. package/dist/modules/payment_gateways/lib/webhook-utils.js.map +7 -0
  91. package/dist/modules/payment_gateways/migrations/Migration20260305122155.js +19 -0
  92. package/dist/modules/payment_gateways/migrations/Migration20260305122155.js.map +7 -0
  93. package/dist/modules/payment_gateways/setup.js +13 -0
  94. package/dist/modules/payment_gateways/setup.js.map +7 -0
  95. package/dist/modules/payment_gateways/widgets/injection-table.js +7 -0
  96. package/dist/modules/payment_gateways/widgets/injection-table.js.map +7 -0
  97. package/dist/modules/payment_gateways/workers/status-poller.js +44 -0
  98. package/dist/modules/payment_gateways/workers/status-poller.js.map +7 -0
  99. package/dist/modules/payment_gateways/workers/webhook-processor.js +20 -0
  100. package/dist/modules/payment_gateways/workers/webhook-processor.js.map +7 -0
  101. package/dist/modules/sales/data/enrichers.js +72 -0
  102. package/dist/modules/sales/data/enrichers.js.map +7 -0
  103. package/dist/modules/sales/lib/makeSalesLineRoute.js +3 -0
  104. package/dist/modules/sales/lib/makeSalesLineRoute.js.map +2 -2
  105. package/dist/modules/sales/widgets/injection/payment-gateway-config-field/widget.js +29 -0
  106. package/dist/modules/sales/widgets/injection/payment-gateway-config-field/widget.js.map +7 -0
  107. package/dist/modules/sales/widgets/injection/payment-gateway-status-column/widget.js +23 -0
  108. package/dist/modules/sales/widgets/injection/payment-gateway-status-column/widget.js.map +7 -0
  109. package/dist/modules/sales/widgets/injection-table.js +13 -1
  110. package/dist/modules/sales/widgets/injection-table.js.map +2 -2
  111. package/dist/modules/shipping_carriers/acl.js +10 -0
  112. package/dist/modules/shipping_carriers/acl.js.map +7 -0
  113. package/dist/modules/shipping_carriers/api/cancel/route.js +55 -0
  114. package/dist/modules/shipping_carriers/api/cancel/route.js.map +7 -0
  115. package/dist/modules/shipping_carriers/api/interceptors.js +21 -0
  116. package/dist/modules/shipping_carriers/api/interceptors.js.map +7 -0
  117. package/dist/modules/shipping_carriers/api/openapi.js +5 -0
  118. package/dist/modules/shipping_carriers/api/openapi.js.map +7 -0
  119. package/dist/modules/shipping_carriers/api/rates/route.js +55 -0
  120. package/dist/modules/shipping_carriers/api/rates/route.js.map +7 -0
  121. package/dist/modules/shipping_carriers/api/shipments/route.js +61 -0
  122. package/dist/modules/shipping_carriers/api/shipments/route.js.map +7 -0
  123. package/dist/modules/shipping_carriers/api/tracking/route.js +58 -0
  124. package/dist/modules/shipping_carriers/api/tracking/route.js.map +7 -0
  125. package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js +119 -0
  126. package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js.map +7 -0
  127. package/dist/modules/shipping_carriers/data/enrichers.js +82 -0
  128. package/dist/modules/shipping_carriers/data/enrichers.js.map +7 -0
  129. package/dist/modules/shipping_carriers/data/entities.js +80 -0
  130. package/dist/modules/shipping_carriers/data/entities.js.map +7 -0
  131. package/dist/modules/shipping_carriers/data/validators.js +49 -0
  132. package/dist/modules/shipping_carriers/data/validators.js.map +7 -0
  133. package/dist/modules/shipping_carriers/di.js +15 -0
  134. package/dist/modules/shipping_carriers/di.js.map +7 -0
  135. package/dist/modules/shipping_carriers/events.js +19 -0
  136. package/dist/modules/shipping_carriers/events.js.map +7 -0
  137. package/dist/modules/shipping_carriers/i18n/en.js +11 -0
  138. package/dist/modules/shipping_carriers/i18n/en.js.map +7 -0
  139. package/dist/modules/shipping_carriers/i18n/pl.js +11 -0
  140. package/dist/modules/shipping_carriers/i18n/pl.js.map +7 -0
  141. package/dist/modules/shipping_carriers/index.js +9 -0
  142. package/dist/modules/shipping_carriers/index.js.map +7 -0
  143. package/dist/modules/shipping_carriers/lib/adapter-registry.js +29 -0
  144. package/dist/modules/shipping_carriers/lib/adapter-registry.js.map +7 -0
  145. package/dist/modules/shipping_carriers/lib/adapter.js +1 -0
  146. package/dist/modules/shipping_carriers/lib/adapter.js.map +7 -0
  147. package/dist/modules/shipping_carriers/lib/queue.js +17 -0
  148. package/dist/modules/shipping_carriers/lib/queue.js.map +7 -0
  149. package/dist/modules/shipping_carriers/lib/shipping-service.js +155 -0
  150. package/dist/modules/shipping_carriers/lib/shipping-service.js.map +7 -0
  151. package/dist/modules/shipping_carriers/lib/status-sync.js +37 -0
  152. package/dist/modules/shipping_carriers/lib/status-sync.js.map +7 -0
  153. package/dist/modules/shipping_carriers/migrations/Migration20260305170000.js +16 -0
  154. package/dist/modules/shipping_carriers/migrations/Migration20260305170000.js.map +7 -0
  155. package/dist/modules/shipping_carriers/setup.js +13 -0
  156. package/dist/modules/shipping_carriers/setup.js.map +7 -0
  157. package/dist/modules/shipping_carriers/widgets/injection/create-shipment-button/widget.js +25 -0
  158. package/dist/modules/shipping_carriers/widgets/injection/create-shipment-button/widget.js.map +7 -0
  159. package/dist/modules/shipping_carriers/widgets/injection/tracking-column/widget.js +23 -0
  160. package/dist/modules/shipping_carriers/widgets/injection/tracking-column/widget.js.map +7 -0
  161. package/dist/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.js +40 -0
  162. package/dist/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.js.map +7 -0
  163. package/dist/modules/shipping_carriers/widgets/injection-table.js +24 -0
  164. package/dist/modules/shipping_carriers/widgets/injection-table.js.map +7 -0
  165. package/dist/modules/shipping_carriers/workers/status-poller.js +21 -0
  166. package/dist/modules/shipping_carriers/workers/status-poller.js.map +7 -0
  167. package/dist/modules/shipping_carriers/workers/webhook-processor.js +54 -0
  168. package/dist/modules/shipping_carriers/workers/webhook-processor.js.map +7 -0
  169. package/dist/modules/translations/api/get/locales.js +1 -0
  170. package/dist/modules/translations/api/get/locales.js.map +2 -2
  171. package/dist/modules/translations/api/put/locales.js +1 -0
  172. package/dist/modules/translations/api/put/locales.js.map +2 -2
  173. package/generated/entities/carrier_shipment/index.ts +17 -0
  174. package/generated/entities/gateway_transaction/index.ts +22 -0
  175. package/generated/entities/webhook_processed_event/index.ts +7 -0
  176. package/generated/entities.ids.generated.ts +10 -1
  177. package/generated/entity-fields-registry.ts +6 -0
  178. package/jest.config.cjs +1 -0
  179. package/package.json +5 -2
  180. package/src/modules/auth/i18n/de.json +1 -0
  181. package/src/modules/auth/i18n/en.json +1 -0
  182. package/src/modules/auth/i18n/es.json +1 -0
  183. package/src/modules/auth/i18n/pl.json +1 -0
  184. package/src/modules/data_sync/api/runs/[id]/cancel.ts +18 -5
  185. package/src/modules/data_sync/backend/data-sync/page.meta.ts +2 -2
  186. package/src/modules/data_sync/backend/data-sync/runs/[id]/page.tsx +50 -12
  187. package/src/modules/directory/api/get/tenants/lookup.ts +1 -0
  188. package/src/modules/integrations/AGENTS.md +31 -0
  189. package/src/modules/integrations/api/[id]/route.ts +38 -11
  190. package/src/modules/integrations/api/logs/route.ts +53 -27
  191. package/src/modules/integrations/api/route.ts +31 -1
  192. package/src/modules/integrations/api/umes-read.ts +177 -0
  193. package/src/modules/integrations/backend/integrations/[id]/page.tsx +902 -202
  194. package/src/modules/integrations/backend/integrations/bundle/[id]/page.tsx +43 -9
  195. package/src/modules/integrations/backend/integrations/detail-page-widgets.ts +74 -0
  196. package/src/modules/integrations/backend/integrations/page.meta.ts +2 -2
  197. package/src/modules/integrations/backend/integrations/page.tsx +65 -54
  198. package/src/modules/integrations/i18n/de.json +15 -0
  199. package/src/modules/integrations/i18n/en.json +15 -0
  200. package/src/modules/integrations/i18n/es.json +15 -0
  201. package/src/modules/integrations/i18n/pl.json +15 -0
  202. package/src/modules/integrations/setup.ts +2 -2
  203. package/src/modules/payment_gateways/acl.ts +8 -0
  204. package/src/modules/payment_gateways/api/cancel/route.ts +56 -0
  205. package/src/modules/payment_gateways/api/capture/route.ts +56 -0
  206. package/src/modules/payment_gateways/api/interceptors.ts +22 -0
  207. package/src/modules/payment_gateways/api/openapi.ts +1 -0
  208. package/src/modules/payment_gateways/api/refund/route.ts +57 -0
  209. package/src/modules/payment_gateways/api/sessions/route.ts +76 -0
  210. package/src/modules/payment_gateways/api/status/route.ts +69 -0
  211. package/src/modules/payment_gateways/api/transactions/[id]/route.ts +123 -0
  212. package/src/modules/payment_gateways/api/transactions/route.ts +120 -0
  213. package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +161 -0
  214. package/src/modules/payment_gateways/backend/payment-gateways/page.meta.ts +19 -0
  215. package/src/modules/payment_gateways/backend/payment-gateways/page.tsx +660 -0
  216. package/src/modules/payment_gateways/data/enrichers.ts +8 -0
  217. package/src/modules/payment_gateways/data/entities.ts +106 -0
  218. package/src/modules/payment_gateways/data/validators.ts +67 -0
  219. package/src/modules/payment_gateways/di.ts +26 -0
  220. package/src/modules/payment_gateways/events.ts +17 -0
  221. package/src/modules/payment_gateways/i18n/de.json +77 -0
  222. package/src/modules/payment_gateways/i18n/en.json +77 -0
  223. package/src/modules/payment_gateways/i18n/en.ts +4 -0
  224. package/src/modules/payment_gateways/i18n/es.json +77 -0
  225. package/src/modules/payment_gateways/i18n/pl.json +77 -0
  226. package/src/modules/payment_gateways/i18n/pl.ts +4 -0
  227. package/src/modules/payment_gateways/index.ts +5 -0
  228. package/src/modules/payment_gateways/lib/gateway-service.ts +486 -0
  229. package/src/modules/payment_gateways/lib/queue.ts +19 -0
  230. package/src/modules/payment_gateways/lib/status-machine.ts +28 -0
  231. package/src/modules/payment_gateways/lib/webhook-processor.ts +133 -0
  232. package/src/modules/payment_gateways/lib/webhook-utils.ts +52 -0
  233. package/src/modules/payment_gateways/migrations/.snapshot-open-mercato.json +373 -0
  234. package/src/modules/payment_gateways/migrations/Migration20260305122155.ts +20 -0
  235. package/src/modules/payment_gateways/setup.ts +11 -0
  236. package/src/modules/payment_gateways/widgets/injection-table.ts +9 -0
  237. package/src/modules/payment_gateways/workers/status-poller.ts +58 -0
  238. package/src/modules/payment_gateways/workers/webhook-processor.ts +30 -0
  239. package/src/modules/sales/data/enrichers.ts +120 -0
  240. package/src/modules/sales/lib/makeSalesLineRoute.ts +3 -0
  241. package/src/modules/sales/widgets/injection/payment-gateway-config-field/widget.ts +28 -0
  242. package/src/modules/sales/widgets/injection/payment-gateway-status-column/widget.ts +22 -0
  243. package/src/modules/sales/widgets/injection-table.ts +12 -0
  244. package/src/modules/shipping_carriers/acl.ts +6 -0
  245. package/src/modules/shipping_carriers/api/cancel/route.ts +53 -0
  246. package/src/modules/shipping_carriers/api/interceptors.ts +19 -0
  247. package/src/modules/shipping_carriers/api/openapi.ts +1 -0
  248. package/src/modules/shipping_carriers/api/rates/route.ts +53 -0
  249. package/src/modules/shipping_carriers/api/shipments/route.ts +59 -0
  250. package/src/modules/shipping_carriers/api/tracking/route.ts +56 -0
  251. package/src/modules/shipping_carriers/api/webhook/[provider]/route.ts +134 -0
  252. package/src/modules/shipping_carriers/data/enrichers.ts +89 -0
  253. package/src/modules/shipping_carriers/data/entities.ts +60 -0
  254. package/src/modules/shipping_carriers/data/validators.ts +48 -0
  255. package/src/modules/shipping_carriers/di.ts +20 -0
  256. package/src/modules/shipping_carriers/events.ts +16 -0
  257. package/src/modules/shipping_carriers/i18n/de.json +7 -0
  258. package/src/modules/shipping_carriers/i18n/en.json +7 -0
  259. package/src/modules/shipping_carriers/i18n/en.ts +7 -0
  260. package/src/modules/shipping_carriers/i18n/es.json +7 -0
  261. package/src/modules/shipping_carriers/i18n/pl.json +7 -0
  262. package/src/modules/shipping_carriers/i18n/pl.ts +7 -0
  263. package/src/modules/shipping_carriers/index.ts +5 -0
  264. package/src/modules/shipping_carriers/lib/adapter-registry.ts +33 -0
  265. package/src/modules/shipping_carriers/lib/adapter.ts +93 -0
  266. package/src/modules/shipping_carriers/lib/queue.ts +19 -0
  267. package/src/modules/shipping_carriers/lib/shipping-service.ts +204 -0
  268. package/src/modules/shipping_carriers/lib/status-sync.ts +38 -0
  269. package/src/modules/shipping_carriers/migrations/Migration20260305170000.ts +14 -0
  270. package/src/modules/shipping_carriers/setup.ts +11 -0
  271. package/src/modules/shipping_carriers/widgets/injection/create-shipment-button/widget.ts +24 -0
  272. package/src/modules/shipping_carriers/widgets/injection/tracking-column/widget.ts +22 -0
  273. package/src/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.tsx +44 -0
  274. package/src/modules/shipping_carriers/widgets/injection-table.ts +22 -0
  275. package/src/modules/shipping_carriers/workers/status-poller.ts +33 -0
  276. package/src/modules/shipping_carriers/workers/webhook-processor.ts +79 -0
  277. package/src/modules/translations/api/get/locales.ts +1 -0
  278. package/src/modules/translations/api/put/locales.ts +1 -0
@@ -0,0 +1,660 @@
1
+ "use client"
2
+ import * as React from 'react'
3
+ import type { ColumnDef } from '@tanstack/react-table'
4
+ import type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'
5
+ import { Page, PageHeader, PageBody } from '@open-mercato/ui/backend/Page'
6
+ import { DataTable } from '@open-mercato/ui/backend/DataTable'
7
+ import { JsonDisplay } from '@open-mercato/ui/backend/JsonDisplay'
8
+ import { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'
9
+ import { RowActions } from '@open-mercato/ui/backend/RowActions'
10
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
11
+ import { flash } from '@open-mercato/ui/backend/FlashMessages'
12
+ import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
13
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
14
+ import { Badge } from '@open-mercato/ui/primitives/badge'
15
+ import { Button } from '@open-mercato/ui/primitives/button'
16
+ import { Card, CardContent, CardHeader, CardTitle } from '@open-mercato/ui/primitives/card'
17
+ import { Spinner } from '@open-mercato/ui/primitives/spinner'
18
+ import { ChevronDown, ChevronRight, CreditCard, RefreshCw, Webhook } from 'lucide-react'
19
+
20
+ type TransactionRow = {
21
+ id: string
22
+ paymentId: string
23
+ providerKey: string
24
+ providerSessionId?: string | null
25
+ gatewayPaymentId?: string | null
26
+ gatewayRefundId?: string | null
27
+ unifiedStatus: string
28
+ gatewayStatus?: string | null
29
+ amount: string
30
+ currencyCode: string
31
+ redirectUrl?: string | null
32
+ lastWebhookAt?: string | null
33
+ lastPolledAt?: string | null
34
+ createdAt: string | null
35
+ updatedAt: string | null
36
+ }
37
+
38
+ type TransactionLogEntry = {
39
+ id: string
40
+ integrationId: string
41
+ runId?: string | null
42
+ scopeEntityType?: string | null
43
+ scopeEntityId?: string | null
44
+ level: 'info' | 'warn' | 'error'
45
+ message: string
46
+ code?: string | null
47
+ payload?: Record<string, unknown> | null
48
+ createdAt: string | null
49
+ }
50
+
51
+ type TransactionDetail = {
52
+ transaction: {
53
+ id: string
54
+ paymentId: string
55
+ providerKey: string
56
+ providerSessionId?: string | null
57
+ gatewayPaymentId?: string | null
58
+ gatewayRefundId?: string | null
59
+ unifiedStatus: string
60
+ gatewayStatus?: string | null
61
+ redirectUrl?: string | null
62
+ amount: string
63
+ currencyCode: string
64
+ gatewayMetadata?: Record<string, unknown> | null
65
+ webhookLog?: Array<{
66
+ eventType: string
67
+ receivedAt: string
68
+ idempotencyKey: string
69
+ unifiedStatus: string
70
+ processed: boolean
71
+ }> | null
72
+ lastWebhookAt?: string | null
73
+ lastPolledAt?: string | null
74
+ expiresAt?: string | null
75
+ createdAt: string | null
76
+ updatedAt: string | null
77
+ }
78
+ logs: TransactionLogEntry[]
79
+ }
80
+
81
+ type TransactionsResponse = {
82
+ items: TransactionRow[]
83
+ total: number
84
+ page: number
85
+ pageSize: number
86
+ totalPages: number
87
+ }
88
+
89
+ const STATUS_STYLES: Record<string, string> = {
90
+ pending: 'bg-slate-100 text-slate-800',
91
+ authorized: 'bg-blue-100 text-blue-800',
92
+ captured: 'bg-green-100 text-green-800',
93
+ partially_captured: 'bg-emerald-100 text-emerald-800',
94
+ refunded: 'bg-amber-100 text-amber-800',
95
+ partially_refunded: 'bg-orange-100 text-orange-800',
96
+ cancelled: 'bg-zinc-200 text-zinc-900',
97
+ failed: 'bg-red-100 text-red-800',
98
+ expired: 'bg-neutral-200 text-neutral-900',
99
+ unknown: 'bg-purple-100 text-purple-800',
100
+ }
101
+
102
+ const LOG_LEVEL_STYLES: Record<string, string> = {
103
+ info: 'bg-blue-100 text-blue-800',
104
+ warn: 'bg-yellow-100 text-yellow-800',
105
+ error: 'bg-red-100 text-red-800',
106
+ }
107
+
108
+ function formatDateTime(value: string | null | undefined): string {
109
+ if (!value) return '—'
110
+ const parsed = new Date(value)
111
+ if (Number.isNaN(parsed.getTime())) return value
112
+ return parsed.toLocaleString()
113
+ }
114
+
115
+ function formatAmount(value: string, currencyCode: string): string {
116
+ const amount = Number(value)
117
+ if (Number.isNaN(amount)) return `${value} ${currencyCode}`
118
+ try {
119
+ return new Intl.NumberFormat(undefined, {
120
+ style: 'currency',
121
+ currency: currencyCode,
122
+ }).format(amount)
123
+ } catch {
124
+ return `${amount.toFixed(2)} ${currencyCode}`
125
+ }
126
+ }
127
+
128
+ function formatTypeLabel(value: string): string {
129
+ return value
130
+ .split('_')
131
+ .filter(Boolean)
132
+ .map((part) => part[0]?.toUpperCase() + part.slice(1))
133
+ .join(' ')
134
+ }
135
+
136
+ function formatLogDetailLabel(key: string): string {
137
+ return key
138
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
139
+ .replace(/[_-]+/g, ' ')
140
+ .split(' ')
141
+ .filter(Boolean)
142
+ .map((part) => part[0]?.toUpperCase() + part.slice(1))
143
+ .join(' ')
144
+ }
145
+
146
+ function isPrimitiveLogValue(value: unknown): value is string | number | boolean | null {
147
+ return value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean'
148
+ }
149
+
150
+ function splitLogPayload(payload: Record<string, unknown> | null | undefined) {
151
+ if (!payload) {
152
+ return {
153
+ inlineEntries: [] as Array<[string, string | number | boolean | null]>,
154
+ nestedEntries: [] as Array<[string, unknown]>,
155
+ }
156
+ }
157
+
158
+ const inlineEntries: Array<[string, string | number | boolean | null]> = []
159
+ const nestedEntries: Array<[string, unknown]> = []
160
+ Object.entries(payload).forEach(([key, value]) => {
161
+ if (isPrimitiveLogValue(value)) {
162
+ inlineEntries.push([key, value])
163
+ return
164
+ }
165
+ nestedEntries.push([key, value])
166
+ })
167
+ return { inlineEntries, nestedEntries }
168
+ }
169
+
170
+ function DetailStat({ label, value }: { label: string; value: React.ReactNode }) {
171
+ return (
172
+ <div className="rounded-lg border bg-muted/20 px-4 py-3">
173
+ <div className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">{label}</div>
174
+ <div className="mt-1 text-sm font-medium">{value}</div>
175
+ </div>
176
+ )
177
+ }
178
+
179
+ export default function PaymentTransactionsPage() {
180
+ const t = useT()
181
+ const scopeVersion = useOrganizationScopeVersion()
182
+ const [rows, setRows] = React.useState<TransactionRow[]>([])
183
+ const [page, setPage] = React.useState(1)
184
+ const [total, setTotal] = React.useState(0)
185
+ const [totalPages, setTotalPages] = React.useState(1)
186
+ const [search, setSearch] = React.useState('')
187
+ const [filterValues, setFilterValues] = React.useState<FilterValues>({})
188
+ const [isLoading, setIsLoading] = React.useState(true)
189
+ const [selectedId, setSelectedId] = React.useState<string | null>(null)
190
+ const [detail, setDetail] = React.useState<TransactionDetail | null>(null)
191
+ const [isLoadingDetail, setIsLoadingDetail] = React.useState(false)
192
+ const [detailError, setDetailError] = React.useState<string | null>(null)
193
+ const [expandedLogId, setExpandedLogId] = React.useState<string | null>(null)
194
+ const [isRefreshingStatus, setIsRefreshingStatus] = React.useState(false)
195
+ const noneLabel = t('common.none', 'None')
196
+
197
+ const formatLogPrimitiveValue = React.useCallback((value: string | number | boolean | null): string => {
198
+ if (value === null) return noneLabel
199
+ if (typeof value === 'boolean') return value ? t('common.yes', 'Yes') : t('common.no', 'No')
200
+ return String(value)
201
+ }, [noneLabel, t])
202
+
203
+ const loadRows = React.useCallback(async () => {
204
+ setIsLoading(true)
205
+ const params = new URLSearchParams({
206
+ page: String(page),
207
+ pageSize: '20',
208
+ })
209
+ if (search.trim()) params.set('search', search.trim())
210
+ if (typeof filterValues.providerKey === 'string' && filterValues.providerKey) {
211
+ params.set('providerKey', filterValues.providerKey)
212
+ }
213
+ if (typeof filterValues.status === 'string' && filterValues.status) {
214
+ params.set('status', filterValues.status)
215
+ }
216
+ const fallback: TransactionsResponse = { items: [], total: 0, page, pageSize: 20, totalPages: 1 }
217
+ const call = await apiCall<TransactionsResponse>(`/api/payment_gateways/transactions?${params.toString()}`, undefined, { fallback })
218
+ if (call.ok && call.result) {
219
+ setRows(Array.isArray(call.result.items) ? call.result.items : [])
220
+ setTotal(call.result.total ?? 0)
221
+ setTotalPages(call.result.totalPages ?? 1)
222
+ } else {
223
+ flash(t('payment_gateways.transactions.error.load', 'Failed to load payment transactions'), 'error')
224
+ setRows([])
225
+ setTotal(0)
226
+ setTotalPages(1)
227
+ }
228
+ setIsLoading(false)
229
+ }, [filterValues.providerKey, filterValues.status, page, search, t])
230
+
231
+ const loadDetail = React.useCallback(async (transactionId: string) => {
232
+ setIsLoadingDetail(true)
233
+ setDetailError(null)
234
+ const call = await apiCall<TransactionDetail>(`/api/payment_gateways/transactions/${encodeURIComponent(transactionId)}`, undefined, { fallback: null })
235
+ if (call.ok && call.result) {
236
+ setDetail(call.result)
237
+ setExpandedLogId((current) => (current && call.result?.logs.some((log) => log.id === current) ? current : null))
238
+ } else {
239
+ setDetail(null)
240
+ setDetailError(t('payment_gateways.transactions.error.loadDetail', 'Failed to load transaction details'))
241
+ }
242
+ setIsLoadingDetail(false)
243
+ }, [t])
244
+
245
+ React.useEffect(() => {
246
+ void loadRows()
247
+ }, [loadRows, scopeVersion])
248
+
249
+ React.useEffect(() => {
250
+ if (!selectedId) {
251
+ setDetail(null)
252
+ setDetailError(null)
253
+ return
254
+ }
255
+ void loadDetail(selectedId)
256
+ }, [loadDetail, selectedId])
257
+
258
+ React.useEffect(() => {
259
+ setSelectedId((current) => (current && rows.some((row) => row.id === current) ? current : null))
260
+ }, [rows])
261
+
262
+ const handleFiltersApply = React.useCallback((values: FilterValues) => {
263
+ const next: FilterValues = {}
264
+ Object.entries(values).forEach(([key, value]) => {
265
+ if (typeof value === 'string' && value.trim()) next[key] = value
266
+ })
267
+ setFilterValues(next)
268
+ setPage(1)
269
+ }, [])
270
+
271
+ const handleFiltersClear = React.useCallback(() => {
272
+ setFilterValues({})
273
+ setPage(1)
274
+ }, [])
275
+
276
+ const handleRefreshStatus = React.useCallback(async () => {
277
+ if (!selectedId) return
278
+ setIsRefreshingStatus(true)
279
+ const call = await apiCall(`/api/payment_gateways/status?transactionId=${encodeURIComponent(selectedId)}`, undefined, { fallback: null })
280
+ if (!call.ok) {
281
+ flash(t('payment_gateways.transactions.error.refreshStatus', 'Failed to refresh transaction status'), 'error')
282
+ setIsRefreshingStatus(false)
283
+ return
284
+ }
285
+ await Promise.all([
286
+ loadRows(),
287
+ loadDetail(selectedId),
288
+ ])
289
+ flash(t('payment_gateways.transactions.success.refreshStatus', 'Transaction status refreshed'), 'success')
290
+ setIsRefreshingStatus(false)
291
+ }, [loadDetail, loadRows, selectedId, t])
292
+
293
+ const providerOptions = React.useMemo(() => {
294
+ const values = Array.from(new Set(rows.map((row) => row.providerKey).filter(Boolean))).sort()
295
+ return values.map((value) => ({
296
+ label: formatTypeLabel(value),
297
+ value,
298
+ }))
299
+ }, [rows])
300
+
301
+ const filters = React.useMemo<FilterDef[]>(() => [
302
+ {
303
+ id: 'providerKey',
304
+ type: 'select',
305
+ label: t('payment_gateways.transactions.filters.provider', 'Provider'),
306
+ options: [
307
+ { label: t('payment_gateways.transactions.filters.allProviders', 'All providers'), value: '' },
308
+ ...providerOptions,
309
+ ],
310
+ },
311
+ {
312
+ id: 'status',
313
+ type: 'select',
314
+ label: t('payment_gateways.transactions.filters.status', 'Status'),
315
+ options: [
316
+ { label: t('payment_gateways.transactions.filters.allStatuses', 'All statuses'), value: '' },
317
+ { label: t('payment_gateways.status.pending', 'Pending'), value: 'pending' },
318
+ { label: t('payment_gateways.status.authorized', 'Authorized'), value: 'authorized' },
319
+ { label: t('payment_gateways.status.captured', 'Captured'), value: 'captured' },
320
+ { label: t('payment_gateways.status.partially_captured', 'Partially Captured'), value: 'partially_captured' },
321
+ { label: t('payment_gateways.status.refunded', 'Refunded'), value: 'refunded' },
322
+ { label: t('payment_gateways.status.partially_refunded', 'Partially Refunded'), value: 'partially_refunded' },
323
+ { label: t('payment_gateways.status.cancelled', 'Cancelled'), value: 'cancelled' },
324
+ { label: t('payment_gateways.status.failed', 'Failed'), value: 'failed' },
325
+ { label: t('payment_gateways.status.expired', 'Expired'), value: 'expired' },
326
+ { label: t('payment_gateways.status.unknown', 'Unknown'), value: 'unknown' },
327
+ ],
328
+ },
329
+ ], [providerOptions, t])
330
+
331
+ const columns = React.useMemo<ColumnDef<TransactionRow>[]>(() => [
332
+ {
333
+ accessorKey: 'paymentId',
334
+ header: t('payment_gateways.transactions.columns.paymentId', 'Payment'),
335
+ cell: ({ row }) => (
336
+ <div className="space-y-1">
337
+ <div className="font-medium">{row.original.paymentId}</div>
338
+ <div className="text-xs text-muted-foreground">{row.original.id}</div>
339
+ </div>
340
+ ),
341
+ meta: { maxWidth: '20rem' },
342
+ },
343
+ {
344
+ accessorKey: 'providerKey',
345
+ header: t('payment_gateways.transactions.columns.provider', 'Provider'),
346
+ cell: ({ row }) => (
347
+ <div className="flex items-center gap-2">
348
+ <CreditCard className="h-4 w-4 text-muted-foreground" />
349
+ <span>{formatTypeLabel(row.original.providerKey)}</span>
350
+ </div>
351
+ ),
352
+ },
353
+ {
354
+ accessorKey: 'unifiedStatus',
355
+ header: t('payment_gateways.transactions.columns.status', 'Status'),
356
+ cell: ({ row }) => (
357
+ <Badge variant="secondary" className={STATUS_STYLES[row.original.unifiedStatus] ?? ''}>
358
+ {t(`payment_gateways.status.${row.original.unifiedStatus}`, formatTypeLabel(row.original.unifiedStatus))}
359
+ </Badge>
360
+ ),
361
+ },
362
+ {
363
+ accessorKey: 'amount',
364
+ header: t('payment_gateways.transactions.columns.amount', 'Amount'),
365
+ cell: ({ row }) => formatAmount(row.original.amount, row.original.currencyCode),
366
+ },
367
+ {
368
+ accessorKey: 'providerSessionId',
369
+ header: t('payment_gateways.transactions.columns.session', 'Session'),
370
+ cell: ({ row }) => (
371
+ <span className="font-mono text-xs text-muted-foreground">
372
+ {row.original.providerSessionId ?? '—'}
373
+ </span>
374
+ ),
375
+ meta: { maxWidth: '18rem', truncate: true },
376
+ },
377
+ {
378
+ accessorKey: 'updatedAt',
379
+ header: t('payment_gateways.transactions.columns.updatedAt', 'Updated'),
380
+ cell: ({ row }) => formatDateTime(row.original.updatedAt),
381
+ },
382
+ ], [t])
383
+
384
+ const selectedSummary = React.useMemo(
385
+ () => rows.find((row) => row.id === selectedId) ?? null,
386
+ [rows, selectedId],
387
+ )
388
+
389
+ return (
390
+ <Page>
391
+ <PageHeader
392
+ title={t('payment_gateways.transactions.title', 'Payment Transactions')}
393
+ description={t('payment_gateways.transactions.description', 'Track all payment-gateway transactions, inspect webhook activity, and review provider logs from one place.')}
394
+ />
395
+ <PageBody className="space-y-6">
396
+ <DataTable
397
+ title={t('payment_gateways.transactions.tableTitle', 'Transactions')}
398
+ columns={columns}
399
+ data={rows}
400
+ filters={filters}
401
+ filterValues={filterValues}
402
+ onFiltersApply={handleFiltersApply}
403
+ onFiltersClear={handleFiltersClear}
404
+ searchValue={search}
405
+ onSearchChange={(value) => { setSearch(value); setPage(1) }}
406
+ searchPlaceholder={t('payment_gateways.transactions.searchPlaceholder', 'Search by payment, transaction, session, or gateway id')}
407
+ perspective={{ tableId: 'payment_gateways.transactions.list' }}
408
+ pagination={{ page, pageSize: 20, total, totalPages, onPageChange: setPage }}
409
+ isLoading={isLoading}
410
+ onRowClick={(row) => setSelectedId((current) => current === row.id ? null : row.id)}
411
+ rowActions={(row) => (
412
+ <RowActions items={[
413
+ {
414
+ id: 'details',
415
+ label: selectedId === row.id
416
+ ? t('payment_gateways.transactions.actions.hideDetails', 'Hide details')
417
+ : t('payment_gateways.transactions.actions.showDetails', 'Show details'),
418
+ onSelect: () => setSelectedId((current) => current === row.id ? null : row.id),
419
+ },
420
+ ]} />
421
+ )}
422
+ />
423
+
424
+ {selectedId ? (
425
+ <Card>
426
+ <CardHeader className="gap-4 sm:flex-row sm:items-start sm:justify-between">
427
+ <div className="space-y-1">
428
+ <CardTitle>{t('payment_gateways.transactions.detail.title', 'Transaction details')}</CardTitle>
429
+ {selectedSummary ? (
430
+ <div className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
431
+ <span>{selectedSummary.paymentId}</span>
432
+ <span>•</span>
433
+ <span>{formatTypeLabel(selectedSummary.providerKey)}</span>
434
+ <Badge variant="secondary" className={STATUS_STYLES[selectedSummary.unifiedStatus] ?? ''}>
435
+ {t(`payment_gateways.status.${selectedSummary.unifiedStatus}`, formatTypeLabel(selectedSummary.unifiedStatus))}
436
+ </Badge>
437
+ </div>
438
+ ) : null}
439
+ </div>
440
+ <Button
441
+ type="button"
442
+ variant="outline"
443
+ size="sm"
444
+ onClick={() => void handleRefreshStatus()}
445
+ disabled={isRefreshingStatus || isLoadingDetail}
446
+ >
447
+ {isRefreshingStatus ? <Spinner className="mr-2 h-4 w-4" /> : <RefreshCw className="mr-2 h-4 w-4" />}
448
+ {t('payment_gateways.transactions.actions.refreshStatus', 'Refresh status')}
449
+ </Button>
450
+ </CardHeader>
451
+ <CardContent className="space-y-6">
452
+ {isLoadingDetail ? <LoadingMessage label={t('payment_gateways.transactions.detail.loading', 'Loading transaction details')} /> : null}
453
+ {!isLoadingDetail && detailError ? <ErrorMessage label={detailError} /> : null}
454
+ {!isLoadingDetail && !detailError && detail ? (
455
+ <>
456
+ <div className="grid gap-4 lg:grid-cols-4">
457
+ <DetailStat
458
+ label={t('payment_gateways.transactions.detail.summary.status', 'Status')}
459
+ value={(
460
+ <Badge variant="secondary" className={STATUS_STYLES[detail.transaction.unifiedStatus] ?? ''}>
461
+ {t(`payment_gateways.status.${detail.transaction.unifiedStatus}`, formatTypeLabel(detail.transaction.unifiedStatus))}
462
+ </Badge>
463
+ )}
464
+ />
465
+ <DetailStat
466
+ label={t('payment_gateways.transactions.detail.summary.gatewayStatus', 'Gateway status')}
467
+ value={detail.transaction.gatewayStatus ?? noneLabel}
468
+ />
469
+ <DetailStat
470
+ label={t('payment_gateways.transactions.detail.summary.amount', 'Amount')}
471
+ value={formatAmount(detail.transaction.amount, detail.transaction.currencyCode)}
472
+ />
473
+ <DetailStat
474
+ label={t('payment_gateways.transactions.detail.summary.provider', 'Provider')}
475
+ value={formatTypeLabel(detail.transaction.providerKey)}
476
+ />
477
+ </div>
478
+
479
+ <div className="grid gap-6 xl:grid-cols-[minmax(0,1.2fr)_minmax(0,0.8fr)]">
480
+ <div className="space-y-6">
481
+ <section className="space-y-3">
482
+ <h3 className="text-sm font-semibold">{t('payment_gateways.transactions.detail.identifiers', 'Identifiers')}</h3>
483
+ <dl className="grid gap-3 md:grid-cols-2">
484
+ {[
485
+ [t('payment_gateways.transactions.columns.transactionId', 'Transaction ID'), detail.transaction.id],
486
+ [t('payment_gateways.transactions.columns.paymentId', 'Payment ID'), detail.transaction.paymentId],
487
+ [t('payment_gateways.transactions.columns.session', 'Session ID'), detail.transaction.providerSessionId ?? '—'],
488
+ [t('payment_gateways.transactions.columns.gatewayPaymentId', 'Gateway payment ID'), detail.transaction.gatewayPaymentId ?? '—'],
489
+ [t('payment_gateways.transactions.columns.gatewayRefundId', 'Gateway refund ID'), detail.transaction.gatewayRefundId ?? '—'],
490
+ [t('payment_gateways.transactions.columns.redirectUrl', 'Redirect URL'), detail.transaction.redirectUrl ?? '—'],
491
+ [t('payment_gateways.transactions.columns.createdAt', 'Created at'), formatDateTime(detail.transaction.createdAt)],
492
+ [t('payment_gateways.transactions.columns.updatedAt', 'Updated at'), formatDateTime(detail.transaction.updatedAt)],
493
+ [t('payment_gateways.transactions.columns.lastWebhookAt', 'Last webhook'), formatDateTime(detail.transaction.lastWebhookAt)],
494
+ [t('payment_gateways.transactions.columns.lastPolledAt', 'Last poll'), formatDateTime(detail.transaction.lastPolledAt)],
495
+ ].map(([label, value]) => (
496
+ <div key={label} className="rounded-lg border bg-muted/20 px-4 py-3">
497
+ <dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">{label}</dt>
498
+ <dd className="mt-1 break-all text-sm">{value}</dd>
499
+ </div>
500
+ ))}
501
+ </dl>
502
+ </section>
503
+
504
+ <section className="space-y-3">
505
+ <div className="flex items-center gap-2">
506
+ <Webhook className="h-4 w-4 text-muted-foreground" />
507
+ <h3 className="text-sm font-semibold">{t('payment_gateways.transactions.detail.webhooks', 'Webhook activity')}</h3>
508
+ </div>
509
+ {detail.transaction.webhookLog && detail.transaction.webhookLog.length > 0 ? (
510
+ <div className="overflow-hidden rounded-lg border">
511
+ <table className="w-full text-sm">
512
+ <thead>
513
+ <tr className="border-b bg-muted/40">
514
+ <th className="px-4 py-2 text-left font-medium">{t('payment_gateways.transactions.columns.eventType', 'Event')}</th>
515
+ <th className="px-4 py-2 text-left font-medium">{t('payment_gateways.transactions.columns.status', 'Status')}</th>
516
+ <th className="px-4 py-2 text-left font-medium">{t('payment_gateways.transactions.columns.processed', 'Processed')}</th>
517
+ <th className="px-4 py-2 text-left font-medium">{t('payment_gateways.transactions.columns.receivedAt', 'Received')}</th>
518
+ </tr>
519
+ </thead>
520
+ <tbody>
521
+ {detail.transaction.webhookLog.map((entry) => (
522
+ <tr key={`${entry.idempotencyKey}:${entry.receivedAt}`} className="border-b last:border-0">
523
+ <td className="px-4 py-2 font-medium">{entry.eventType}</td>
524
+ <td className="px-4 py-2">
525
+ <Badge variant="secondary" className={STATUS_STYLES[entry.unifiedStatus] ?? ''}>
526
+ {t(`payment_gateways.status.${entry.unifiedStatus}`, formatTypeLabel(entry.unifiedStatus))}
527
+ </Badge>
528
+ </td>
529
+ <td className="px-4 py-2">{entry.processed ? t('common.yes', 'Yes') : t('common.no', 'No')}</td>
530
+ <td className="px-4 py-2 text-muted-foreground">{formatDateTime(entry.receivedAt)}</td>
531
+ </tr>
532
+ ))}
533
+ </tbody>
534
+ </table>
535
+ </div>
536
+ ) : (
537
+ <p className="text-sm text-muted-foreground">{t('payment_gateways.transactions.detail.webhooksEmpty', 'No webhook events have been recorded for this transaction yet.')}</p>
538
+ )}
539
+ </section>
540
+
541
+ <section className="space-y-3">
542
+ <h3 className="text-sm font-semibold">{t('payment_gateways.transactions.detail.logs', 'Gateway logs')}</h3>
543
+ {detail.logs.length === 0 ? (
544
+ <p className="text-sm text-muted-foreground">{t('payment_gateways.transactions.detail.logsEmpty', 'No transaction-scoped logs are available yet.')}</p>
545
+ ) : (
546
+ <div className="overflow-hidden rounded-lg border">
547
+ <table className="w-full text-sm">
548
+ <thead>
549
+ <tr className="border-b bg-muted/40">
550
+ <th className="px-4 py-2 text-left font-medium">{t('payment_gateways.transactions.columns.time', 'Time')}</th>
551
+ <th className="px-4 py-2 text-left font-medium">{t('payment_gateways.transactions.columns.level', 'Level')}</th>
552
+ <th className="px-4 py-2 text-left font-medium">{t('payment_gateways.transactions.columns.message', 'Message')}</th>
553
+ </tr>
554
+ </thead>
555
+ <tbody>
556
+ {detail.logs.map((log) => {
557
+ const isExpanded = expandedLogId === log.id
558
+ const metadataEntries = [
559
+ [t('payment_gateways.transactions.log.time', 'Time'), formatDateTime(log.createdAt)],
560
+ [t('payment_gateways.transactions.log.level', 'Level'), t(`payment_gateways.transactions.level.${log.level}`, log.level)],
561
+ [t('payment_gateways.transactions.log.code', 'Code'), log.code ?? null],
562
+ [t('payment_gateways.transactions.log.runId', 'Run ID'), log.runId ?? null],
563
+ [t('payment_gateways.transactions.log.entityType', 'Entity Type'), log.scopeEntityType ?? null],
564
+ [t('payment_gateways.transactions.log.entityId', 'Entity ID'), log.scopeEntityId ?? null],
565
+ ].filter((entry): entry is [string, string] => typeof entry[1] === 'string' && entry[1].trim().length > 0)
566
+ const { inlineEntries, nestedEntries } = splitLogPayload(log.payload)
567
+
568
+ return (
569
+ <React.Fragment key={log.id}>
570
+ <tr className="border-b last:border-0">
571
+ <td className="whitespace-nowrap px-4 py-2 text-muted-foreground">{formatDateTime(log.createdAt)}</td>
572
+ <td className="px-4 py-2">
573
+ <Badge variant="secondary" className={LOG_LEVEL_STYLES[log.level] ?? ''}>
574
+ {t(`payment_gateways.transactions.level.${log.level}`, log.level)}
575
+ </Badge>
576
+ </td>
577
+ <td className="px-4 py-2">
578
+ <Button
579
+ type="button"
580
+ variant="ghost"
581
+ size="sm"
582
+ className="h-auto w-full justify-start gap-2 px-0 py-0 text-left hover:bg-transparent"
583
+ onClick={() => setExpandedLogId((current) => current === log.id ? null : log.id)}
584
+ >
585
+ {isExpanded ? <ChevronDown className="h-4 w-4 shrink-0" /> : <ChevronRight className="h-4 w-4 shrink-0" />}
586
+ <span className="truncate">{log.message}</span>
587
+ </Button>
588
+ </td>
589
+ </tr>
590
+ {isExpanded ? (
591
+ <tr className="border-b bg-muted/15 last:border-0">
592
+ <td colSpan={3} className="px-4 py-4">
593
+ <div className="space-y-4 rounded-lg border bg-card p-4">
594
+ {metadataEntries.length > 0 ? (
595
+ <dl className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
596
+ {metadataEntries.map(([label, value]) => (
597
+ <div key={label} className="rounded-md border bg-muted/20 px-3 py-2">
598
+ <dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">{label}</dt>
599
+ <dd className="mt-1 break-all text-sm">{value}</dd>
600
+ </div>
601
+ ))}
602
+ </dl>
603
+ ) : null}
604
+ {inlineEntries.length > 0 ? (
605
+ <dl className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
606
+ {inlineEntries.map(([key, value]) => (
607
+ <div key={key} className="rounded-md border bg-muted/20 px-3 py-2">
608
+ <dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">{formatLogDetailLabel(key)}</dt>
609
+ <dd className="mt-1 break-words text-sm">{formatLogPrimitiveValue(value)}</dd>
610
+ </div>
611
+ ))}
612
+ </dl>
613
+ ) : null}
614
+ {nestedEntries.map(([key, value]) => (
615
+ <JsonDisplay
616
+ key={key}
617
+ data={value}
618
+ title={formatLogDetailLabel(key)}
619
+ defaultExpanded
620
+ maxInitialDepth={1}
621
+ theme="dark"
622
+ maxHeight="16rem"
623
+ className="p-4"
624
+ />
625
+ ))}
626
+ </div>
627
+ </td>
628
+ </tr>
629
+ ) : null}
630
+ </React.Fragment>
631
+ )
632
+ })}
633
+ </tbody>
634
+ </table>
635
+ </div>
636
+ )}
637
+ </section>
638
+ </div>
639
+
640
+ <div className="space-y-4">
641
+ <JsonDisplay
642
+ data={detail.transaction.gatewayMetadata ?? {}}
643
+ title={t('payment_gateways.transactions.detail.gatewayMetadata', 'Gateway metadata')}
644
+ defaultExpanded
645
+ maxInitialDepth={1}
646
+ theme="dark"
647
+ maxHeight="24rem"
648
+ className="p-4"
649
+ />
650
+ </div>
651
+ </div>
652
+ </>
653
+ ) : null}
654
+ </CardContent>
655
+ </Card>
656
+ ) : null}
657
+ </PageBody>
658
+ </Page>
659
+ )
660
+ }
@@ -0,0 +1,8 @@
1
+ import type { ResponseEnricher } from '@open-mercato/shared/lib/crud/response-enricher'
2
+
3
+ /**
4
+ * Keep payment_gateways module decoupled from feature modules.
5
+ * Module-specific bindings (for example sales.payment enrichment) should be
6
+ * declared by those feature modules and consume payment_gateways as a dependency.
7
+ */
8
+ export const enrichers: ResponseEnricher[] = []