@easypayment/medusa-paypal 0.2.6 → 0.2.8

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 (63) hide show
  1. package/.medusa/server/src/admin/index.js +689 -934
  2. package/.medusa/server/src/admin/index.mjs +689 -934
  3. package/.medusa/server/src/api/store/payment-collections/[id]/payment-sessions/route.d.ts.map +1 -1
  4. package/.medusa/server/src/api/store/payment-collections/[id]/payment-sessions/route.js +1 -0
  5. package/.medusa/server/src/api/store/payment-collections/[id]/payment-sessions/route.js.map +1 -1
  6. package/.medusa/server/src/api/store/paypal/capture-order/route.d.ts.map +1 -1
  7. package/.medusa/server/src/api/store/paypal/capture-order/route.js +62 -74
  8. package/.medusa/server/src/api/store/paypal/capture-order/route.js.map +1 -1
  9. package/.medusa/server/src/api/store/paypal/config/route.d.ts.map +1 -1
  10. package/.medusa/server/src/api/store/paypal/config/route.js +9 -2
  11. package/.medusa/server/src/api/store/paypal/config/route.js.map +1 -1
  12. package/.medusa/server/src/api/store/paypal/create-order/route.d.ts.map +1 -1
  13. package/.medusa/server/src/api/store/paypal/create-order/route.js +3 -24
  14. package/.medusa/server/src/api/store/paypal/create-order/route.js.map +1 -1
  15. package/.medusa/server/src/api/store/paypal/settings/route.d.ts.map +1 -1
  16. package/.medusa/server/src/api/store/paypal/settings/route.js +7 -1
  17. package/.medusa/server/src/api/store/paypal/settings/route.js.map +1 -1
  18. package/.medusa/server/src/api/store/paypal-complete/route.d.ts +1 -8
  19. package/.medusa/server/src/api/store/paypal-complete/route.d.ts.map +1 -1
  20. package/.medusa/server/src/api/store/paypal-complete/route.js +47 -39
  21. package/.medusa/server/src/api/store/paypal-complete/route.js.map +1 -1
  22. package/.medusa/server/src/jobs/paypal-reconcile.d.ts.map +1 -1
  23. package/.medusa/server/src/jobs/paypal-reconcile.js +19 -5
  24. package/.medusa/server/src/jobs/paypal-reconcile.js.map +1 -1
  25. package/.medusa/server/src/modules/paypal/payment-provider/card-service.d.ts.map +1 -1
  26. package/.medusa/server/src/modules/paypal/payment-provider/card-service.js +54 -4
  27. package/.medusa/server/src/modules/paypal/payment-provider/card-service.js.map +1 -1
  28. package/.medusa/server/src/modules/paypal/payment-provider/service.d.ts +4 -1
  29. package/.medusa/server/src/modules/paypal/payment-provider/service.d.ts.map +1 -1
  30. package/.medusa/server/src/modules/paypal/payment-provider/service.js +35 -8
  31. package/.medusa/server/src/modules/paypal/payment-provider/service.js.map +1 -1
  32. package/.medusa/server/src/modules/paypal/service.d.ts +67 -61
  33. package/.medusa/server/src/modules/paypal/service.d.ts.map +1 -1
  34. package/.medusa/server/src/modules/paypal/service.js +34 -4
  35. package/.medusa/server/src/modules/paypal/service.js.map +1 -1
  36. package/.medusa/server/src/modules/paypal/utils/paypal-auth.d.ts +14 -0
  37. package/.medusa/server/src/modules/paypal/utils/paypal-auth.d.ts.map +1 -0
  38. package/.medusa/server/src/modules/paypal/utils/paypal-auth.js +32 -0
  39. package/.medusa/server/src/modules/paypal/utils/paypal-auth.js.map +1 -0
  40. package/.medusa/server/src/modules/paypal/webhook-processor.d.ts +9 -9
  41. package/.medusa/server/src/modules/paypal/webhook-processor.d.ts.map +1 -1
  42. package/.medusa/server/src/modules/paypal/webhook-processor.js +20 -7
  43. package/.medusa/server/src/modules/paypal/webhook-processor.js.map +1 -1
  44. package/package.json +1 -1
  45. package/src/admin/routes/settings/paypal/additional-settings/page.tsx +226 -346
  46. package/src/admin/routes/settings/paypal/advanced-card-payments/page.tsx +227 -381
  47. package/src/admin/routes/settings/paypal/audit-logs/page.tsx +127 -131
  48. package/src/admin/routes/settings/paypal/disputes/page.tsx +186 -259
  49. package/src/admin/routes/settings/paypal/paypal-settings/page.tsx +599 -557
  50. package/src/admin/routes/settings/paypal/reconciliation-status/page.tsx +120 -165
  51. package/src/api/store/payment-collections/[id]/payment-sessions/route.ts +12 -1
  52. package/src/api/store/paypal/capture-order/route.ts +276 -284
  53. package/src/api/store/paypal/config/route.ts +12 -8
  54. package/src/api/store/paypal/create-order/route.ts +2 -32
  55. package/src/api/store/paypal/settings/route.ts +8 -1
  56. package/src/api/store/paypal-complete/route.ts +76 -65
  57. package/src/jobs/paypal-reconcile.ts +21 -6
  58. package/src/modules/paypal/payment-provider/card-service.ts +54 -4
  59. package/src/modules/paypal/payment-provider/service.ts +47 -20
  60. package/src/modules/paypal/service.ts +39 -4
  61. package/src/modules/paypal/utils/paypal-auth.ts +32 -0
  62. package/src/modules/paypal/webhook-processor.ts +22 -8
  63. package/tsconfig.json +1 -1
@@ -1,259 +1,186 @@
1
- import React, { useCallback, useEffect, useMemo, useState } from "react"
2
- import PayPalTabs from "../_components/Tabs"
3
-
4
- type Dispute = {
5
- id: string
6
- dispute_id: string
7
- status?: string | null
8
- reason?: string | null
9
- stage?: string | null
10
- amount?: string | null
11
- currency_code?: string | null
12
- transaction_id?: string | null
13
- seller_transaction_id?: string | null
14
- order_id?: string | null
15
- cart_id?: string | null
16
- created_at?: string
17
- updated_at?: string
18
- }
19
-
20
- type Filters = {
21
- dispute_id: string
22
- status: string
23
- order_id: string
24
- cart_id: string
25
- }
26
-
27
- const EMPTY_FILTERS: Filters = {
28
- dispute_id: "",
29
- status: "",
30
- order_id: "",
31
- cart_id: "",
32
- }
33
-
34
- function formatDate(value?: string) {
35
- if (!value) {
36
- return ""
37
- }
38
- const parsed = new Date(value)
39
- if (Number.isNaN(parsed.getTime())) {
40
- return value
41
- }
42
- return parsed.toLocaleString()
43
- }
44
-
45
- export default function PayPalDisputesPage() {
46
- const [filters, setFilters] = useState<Filters>({ ...EMPTY_FILTERS })
47
- const [disputes, setDisputes] = useState<Dispute[]>([])
48
- const [loading, setLoading] = useState(false)
49
- const [error, setError] = useState<string | null>(null)
50
-
51
- const queryString = useMemo(() => {
52
- const params = new URLSearchParams()
53
- Object.entries(filters).forEach(([key, value]) => {
54
- if (value.trim()) {
55
- params.set(key, value.trim())
56
- }
57
- })
58
- const qs = params.toString()
59
- return qs ? `?${qs}` : ""
60
- }, [filters])
61
-
62
- const fetchDisputes = useCallback(async (source: Filters) => {
63
- try {
64
- setLoading(true)
65
- setError(null)
66
- const params = new URLSearchParams()
67
- Object.entries(source).forEach(([key, value]) => {
68
- if (value.trim()) {
69
- params.set(key, value.trim())
70
- }
71
- })
72
-
73
- const qs = params.toString()
74
- const resp = await fetch(`/admin/paypal/disputes${qs ? `?${qs}` : ""}`, {
75
- credentials: "include",
76
- headers: {
77
- Accept: "application/json",
78
- },
79
- })
80
-
81
- if (!resp.ok) {
82
- const message = await resp.text().catch(() => "")
83
- throw new Error(message || "Failed to load disputes")
84
- }
85
-
86
- const json = await resp.json().catch(() => ({}))
87
- setDisputes((json?.disputes as Dispute[]) || [])
88
- } catch (fetchError: any) {
89
- setError(fetchError?.message || "Failed to load disputes")
90
- setDisputes([])
91
- } finally {
92
- setLoading(false)
93
- }
94
- }, [])
95
-
96
- useEffect(() => {
97
- fetchDisputes(EMPTY_FILTERS)
98
- }, [fetchDisputes])
99
-
100
- const onSubmit = (event: React.FormEvent) => {
101
- event.preventDefault()
102
- fetchDisputes(filters)
103
- }
104
-
105
- const onReset = () => {
106
- setFilters({ ...EMPTY_FILTERS })
107
- fetchDisputes(EMPTY_FILTERS)
108
- }
109
-
110
- return (
111
- <div className="p-6">
112
- <div className="flex flex-col gap-6">
113
- <div>
114
- <h1 className="text-xl font-semibold text-ui-fg-base">PayPal Disputes</h1>
115
- <p className="mt-1 text-sm text-ui-fg-subtle">
116
- Review PayPal dispute activity tied to your Medusa orders. This view is read-only.
117
- </p>
118
- </div>
119
-
120
- <PayPalTabs />
121
-
122
- <div className="rounded-xl border border-ui-border-base bg-ui-bg-base shadow-sm">
123
- <div className="border-b border-ui-border-base p-4">
124
- <div className="text-base font-semibold text-ui-fg-base">Filters</div>
125
- </div>
126
- <div className="p-4">
127
- <form onSubmit={onSubmit} className="flex flex-col gap-4">
128
- <div className="grid grid-cols-1 gap-4 md:grid-cols-4">
129
- <label className="flex flex-col gap-1 text-sm text-ui-fg-subtle">
130
- Dispute ID
131
- <input
132
- className="rounded-md border border-ui-border-base bg-ui-bg-base px-3 py-2 text-sm text-ui-fg-base"
133
- value={filters.dispute_id}
134
- onChange={(event) =>
135
- setFilters((prev) => ({ ...prev, dispute_id: event.target.value }))
136
- }
137
- placeholder="PP-D-123"
138
- />
139
- </label>
140
- <label className="flex flex-col gap-1 text-sm text-ui-fg-subtle">
141
- Status
142
- <input
143
- className="rounded-md border border-ui-border-base bg-ui-bg-base px-3 py-2 text-sm text-ui-fg-base"
144
- value={filters.status}
145
- onChange={(event) =>
146
- setFilters((prev) => ({ ...prev, status: event.target.value }))
147
- }
148
- placeholder="OPEN"
149
- />
150
- </label>
151
- <label className="flex flex-col gap-1 text-sm text-ui-fg-subtle">
152
- Order ID
153
- <input
154
- className="rounded-md border border-ui-border-base bg-ui-bg-base px-3 py-2 text-sm text-ui-fg-base"
155
- value={filters.order_id}
156
- onChange={(event) =>
157
- setFilters((prev) => ({ ...prev, order_id: event.target.value }))
158
- }
159
- placeholder="order_..."
160
- />
161
- </label>
162
- <label className="flex flex-col gap-1 text-sm text-ui-fg-subtle">
163
- Cart ID
164
- <input
165
- className="rounded-md border border-ui-border-base bg-ui-bg-base px-3 py-2 text-sm text-ui-fg-base"
166
- value={filters.cart_id}
167
- onChange={(event) =>
168
- setFilters((prev) => ({ ...prev, cart_id: event.target.value }))
169
- }
170
- placeholder="cart_..."
171
- />
172
- </label>
173
- </div>
174
- <div className="flex flex-wrap gap-3">
175
- <button
176
- type="submit"
177
- className="rounded-md bg-ui-fg-base px-4 py-2 text-sm font-medium text-ui-bg-base"
178
- disabled={loading}
179
- >
180
- {loading ? "Loading..." : "Apply filters"}
181
- </button>
182
- <button
183
- type="button"
184
- className="rounded-md border border-ui-border-base px-4 py-2 text-sm text-ui-fg-base"
185
- onClick={onReset}
186
- disabled={loading}
187
- >
188
- Reset
189
- </button>
190
- <span className="text-sm text-ui-fg-subtle">
191
- Showing {disputes.length} dispute{disputes.length === 1 ? "" : "s"}
192
- {queryString ? " (filtered)" : ""}
193
- </span>
194
- </div>
195
- </form>
196
- </div>
197
- </div>
198
-
199
- <div className="rounded-xl border border-ui-border-base bg-ui-bg-base shadow-sm">
200
- <div className="border-b border-ui-border-base p-4">
201
- <div className="text-base font-semibold text-ui-fg-base">Dispute Records</div>
202
- </div>
203
- <div className="overflow-x-auto">
204
- <table className="min-w-full divide-y divide-ui-border-base text-sm">
205
- <thead className="bg-ui-bg-subtle">
206
- <tr className="text-left text-ui-fg-subtle">
207
- <th className="px-4 py-3 font-medium">Dispute</th>
208
- <th className="px-4 py-3 font-medium">Status</th>
209
- <th className="px-4 py-3 font-medium">Reason</th>
210
- <th className="px-4 py-3 font-medium">Stage</th>
211
- <th className="px-4 py-3 font-medium">Amount</th>
212
- <th className="px-4 py-3 font-medium">Order</th>
213
- <th className="px-4 py-3 font-medium">Cart</th>
214
- <th className="px-4 py-3 font-medium">Updated</th>
215
- </tr>
216
- </thead>
217
- <tbody className="divide-y divide-ui-border-base text-ui-fg-base">
218
- {disputes.length === 0 ? (
219
- <tr>
220
- <td className="px-4 py-6 text-center text-ui-fg-subtle" colSpan={8}>
221
- {loading ? "Loading disputes..." : "No disputes found."}
222
- </td>
223
- </tr>
224
- ) : (
225
- disputes.map((dispute) => (
226
- <tr key={dispute.id}>
227
- <td className="px-4 py-3">
228
- <div className="font-medium text-ui-fg-base">{dispute.dispute_id}</div>
229
- <div className="text-xs text-ui-fg-subtle">
230
- {dispute.transaction_id || "No transaction"}
231
- </div>
232
- </td>
233
- <td className="px-4 py-3">{dispute.status || "Unknown"}</td>
234
- <td className="px-4 py-3">{dispute.reason || "-"}</td>
235
- <td className="px-4 py-3">{dispute.stage || "-"}</td>
236
- <td className="px-4 py-3">
237
- {dispute.amount ? `${dispute.amount} ${dispute.currency_code || ""}` : "-"}
238
- </td>
239
- <td className="px-4 py-3">{dispute.order_id || "-"}</td>
240
- <td className="px-4 py-3">{dispute.cart_id || "-"}</td>
241
- <td className="px-4 py-3 text-ui-fg-subtle">
242
- {formatDate(dispute.updated_at || dispute.created_at)}
243
- </td>
244
- </tr>
245
- ))
246
- )}
247
- </tbody>
248
- </table>
249
- </div>
250
- {error ? (
251
- <div className="border-t border-ui-border-base px-4 py-3 text-sm text-ui-fg-error">
252
- {error}
253
- </div>
254
- ) : null}
255
- </div>
256
- </div>
257
- </div>
258
- )
259
- }
1
+ import React, { useCallback, useEffect, useMemo, useState } from "react"
2
+ import PayPalTabs from "../_components/Tabs"
3
+
4
+ type AdminFetchOptions = {
5
+ method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH"
6
+ body?: Record<string, unknown>
7
+ query?: Record<string, string>
8
+ }
9
+
10
+ async function adminFetch<T = unknown>(path: string, opts: AdminFetchOptions = {}): Promise<T> {
11
+ const { method = "GET", body, query } = opts
12
+ let url = path
13
+ if (query && Object.keys(query).length > 0) {
14
+ const params = new URLSearchParams(query)
15
+ url = `${path}?${params.toString()}`
16
+ }
17
+ const headers: Record<string, string> = { Accept: "application/json" }
18
+ if (body !== undefined) headers["Content-Type"] = "application/json"
19
+ if (typeof window !== "undefined") {
20
+ const token = (window as any).__medusa__?.token
21
+ if (token) headers["Authorization"] = `Bearer ${token}`
22
+ }
23
+ const res = await fetch(url, {
24
+ method, headers, credentials: "include",
25
+ body: body !== undefined ? JSON.stringify(body) : undefined,
26
+ })
27
+ const text = await res.text().catch(() => "")
28
+ if (!res.ok) {
29
+ if (res.status === 401) throw new Error("Unauthorized (401) - session may have expired. Please reload and log in again.")
30
+ if (res.status === 403) throw new Error("Forbidden (403) - you do not have permission to perform this action.")
31
+ throw new Error(text || `Request failed with status ${res.status}`)
32
+ }
33
+ if (!text) return {} as T
34
+ try { return JSON.parse(text) as T } catch { return {} as T }
35
+ }
36
+
37
+ type Dispute = {
38
+ id: string
39
+ dispute_id: string
40
+ status?: string | null
41
+ reason?: string | null
42
+ stage?: string | null
43
+ amount?: string | null
44
+ currency_code?: string | null
45
+ transaction_id?: string | null
46
+ seller_transaction_id?: string | null
47
+ order_id?: string | null
48
+ cart_id?: string | null
49
+ created_at?: string
50
+ updated_at?: string
51
+ }
52
+
53
+ type Filters = {
54
+ dispute_id: string
55
+ status: string
56
+ order_id: string
57
+ cart_id: string
58
+ }
59
+
60
+ const EMPTY_FILTERS: Filters = { dispute_id: "", status: "", order_id: "", cart_id: "" }
61
+
62
+ function formatDate(value?: string) {
63
+ if (!value) return ""
64
+ const parsed = new Date(value)
65
+ if (Number.isNaN(parsed.getTime())) return value
66
+ return parsed.toLocaleString()
67
+ }
68
+
69
+ export default function PayPalDisputesPage() {
70
+ const [filters, setFilters] = useState<Filters>({ ...EMPTY_FILTERS })
71
+ const [disputes, setDisputes] = useState<Dispute[]>([])
72
+ const [loading, setLoading] = useState(false)
73
+ const [error, setError] = useState<string | null>(null)
74
+
75
+ const queryString = useMemo(() => {
76
+ const params = new URLSearchParams()
77
+ Object.entries(filters).forEach(([key, value]) => { if (value.trim()) params.set(key, value.trim()) })
78
+ const qs = params.toString()
79
+ return qs ? `?${qs}` : ""
80
+ }, [filters])
81
+
82
+ const fetchDisputes = useCallback(async (source: Filters) => {
83
+ try {
84
+ setLoading(true)
85
+ setError(null)
86
+ const query: Record<string, string> = {}
87
+ Object.entries(source).forEach(([key, value]) => { if (value.trim()) query[key] = value.trim() })
88
+ const json = await adminFetch<{ disputes: Dispute[] }>("/admin/paypal/disputes", { query })
89
+ setDisputes(json?.disputes ?? [])
90
+ } catch (fetchError: unknown) {
91
+ setError(fetchError instanceof Error ? fetchError.message : "Failed to load disputes")
92
+ setDisputes([])
93
+ } finally {
94
+ setLoading(false)
95
+ }
96
+ }, [])
97
+
98
+ useEffect(() => { fetchDisputes(EMPTY_FILTERS) }, [fetchDisputes])
99
+
100
+ const onSubmit = (event: React.FormEvent) => { event.preventDefault(); fetchDisputes(filters) }
101
+ const onReset = () => { setFilters({ ...EMPTY_FILTERS }); fetchDisputes(EMPTY_FILTERS) }
102
+
103
+ return (
104
+ <div className="p-6">
105
+ <div className="flex flex-col gap-6">
106
+ <div>
107
+ <h1 className="text-xl font-semibold text-ui-fg-base">PayPal Disputes</h1>
108
+ <p className="mt-1 text-sm text-ui-fg-subtle">Review PayPal dispute activity tied to your Medusa orders. This view is read-only.</p>
109
+ </div>
110
+ <PayPalTabs />
111
+ <div className="rounded-xl border border-ui-border-base bg-ui-bg-base shadow-sm">
112
+ <div className="border-b border-ui-border-base p-4">
113
+ <div className="text-base font-semibold text-ui-fg-base">Filters</div>
114
+ </div>
115
+ <div className="p-4">
116
+ <form onSubmit={onSubmit} className="flex flex-col gap-4">
117
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-4">
118
+ <label className="flex flex-col gap-1 text-sm text-ui-fg-subtle">
119
+ Dispute ID
120
+ <input className="rounded-md border border-ui-border-base bg-ui-bg-base px-3 py-2 text-sm text-ui-fg-base" value={filters.dispute_id} onChange={(e) => setFilters((p) => ({ ...p, dispute_id: e.target.value }))} placeholder="PP-D-123" />
121
+ </label>
122
+ <label className="flex flex-col gap-1 text-sm text-ui-fg-subtle">
123
+ Status
124
+ <input className="rounded-md border border-ui-border-base bg-ui-bg-base px-3 py-2 text-sm text-ui-fg-base" value={filters.status} onChange={(e) => setFilters((p) => ({ ...p, status: e.target.value }))} placeholder="OPEN" />
125
+ </label>
126
+ <label className="flex flex-col gap-1 text-sm text-ui-fg-subtle">
127
+ Order ID
128
+ <input className="rounded-md border border-ui-border-base bg-ui-bg-base px-3 py-2 text-sm text-ui-fg-base" value={filters.order_id} onChange={(e) => setFilters((p) => ({ ...p, order_id: e.target.value }))} placeholder="order_..." />
129
+ </label>
130
+ <label className="flex flex-col gap-1 text-sm text-ui-fg-subtle">
131
+ Cart ID
132
+ <input className="rounded-md border border-ui-border-base bg-ui-bg-base px-3 py-2 text-sm text-ui-fg-base" value={filters.cart_id} onChange={(e) => setFilters((p) => ({ ...p, cart_id: e.target.value }))} placeholder="cart_..." />
133
+ </label>
134
+ </div>
135
+ <div className="flex flex-wrap gap-3">
136
+ <button type="submit" className="rounded-md bg-ui-fg-base px-4 py-2 text-sm font-medium text-ui-bg-base" disabled={loading}>{loading ? "Loading..." : "Apply filters"}</button>
137
+ <button type="button" className="rounded-md border border-ui-border-base px-4 py-2 text-sm text-ui-fg-base" onClick={onReset} disabled={loading}>Reset</button>
138
+ <span className="text-sm text-ui-fg-subtle">Showing {disputes.length} dispute{disputes.length === 1 ? "" : "s"}{queryString ? " (filtered)" : ""}</span>
139
+ </div>
140
+ </form>
141
+ </div>
142
+ </div>
143
+ <div className="rounded-xl border border-ui-border-base bg-ui-bg-base shadow-sm">
144
+ <div className="border-b border-ui-border-base p-4">
145
+ <div className="text-base font-semibold text-ui-fg-base">Dispute Records</div>
146
+ </div>
147
+ <div className="overflow-x-auto">
148
+ <table className="min-w-full divide-y divide-ui-border-base text-sm">
149
+ <thead className="bg-ui-bg-subtle">
150
+ <tr className="text-left text-ui-fg-subtle">
151
+ <th className="px-4 py-3 font-medium">Dispute</th>
152
+ <th className="px-4 py-3 font-medium">Status</th>
153
+ <th className="px-4 py-3 font-medium">Reason</th>
154
+ <th className="px-4 py-3 font-medium">Stage</th>
155
+ <th className="px-4 py-3 font-medium">Amount</th>
156
+ <th className="px-4 py-3 font-medium">Order</th>
157
+ <th className="px-4 py-3 font-medium">Cart</th>
158
+ <th className="px-4 py-3 font-medium">Updated</th>
159
+ </tr>
160
+ </thead>
161
+ <tbody className="divide-y divide-ui-border-base text-ui-fg-base">
162
+ {disputes.length === 0 ? (
163
+ <tr><td className="px-4 py-6 text-center text-ui-fg-subtle" colSpan={8}>{loading ? "Loading disputes..." : "No disputes found."}</td></tr>
164
+ ) : (
165
+ disputes.map((dispute) => (
166
+ <tr key={dispute.id}>
167
+ <td className="px-4 py-3"><div className="font-medium text-ui-fg-base">{dispute.dispute_id}</div><div className="text-xs text-ui-fg-subtle">{dispute.transaction_id || "No transaction"}</div></td>
168
+ <td className="px-4 py-3">{dispute.status || "Unknown"}</td>
169
+ <td className="px-4 py-3">{dispute.reason || "-"}</td>
170
+ <td className="px-4 py-3">{dispute.stage || "-"}</td>
171
+ <td className="px-4 py-3">{dispute.amount ? `${dispute.amount} ${dispute.currency_code || ""}` : "-"}</td>
172
+ <td className="px-4 py-3">{dispute.order_id || "-"}</td>
173
+ <td className="px-4 py-3">{dispute.cart_id || "-"}</td>
174
+ <td className="px-4 py-3 text-ui-fg-subtle">{formatDate(dispute.updated_at || dispute.created_at)}</td>
175
+ </tr>
176
+ ))
177
+ )}
178
+ </tbody>
179
+ </table>
180
+ </div>
181
+ {error ? <div className="border-t border-ui-border-base px-4 py-3 text-sm text-ui-fg-error">{error}</div> : null}
182
+ </div>
183
+ </div>
184
+ </div>
185
+ )
186
+ }