@dineway-ai/plugin-payment 0.2.1 → 0.2.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.
- package/package.json +2 -2
- package/src/admin/form.ts +1 -5
- package/src/admin.tsx +186 -30
- package/src/configuration/repository.ts +4 -12
- package/src/configuration/service.ts +39 -34
- package/src/configuration/types.ts +1 -5
- package/src/index.ts +3 -1
- package/src/routes/index.ts +4 -6
- package/src/stripe/account-probe.ts +1 -1
- package/src/stripe/checkout-provider.ts +19 -5
- package/src/stripe/refund-provider.ts +2 -1
- package/src/stripe/webhook-verifier.ts +1 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dineway-ai/plugin-payment",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Provider-neutral payment infrastructure and Stripe integration for Dineway plugins",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"license": "MIT",
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"react": "^18.0.0 || ^19.0.0",
|
|
31
|
-
"dineway": "^0.2.
|
|
31
|
+
"dineway": "^0.2.19"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@stripe/stripe-js": "9.9.0",
|
package/src/admin/form.ts
CHANGED
|
@@ -39,11 +39,7 @@ export function buildPaymentSettingsUpdate(
|
|
|
39
39
|
if (webhookSecret && !webhookSecret.startsWith("whsec_")) {
|
|
40
40
|
throw new Error("Webhook signing secret must start with whsec_");
|
|
41
41
|
}
|
|
42
|
-
if (
|
|
43
|
-
configuration.secretKey &&
|
|
44
|
-
configuration.settings.mode !== form.mode &&
|
|
45
|
-
!secretKey
|
|
46
|
-
) {
|
|
42
|
+
if (configuration.secretKey && configuration.settings.mode !== form.mode && !secretKey) {
|
|
47
43
|
throw new Error("Enter the matching secret key when changing Stripe mode");
|
|
48
44
|
}
|
|
49
45
|
return {
|
package/src/admin.tsx
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
|
|
3
|
-
import type { PaymentStripeConfiguration } from "./configuration/service.js";
|
|
4
3
|
import { postPaymentRoute } from "./admin/api.js";
|
|
5
4
|
import {
|
|
6
5
|
buildPaymentSettingsUpdate,
|
|
7
6
|
paymentSettingsForm,
|
|
8
7
|
type PaymentSettingsForm,
|
|
9
8
|
} from "./admin/form.js";
|
|
9
|
+
import type { PaymentStripeConfiguration } from "./configuration/service.js";
|
|
10
10
|
|
|
11
11
|
type CredentialName = "secretKey" | "webhookSecret";
|
|
12
12
|
|
|
13
13
|
export function PaymentSettingsPage() {
|
|
14
|
-
const [configuration, setConfiguration] =
|
|
15
|
-
React.useState<PaymentStripeConfiguration | null>(null);
|
|
14
|
+
const [configuration, setConfiguration] = React.useState<PaymentStripeConfiguration | null>(null);
|
|
16
15
|
const [form, setForm] = React.useState<PaymentSettingsForm | null>(null);
|
|
17
16
|
const [loading, setLoading] = React.useState(true);
|
|
18
17
|
const [pending, setPending] = React.useState(false);
|
|
@@ -26,9 +25,7 @@ export function PaymentSettingsPage() {
|
|
|
26
25
|
|
|
27
26
|
React.useEffect(() => {
|
|
28
27
|
let cancelled = false;
|
|
29
|
-
void postPaymentRoute<{ configuration: PaymentStripeConfiguration }>(
|
|
30
|
-
"stripe/configuration/get",
|
|
31
|
-
)
|
|
28
|
+
void postPaymentRoute<{ configuration: PaymentStripeConfiguration }>("stripe/configuration/get")
|
|
32
29
|
.then((response) => {
|
|
33
30
|
if (!cancelled) applyConfiguration(response.configuration);
|
|
34
31
|
return undefined;
|
|
@@ -94,54 +91,213 @@ export function PaymentSettingsPage() {
|
|
|
94
91
|
|
|
95
92
|
if (loading) return <p className="text-sm text-kumo-subtle">Loading payment settings…</p>;
|
|
96
93
|
if (!configuration || !form) {
|
|
97
|
-
return
|
|
94
|
+
return (
|
|
95
|
+
<p className="rounded-lg bg-kumo-danger/10 p-4 text-sm text-kumo-danger">
|
|
96
|
+
{error ?? "Payment configuration is unavailable."}
|
|
97
|
+
</p>
|
|
98
|
+
);
|
|
98
99
|
}
|
|
99
100
|
const canTest = Boolean(
|
|
100
101
|
configuration.settings.publishableKey &&
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
configuration.secretKey &&
|
|
103
|
+
form.mode === configuration.settings.mode &&
|
|
104
|
+
form.publishableKey.trim() === configuration.settings.publishableKey &&
|
|
105
|
+
!form.secretKey.trim(),
|
|
105
106
|
);
|
|
106
107
|
|
|
107
108
|
return (
|
|
108
109
|
<div className="space-y-6">
|
|
109
110
|
<header className="flex flex-wrap items-start justify-between gap-3">
|
|
110
|
-
<div
|
|
111
|
-
|
|
111
|
+
<div>
|
|
112
|
+
<h1 className="text-2xl font-semibold">Payments</h1>
|
|
113
|
+
<p className="mt-1 text-sm text-kumo-subtle">
|
|
114
|
+
Shared Stripe account and encrypted credentials for every transactional plugin.
|
|
115
|
+
</p>
|
|
116
|
+
</div>
|
|
117
|
+
<span className="rounded-full bg-kumo-tint px-3 py-1 text-xs font-semibold uppercase">
|
|
118
|
+
{configuration.settings.configurationStatus}
|
|
119
|
+
</span>
|
|
112
120
|
</header>
|
|
113
121
|
<Readiness configuration={configuration} />
|
|
114
|
-
{error ?
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
<
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
{error ? (
|
|
123
|
+
<p className="rounded-lg bg-kumo-danger/10 p-3 text-sm text-kumo-danger" role="alert">
|
|
124
|
+
{error}
|
|
125
|
+
</p>
|
|
126
|
+
) : null}
|
|
127
|
+
{notice ? (
|
|
128
|
+
<p className="rounded-lg bg-kumo-success/10 p-3 text-sm text-kumo-success" role="status">
|
|
129
|
+
{notice}
|
|
130
|
+
</p>
|
|
131
|
+
) : null}
|
|
132
|
+
<form
|
|
133
|
+
className="grid max-w-3xl gap-5 rounded-xl border border-kumo-line p-5"
|
|
134
|
+
onSubmit={(event) => void save(event)}
|
|
135
|
+
>
|
|
136
|
+
<fieldset className="grid gap-2" disabled={pending}>
|
|
137
|
+
<legend className="mb-2 text-sm font-medium">Stripe mode</legend>
|
|
138
|
+
<div className="flex gap-2">
|
|
139
|
+
{(["test", "live"] as const).map((mode) => (
|
|
140
|
+
<button
|
|
141
|
+
type="button"
|
|
142
|
+
key={mode}
|
|
143
|
+
className={`rounded-lg border px-4 py-2 text-sm ${form.mode === mode ? "border-kumo-contrast bg-kumo-contrast text-kumo-base" : "border-kumo-line"}`}
|
|
144
|
+
onClick={() => setForm({ ...form, mode })}
|
|
145
|
+
>
|
|
146
|
+
{mode === "test" ? "Test" : "Live"}
|
|
147
|
+
</button>
|
|
148
|
+
))}
|
|
149
|
+
</div>
|
|
150
|
+
</fieldset>
|
|
151
|
+
<Field
|
|
152
|
+
label="Publishable key"
|
|
153
|
+
value={form.publishableKey}
|
|
154
|
+
onChange={(value) => setForm({ ...form, publishableKey: value })}
|
|
155
|
+
placeholder={form.mode === "test" ? "pk_test_…" : "pk_live_…"}
|
|
156
|
+
/>
|
|
157
|
+
<Field
|
|
158
|
+
label="Secret key"
|
|
159
|
+
value={form.secretKey}
|
|
160
|
+
onChange={(value) => setForm({ ...form, secretKey: value })}
|
|
161
|
+
placeholder={
|
|
162
|
+
configuration.secretKey
|
|
163
|
+
? "Leave blank to keep the current key"
|
|
164
|
+
: form.mode === "test"
|
|
165
|
+
? "sk_test_…"
|
|
166
|
+
: "sk_live_…"
|
|
167
|
+
}
|
|
168
|
+
type="password"
|
|
169
|
+
/>
|
|
170
|
+
<Credential
|
|
171
|
+
label="Secret key"
|
|
172
|
+
present={Boolean(configuration.secretKey)}
|
|
173
|
+
fingerprint={configuration.secretKey?.fingerprint ?? null}
|
|
174
|
+
onRemove={() => void removeCredential("secretKey")}
|
|
175
|
+
disabled={pending}
|
|
176
|
+
/>
|
|
177
|
+
<Field
|
|
178
|
+
label="Webhook signing secret"
|
|
179
|
+
value={form.webhookSecret}
|
|
180
|
+
onChange={(value) => setForm({ ...form, webhookSecret: value })}
|
|
181
|
+
placeholder={
|
|
182
|
+
configuration.webhookSecret ? "Leave blank to keep the current secret" : "whsec_…"
|
|
183
|
+
}
|
|
184
|
+
type="password"
|
|
185
|
+
/>
|
|
186
|
+
<Credential
|
|
187
|
+
label="Webhook secret"
|
|
188
|
+
present={Boolean(configuration.webhookSecret)}
|
|
189
|
+
fingerprint={configuration.webhookSecret?.fingerprint ?? null}
|
|
190
|
+
onRemove={() => void removeCredential("webhookSecret")}
|
|
191
|
+
disabled={pending}
|
|
192
|
+
/>
|
|
193
|
+
<div className="flex flex-wrap gap-2">
|
|
194
|
+
<button
|
|
195
|
+
className="rounded-lg bg-kumo-contrast px-4 py-2 text-sm font-medium text-kumo-base"
|
|
196
|
+
disabled={pending}
|
|
197
|
+
>
|
|
198
|
+
{pending ? "Saving…" : "Save account"}
|
|
199
|
+
</button>
|
|
200
|
+
<button
|
|
201
|
+
type="button"
|
|
202
|
+
className="rounded-lg border border-kumo-line px-4 py-2 text-sm font-medium"
|
|
203
|
+
disabled={!canTest || pending}
|
|
204
|
+
onClick={() => void testConnection()}
|
|
205
|
+
>
|
|
206
|
+
{pending ? "Working…" : "Test connection"}
|
|
207
|
+
</button>
|
|
208
|
+
</div>
|
|
124
209
|
</form>
|
|
125
|
-
<section className="max-w-3xl rounded-xl bg-kumo-tint/40 p-5 text-sm text-kumo-subtle"
|
|
210
|
+
<section className="max-w-3xl rounded-xl bg-kumo-tint/40 p-5 text-sm text-kumo-subtle">
|
|
211
|
+
<h2 className="font-semibold text-kumo-default">Webhook endpoint</h2>
|
|
212
|
+
<p className="mt-2">
|
|
213
|
+
Configure Stripe to send domain events to the webhook shown by each installed
|
|
214
|
+
transactional plugin. Venue and Restaurant validate exact request bytes with this shared
|
|
215
|
+
signing secret.
|
|
216
|
+
</p>
|
|
217
|
+
</section>
|
|
126
218
|
</div>
|
|
127
219
|
);
|
|
128
220
|
}
|
|
129
221
|
|
|
130
222
|
function Readiness({ configuration }: { configuration: PaymentStripeConfiguration }) {
|
|
131
223
|
const settings = configuration.settings;
|
|
132
|
-
return
|
|
224
|
+
return (
|
|
225
|
+
<div className="grid gap-3 md:grid-cols-4">
|
|
226
|
+
<Metric label="Account" value={settings.stripeAccountId ?? "Not verified"} />
|
|
227
|
+
<Metric
|
|
228
|
+
label="Charges"
|
|
229
|
+
value={
|
|
230
|
+
settings.chargesEnabled === null
|
|
231
|
+
? "Unknown"
|
|
232
|
+
: settings.chargesEnabled
|
|
233
|
+
? "Enabled"
|
|
234
|
+
: "Restricted"
|
|
235
|
+
}
|
|
236
|
+
/>
|
|
237
|
+
<Metric label="Tax" value={settings.taxStatus} />
|
|
238
|
+
<Metric label="Connection" value={settings.connectStatus} />
|
|
239
|
+
</div>
|
|
240
|
+
);
|
|
133
241
|
}
|
|
134
242
|
|
|
135
243
|
function Metric({ label, value }: { label: string; value: string }) {
|
|
136
|
-
return
|
|
244
|
+
return (
|
|
245
|
+
<div className="rounded-xl border border-kumo-line p-4">
|
|
246
|
+
<div className="text-xs font-medium uppercase text-kumo-subtle">{label}</div>
|
|
247
|
+
<div className="mt-1 truncate font-semibold">{value}</div>
|
|
248
|
+
</div>
|
|
249
|
+
);
|
|
137
250
|
}
|
|
138
251
|
|
|
139
|
-
function Field(props: {
|
|
140
|
-
|
|
252
|
+
function Field(props: {
|
|
253
|
+
label: string;
|
|
254
|
+
value: string;
|
|
255
|
+
onChange: (value: string) => void;
|
|
256
|
+
placeholder: string;
|
|
257
|
+
type?: string;
|
|
258
|
+
}) {
|
|
259
|
+
return (
|
|
260
|
+
<label className="grid gap-1 text-sm font-medium">
|
|
261
|
+
{props.label}
|
|
262
|
+
<input
|
|
263
|
+
className="rounded-lg border border-kumo-line bg-transparent px-3 py-2 font-normal"
|
|
264
|
+
type={props.type ?? "text"}
|
|
265
|
+
value={props.value}
|
|
266
|
+
onChange={(event) => props.onChange(event.target.value)}
|
|
267
|
+
placeholder={props.placeholder}
|
|
268
|
+
autoComplete="off"
|
|
269
|
+
disabled={false}
|
|
270
|
+
/>
|
|
271
|
+
</label>
|
|
272
|
+
);
|
|
141
273
|
}
|
|
142
274
|
|
|
143
|
-
function Credential(props: {
|
|
144
|
-
|
|
275
|
+
function Credential(props: {
|
|
276
|
+
label: string;
|
|
277
|
+
present: boolean;
|
|
278
|
+
fingerprint: string | null;
|
|
279
|
+
onRemove: () => void;
|
|
280
|
+
disabled: boolean;
|
|
281
|
+
}) {
|
|
282
|
+
return (
|
|
283
|
+
<div className="flex flex-wrap items-center justify-between gap-3 rounded-lg bg-kumo-tint/50 px-3 py-2 text-sm">
|
|
284
|
+
<span>
|
|
285
|
+
{props.present
|
|
286
|
+
? `${props.label} stored · ${props.fingerprint ?? "encrypted"}`
|
|
287
|
+
: `${props.label} not stored`}
|
|
288
|
+
</span>
|
|
289
|
+
{props.present ? (
|
|
290
|
+
<button
|
|
291
|
+
type="button"
|
|
292
|
+
className="text-kumo-danger underline"
|
|
293
|
+
onClick={props.onRemove}
|
|
294
|
+
disabled={props.disabled}
|
|
295
|
+
>
|
|
296
|
+
Remove
|
|
297
|
+
</button>
|
|
298
|
+
) : null}
|
|
299
|
+
</div>
|
|
300
|
+
);
|
|
145
301
|
}
|
|
146
302
|
|
|
147
303
|
function message(value: unknown): string {
|
|
@@ -34,15 +34,11 @@ export class PaymentStripeConfigurationRepository {
|
|
|
34
34
|
): Promise<PaymentStripeSettings> {
|
|
35
35
|
const update: Updateable<Database["payment_stripe_settings"]> = {
|
|
36
36
|
...(input.mode === undefined ? {} : { mode: input.mode }),
|
|
37
|
-
...(input.publishableKey === undefined
|
|
38
|
-
? {}
|
|
39
|
-
: { publishable_key: input.publishableKey }),
|
|
37
|
+
...(input.publishableKey === undefined ? {} : { publishable_key: input.publishableKey }),
|
|
40
38
|
...(input.configurationStatus === undefined
|
|
41
39
|
? {}
|
|
42
40
|
: { configuration_status: input.configurationStatus }),
|
|
43
|
-
...(input.stripeAccountId === undefined
|
|
44
|
-
? {}
|
|
45
|
-
: { stripe_account_id: input.stripeAccountId }),
|
|
41
|
+
...(input.stripeAccountId === undefined ? {} : { stripe_account_id: input.stripeAccountId }),
|
|
46
42
|
...(input.chargesEnabled === undefined
|
|
47
43
|
? {}
|
|
48
44
|
: { charges_enabled: optionalBoolean(input.chargesEnabled) }),
|
|
@@ -53,12 +49,8 @@ export class PaymentStripeConfigurationRepository {
|
|
|
53
49
|
...(input.validationErrorCode === undefined
|
|
54
50
|
? {}
|
|
55
51
|
: { validation_error_code: input.validationErrorCode }),
|
|
56
|
-
...(input.lastValidatedAt === undefined
|
|
57
|
-
|
|
58
|
-
: { last_validated_at: input.lastValidatedAt }),
|
|
59
|
-
...(input.connectStatus === undefined
|
|
60
|
-
? {}
|
|
61
|
-
: { connect_status: input.connectStatus }),
|
|
52
|
+
...(input.lastValidatedAt === undefined ? {} : { last_validated_at: input.lastValidatedAt }),
|
|
53
|
+
...(input.connectStatus === undefined ? {} : { connect_status: input.connectStatus }),
|
|
62
54
|
version: expectedVersion + 1,
|
|
63
55
|
updated_at: new Date().toISOString(),
|
|
64
56
|
};
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
Database,
|
|
3
|
-
PluginSecretStatus,
|
|
4
|
-
PluginSecretStoreOptions,
|
|
5
|
-
} from "dineway";
|
|
1
|
+
import type { Database, PluginSecretStatus, PluginSecretStoreOptions } from "dineway";
|
|
6
2
|
import type { Kysely, Transaction } from "kysely";
|
|
7
3
|
|
|
8
4
|
import type { StripeAccountProbe, StripeMode } from "../types.js";
|
|
@@ -103,7 +99,9 @@ export async function updatePaymentStripeConfiguration(
|
|
|
103
99
|
taxStatus: "unknown" as const,
|
|
104
100
|
validationErrorCode: null,
|
|
105
101
|
lastValidatedAt: null,
|
|
106
|
-
connectStatus: credentialsComplete
|
|
102
|
+
connectStatus: credentialsComplete
|
|
103
|
+
? ("restricted" as const)
|
|
104
|
+
: ("not_configured" as const),
|
|
107
105
|
}
|
|
108
106
|
: {}),
|
|
109
107
|
});
|
|
@@ -143,28 +141,31 @@ export async function testPaymentStripeConnection(
|
|
|
143
141
|
|
|
144
142
|
return db.transaction().execute(async (trx) => {
|
|
145
143
|
const repository = new PaymentStripeConfigurationRepository(trx);
|
|
146
|
-
await repository.updateSettings(
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
144
|
+
await repository.updateSettings(
|
|
145
|
+
before.version,
|
|
146
|
+
result.ok
|
|
147
|
+
? {
|
|
148
|
+
configurationStatus: "valid",
|
|
149
|
+
stripeAccountId: result.accountId,
|
|
150
|
+
chargesEnabled: result.chargesEnabled,
|
|
151
|
+
detailsSubmitted: result.detailsSubmitted,
|
|
152
|
+
taxStatus: result.taxStatus,
|
|
153
|
+
validationErrorCode: null,
|
|
154
|
+
lastValidatedAt: new Date().toISOString(),
|
|
155
|
+
connectStatus:
|
|
156
|
+
result.chargesEnabled && result.detailsSubmitted ? "ready" : "restricted",
|
|
157
|
+
}
|
|
158
|
+
: {
|
|
159
|
+
configurationStatus: "invalid",
|
|
160
|
+
stripeAccountId: null,
|
|
161
|
+
chargesEnabled: null,
|
|
162
|
+
detailsSubmitted: null,
|
|
163
|
+
taxStatus: "unknown",
|
|
164
|
+
validationErrorCode: result.errorCode,
|
|
165
|
+
lastValidatedAt: new Date().toISOString(),
|
|
166
|
+
connectStatus: "restricted",
|
|
167
|
+
},
|
|
168
|
+
);
|
|
168
169
|
return loadConfiguration(trx, secretOptions);
|
|
169
170
|
});
|
|
170
171
|
}
|
|
@@ -196,7 +197,10 @@ async function updateSecret(
|
|
|
196
197
|
await store.set(name, value.trim());
|
|
197
198
|
}
|
|
198
199
|
|
|
199
|
-
function normalizeOptionalKey(
|
|
200
|
+
function normalizeOptionalKey(
|
|
201
|
+
value: string | null | undefined,
|
|
202
|
+
current: string | null,
|
|
203
|
+
): string | null {
|
|
200
204
|
if (value === undefined) return current;
|
|
201
205
|
if (value === null) return null;
|
|
202
206
|
return value.trim() || null;
|
|
@@ -211,7 +215,11 @@ function validateUpdate(input: UpdatePaymentStripeConfigurationInput): void {
|
|
|
211
215
|
if (!Number.isSafeInteger(input.expectedVersion) || input.expectedVersion < 1) {
|
|
212
216
|
throw new PaymentConfigurationError("VALIDATION_ERROR", "Expected version is invalid");
|
|
213
217
|
}
|
|
214
|
-
if (
|
|
218
|
+
if (
|
|
219
|
+
input.publishableKey !== undefined &&
|
|
220
|
+
input.publishableKey !== null &&
|
|
221
|
+
input.publishableKey.length > 512
|
|
222
|
+
) {
|
|
215
223
|
throw new PaymentConfigurationError("VALIDATION_ERROR", "Publishable key is too long");
|
|
216
224
|
}
|
|
217
225
|
if (input.secretKey !== undefined && input.secretKey !== null && input.secretKey.length > 512) {
|
|
@@ -233,9 +241,6 @@ function validateModeKeys(
|
|
|
233
241
|
);
|
|
234
242
|
}
|
|
235
243
|
if (validateSecret && secretKey && !secretKey.startsWith(`sk_${keyMode}_`)) {
|
|
236
|
-
throw new PaymentConfigurationError(
|
|
237
|
-
"VALIDATION_ERROR",
|
|
238
|
-
`Secret key must match ${mode} mode`,
|
|
239
|
-
);
|
|
244
|
+
throw new PaymentConfigurationError("VALIDATION_ERROR", `Secret key must match ${mode} mode`);
|
|
240
245
|
}
|
|
241
246
|
}
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import type { StripeMode, StripeTaxStatus } from "../types.js";
|
|
2
2
|
|
|
3
|
-
export type PaymentStripeConfigurationStatus =
|
|
4
|
-
| "unconfigured"
|
|
5
|
-
| "unverified"
|
|
6
|
-
| "valid"
|
|
7
|
-
| "invalid";
|
|
3
|
+
export type PaymentStripeConfigurationStatus = "unconfigured" | "unverified" | "valid" | "invalid";
|
|
8
4
|
export type PaymentStripeConnectStatus = "not_configured" | "restricted" | "ready";
|
|
9
5
|
|
|
10
6
|
export interface PaymentStripeSettings {
|
package/src/index.ts
CHANGED
|
@@ -7,7 +7,9 @@ import { PAYMENT_PLUGIN_ID } from "./constants.js";
|
|
|
7
7
|
import { createPaymentRoutes } from "./routes/index.js";
|
|
8
8
|
|
|
9
9
|
const PAYMENT_ADMIN_ENTRY = "@dineway-ai/plugin-payment/admin";
|
|
10
|
-
const PAYMENT_ADMIN_PAGES = [
|
|
10
|
+
const PAYMENT_ADMIN_PAGES = [
|
|
11
|
+
{ path: "/payments", label: "Payments", icon: "credit-card" },
|
|
12
|
+
] as const;
|
|
11
13
|
|
|
12
14
|
export interface PaymentPluginOptions {
|
|
13
15
|
appName?: string;
|
package/src/routes/index.ts
CHANGED
|
@@ -31,7 +31,9 @@ export interface PaymentRouteOptions {
|
|
|
31
31
|
secretOptions?: PluginSecretStoreOptions;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
export function createPaymentRoutes(
|
|
34
|
+
export function createPaymentRoutes(
|
|
35
|
+
options: PaymentRouteOptions = {},
|
|
36
|
+
): Record<string, PluginRoute> {
|
|
35
37
|
const probe = options.accountProbe ?? new StripeNodeAccountProbe();
|
|
36
38
|
return {
|
|
37
39
|
"stripe/configuration/get": route(getPaymentStripeConfigurationSchema, async () => ({
|
|
@@ -48,11 +50,7 @@ export function createPaymentRoutes(options: PaymentRouteOptions = {}): Record<s
|
|
|
48
50
|
}),
|
|
49
51
|
),
|
|
50
52
|
"stripe/configuration/test": route(testPaymentStripeConnectionSchema, async () => ({
|
|
51
|
-
configuration: await testPaymentStripeConnection(
|
|
52
|
-
await getDb(),
|
|
53
|
-
probe,
|
|
54
|
-
options.secretOptions,
|
|
55
|
-
),
|
|
53
|
+
configuration: await testPaymentStripeConnection(await getDb(), probe, options.secretOptions),
|
|
56
54
|
})),
|
|
57
55
|
};
|
|
58
56
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import Stripe from "stripe";
|
|
2
2
|
|
|
3
|
-
import { stripeProviderFailure } from "./errors.js";
|
|
4
3
|
import type {
|
|
5
4
|
StripeAccountProbe,
|
|
6
5
|
StripeAccountProbeInput,
|
|
7
6
|
StripeAccountProbeResult,
|
|
8
7
|
} from "../types.js";
|
|
8
|
+
import { stripeProviderFailure } from "./errors.js";
|
|
9
9
|
|
|
10
10
|
export interface StripeAccountProbeClient {
|
|
11
11
|
retrieveCurrentAccount(): Promise<{
|
|
@@ -360,7 +360,11 @@ function stripeAddress(address: StripeAddress) {
|
|
|
360
360
|
}
|
|
361
361
|
|
|
362
362
|
function assertTaxCalculationInput(input: StripeTaxCalculationInput): void {
|
|
363
|
-
if (
|
|
363
|
+
if (
|
|
364
|
+
!input.secretKey.trim() ||
|
|
365
|
+
!input.idempotencyKey.trim() ||
|
|
366
|
+
!CURRENCY_PATTERN.test(input.currency)
|
|
367
|
+
) {
|
|
364
368
|
throw new Error("Stripe Tax credentials or currency are invalid");
|
|
365
369
|
}
|
|
366
370
|
if (input.lines.length === 0 || input.lines.length > 100) {
|
|
@@ -399,7 +403,10 @@ function assertTaxCalculationResponse(
|
|
|
399
403
|
}
|
|
400
404
|
}
|
|
401
405
|
const responseLines = new Map(response.lineItems.map((line) => [line.reference, line] as const));
|
|
402
|
-
if (
|
|
406
|
+
if (
|
|
407
|
+
response.lineItems.length !== input.lines.length ||
|
|
408
|
+
responseLines.size !== input.lines.length
|
|
409
|
+
) {
|
|
403
410
|
throw new Error("Stripe Tax response line set does not match the request");
|
|
404
411
|
}
|
|
405
412
|
let calculatedTaxMinor = 0;
|
|
@@ -459,7 +466,10 @@ function assertPaymentIntentResponse(
|
|
|
459
466
|
}
|
|
460
467
|
|
|
461
468
|
function assertOperationInput(
|
|
462
|
-
input:
|
|
469
|
+
input:
|
|
470
|
+
| StripeCancelPaymentIntentInput
|
|
471
|
+
| StripeCapturePaymentIntentInput
|
|
472
|
+
| StripeIncrementAuthorizationInput,
|
|
463
473
|
requireAmount: boolean,
|
|
464
474
|
): void {
|
|
465
475
|
if (
|
|
@@ -517,7 +527,10 @@ function assertCancelResponse(
|
|
|
517
527
|
}
|
|
518
528
|
|
|
519
529
|
function assertMatchingOperationResponse(
|
|
520
|
-
input:
|
|
530
|
+
input:
|
|
531
|
+
| StripeCancelPaymentIntentInput
|
|
532
|
+
| StripeCapturePaymentIntentInput
|
|
533
|
+
| StripeIncrementAuthorizationInput,
|
|
521
534
|
response: StripeCheckoutPaymentIntentResponse,
|
|
522
535
|
): void {
|
|
523
536
|
if (
|
|
@@ -553,7 +566,8 @@ function validMetadata(metadata: Record<string, string> | undefined): boolean {
|
|
|
553
566
|
return (
|
|
554
567
|
entries.length <= 50 &&
|
|
555
568
|
entries.every(
|
|
556
|
-
([key, value]) =>
|
|
569
|
+
([key, value]) =>
|
|
570
|
+
key.length > 0 && key.length <= 40 && value.length > 0 && value.length <= 500,
|
|
557
571
|
)
|
|
558
572
|
);
|
|
559
573
|
}
|
|
@@ -125,7 +125,8 @@ function validMetadata(metadata: Record<string, string> | undefined): boolean {
|
|
|
125
125
|
return (
|
|
126
126
|
entries.length <= 50 &&
|
|
127
127
|
entries.every(
|
|
128
|
-
([key, value]) =>
|
|
128
|
+
([key, value]) =>
|
|
129
|
+
key.length > 0 && key.length <= 40 && value.length > 0 && value.length <= 500,
|
|
129
130
|
)
|
|
130
131
|
);
|
|
131
132
|
}
|
|
@@ -96,8 +96,7 @@ function projectEvent(event: Stripe.Event, rawBody: Uint8Array): StripeWebhookEv
|
|
|
96
96
|
payloadSha256: `sha256:${createHash("sha256").update(rawBody).digest("hex")}`,
|
|
97
97
|
objectType,
|
|
98
98
|
objectId,
|
|
99
|
-
paymentIntent:
|
|
100
|
-
objectType === "payment_intent" ? projectPaymentIntent(objectRecord) : null,
|
|
99
|
+
paymentIntent: objectType === "payment_intent" ? projectPaymentIntent(objectRecord) : null,
|
|
101
100
|
refund: objectType === "refund" ? projectRefund(objectRecord) : null,
|
|
102
101
|
};
|
|
103
102
|
}
|