@paytree/medusa-payment-paytree 0.1.0

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 (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +195 -0
  3. package/dist/index.d.ts +28 -0
  4. package/dist/index.js +55 -0
  5. package/dist/modules/paytree/index.d.ts +36 -0
  6. package/dist/modules/paytree/index.js +13 -0
  7. package/dist/modules/paytree/migrations/Migration20260617000000.d.ts +5 -0
  8. package/dist/modules/paytree/migrations/Migration20260617000000.js +54 -0
  9. package/dist/modules/paytree/migrations/Migration20260618000000.d.ts +5 -0
  10. package/dist/modules/paytree/migrations/Migration20260618000000.js +19 -0
  11. package/dist/modules/paytree/models/paytree-event-log.d.ts +25 -0
  12. package/dist/modules/paytree/models/paytree-event-log.js +29 -0
  13. package/dist/modules/paytree/models/paytree-setting.d.ts +23 -0
  14. package/dist/modules/paytree/models/paytree-setting.js +27 -0
  15. package/dist/modules/paytree/service.d.ts +60 -0
  16. package/dist/modules/paytree/service.js +47 -0
  17. package/dist/modules/paytree-payment/index.d.ts +8 -0
  18. package/dist/modules/paytree-payment/index.js +16 -0
  19. package/dist/modules/paytree-payment/lib/amount.d.ts +23 -0
  20. package/dist/modules/paytree-payment/lib/amount.js +40 -0
  21. package/dist/modules/paytree-payment/lib/audit.d.ts +26 -0
  22. package/dist/modules/paytree-payment/lib/audit.js +51 -0
  23. package/dist/modules/paytree-payment/lib/client.d.ts +28 -0
  24. package/dist/modules/paytree-payment/lib/client.js +81 -0
  25. package/dist/modules/paytree-payment/lib/constants.d.ts +54 -0
  26. package/dist/modules/paytree-payment/lib/constants.js +55 -0
  27. package/dist/modules/paytree-payment/lib/logger.d.ts +27 -0
  28. package/dist/modules/paytree-payment/lib/logger.js +73 -0
  29. package/dist/modules/paytree-payment/lib/methods.d.ts +69 -0
  30. package/dist/modules/paytree-payment/lib/methods.js +68 -0
  31. package/dist/modules/paytree-payment/lib/redact.d.ts +16 -0
  32. package/dist/modules/paytree-payment/lib/redact.js +60 -0
  33. package/dist/modules/paytree-payment/lib/settings-reader.d.ts +24 -0
  34. package/dist/modules/paytree-payment/lib/settings-reader.js +74 -0
  35. package/dist/modules/paytree-payment/lib/status-map.d.ts +26 -0
  36. package/dist/modules/paytree-payment/lib/status-map.js +66 -0
  37. package/dist/modules/paytree-payment/lib/types.d.ts +144 -0
  38. package/dist/modules/paytree-payment/lib/types.js +2 -0
  39. package/dist/modules/paytree-payment/service.d.ts +36 -0
  40. package/dist/modules/paytree-payment/service.js +369 -0
  41. package/docs/api-reference.md +121 -0
  42. package/docs/architecture.md +97 -0
  43. package/docs/configuration.md +62 -0
  44. package/docs/getting-started.md +111 -0
  45. package/docs/storefront-integration.md +85 -0
  46. package/integration/README.md +46 -0
  47. package/integration/admin/routes/paytree/api.ts +16 -0
  48. package/integration/admin/routes/paytree/method-icons.tsx +106 -0
  49. package/integration/admin/routes/paytree/page.tsx +18 -0
  50. package/integration/admin/routes/paytree/payments/page.tsx +240 -0
  51. package/integration/admin/routes/paytree/settings/page.tsx +214 -0
  52. package/integration/api/admin/paytree/events/route.ts +35 -0
  53. package/integration/api/admin/paytree/settings/route.ts +86 -0
  54. package/integration/api/hooks/paytree/route.ts +195 -0
  55. package/integration/api/store/paytree/methods/route.ts +28 -0
  56. package/package.json +57 -0
  57. package/storefront/README.md +25 -0
  58. package/storefront/components/payment-button.tsx +361 -0
  59. package/storefront/components/payment.tsx +356 -0
  60. package/storefront/components/paytree-method-icon.tsx +106 -0
  61. package/storefront/lib/paytree.ts +23 -0
@@ -0,0 +1,214 @@
1
+ import { defineRouteConfig } from "@medusajs/admin-sdk"
2
+ import { CogSixTooth } from "@medusajs/icons"
3
+ import {
4
+ Badge,
5
+ Button,
6
+ Container,
7
+ Heading,
8
+ Input,
9
+ Label,
10
+ Switch,
11
+ Text,
12
+ toast,
13
+ } from "@medusajs/ui"
14
+ import { useEffect, useState } from "react"
15
+ import { MethodIcon } from "../method-icons"
16
+ import { api } from "../api"
17
+
18
+ type MethodConfig = {
19
+ method: string
20
+ enabled: boolean
21
+ label: string
22
+ }
23
+
24
+ type Settings = {
25
+ base_url: string
26
+ log_request: boolean
27
+ log_response: boolean
28
+ methods: MethodConfig[]
29
+ }
30
+
31
+ const SettingsPage = () => {
32
+ const [settings, setSettings] = useState<Settings>({
33
+ base_url: "",
34
+ log_request: false,
35
+ log_response: false,
36
+ methods: [],
37
+ })
38
+ const [keyConfigured, setKeyConfigured] = useState(false)
39
+ const [loading, setLoading] = useState(true)
40
+ const [saving, setSaving] = useState(false)
41
+
42
+ const updateMethod = (method: string, patch: Partial<MethodConfig>) =>
43
+ setSettings((s) => ({
44
+ ...s,
45
+ methods: s.methods.map((m) =>
46
+ m.method === method ? { ...m, ...patch } : m
47
+ ),
48
+ }))
49
+
50
+ const enabledCount = settings.methods.filter((m) => m.enabled).length
51
+
52
+ useEffect(() => {
53
+ api("settings")
54
+ .then((d) => {
55
+ setSettings(d.settings)
56
+ setKeyConfigured(d.api_key_configured)
57
+ })
58
+ .catch((e) => toast.error(e.message))
59
+ .finally(() => setLoading(false))
60
+ }, [])
61
+
62
+ const save = async () => {
63
+ setSaving(true)
64
+ try {
65
+ const d = await api("settings", {
66
+ method: "POST",
67
+ body: JSON.stringify(settings),
68
+ })
69
+ setSettings(d.settings)
70
+ toast.success("Settings saved")
71
+ } catch (e: any) {
72
+ toast.error(e.message)
73
+ } finally {
74
+ setSaving(false)
75
+ }
76
+ }
77
+
78
+ return (
79
+ <div className="flex flex-col gap-y-3">
80
+ <Container className="divide-y p-0">
81
+ <div className="flex items-center justify-between px-6 py-4">
82
+ <Heading level="h2">Paytree settings</Heading>
83
+ <Badge color={keyConfigured ? "green" : "red"} size="2xsmall">
84
+ {keyConfigured ? "API key configured" : "API key missing"}
85
+ </Badge>
86
+ </div>
87
+
88
+ <div className="flex flex-col gap-6 px-6 py-6">
89
+ <div className="flex flex-col gap-2">
90
+ <Label htmlFor="base_url">Paytree API endpoint</Label>
91
+ <Input
92
+ id="base_url"
93
+ placeholder="https://api.paytree.tech"
94
+ value={settings.base_url}
95
+ disabled={loading}
96
+ onChange={(e) =>
97
+ setSettings({ ...settings, base_url: e.target.value })
98
+ }
99
+ />
100
+ <Text size="small" className="text-ui-fg-subtle">
101
+ Requests are sent here with your API key. Any https endpoint is
102
+ accepted. The API key itself is set with the PAYTREE_API_KEY
103
+ environment variable, not here.
104
+ </Text>
105
+ </div>
106
+
107
+ <div className="flex flex-col gap-3">
108
+ <div className="flex items-center justify-between">
109
+ <div className="flex flex-col">
110
+ <Label>Payment methods</Label>
111
+ <Text size="small" className="text-ui-fg-subtle">
112
+ Choose which methods customers can select at checkout, and
113
+ rename any of them. Disabled methods are never shown.
114
+ </Text>
115
+ </div>
116
+ <Badge size="2xsmall" color={enabledCount > 0 ? "green" : "orange"}>
117
+ {enabledCount} enabled
118
+ </Badge>
119
+ </div>
120
+
121
+ <div className="flex flex-col divide-y rounded-lg border border-ui-border-base">
122
+ {settings.methods.map((m) => (
123
+ <div
124
+ key={m.method}
125
+ className="flex items-center gap-3 px-3 py-2.5"
126
+ >
127
+ <div className="flex h-8 w-8 shrink-0 items-center justify-center text-ui-fg-subtle">
128
+ <MethodIcon method={m.method} />
129
+ </div>
130
+ <Input
131
+ aria-label={`${m.method} label`}
132
+ className="flex-1"
133
+ value={m.label}
134
+ disabled={loading || !m.enabled}
135
+ onChange={(e) =>
136
+ updateMethod(m.method, { label: e.target.value })
137
+ }
138
+ />
139
+ <Switch
140
+ checked={m.enabled}
141
+ disabled={loading}
142
+ onCheckedChange={(v) => updateMethod(m.method, { enabled: v })}
143
+ />
144
+ </div>
145
+ ))}
146
+ </div>
147
+ </div>
148
+ </div>
149
+ </Container>
150
+
151
+ <Container className="divide-y p-0">
152
+ <div className="px-6 py-4">
153
+ <Heading level="h2">Logging</Heading>
154
+ </div>
155
+
156
+ <div className="flex flex-col gap-3 px-6 py-6">
157
+ <Text size="small" className="text-ui-fg-subtle">
158
+ Diagnostic logging for Paytree traffic. Sensitive data is always
159
+ masked.
160
+ </Text>
161
+
162
+ <div className="flex flex-col divide-y rounded-lg border border-ui-border-base">
163
+ <div className="flex items-center justify-between gap-4 px-4 py-3">
164
+ <div className="flex flex-col">
165
+ <Label htmlFor="log_request">Log outgoing requests</Label>
166
+ <Text size="small" className="text-ui-fg-subtle">
167
+ Records request bodies sent to Paytree. Card data, the API key,
168
+ and personal details are always masked.
169
+ </Text>
170
+ </div>
171
+ <Switch
172
+ id="log_request"
173
+ checked={settings.log_request}
174
+ onCheckedChange={(v) =>
175
+ setSettings({ ...settings, log_request: v })
176
+ }
177
+ />
178
+ </div>
179
+
180
+ <div className="flex items-center justify-between gap-4 px-4 py-3">
181
+ <div className="flex flex-col">
182
+ <Label htmlFor="log_response">Log responses</Label>
183
+ <Text size="small" className="text-ui-fg-subtle">
184
+ Records responses received from Paytree, with sensitive fields
185
+ masked.
186
+ </Text>
187
+ </div>
188
+ <Switch
189
+ id="log_response"
190
+ checked={settings.log_response}
191
+ onCheckedChange={(v) =>
192
+ setSettings({ ...settings, log_response: v })
193
+ }
194
+ />
195
+ </div>
196
+ </div>
197
+ </div>
198
+ </Container>
199
+
200
+ <div className="flex justify-end">
201
+ <Button onClick={save} isLoading={saving} disabled={loading}>
202
+ Save changes
203
+ </Button>
204
+ </div>
205
+ </div>
206
+ )
207
+ }
208
+
209
+ export const config = defineRouteConfig({
210
+ label: "Settings",
211
+ icon: CogSixTooth,
212
+ })
213
+
214
+ export default SettingsPage
@@ -0,0 +1,35 @@
1
+ import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http";
2
+ import { PAYTREE_MODULE } from "@paytree/medusa-payment-paytree";
3
+
4
+ /**
5
+ * GET audit-log events for the admin UI. Supports filtering by session id,
6
+ * Paytree intent id, and event type, plus simple pagination.
7
+ */
8
+ export async function GET(
9
+ req: MedusaRequest,
10
+ res: MedusaResponse,
11
+ ): Promise<void> {
12
+ const paytree: any = req.scope.resolve(PAYTREE_MODULE);
13
+
14
+ const limit = Math.min(Number(req.query.limit ?? 50), 200);
15
+ const offset = Number(req.query.offset ?? 0);
16
+
17
+ const filters: Record<string, unknown> = {};
18
+ if (req.query.session_id) {
19
+ filters.session_id = String(req.query.session_id);
20
+ }
21
+ if (req.query.paytree_id) {
22
+ filters.paytree_id = String(req.query.paytree_id);
23
+ }
24
+ if (req.query.event) {
25
+ filters.event = String(req.query.event);
26
+ }
27
+
28
+ const [events, count] = await paytree.listAndCountPaytreeEventLogs(filters, {
29
+ take: limit,
30
+ skip: offset,
31
+ order: { created_at: "DESC" },
32
+ });
33
+
34
+ res.json({ events, count, limit, offset });
35
+ }
@@ -0,0 +1,86 @@
1
+ import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http";
2
+ import { MedusaError } from "@medusajs/framework/utils";
3
+ import {
4
+ PAYTREE_MODULE,
5
+ DEFAULT_BASE_URL,
6
+ normalizeMethods,
7
+ resolveMethods,
8
+ type StoredMethodConfig,
9
+ } from "@paytree/medusa-payment-paytree";
10
+
11
+ /**
12
+ * GET current settings. The API key is never returned — it lives only in the
13
+ * PAYTREE_API_KEY environment variable. We return whether it is configured so
14
+ * the UI can show a status, without exposing the value.
15
+ */
16
+ export async function GET(
17
+ req: MedusaRequest,
18
+ res: MedusaResponse,
19
+ ): Promise<void> {
20
+ const paytree: any = req.scope.resolve(PAYTREE_MODULE);
21
+ const settings = await paytree.getSettings({ base_url: DEFAULT_BASE_URL });
22
+ res.json({
23
+ settings: {
24
+ base_url: settings.base_url,
25
+ log_request: settings.log_request,
26
+ log_response: settings.log_response,
27
+ methods: resolveMethods(settings.methods as StoredMethodConfig[] | null),
28
+ },
29
+ api_key_configured: Boolean(process.env.PAYTREE_API_KEY),
30
+ });
31
+ }
32
+
33
+ interface UpdateBody {
34
+ base_url?: string;
35
+ log_request?: boolean;
36
+ log_response?: boolean;
37
+ methods?: StoredMethodConfig[];
38
+ }
39
+
40
+ /**
41
+ * Update settings. The base URL accepts any endpoint (no host restriction);
42
+ * we only require a well-formed https URL so the API key is never sent over
43
+ * cleartext http.
44
+ */
45
+ export async function POST(
46
+ req: MedusaRequest,
47
+ res: MedusaResponse,
48
+ ): Promise<void> {
49
+ const body = (req.body ?? {}) as UpdateBody;
50
+
51
+ const baseUrl = (body.base_url ?? "").trim();
52
+ let parsed: URL;
53
+ try {
54
+ parsed = new URL(baseUrl);
55
+ } catch {
56
+ throw new MedusaError(
57
+ MedusaError.Types.INVALID_DATA,
58
+ "Enter a valid URL, e.g. https://api.paytree.tech",
59
+ );
60
+ }
61
+ if (parsed.protocol !== "https:") {
62
+ throw new MedusaError(
63
+ MedusaError.Types.INVALID_DATA,
64
+ "The base URL must use https so the API key is never sent in cleartext.",
65
+ );
66
+ }
67
+
68
+ const methods = normalizeMethods(body.methods);
69
+
70
+ const paytree: any = req.scope.resolve(PAYTREE_MODULE);
71
+ const saved = await paytree.upsertSettings({
72
+ base_url: baseUrl.replace(/\/+$/, ""),
73
+ log_request: Boolean(body.log_request),
74
+ log_response: Boolean(body.log_response),
75
+ methods,
76
+ });
77
+
78
+ res.json({
79
+ settings: {
80
+ base_url: saved.base_url,
81
+ log_request: saved.log_request,
82
+ log_response: saved.log_response,
83
+ methods: resolveMethods(saved.methods as StoredMethodConfig[] | null),
84
+ },
85
+ });
86
+ }
@@ -0,0 +1,195 @@
1
+ import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http";
2
+ import { ContainerRegistrationKeys, Modules } from "@medusajs/framework/utils";
3
+ import {
4
+ processPaymentWorkflow,
5
+ refundPaymentWorkflow,
6
+ } from "@medusajs/medusa/core-flows";
7
+
8
+ import {
9
+ PAYTREE_MODULE,
10
+ PaytreeClient,
11
+ PaytreeLogger,
12
+ callbackOutcome,
13
+ toSessionStatus,
14
+ newlyRefundedAmount,
15
+ fromPaytreeAmount,
16
+ AuditEvent,
17
+ DEFAULT_BASE_URL,
18
+ } from "@paytree/medusa-payment-paytree";
19
+
20
+ /**
21
+ * Paytree calls this GET endpoint when a payment status changes, substituting
22
+ * the {payment_intent_id} and {transaction_id} tags in the notification URL.
23
+ *
24
+ * The callback body is NOT trusted. We take only the ids and fetch the
25
+ * authoritative status from Paytree using our secret API key — that lookup is
26
+ * the trust mechanism (no signature is involved). The handler is idempotent,
27
+ * so repeated or spoofed pings cause at most a harmless re-read.
28
+ *
29
+ * Always returns 200 on a handled event. A non-200 triggers Paytree's retry
30
+ * schedule, which we reserve for genuine internal failures.
31
+ */
32
+ export async function GET(
33
+ req: MedusaRequest,
34
+ res: MedusaResponse,
35
+ ): Promise<void> {
36
+ const logger = new PaytreeLogger(req.scope);
37
+ const transactionId = String(req.query.transaction_id ?? "");
38
+ const paytreeId = String(req.query.payment_intent_id ?? "");
39
+ const c = { sessionId: transactionId, paytreeId };
40
+
41
+ const paytree: any = req.scope.resolve(PAYTREE_MODULE);
42
+ const query = req.scope.resolve(ContainerRegistrationKeys.QUERY);
43
+
44
+ logger.info("callback received", c);
45
+ await safeAudit(paytree, {
46
+ event: AuditEvent.WEBHOOK_RECEIVED,
47
+ session_id: transactionId,
48
+ paytree_id: paytreeId,
49
+ });
50
+
51
+ if (!transactionId && !paytreeId) {
52
+ // Nothing to correlate; acknowledge so Paytree doesn't retry forever.
53
+ res.sendStatus(200);
54
+ return;
55
+ }
56
+
57
+ try {
58
+ const settings = await paytree.getSettings({ base_url: DEFAULT_BASE_URL });
59
+ const apiKey = process.env.PAYTREE_API_KEY || "";
60
+ const client = new PaytreeClient(
61
+ {
62
+ apiKey,
63
+ baseUrl: settings.base_url || DEFAULT_BASE_URL,
64
+ logRequest: Boolean(settings.log_request),
65
+ logResponse: Boolean(settings.log_response),
66
+ },
67
+ logger,
68
+ );
69
+
70
+ // Authoritative lookup by our own ref (avoids the create-id vs UUID issue).
71
+ const intent = transactionId
72
+ ? await client.findByTransactionRef(transactionId, c)
73
+ : await client.getPaymentIntent(paytreeId, c);
74
+
75
+ if (!intent) {
76
+ logger.warn("callback intent not found in Paytree", c);
77
+ res.sendStatus(200);
78
+ return;
79
+ }
80
+
81
+ const payment = intent.payment?.[0];
82
+ const status = payment?.status ?? "pending";
83
+ const sessionId = intent.transaction_ref || transactionId;
84
+ const outcome = callbackOutcome(status);
85
+
86
+ if (outcome.kind === "action") {
87
+ await processPaymentWorkflow(req.scope).run({
88
+ input: {
89
+ action: outcome.action,
90
+ data: {
91
+ session_id: sessionId,
92
+ amount: fromPaytreeAmount(payment?.amount ?? intent.amount),
93
+ },
94
+ },
95
+ });
96
+ } else if (outcome.kind === "refund") {
97
+ await reflectRefund(
98
+ req.scope,
99
+ query,
100
+ sessionId,
101
+ payment?.refunded ?? "0",
102
+ c,
103
+ logger,
104
+ );
105
+ }
106
+
107
+ await safeAudit(paytree, {
108
+ event: AuditEvent.WEBHOOK_PROCESSED,
109
+ session_id: sessionId,
110
+ paytree_id: paytreeId,
111
+ status,
112
+ detail: { medusa_status: toSessionStatus(status), outcome: outcome.kind },
113
+ });
114
+
115
+ res.sendStatus(200);
116
+ } catch (e) {
117
+ logger.error("callback processing failed", e, c);
118
+ await safeAudit(paytree, {
119
+ event: AuditEvent.ERROR,
120
+ session_id: transactionId,
121
+ paytree_id: paytreeId,
122
+ error: e instanceof Error ? e.message : String(e),
123
+ detail: { phase: AuditEvent.WEBHOOK_PROCESSED },
124
+ });
125
+ // Signal failure so Paytree retries the callback.
126
+ res.sendStatus(500);
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Record an externally-initiated Paytree refund in Medusa. Medusa has no
132
+ * "refunded" webhook action, so we drive the refund workflow directly. The
133
+ * workflow calls the provider's refundPayment, which verifies the refund
134
+ * against Paytree before recording — so this is safe to call idempotently.
135
+ */
136
+ async function reflectRefund(
137
+ scope: any,
138
+ query: any,
139
+ sessionId: string,
140
+ paytreeRefundedTotal: string,
141
+ c: { sessionId?: string; paytreeId?: string },
142
+ logger: PaytreeLogger,
143
+ ): Promise<void> {
144
+ // Find the Medusa payment + how much is already refunded there.
145
+ const { data: sessions } = await query.graph({
146
+ entity: "payment_session",
147
+ fields: [
148
+ "id",
149
+ "payment_collection.payments.id",
150
+ "payment_collection.payments.amount",
151
+ "payment_collection.payments.refunds.amount",
152
+ ],
153
+ filters: { id: sessionId },
154
+ });
155
+
156
+ const payment = sessions?.[0]?.payment_collection?.payments?.[0];
157
+ if (!payment) {
158
+ logger.warn("refund callback: no Medusa payment found for session", c);
159
+ return;
160
+ }
161
+
162
+ const alreadyRefunded = (payment.refunds ?? []).reduce(
163
+ (sum: number, r: any) => sum + Number(r.amount ?? 0),
164
+ 0,
165
+ );
166
+
167
+ const delta = newlyRefundedAmount(paytreeRefundedTotal, alreadyRefunded);
168
+ if (!delta) {
169
+ logger.info("refund callback: nothing new to record", c);
170
+ return;
171
+ }
172
+
173
+ await refundPaymentWorkflow(scope).run({
174
+ input: {
175
+ payment_id: payment.id,
176
+ amount: fromPaytreeAmount(delta),
177
+ note: "Reflected from Paytree back-office refund",
178
+ },
179
+ });
180
+ logger.info(`recorded refund of ${delta}`, c);
181
+ }
182
+
183
+ async function safeAudit(paytree: any, record: any): Promise<void> {
184
+ try {
185
+ await paytree.createPaytreeEventLogs([
186
+ {
187
+ id: `ptlog_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
188
+ ...record,
189
+ detail: record.detail ?? null,
190
+ },
191
+ ]);
192
+ } catch {
193
+ // Audit write must never break the callback.
194
+ }
195
+ }
@@ -0,0 +1,28 @@
1
+ import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http";
2
+ import {
3
+ PAYTREE_MODULE,
4
+ DEFAULT_BASE_URL,
5
+ resolveMethods,
6
+ type StoredMethodConfig,
7
+ } from "@paytree/medusa-payment-paytree";
8
+
9
+ /**
10
+ * Public list of the Paytree payment methods the admin has enabled, with their
11
+ * (possibly overridden) display labels. The storefront renders this as the
12
+ * method picker shown after Paytree is selected at checkout. Only enabled
13
+ * methods are returned, and never any secret/operational config.
14
+ */
15
+ export async function GET(
16
+ req: MedusaRequest,
17
+ res: MedusaResponse,
18
+ ): Promise<void> {
19
+ const paytree: any = req.scope.resolve(PAYTREE_MODULE);
20
+ const settings = await paytree.getSettings({ base_url: DEFAULT_BASE_URL });
21
+ const methods = resolveMethods(
22
+ settings.methods as StoredMethodConfig[] | null,
23
+ )
24
+ .filter((m) => m.enabled)
25
+ .map((m) => ({ method: m.method, label: m.label }));
26
+
27
+ res.json({ methods });
28
+ }
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@paytree/medusa-payment-paytree",
3
+ "version": "0.1.0",
4
+ "description": "Paytree hosted-payment provider for Medusa 2.0.",
5
+ "author": "Paytree",
6
+ "license": "MIT",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "files": [
10
+ "dist",
11
+ "!dist/**/__tests__",
12
+ "integration",
13
+ "storefront",
14
+ "docs",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "keywords": [
19
+ "medusa",
20
+ "medusa-plugin",
21
+ "medusa-plugin-payment",
22
+ "payment",
23
+ "payment-provider",
24
+ "paytree",
25
+ "ecommerce"
26
+ ],
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/paytree/medusa-payment-paytree"
30
+ },
31
+ "engines": {
32
+ "node": ">=20"
33
+ },
34
+ "scripts": {
35
+ "build": "rimraf dist && tsc --build tsconfig.json",
36
+ "watch": "tsc --watch",
37
+ "test": "jest --passWithNoTests src",
38
+ "prepublishOnly": "npm run build"
39
+ },
40
+ "peerDependencies": {
41
+ "@medusajs/framework": "2.13.6"
42
+ },
43
+ "devDependencies": {
44
+ "@medusajs/framework": "2.13.6",
45
+ "@mikro-orm/migrations": "6.6.12",
46
+ "@swc/core": "1.5.7",
47
+ "@swc/jest": "^0.2.39",
48
+ "@types/jest": "^29.5.14",
49
+ "@types/node": "^20.19.25",
50
+ "jest": "^29.7.0",
51
+ "rimraf": "^5.0.10",
52
+ "typescript": "^5.9.3"
53
+ },
54
+ "publishConfig": {
55
+ "access": "public"
56
+ }
57
+ }
@@ -0,0 +1,25 @@
1
+ # Paytree storefront reference components
2
+
3
+ Reference Next.js components for the Paytree checkout experience, targeting the
4
+ [Medusa Next.js starter](https://github.com/medusajs/nextjs-starter-medusa)
5
+ layout. These are **copy-paste sources**, not a compiled/importable module — the
6
+ storefront owns its own build, styling, and import aliases.
7
+
8
+ ## Contents
9
+
10
+ | File | Purpose | Copy to |
11
+ | ---- | ------- | ------- |
12
+ | `lib/paytree.ts` | `listPaytreeMethods()` server action (`GET /store/paytree/methods`). | `src/lib/data/paytree.ts` |
13
+ | `components/paytree-method-icon.tsx` | SVG icons for each method in the picker. | `src/modules/checkout/components/paytree-method-icon/index.tsx` |
14
+ | `components/payment.tsx` | Payment step with the Paytree method picker. | `src/modules/checkout/components/payment/index.tsx` |
15
+ | `components/payment-button.tsx` | Place-order button that redirects to the hosted page. | `src/modules/checkout/components/payment-button/index.tsx` |
16
+
17
+ ## Peer requirements
18
+
19
+ These components assume the starter's dependencies are present:
20
+ `@medusajs/ui`, `@medusajs/icons`, `@medusajs/types`, `@headlessui/react`,
21
+ `next`, `react`, and the starter's `@lib`/`@modules` path aliases.
22
+
23
+ See [`../docs/storefront-integration.md`](../docs/storefront-integration.md) for
24
+ the full wiring guide, including the shared-file edits to `constants.tsx` and
25
+ `cart.ts`.