@86d-app/payments 0.0.3

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.
@@ -0,0 +1,521 @@
1
+ "use client";
2
+
3
+ import { useModuleClient } from "@86d-app/core/client";
4
+ import { useState } from "react";
5
+ import PaymentsAdminTemplate from "./payments-admin.mdx";
6
+
7
+ interface PaymentIntent {
8
+ id: string;
9
+ providerIntentId?: string | null;
10
+ customerId?: string | null;
11
+ email?: string | null;
12
+ amount: number;
13
+ currency: string;
14
+ status: string;
15
+ orderId?: string | null;
16
+ createdAt: string;
17
+ }
18
+
19
+ function formatPrice(cents: number, currency = "USD"): string {
20
+ return new Intl.NumberFormat("en-US", {
21
+ style: "currency",
22
+ currency,
23
+ }).format(cents / 100);
24
+ }
25
+
26
+ function timeAgo(dateStr: string): string {
27
+ const diff = Date.now() - new Date(dateStr).getTime();
28
+ const mins = Math.floor(diff / 60000);
29
+ if (mins < 1) return "just now";
30
+ if (mins < 60) return `${mins}m ago`;
31
+ const hrs = Math.floor(mins / 60);
32
+ if (hrs < 24) return `${hrs}h ago`;
33
+ const days = Math.floor(hrs / 24);
34
+ return `${days}d ago`;
35
+ }
36
+
37
+ const INTENT_STATUS_COLORS: Record<string, string> = {
38
+ pending:
39
+ "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400",
40
+ processing:
41
+ "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400",
42
+ succeeded:
43
+ "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400",
44
+ failed: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400",
45
+ cancelled: "bg-gray-100 text-gray-600 dark:bg-gray-900/30 dark:text-gray-400",
46
+ refunded:
47
+ "bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400",
48
+ };
49
+
50
+ const REFUND_STATUS_COLORS: Record<string, string> = {
51
+ pending:
52
+ "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400",
53
+ succeeded:
54
+ "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400",
55
+ failed: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400",
56
+ };
57
+
58
+ function usePaymentsAdminApi() {
59
+ const client = useModuleClient();
60
+ return {
61
+ listIntents: client.module("payments").admin["/admin/payments"],
62
+ getIntent: client.module("payments").admin["/admin/payments/:id"],
63
+ refundIntent: client.module("payments").admin["/admin/payments/:id/refund"],
64
+ listRefunds: client.module("payments").admin["/admin/payments/:id/refunds"],
65
+ };
66
+ }
67
+
68
+ function RefundModal({
69
+ intent,
70
+ onClose,
71
+ onSuccess,
72
+ }: {
73
+ intent: PaymentIntent;
74
+ onClose: () => void;
75
+ onSuccess: () => void;
76
+ }) {
77
+ const api = usePaymentsAdminApi();
78
+ const [amount, setAmount] = useState("");
79
+ const [reason, setReason] = useState("");
80
+ const [error, setError] = useState("");
81
+
82
+ const refundMutation = api.refundIntent.useMutation({
83
+ onSuccess: () => {
84
+ void api.listIntents.invalidate();
85
+ onSuccess();
86
+ },
87
+ onError: () => {
88
+ setError("Refund failed. Please try again.");
89
+ },
90
+ });
91
+
92
+ const handleSubmit = (e: React.FormEvent) => {
93
+ e.preventDefault();
94
+ setError("");
95
+
96
+ // biome-ignore lint/suspicious/noExplicitAny: building dynamic body for refund endpoint
97
+ const body: Record<string, any> = { params: { id: intent.id } };
98
+ if (reason) body.reason = reason;
99
+ if (amount) {
100
+ const parsed = Math.round(Number.parseFloat(amount) * 100);
101
+ if (Number.isNaN(parsed) || parsed <= 0) {
102
+ setError("Invalid amount");
103
+ return;
104
+ }
105
+ body.amount = parsed;
106
+ }
107
+ refundMutation.mutate(body);
108
+ };
109
+
110
+ const maxAmount = intent.amount / 100;
111
+
112
+ return (
113
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
114
+ <div className="w-full max-w-md rounded-xl border border-border bg-card shadow-xl">
115
+ <div className="flex items-center justify-between border-border border-b px-6 py-4">
116
+ <h2 className="font-semibold text-foreground text-lg">
117
+ Issue Refund
118
+ </h2>
119
+ <button
120
+ type="button"
121
+ onClick={onClose}
122
+ className="rounded-md p-1 text-muted-foreground hover:bg-muted"
123
+ >
124
+ <svg
125
+ xmlns="http://www.w3.org/2000/svg"
126
+ width="16"
127
+ height="16"
128
+ viewBox="0 0 24 24"
129
+ fill="none"
130
+ stroke="currentColor"
131
+ strokeWidth="2"
132
+ strokeLinecap="round"
133
+ strokeLinejoin="round"
134
+ aria-hidden="true"
135
+ >
136
+ <line x1="18" y1="6" x2="6" y2="18" />
137
+ <line x1="6" y1="6" x2="18" y2="18" />
138
+ </svg>
139
+ </button>
140
+ </div>
141
+
142
+ <div className="px-6 py-4">
143
+ <div className="mb-4 rounded-lg bg-muted/50 p-3 text-sm">
144
+ <div className="flex justify-between">
145
+ <span className="text-muted-foreground">Intent ID</span>
146
+ <span className="font-mono text-foreground text-xs">
147
+ {intent.id.slice(0, 8)}&hellip;
148
+ </span>
149
+ </div>
150
+ <div className="mt-1 flex justify-between">
151
+ <span className="text-muted-foreground">Original amount</span>
152
+ <span className="font-medium text-foreground">
153
+ {formatPrice(intent.amount, intent.currency)}
154
+ </span>
155
+ </div>
156
+ </div>
157
+
158
+ <form onSubmit={handleSubmit} className="space-y-4">
159
+ <div>
160
+ <label
161
+ htmlFor="refund-amount"
162
+ className="mb-1 block font-medium text-foreground text-sm"
163
+ >
164
+ Refund amount{" "}
165
+ <span className="font-normal text-muted-foreground">
166
+ (leave blank for full refund)
167
+ </span>
168
+ </label>
169
+ <div className="relative">
170
+ <span className="absolute top-1/2 left-3 -translate-y-1/2 text-muted-foreground text-sm">
171
+ $
172
+ </span>
173
+ <input
174
+ id="refund-amount"
175
+ type="number"
176
+ step="0.01"
177
+ min="0.01"
178
+ max={maxAmount}
179
+ value={amount}
180
+ onChange={(e) => setAmount(e.target.value)}
181
+ placeholder={String(maxAmount.toFixed(2))}
182
+ className="h-9 w-full rounded-md border border-border bg-background pr-3 pl-7 text-foreground text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
183
+ />
184
+ </div>
185
+ </div>
186
+
187
+ <div>
188
+ <label
189
+ htmlFor="refund-reason"
190
+ className="mb-1 block font-medium text-foreground text-sm"
191
+ >
192
+ Reason{" "}
193
+ <span className="font-normal text-muted-foreground">
194
+ (optional)
195
+ </span>
196
+ </label>
197
+ <input
198
+ id="refund-reason"
199
+ type="text"
200
+ value={reason}
201
+ onChange={(e) => setReason(e.target.value)}
202
+ placeholder="e.g. customer request, duplicate charge"
203
+ className="h-9 w-full rounded-md border border-border bg-background px-3 text-foreground text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
204
+ />
205
+ </div>
206
+
207
+ {error && (
208
+ <p className="rounded-md bg-red-50 px-3 py-2 text-red-600 text-sm dark:bg-red-900/20 dark:text-red-400">
209
+ {error}
210
+ </p>
211
+ )}
212
+
213
+ <div className="flex justify-end gap-2 pt-2">
214
+ <button
215
+ type="button"
216
+ onClick={onClose}
217
+ className="rounded-md border border-border px-4 py-2 text-foreground text-sm hover:bg-muted"
218
+ >
219
+ Cancel
220
+ </button>
221
+ <button
222
+ type="submit"
223
+ disabled={refundMutation.isPending}
224
+ className="rounded-md bg-destructive px-4 py-2 font-medium text-destructive-foreground text-sm hover:bg-destructive/90 disabled:opacity-50"
225
+ >
226
+ {refundMutation.isPending ? "Processing…" : "Issue refund"}
227
+ </button>
228
+ </div>
229
+ </form>
230
+ </div>
231
+ </div>
232
+ </div>
233
+ );
234
+ }
235
+
236
+ function IntentsTab() {
237
+ const api = usePaymentsAdminApi();
238
+ const [page, setPage] = useState(1);
239
+ const [statusFilter, setStatusFilter] = useState("");
240
+ const [refundTarget, setRefundTarget] = useState<PaymentIntent | null>(null);
241
+ const pageSize = 20;
242
+
243
+ const queryInput: Record<string, string> = {
244
+ page: String(page),
245
+ limit: String(pageSize),
246
+ };
247
+ if (statusFilter) queryInput.status = statusFilter;
248
+
249
+ const { data, isLoading: loading } = api.listIntents.useQuery(queryInput) as {
250
+ data: { intents: PaymentIntent[] } | undefined;
251
+ isLoading: boolean;
252
+ };
253
+
254
+ const intents = data?.intents ?? [];
255
+ const total = intents.length;
256
+ const totalPages = Math.max(1, Math.ceil(total / pageSize));
257
+
258
+ const handleRefundSuccess = () => {
259
+ setRefundTarget(null);
260
+ };
261
+
262
+ return (
263
+ <>
264
+ <div className="mb-4 flex flex-wrap gap-3">
265
+ <select
266
+ value={statusFilter}
267
+ onChange={(e) => {
268
+ setStatusFilter(e.target.value);
269
+ setPage(1);
270
+ }}
271
+ className="h-9 rounded-md border border-border bg-background px-3 text-foreground text-sm focus:outline-none focus:ring-2 focus:ring-ring"
272
+ >
273
+ <option value="">All statuses</option>
274
+ <option value="pending">Pending</option>
275
+ <option value="processing">Processing</option>
276
+ <option value="succeeded">Succeeded</option>
277
+ <option value="failed">Failed</option>
278
+ <option value="cancelled">Cancelled</option>
279
+ <option value="refunded">Refunded</option>
280
+ </select>
281
+ </div>
282
+
283
+ <div className="overflow-hidden rounded-lg border border-border bg-card">
284
+ <table className="w-full">
285
+ <thead>
286
+ <tr className="border-border border-b bg-muted/50">
287
+ <th className="px-4 py-3 text-left font-semibold text-muted-foreground text-xs uppercase tracking-wide">
288
+ ID
289
+ </th>
290
+ <th className="hidden px-4 py-3 text-left font-semibold text-muted-foreground text-xs uppercase tracking-wide sm:table-cell">
291
+ Customer
292
+ </th>
293
+ <th className="px-4 py-3 text-left font-semibold text-muted-foreground text-xs uppercase tracking-wide">
294
+ Status
295
+ </th>
296
+ <th className="px-4 py-3 text-right font-semibold text-muted-foreground text-xs uppercase tracking-wide">
297
+ Amount
298
+ </th>
299
+ <th className="hidden px-4 py-3 text-right font-semibold text-muted-foreground text-xs uppercase tracking-wide lg:table-cell">
300
+ Date
301
+ </th>
302
+ <th className="px-4 py-3 text-right font-semibold text-muted-foreground text-xs uppercase tracking-wide">
303
+ Actions
304
+ </th>
305
+ </tr>
306
+ </thead>
307
+ <tbody className="divide-y divide-border">
308
+ {loading ? (
309
+ Array.from({ length: 5 }).map((_, i) => (
310
+ <tr key={`skeleton-${i}`}>
311
+ {Array.from({ length: 6 }).map((_, j) => (
312
+ <td key={`skeleton-cell-${j}`} className="px-4 py-3">
313
+ <div className="h-4 w-24 animate-pulse rounded bg-muted" />
314
+ </td>
315
+ ))}
316
+ </tr>
317
+ ))
318
+ ) : intents.length === 0 ? (
319
+ <tr>
320
+ <td colSpan={6} className="px-4 py-12 text-center">
321
+ <p className="font-medium text-foreground text-sm">
322
+ No payment intents found
323
+ </p>
324
+ <p className="mt-1 text-muted-foreground text-xs">
325
+ Payment intents will appear here once customers initiate
326
+ checkout
327
+ </p>
328
+ </td>
329
+ </tr>
330
+ ) : (
331
+ intents.map((intent) => (
332
+ <tr
333
+ key={intent.id}
334
+ className="transition-colors hover:bg-muted/30"
335
+ >
336
+ <td className="px-4 py-3">
337
+ <span className="font-mono text-foreground text-xs">
338
+ {intent.providerIntentId
339
+ ? `${intent.providerIntentId.slice(0, 16)}…`
340
+ : `${intent.id.slice(0, 8)}…`}
341
+ </span>
342
+ </td>
343
+ <td className="hidden px-4 py-3 text-foreground text-sm sm:table-cell">
344
+ {intent.email ?? (
345
+ <span className="text-muted-foreground">&mdash;</span>
346
+ )}
347
+ </td>
348
+ <td className="px-4 py-3">
349
+ <span
350
+ className={`inline-flex items-center rounded-full px-2 py-0.5 font-medium text-xs ${INTENT_STATUS_COLORS[intent.status] ?? "bg-muted text-muted-foreground"}`}
351
+ >
352
+ {intent.status}
353
+ </span>
354
+ </td>
355
+ <td className="px-4 py-3 text-right font-medium text-foreground text-sm">
356
+ {formatPrice(intent.amount, intent.currency)}
357
+ </td>
358
+ <td className="hidden px-4 py-3 text-right text-muted-foreground text-xs lg:table-cell">
359
+ {timeAgo(intent.createdAt)}
360
+ </td>
361
+ <td className="px-4 py-3 text-right">
362
+ {intent.status === "succeeded" && (
363
+ <button
364
+ type="button"
365
+ onClick={() => setRefundTarget(intent)}
366
+ className="rounded-md px-2 py-1 font-medium text-destructive text-xs hover:bg-destructive/10"
367
+ >
368
+ Refund
369
+ </button>
370
+ )}
371
+ </td>
372
+ </tr>
373
+ ))
374
+ )}
375
+ </tbody>
376
+ </table>
377
+ </div>
378
+
379
+ {totalPages > 1 && (
380
+ <div className="mt-4 flex items-center justify-center gap-2">
381
+ <button
382
+ type="button"
383
+ onClick={() => setPage((p) => Math.max(1, p - 1))}
384
+ disabled={page === 1}
385
+ className="rounded-md border border-border px-3 py-1.5 text-foreground text-sm hover:bg-muted disabled:opacity-50"
386
+ >
387
+ Previous
388
+ </button>
389
+ <span className="text-muted-foreground text-sm">
390
+ Page {page} of {totalPages}
391
+ </span>
392
+ <button
393
+ type="button"
394
+ onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
395
+ disabled={page === totalPages}
396
+ className="rounded-md border border-border px-3 py-1.5 text-foreground text-sm hover:bg-muted disabled:opacity-50"
397
+ >
398
+ Next
399
+ </button>
400
+ </div>
401
+ )}
402
+
403
+ {refundTarget && (
404
+ <RefundModal
405
+ intent={refundTarget}
406
+ onClose={() => setRefundTarget(null)}
407
+ onSuccess={handleRefundSuccess}
408
+ />
409
+ )}
410
+ </>
411
+ );
412
+ }
413
+
414
+ function RefundsTab() {
415
+ const api = usePaymentsAdminApi();
416
+
417
+ const { data: intentsData, isLoading: loadingIntents } =
418
+ api.listIntents.useQuery({ limit: "100" }) as {
419
+ data: { intents: PaymentIntent[] } | undefined;
420
+ isLoading: boolean;
421
+ };
422
+
423
+ const refundedIntents = (intentsData?.intents ?? []).filter(
424
+ (i) => i.status === "refunded",
425
+ );
426
+
427
+ return (
428
+ <div className="overflow-hidden rounded-lg border border-border bg-card">
429
+ <table className="w-full">
430
+ <thead>
431
+ <tr className="border-border border-b bg-muted/50">
432
+ <th className="px-4 py-3 text-left font-semibold text-muted-foreground text-xs uppercase tracking-wide">
433
+ Intent ID
434
+ </th>
435
+ <th className="hidden px-4 py-3 text-left font-semibold text-muted-foreground text-xs uppercase tracking-wide sm:table-cell">
436
+ Customer
437
+ </th>
438
+ <th className="px-4 py-3 text-left font-semibold text-muted-foreground text-xs uppercase tracking-wide">
439
+ Status
440
+ </th>
441
+ <th className="px-4 py-3 text-right font-semibold text-muted-foreground text-xs uppercase tracking-wide">
442
+ Amount
443
+ </th>
444
+ <th className="hidden px-4 py-3 text-right font-semibold text-muted-foreground text-xs uppercase tracking-wide lg:table-cell">
445
+ Date
446
+ </th>
447
+ </tr>
448
+ </thead>
449
+ <tbody className="divide-y divide-border">
450
+ {loadingIntents ? (
451
+ Array.from({ length: 4 }).map((_, i) => (
452
+ <tr key={`skeleton-${i}`}>
453
+ {Array.from({ length: 5 }).map((_, j) => (
454
+ <td key={`skeleton-cell-${j}`} className="px-4 py-3">
455
+ <div className="h-4 w-24 animate-pulse rounded bg-muted" />
456
+ </td>
457
+ ))}
458
+ </tr>
459
+ ))
460
+ ) : refundedIntents.length === 0 ? (
461
+ <tr>
462
+ <td colSpan={5} className="px-4 py-12 text-center">
463
+ <p className="font-medium text-foreground text-sm">
464
+ No refunds issued
465
+ </p>
466
+ <p className="mt-1 text-muted-foreground text-xs">
467
+ Refunds will appear here after being processed
468
+ </p>
469
+ </td>
470
+ </tr>
471
+ ) : (
472
+ refundedIntents.map((intent) => (
473
+ <tr
474
+ key={intent.id}
475
+ className="transition-colors hover:bg-muted/30"
476
+ >
477
+ <td className="px-4 py-3">
478
+ <span className="font-mono text-foreground text-xs">
479
+ {intent.providerIntentId
480
+ ? `${intent.providerIntentId.slice(0, 16)}…`
481
+ : `${intent.id.slice(0, 8)}…`}
482
+ </span>
483
+ </td>
484
+ <td className="hidden px-4 py-3 text-foreground text-sm sm:table-cell">
485
+ {intent.email ?? (
486
+ <span className="text-muted-foreground">&mdash;</span>
487
+ )}
488
+ </td>
489
+ <td className="px-4 py-3">
490
+ <span
491
+ className={`inline-flex items-center rounded-full px-2 py-0.5 font-medium text-xs ${REFUND_STATUS_COLORS.succeeded}`}
492
+ >
493
+ refunded
494
+ </span>
495
+ </td>
496
+ <td className="px-4 py-3 text-right font-medium text-foreground text-sm">
497
+ {formatPrice(intent.amount, intent.currency)}
498
+ </td>
499
+ <td className="hidden px-4 py-3 text-right text-muted-foreground text-xs lg:table-cell">
500
+ {timeAgo(intent.createdAt)}
501
+ </td>
502
+ </tr>
503
+ ))
504
+ )}
505
+ </tbody>
506
+ </table>
507
+ </div>
508
+ );
509
+ }
510
+
511
+ export function PaymentsAdmin() {
512
+ const [tab, setTab] = useState<"intents" | "refunds">("intents");
513
+
514
+ return (
515
+ <PaymentsAdminTemplate
516
+ tab={tab}
517
+ onTabChange={setTab}
518
+ tabContent={tab === "intents" ? <IntentsTab /> : <RefundsTab />}
519
+ />
520
+ );
521
+ }
@@ -0,0 +1,25 @@
1
+ import { createAdminEndpoint, sanitizeText, z } from "@86d-app/core";
2
+ import type { PaymentController } from "../../service";
3
+
4
+ export const createRefund = createAdminEndpoint(
5
+ "/admin/payments/:id/refund",
6
+ {
7
+ method: "POST",
8
+ params: z.object({ id: z.string() }),
9
+ body: z.object({
10
+ amount: z.number().int().positive().optional(),
11
+ reason: z.string().max(500).transform(sanitizeText).optional(),
12
+ }),
13
+ },
14
+ async (ctx) => {
15
+ const controller = ctx.context.controllers.payments as PaymentController;
16
+ const intent = await controller.getIntent(ctx.params.id);
17
+ if (!intent) return { error: "Payment intent not found", status: 404 };
18
+ const refund = await controller.createRefund({
19
+ intentId: ctx.params.id,
20
+ amount: ctx.body.amount,
21
+ reason: ctx.body.reason,
22
+ });
23
+ return { refund };
24
+ },
25
+ );
@@ -0,0 +1,16 @@
1
+ import { createAdminEndpoint, z } from "@86d-app/core";
2
+ import type { PaymentController } from "../../service";
3
+
4
+ export const getIntentAdmin = createAdminEndpoint(
5
+ "/admin/payments/:id",
6
+ {
7
+ method: "GET",
8
+ params: z.object({ id: z.string() }),
9
+ },
10
+ async (ctx) => {
11
+ const controller = ctx.context.controllers.payments as PaymentController;
12
+ const intent = await controller.getIntent(ctx.params.id);
13
+ if (!intent) return { error: "Payment intent not found", status: 404 };
14
+ return { intent };
15
+ },
16
+ );
@@ -0,0 +1,11 @@
1
+ import { createRefund } from "./create-refund";
2
+ import { getIntentAdmin } from "./get-intent";
3
+ import { listIntents } from "./list-intents";
4
+ import { listRefunds } from "./list-refunds";
5
+
6
+ export const adminEndpoints = {
7
+ "/admin/payments": listIntents,
8
+ "/admin/payments/:id": getIntentAdmin,
9
+ "/admin/payments/:id/refund": createRefund,
10
+ "/admin/payments/:id/refunds": listRefunds,
11
+ };
@@ -0,0 +1,36 @@
1
+ import { createAdminEndpoint, z } from "@86d-app/core";
2
+ import type { PaymentController, PaymentIntentStatus } from "../../service";
3
+
4
+ export const listIntents = createAdminEndpoint(
5
+ "/admin/payments",
6
+ {
7
+ method: "GET",
8
+ query: z.object({
9
+ customerId: z.string().optional(),
10
+ status: z
11
+ .enum([
12
+ "pending",
13
+ "processing",
14
+ "succeeded",
15
+ "failed",
16
+ "cancelled",
17
+ "refunded",
18
+ ])
19
+ .optional(),
20
+ orderId: z.string().optional(),
21
+ take: z.coerce.number().int().min(1).max(100).optional(),
22
+ skip: z.coerce.number().int().min(0).optional(),
23
+ }),
24
+ },
25
+ async (ctx) => {
26
+ const controller = ctx.context.controllers.payments as PaymentController;
27
+ const intents = await controller.listIntents({
28
+ customerId: ctx.query.customerId,
29
+ status: ctx.query.status as PaymentIntentStatus | undefined,
30
+ orderId: ctx.query.orderId,
31
+ take: ctx.query.take ?? 20,
32
+ skip: ctx.query.skip ?? 0,
33
+ });
34
+ return { intents, total: intents.length };
35
+ },
36
+ );
@@ -0,0 +1,15 @@
1
+ import { createAdminEndpoint, z } from "@86d-app/core";
2
+ import type { PaymentController } from "../../service";
3
+
4
+ export const listRefunds = createAdminEndpoint(
5
+ "/admin/payments/:id/refunds",
6
+ {
7
+ method: "GET",
8
+ params: z.object({ id: z.string() }),
9
+ },
10
+ async (ctx) => {
11
+ const controller = ctx.context.controllers.payments as PaymentController;
12
+ const refunds = await controller.listRefunds(ctx.params.id);
13
+ return { refunds };
14
+ },
15
+ );
package/src/index.ts ADDED
@@ -0,0 +1,58 @@
1
+ import type { Module, ModuleContext } from "@86d-app/core";
2
+ import { adminEndpoints } from "./admin/endpoints";
3
+ import { paymentsSchema } from "./schema";
4
+ import type { PaymentProvider } from "./service";
5
+ import { createPaymentController } from "./service-impl";
6
+ import { storeEndpoints } from "./store/endpoints";
7
+
8
+ export type {
9
+ PaymentController,
10
+ PaymentIntent,
11
+ PaymentIntentStatus,
12
+ PaymentMethod,
13
+ PaymentProvider,
14
+ ProviderIntentResult,
15
+ ProviderRefundResult,
16
+ Refund,
17
+ RefundStatus,
18
+ } from "./service";
19
+
20
+ export interface PaymentsOptions {
21
+ /** Default currency for payment intents */
22
+ currency?: string;
23
+ /** Payment provider implementation (e.g. StripePaymentProvider) */
24
+ provider?: PaymentProvider;
25
+ }
26
+
27
+ export default function payments(options?: PaymentsOptions): Module {
28
+ return {
29
+ id: "payments",
30
+ version: "0.0.1",
31
+ schema: paymentsSchema,
32
+ exports: {
33
+ read: ["paymentStatus", "paymentAmount", "paymentMethod"],
34
+ },
35
+ events: {
36
+ emits: ["payment.completed", "payment.failed", "payment.refunded"],
37
+ },
38
+ init: async (ctx: ModuleContext) => {
39
+ const controller = createPaymentController(ctx.data, options?.provider);
40
+ return { controllers: { payments: controller } };
41
+ },
42
+ endpoints: {
43
+ store: storeEndpoints,
44
+ admin: adminEndpoints,
45
+ },
46
+ admin: {
47
+ pages: [
48
+ {
49
+ path: "/admin/payments",
50
+ component: "PaymentsAdmin",
51
+ label: "Payments",
52
+ icon: "CreditCard",
53
+ group: "Sales",
54
+ },
55
+ ],
56
+ },
57
+ };
58
+ }
package/src/mdx.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ declare module "*.mdx" {
2
+ import type { ComponentType } from "react";
3
+ const Component: ComponentType<Record<string, unknown>>;
4
+ export default Component;
5
+ }