@frigear-nu/payments 0.1.1 → 0.1.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/dist/module.d.mts +10 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +14 -18
- package/dist/runtime/app/composables/usePayments.d.ts +5 -7
- package/dist/runtime/app/composables/usePayments.js +13 -6
- package/dist/runtime/app/tabs/Payments.vue +46 -16
- package/dist/runtime/app/widgets/AdminLastPayment.vue +2 -1
- package/dist/runtime/app/widgets/RecentTransactions.vue +2 -1
- package/dist/runtime/app/widgets/RevenueStat.vue +2 -1
- package/dist/runtime/i18n/lang/da.json +1 -0
- package/dist/runtime/i18n/lang/en.json +1 -0
- package/dist/runtime/server/plugins/staff-scopes.js +1 -0
- package/dist/runtime/server/routes/admin/recent.get.js +1 -1
- package/dist/runtime/server/routes/admin/user-payments.get.js +4 -2
- package/dist/runtime/server/routes/me/resume.post.d.ts +2 -0
- package/dist/runtime/server/routes/me/resume.post.js +6 -0
- package/dist/runtime/server/utils/auth.d.ts +5 -0
- package/dist/runtime/server/utils/auth.js +5 -0
- package/dist/runtime/server/utils/payments.js +17 -19
- package/dist/runtime/utils/api.d.ts +14 -0
- package/dist/runtime/utils/api.js +17 -0
- package/dist/runtime/utils/index.d.ts +2 -1
- package/dist/runtime/utils/index.js +1 -0
- package/dist/types.d.mts +1 -1
- package/package.json +16 -11
- package/dist/runtime/server/routes/payments/resume.post.js +0 -7
- /package/dist/runtime/server/{routes/payments/index.get.d.ts → plugins/staff-scopes.d.ts} +0 -0
- /package/dist/runtime/server/routes/{payments/resume.post.d.ts → me/payments.get.d.ts} +0 -0
- /package/dist/runtime/server/routes/{payments/index.get.js → me/payments.get.js} +0 -0
package/dist/module.d.mts
CHANGED
|
@@ -85,6 +85,13 @@ type CheckoutBeforeContext = {
|
|
|
85
85
|
type ResumeResponse = {
|
|
86
86
|
url: string;
|
|
87
87
|
};
|
|
88
|
+
/** Context of the `payments:authorize` hook. */
|
|
89
|
+
type AuthorizeContext = {
|
|
90
|
+
event: H3Event;
|
|
91
|
+
userId: number;
|
|
92
|
+
/** Set `ctx.allowed = true` to let the user see the dashboard's recent payments and revenue. Denied by default. */
|
|
93
|
+
allowed: boolean;
|
|
94
|
+
};
|
|
88
95
|
|
|
89
96
|
declare module '@nuxt/schema' {
|
|
90
97
|
interface NuxtConfig {
|
|
@@ -96,6 +103,8 @@ declare module '@nuxt/schema' {
|
|
|
96
103
|
}
|
|
97
104
|
declare module 'nitropack' {
|
|
98
105
|
interface NitroRuntimeHooks {
|
|
106
|
+
/** Staff gate (the dashboard's recent payments): set `ctx.allowed = true` to let the user in. Denied by default. */
|
|
107
|
+
'payments:authorize': (ctx: AuthorizeContext) => void | Promise<void>;
|
|
99
108
|
/** Policy hook: throw to deny a checkout, or adjust `ctx.params` before it reaches Stripe. */
|
|
100
109
|
'payments:checkout:before': (ctx: CheckoutBeforeContext) => void | Promise<void>;
|
|
101
110
|
/** A payment became `paid` (fired once, from the verified Stripe event). */
|
|
@@ -111,4 +120,4 @@ declare module 'nitropack' {
|
|
|
111
120
|
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
112
121
|
|
|
113
122
|
export { _default as default };
|
|
114
|
-
export type { CheckoutBeforeContext, CreatePaymentInput, CreatePaymentResult, InvoicePaymentInput, ModuleOptions, PaymentDto, PaymentHookContext, PaymentItem, PaymentReference, PaymentStatus, PaymentsRuntimeConfig, ResumeResponse };
|
|
123
|
+
export type { AuthorizeContext, CheckoutBeforeContext, CreatePaymentInput, CreatePaymentResult, InvoicePaymentInput, ModuleOptions, PaymentDto, PaymentHookContext, PaymentItem, PaymentReference, PaymentStatus, PaymentsRuntimeConfig, ResumeResponse };
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { defineNuxtModule, createResolver,
|
|
2
|
-
import { registerModule, registerTailwindSource, registerLocales, registerDatabaseTable,
|
|
1
|
+
import { defineNuxtModule, createResolver, hasNuxtModule, addServerImportsDir, addServerPlugin, addImports } from '@nuxt/kit';
|
|
2
|
+
import { registerModule, registerTailwindSource, registerLocales, registerDatabaseTable, addModuleRoutes, inlineModuleRuntime } from '@frigear-nu/module-kit/utils';
|
|
3
3
|
import { addAuthScope } from '@frigear-nu/auth/utils';
|
|
4
4
|
import { extendUsersAdmin } from '@frigear-nu/users/utils';
|
|
5
5
|
import { addMcpDefinitions } from '@frigear-nu/mcp/utils';
|
|
@@ -21,6 +21,8 @@ const module$1 = defineNuxtModule({
|
|
|
21
21
|
"@frigear-nu/users": { version: ">=0.0.1" },
|
|
22
22
|
"@frigear-nu/stripe": { version: ">=0.0.1" },
|
|
23
23
|
"@frigear-nu/mcp": { version: ">=0.0.1" },
|
|
24
|
+
// `usePayments` loads through `useCachedFetch`; the widgets and the admin tab use its components and helpers.
|
|
25
|
+
"@frigear-nu/ui": { version: ">=0.0.1" },
|
|
24
26
|
"@frigear-nu/widget": { version: ">=0.0.0", optional: true },
|
|
25
27
|
// The admin user tab and card are built from its components, so they are only added with it.
|
|
26
28
|
"@frigear-nu/admin": { version: ">=0.0.0", optional: true }
|
|
@@ -30,7 +32,8 @@ const module$1 = defineNuxtModule({
|
|
|
30
32
|
addAuthScope(
|
|
31
33
|
nuxt,
|
|
32
34
|
{ name: "payments:read", description: "Read your payments" },
|
|
33
|
-
{ name: "payments:write", description: "Resume your payments" }
|
|
35
|
+
{ name: "payments:write", description: "Resume your payments" },
|
|
36
|
+
{ name: "payments:manage", description: "See recent payments and revenue (staff)" }
|
|
34
37
|
);
|
|
35
38
|
const resolver = createResolver(import.meta.url);
|
|
36
39
|
registerTailwindSource(nuxt, resolver.resolve("./runtime/app"));
|
|
@@ -52,16 +55,10 @@ const module$1 = defineNuxtModule({
|
|
|
52
55
|
],
|
|
53
56
|
handler: resolver.resolve("./runtime/server/stripe/payment-events")
|
|
54
57
|
});
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
method: "
|
|
58
|
-
|
|
59
|
-
});
|
|
60
|
-
addServerHandler({
|
|
61
|
-
route: moduleApiUrl("payments", "resume"),
|
|
62
|
-
method: "post",
|
|
63
|
-
handler: resolver.resolve("./runtime/server/routes/payments/resume.post")
|
|
64
|
-
});
|
|
58
|
+
addModuleRoutes(resolver, [
|
|
59
|
+
{ method: "get", path: "me", file: "me/payments.get" },
|
|
60
|
+
{ method: "post", path: "me/:id/resume", file: "me/resume.post" }
|
|
61
|
+
], { base: "/api/payments" });
|
|
65
62
|
await extendUsersAdmin(nuxt, {
|
|
66
63
|
routes: [{ method: "get", path: "payments", handler: resolver.resolve("./runtime/server/routes/admin/user-payments.get") }],
|
|
67
64
|
tab: {
|
|
@@ -79,11 +76,9 @@ const module$1 = defineNuxtModule({
|
|
|
79
76
|
span: 2
|
|
80
77
|
}
|
|
81
78
|
});
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
handler: resolver.resolve("./runtime/server/routes/admin/recent.get")
|
|
86
|
-
});
|
|
79
|
+
addModuleRoutes(resolver, [
|
|
80
|
+
{ method: "get", path: "recent", file: "admin/recent.get" }
|
|
81
|
+
], { base: "/api/admin/payments" });
|
|
87
82
|
if (hasNuxtModule("@frigear-nu/admin") && hasNuxtModule("@frigear-nu/widget")) {
|
|
88
83
|
const { extendWidgets } = await import('@frigear-nu/widget/utils');
|
|
89
84
|
extendWidgets(nuxt, "admin.dashboard.top", { id: "payments-revenue-stat", component: resolver.resolve("./runtime/app/widgets/RevenueStat.vue"), order: 30 });
|
|
@@ -99,6 +94,7 @@ const module$1 = defineNuxtModule({
|
|
|
99
94
|
addServerImportsDir(resolver.resolve("./runtime/server/utils"));
|
|
100
95
|
inlineModuleRuntime(nuxt, resolver);
|
|
101
96
|
addServerPlugin(resolver.resolve("./runtime/server/plugins/invoice-payments"));
|
|
97
|
+
addServerPlugin(resolver.resolve("./runtime/server/plugins/staff-scopes"));
|
|
102
98
|
if (options.settings?.mcp === true) addMcpDefinitions(nuxt, resolver.resolve("./runtime/server/mcp"));
|
|
103
99
|
addImports([
|
|
104
100
|
{ name: "usePayments", from: resolver.resolve("./runtime/app/composables/usePayments") }
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
resume: (paymentId: string) => Promise<void>;
|
|
7
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* The signed-in user's payments, plus a helper to resume an unpaid one. Awaitable: `await usePayments()` in a page
|
|
3
|
+
* renders them on the server. `payments` is `[]` until loaded and after an error: check `loading` and `error` first.
|
|
4
|
+
*/
|
|
5
|
+
export declare function usePayments(): any;
|
|
@@ -1,14 +1,21 @@
|
|
|
1
|
+
import { paymentsApi } from "../../utils/index.js";
|
|
1
2
|
export function usePayments() {
|
|
2
3
|
const { loggedIn } = useUserSession();
|
|
3
|
-
const
|
|
4
|
+
const fetched = useCachedFetch(paymentsApi.me.list, {
|
|
4
5
|
key: "payments:mine",
|
|
5
|
-
|
|
6
|
-
watch: false
|
|
7
|
-
default: () => []
|
|
6
|
+
ttl: 60 * 1e3,
|
|
7
|
+
watch: false
|
|
8
8
|
});
|
|
9
|
+
const { status, error, loading, refresh, clear } = fetched;
|
|
10
|
+
watch(loggedIn, (value) => {
|
|
11
|
+
if (value) refresh();
|
|
12
|
+
else clear();
|
|
13
|
+
});
|
|
14
|
+
const payments = computed(() => fetched.data.value ?? []);
|
|
9
15
|
async function resume(paymentId) {
|
|
10
|
-
const { url } = await $fetch(
|
|
16
|
+
const { url } = await $fetch(paymentsApi.me.resume(paymentId), { method: "POST" });
|
|
11
17
|
await navigateTo(url, { external: true });
|
|
12
18
|
}
|
|
13
|
-
|
|
19
|
+
const api = { payments, refresh, status, error, loading, resume };
|
|
20
|
+
return Object.assign(fetched.then(() => api), api);
|
|
14
21
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { paymentsApi } from "../../utils";
|
|
2
3
|
defineOptions({ name: "UsersAdminPayments" });
|
|
3
4
|
const props = defineProps({
|
|
4
5
|
user: { type: Object, required: true }
|
|
@@ -6,11 +7,13 @@ const props = defineProps({
|
|
|
6
7
|
const t = useModuleT("payments.admin");
|
|
7
8
|
const status = useModuleT("payments.status");
|
|
8
9
|
const format = useFormat();
|
|
9
|
-
const { data: payments, status: fetchStatus } = useLazyFetch(
|
|
10
|
-
() =>
|
|
10
|
+
const { data: payments, status: fetchStatus, error } = useLazyFetch(
|
|
11
|
+
() => paymentsApi.admin.userPayments(props.user.id),
|
|
11
12
|
{ key: () => `admin-payments-${props.user.id}` }
|
|
12
13
|
);
|
|
14
|
+
const loading = computed(() => fetchStatus.value === "pending" && !payments.value);
|
|
13
15
|
const statusLabel = (value) => status(value, value);
|
|
16
|
+
const paymentDate = (payment) => format.dateTime(payment.paidAt ?? payment.createdAt) || "";
|
|
14
17
|
const columns = computed(() => [
|
|
15
18
|
{ accessorKey: "createdAt", header: t("date", "Date") },
|
|
16
19
|
{ accessorKey: "description", header: t("description", "Description") },
|
|
@@ -22,42 +25,69 @@ const columns = computed(() => [
|
|
|
22
25
|
</script>
|
|
23
26
|
|
|
24
27
|
<template>
|
|
25
|
-
<TableCard
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
</template>
|
|
28
|
+
<TableCard
|
|
29
|
+
:title="t('title', 'Payments')"
|
|
30
|
+
:loading="loading"
|
|
31
|
+
:refreshing="fetchStatus === 'pending' && !loading"
|
|
32
|
+
>
|
|
31
33
|
<UTable
|
|
32
34
|
:data="payments ?? []"
|
|
33
35
|
:columns="columns"
|
|
34
|
-
:loading="fetchStatus === 'pending'"
|
|
35
36
|
:ui="tableUi({ tr: '' })"
|
|
36
37
|
>
|
|
37
38
|
<template #createdAt-cell="{ row }">
|
|
38
|
-
<span
|
|
39
|
+
<span
|
|
40
|
+
v-if="paymentDate(row.original)"
|
|
41
|
+
class="whitespace-nowrap tabular-nums"
|
|
42
|
+
>{{ paymentDate(row.original) }}</span>
|
|
43
|
+
<span
|
|
44
|
+
v-else
|
|
45
|
+
class="text-dimmed"
|
|
46
|
+
>–</span>
|
|
39
47
|
</template>
|
|
40
48
|
<template #description-cell="{ row }">
|
|
41
49
|
{{ row.original.description ?? t("defaultDescription", "Payment") }}
|
|
42
50
|
</template>
|
|
43
51
|
<template #amount-cell="{ row }">
|
|
44
|
-
<span class="whitespace-nowrap">{{ format.money(row.original.amount, row.original.currency) }}</span>
|
|
52
|
+
<span class="whitespace-nowrap tabular-nums">{{ format.money(row.original.amount, row.original.currency) }}</span>
|
|
45
53
|
</template>
|
|
46
54
|
<template #status-cell="{ row }">
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
:color="statusColor(row.original.status, { partially_refunded: 'warning' })"
|
|
55
|
+
<StatusBadge
|
|
56
|
+
:status="row.original.status"
|
|
50
57
|
:label="statusLabel(row.original.status)"
|
|
58
|
+
:colors="{ partially_refunded: 'warning' }"
|
|
51
59
|
/>
|
|
52
60
|
</template>
|
|
53
61
|
<template #refundedAmount-cell="{ row }">
|
|
54
|
-
<span
|
|
62
|
+
<span
|
|
63
|
+
v-if="row.original.refundedAmount"
|
|
64
|
+
class="whitespace-nowrap tabular-nums"
|
|
65
|
+
>{{ format.money(row.original.refundedAmount, row.original.currency) }}</span>
|
|
66
|
+
<span
|
|
67
|
+
v-else
|
|
68
|
+
class="text-dimmed"
|
|
69
|
+
>–</span>
|
|
55
70
|
</template>
|
|
56
71
|
<template #referenceType-cell="{ row }">
|
|
57
|
-
<span
|
|
72
|
+
<span
|
|
73
|
+
v-if="row.original.referenceType"
|
|
74
|
+
class="text-muted"
|
|
75
|
+
>{{ `${row.original.referenceType} ${row.original.referenceId ?? ""}`.trim() }}</span>
|
|
76
|
+
<span
|
|
77
|
+
v-else
|
|
78
|
+
class="text-dimmed"
|
|
79
|
+
>–</span>
|
|
58
80
|
</template>
|
|
59
81
|
<template #empty>
|
|
60
82
|
<UEmpty
|
|
83
|
+
v-if="error"
|
|
84
|
+
icon="i-lucide-circle-alert"
|
|
85
|
+
variant="naked"
|
|
86
|
+
:title="t('loadFailed', 'The payments could not be loaded')"
|
|
87
|
+
:description="errorMessage(error, '')"
|
|
88
|
+
/>
|
|
89
|
+
<UEmpty
|
|
90
|
+
v-else
|
|
61
91
|
icon="i-lucide-credit-card"
|
|
62
92
|
variant="naked"
|
|
63
93
|
:title="t('empty', 'No payments yet')"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { paymentsApi } from "../../utils";
|
|
2
3
|
defineOptions({ name: "UsersAdminLastPayment" });
|
|
3
4
|
const props = defineProps({
|
|
4
5
|
user: { type: Object, required: true }
|
|
@@ -7,7 +8,7 @@ const t = useModuleT("payments.admin");
|
|
|
7
8
|
const status = useModuleT("payments.status");
|
|
8
9
|
const format = useFormat();
|
|
9
10
|
const { data: payments, status: fetchStatus } = useLazyFetch(
|
|
10
|
-
() =>
|
|
11
|
+
() => paymentsApi.admin.userPayments(props.user.id),
|
|
11
12
|
{ key: () => `admin-last-payment-${props.user.id}`, query: { limit: 1 } }
|
|
12
13
|
);
|
|
13
14
|
const last = computed(() => payments.value?.[0]);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { paymentsApi } from "../../utils";
|
|
2
3
|
defineOptions({ name: "PaymentsRecentTransactions" });
|
|
3
4
|
const props = defineProps({
|
|
4
5
|
tabId: { type: String, required: false, default: "payments" }
|
|
@@ -9,7 +10,7 @@ const format = useFormat();
|
|
|
9
10
|
const { localize } = useLocaleLink();
|
|
10
11
|
const usersPage = useRuntimeConfig().public.users?.pages?.admin;
|
|
11
12
|
const usersPath = usersPage ? localize(usersPage.replace(/\/+$/, "")) : void 0;
|
|
12
|
-
const { data, status: fetchStatus } = useLazyFetch(
|
|
13
|
+
const { data, status: fetchStatus } = useLazyFetch(paymentsApi.admin.recent, { key: "admin-payments-recent" });
|
|
13
14
|
const userTo = (id) => usersPath ? { path: `${usersPath}/${id}`, query: { tab: props.tabId } } : void 0;
|
|
14
15
|
</script>
|
|
15
16
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { paymentsApi } from "../../utils";
|
|
2
3
|
defineOptions({ name: "PaymentsRevenueStat" });
|
|
3
4
|
const t = useModuleT("payments.admin");
|
|
4
5
|
const format = useFormat();
|
|
5
|
-
const { data, status } = useLazyFetch(
|
|
6
|
+
const { data, status } = useLazyFetch(paymentsApi.admin.recent, { key: "admin-payments-recent" });
|
|
6
7
|
const main = computed(() => data.value?.revenue[0]);
|
|
7
8
|
</script>
|
|
8
9
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default defineStaffScopes(["payments:manage"], (event, userId) => paymentsStaff.can(event, userId));
|
|
@@ -3,7 +3,7 @@ import { and, count, desc, eq, gte, inArray, sql } from "drizzle-orm";
|
|
|
3
3
|
const DAYS = 30;
|
|
4
4
|
const LIMIT = 8;
|
|
5
5
|
export default defineEventHandler(async (event) => {
|
|
6
|
-
await
|
|
6
|
+
await paymentsStaff.require(event);
|
|
7
7
|
const since = new Date(Date.now() - DAYS * 24 * 60 * 60 * 1e3);
|
|
8
8
|
const settled = inArray(schema.payments.status, ["paid", "partially_refunded"]);
|
|
9
9
|
const when = sql`coalesce(${schema.payments.paidAt}, ${schema.payments.createdAt})`;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const querySchema = z.object({ limit: z.coerce.number().int().min(1).max(500).default(100) });
|
|
1
3
|
export default defineEventHandler(async (event) => {
|
|
2
4
|
const { target } = await loadUsersAdminTarget(event, "view");
|
|
3
|
-
const limit =
|
|
4
|
-
return listUserPayments(target.id, { limit
|
|
5
|
+
const { limit } = await getValidatedQuery(event, querySchema.parse);
|
|
6
|
+
return listUserPayments(target.id, { limit });
|
|
5
7
|
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { idParams } from "@frigear-nu/module-kit/server";
|
|
2
|
+
export default defineEventHandler(async (event) => {
|
|
3
|
+
const user = await requireUser(event, { scopes: ["payments:write"] });
|
|
4
|
+
const { id } = await getValidatedRouterParams(event, idParams().parse);
|
|
5
|
+
return resumePayment(id, user.id, event);
|
|
6
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { db, schema } from "@nuxthub/db";
|
|
2
2
|
import { and, desc, eq, sql } from "drizzle-orm";
|
|
3
|
-
import { resolveSameOriginUrl } from "@frigear-nu/
|
|
3
|
+
import { apiError, badRequest, conflict, notFound, resolveSameOriginUrl } from "@frigear-nu/module-kit/server";
|
|
4
4
|
import { sumItems } from "../../utils/index.js";
|
|
5
5
|
export function toPaymentDto(row) {
|
|
6
6
|
return {
|
|
@@ -21,12 +21,10 @@ const newPaymentId = () => `pay_${crypto.randomUUID().replaceAll("-", "")}`;
|
|
|
21
21
|
function safeReturnUrl(input, event, name) {
|
|
22
22
|
if (!event) {
|
|
23
23
|
const absolute = URL.canParse(input) && ["http:", "https:"].includes(new URL(input).protocol);
|
|
24
|
-
if (!absolute) throw
|
|
24
|
+
if (!absolute) throw badRequest(`${name} must be an absolute http(s) URL.`);
|
|
25
25
|
return input;
|
|
26
26
|
}
|
|
27
|
-
|
|
28
|
-
if (!resolved) throw createError({ statusCode: 400, statusMessage: `${name} must be a relative path or an URL on the same origin.` });
|
|
29
|
-
return resolved;
|
|
27
|
+
return resolveSameOriginUrl(event, input, { name });
|
|
30
28
|
}
|
|
31
29
|
function toLineItems(items, currency) {
|
|
32
30
|
return items.map((item) => ({
|
|
@@ -64,13 +62,13 @@ function expiresAt(minutes) {
|
|
|
64
62
|
return Math.floor(Date.now() / 1e3) + clamped * 60;
|
|
65
63
|
}
|
|
66
64
|
export async function createPayment(input, event) {
|
|
67
|
-
if (input.items.length === 0) throw
|
|
65
|
+
if (input.items.length === 0) throw badRequest("A payment needs at least one item.");
|
|
68
66
|
const currency = input.items[0].currency.toLowerCase();
|
|
69
67
|
if (input.items.some((item) => item.currency.toLowerCase() !== currency)) {
|
|
70
|
-
throw
|
|
68
|
+
throw badRequest("All items in a payment must share one currency.");
|
|
71
69
|
}
|
|
72
70
|
const amount = sumItems(input.items);
|
|
73
|
-
if (amount <= 0) throw
|
|
71
|
+
if (amount <= 0) throw badRequest("The payment amount must be greater than zero.");
|
|
74
72
|
const config = useRuntimeConfig().payments;
|
|
75
73
|
const stripe = useStripe();
|
|
76
74
|
const customer = await ensureStripeCustomer(input.userId);
|
|
@@ -89,7 +87,7 @@ export async function createPayment(input, event) {
|
|
|
89
87
|
event
|
|
90
88
|
});
|
|
91
89
|
const session = await stripe.checkout.sessions.create(params);
|
|
92
|
-
if (!session.url) throw
|
|
90
|
+
if (!session.url) throw apiError(502, "bad_gateway", "Stripe did not return a checkout URL.");
|
|
93
91
|
try {
|
|
94
92
|
const [row] = await db.insert(schema.payments).values({
|
|
95
93
|
id,
|
|
@@ -124,19 +122,19 @@ export async function listUserPayments(userId, options = {}) {
|
|
|
124
122
|
}
|
|
125
123
|
export async function resumePayment(paymentId, userId, event) {
|
|
126
124
|
const [row] = await db.select().from(schema.payments).where(and(eq(schema.payments.id, paymentId), eq(schema.payments.userId, userId))).limit(1);
|
|
127
|
-
if (!row) throw
|
|
125
|
+
if (!row) throw notFound("Payment not found.");
|
|
128
126
|
if (row.status === "paid" || row.status === "refunded" || row.status === "partially_refunded") {
|
|
129
|
-
throw
|
|
127
|
+
throw conflict("This payment is already paid.");
|
|
130
128
|
}
|
|
131
129
|
const stripe = useStripe();
|
|
132
|
-
if (!row.checkoutSessionId) throw
|
|
130
|
+
if (!row.checkoutSessionId) throw conflict("This payment cannot be resumed.");
|
|
133
131
|
const session = await stripe.checkout.sessions.retrieve(row.checkoutSessionId);
|
|
134
132
|
if (session.status === "open" && session.url) return { url: session.url };
|
|
135
|
-
if (session.status === "complete") throw
|
|
133
|
+
if (session.status === "complete") throw conflict("This payment is already paid.");
|
|
136
134
|
const origin = event ? getRequestURL(event).origin : void 0;
|
|
137
135
|
const successUrl = session.success_url ?? origin;
|
|
138
136
|
const cancelUrl = session.cancel_url ?? origin;
|
|
139
|
-
if (!successUrl || !cancelUrl) throw
|
|
137
|
+
if (!successUrl || !cancelUrl) throw conflict("Cannot resume this payment.");
|
|
140
138
|
const items = row.items?.length ? row.items : [{ name: row.description ?? "Payment", amount: row.amount, currency: row.currency }];
|
|
141
139
|
const customer = await ensureStripeCustomer(userId);
|
|
142
140
|
const params = await buildCheckoutParams({
|
|
@@ -153,18 +151,18 @@ export async function resumePayment(paymentId, userId, event) {
|
|
|
153
151
|
event
|
|
154
152
|
});
|
|
155
153
|
const next = await stripe.checkout.sessions.create(params);
|
|
156
|
-
if (!next.url) throw
|
|
154
|
+
if (!next.url) throw apiError(502, "bad_gateway", "Stripe did not return a checkout URL.");
|
|
157
155
|
await db.update(schema.payments).set({ checkoutSessionId: next.id, status: "pending" }).where(eq(schema.payments.id, row.id));
|
|
158
156
|
return { url: next.url };
|
|
159
157
|
}
|
|
160
158
|
export async function refundPayment(paymentId, options = {}) {
|
|
161
159
|
const [row] = await db.select().from(schema.payments).where(eq(schema.payments.id, paymentId)).limit(1);
|
|
162
|
-
if (!row) throw
|
|
160
|
+
if (!row) throw notFound("Payment not found.");
|
|
163
161
|
if (!row.paymentIntentId || row.status !== "paid" && row.status !== "partially_refunded") {
|
|
164
|
-
throw
|
|
162
|
+
throw conflict("Only paid payments can be refunded.");
|
|
165
163
|
}
|
|
166
164
|
if (options.amount !== void 0 && (options.amount <= 0 || options.amount > row.amount - row.refundedAmount)) {
|
|
167
|
-
throw
|
|
165
|
+
throw badRequest("Invalid refund amount.");
|
|
168
166
|
}
|
|
169
167
|
await useStripe().refunds.create({
|
|
170
168
|
payment_intent: row.paymentIntentId,
|
|
@@ -194,7 +192,7 @@ export async function recordInvoicePayment(input) {
|
|
|
194
192
|
const [row] = existing ? await db.update(schema.payments).set(values).where(eq(schema.payments.id, existing.id)).returning() : await db.insert(schema.payments).values({ id: newPaymentId(), ...values, ...input.createdAt ? { createdAt: input.createdAt } : {} }).onConflictDoNothing({ target: schema.payments.invoiceId }).returning();
|
|
195
193
|
if (!row) {
|
|
196
194
|
const [current] = await db.select().from(schema.payments).where(eq(schema.payments.invoiceId, input.invoiceId)).limit(1);
|
|
197
|
-
if (!current) throw
|
|
195
|
+
if (!current) throw apiError(500, "internal_error", "Could not record the invoice payment.");
|
|
198
196
|
return toPaymentDto(current);
|
|
199
197
|
}
|
|
200
198
|
const dto = toPaymentDto(row);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const paymentsApi: {
|
|
2
|
+
readonly me: {
|
|
3
|
+
/** `GET` the caller's payments, newest first. */
|
|
4
|
+
readonly list: "/api/payments/me";
|
|
5
|
+
/** `POST` to get a checkout URL for one of the caller's unpaid payments. */
|
|
6
|
+
readonly resume: (id: string) => string;
|
|
7
|
+
};
|
|
8
|
+
readonly admin: {
|
|
9
|
+
/** `GET` the dashboard's recent payments and revenue (staff). */
|
|
10
|
+
readonly recent: "/api/admin/payments/recent";
|
|
11
|
+
/** `GET` one user's payments (staff, `?limit`); mounted on the users admin. */
|
|
12
|
+
readonly userPayments: (userId: number | string) => string;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const enc = encodeURIComponent;
|
|
2
|
+
const BASE = "/api/payments";
|
|
3
|
+
const ADMIN_BASE = "/api/admin/payments";
|
|
4
|
+
export const paymentsApi = {
|
|
5
|
+
me: {
|
|
6
|
+
/** `GET` the caller's payments, newest first. */
|
|
7
|
+
list: `${BASE}/me`,
|
|
8
|
+
/** `POST` to get a checkout URL for one of the caller's unpaid payments. */
|
|
9
|
+
resume: (id) => `${BASE}/me/${enc(id)}/resume`
|
|
10
|
+
},
|
|
11
|
+
admin: {
|
|
12
|
+
/** `GET` the dashboard's recent payments and revenue (staff). */
|
|
13
|
+
recent: `${ADMIN_BASE}/recent`,
|
|
14
|
+
/** `GET` one user's payments (staff, `?limit`); mounted on the users admin. */
|
|
15
|
+
userPayments: (userId) => `/api/admin/users/${enc(String(userId))}/payments`
|
|
16
|
+
}
|
|
17
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PaymentItem } from '../../types.js';
|
|
2
|
-
export
|
|
2
|
+
export * from './api.js';
|
|
3
|
+
export type { AuthorizeContext, CheckoutBeforeContext, CreatePaymentInput, CreatePaymentResult, ModuleOptions, PaymentDto, PaymentHookContext, PaymentItem, PaymentReference, PaymentStatus, RecentPaymentDto, RecentPaymentsDto, ResumeResponse, } from '../../types.js';
|
|
3
4
|
/** Total of a list of items in minor units. */
|
|
4
5
|
export declare function sumItems(items: PaymentItem[]): number;
|
package/dist/types.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { default } from './module.mjs'
|
|
2
2
|
|
|
3
|
-
export { type CheckoutBeforeContext, type CreatePaymentInput, type CreatePaymentResult, type InvoicePaymentInput, type ModuleOptions, type PaymentDto, type PaymentHookContext, type PaymentItem, type PaymentReference, type PaymentStatus, type PaymentsRuntimeConfig, type ResumeResponse } from './module.mjs'
|
|
3
|
+
export { type AuthorizeContext, type CheckoutBeforeContext, type CreatePaymentInput, type CreatePaymentResult, type InvoicePaymentInput, type ModuleOptions, type PaymentDto, type PaymentHookContext, type PaymentItem, type PaymentReference, type PaymentStatus, type PaymentsRuntimeConfig, type ResumeResponse } from './module.mjs'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frigear-nu/payments",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "One-time payments (Stripe Checkout, refunds and payment lifecycle hooks) for the frigear ecosystem, built on @frigear-nu/stripe.",
|
|
5
5
|
"homepage": "https://docs.frigear.dev/modules/payments",
|
|
6
6
|
"repository": {
|
|
@@ -21,6 +21,11 @@
|
|
|
21
21
|
"types": "./dist/runtime/utils/index.d.ts",
|
|
22
22
|
"import": "./dist/runtime/utils/index.js",
|
|
23
23
|
"default": "./dist/runtime/utils/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./api": {
|
|
26
|
+
"types": "./dist/runtime/utils/api.d.ts",
|
|
27
|
+
"import": "./dist/runtime/utils/api.js",
|
|
28
|
+
"default": "./dist/runtime/utils/api.js"
|
|
24
29
|
}
|
|
25
30
|
},
|
|
26
31
|
"files": [
|
|
@@ -30,12 +35,12 @@
|
|
|
30
35
|
"playground"
|
|
31
36
|
],
|
|
32
37
|
"dependencies": {
|
|
33
|
-
"@frigear-nu/auth": "0.1.
|
|
34
|
-
"@frigear-nu/mcp": "0.1.
|
|
35
|
-
"@frigear-nu/module-kit": "1.1.
|
|
36
|
-
"@frigear-nu/stripe": "0.1.
|
|
37
|
-
"@frigear-nu/ui": "0.1.
|
|
38
|
-
"@frigear-nu/users": "1.1.
|
|
38
|
+
"@frigear-nu/auth": "0.1.4",
|
|
39
|
+
"@frigear-nu/mcp": "0.1.3",
|
|
40
|
+
"@frigear-nu/module-kit": "1.1.3",
|
|
41
|
+
"@frigear-nu/stripe": "0.1.3",
|
|
42
|
+
"@frigear-nu/ui": "0.1.3",
|
|
43
|
+
"@frigear-nu/users": "1.1.4",
|
|
39
44
|
"@libsql/client": "^0.18.0",
|
|
40
45
|
"@nuxt/kit": "^4.6.1",
|
|
41
46
|
"drizzle-kit": "^0.31.11",
|
|
@@ -46,8 +51,8 @@
|
|
|
46
51
|
"zod": "^4.6.5"
|
|
47
52
|
},
|
|
48
53
|
"peerDependencies": {
|
|
49
|
-
"@frigear-nu/widget": "0.1.
|
|
50
|
-
"@nuxt/ui": "^4.
|
|
54
|
+
"@frigear-nu/widget": "0.1.3",
|
|
55
|
+
"@nuxt/ui": "^4.12.0",
|
|
51
56
|
"@nuxthub/core": "^0.10.8"
|
|
52
57
|
},
|
|
53
58
|
"peerDependenciesMeta": {
|
|
@@ -59,13 +64,13 @@
|
|
|
59
64
|
}
|
|
60
65
|
},
|
|
61
66
|
"devDependencies": {
|
|
62
|
-
"@frigear-nu/widget": "0.1.
|
|
67
|
+
"@frigear-nu/widget": "0.1.3",
|
|
63
68
|
"@nuxt/devtools": "^3.4.2",
|
|
64
69
|
"@nuxt/eslint-config": "^1.17.0",
|
|
65
70
|
"@nuxt/module-builder": "^1.0.3",
|
|
66
71
|
"@nuxt/schema": "^4.6.1",
|
|
67
72
|
"@nuxt/test-utils": "^4.3.2",
|
|
68
|
-
"@nuxt/ui": "^4.
|
|
73
|
+
"@nuxt/ui": "^4.12.0",
|
|
69
74
|
"@nuxthub/core": "^0.10.8",
|
|
70
75
|
"@types/node": "^26.6.5",
|
|
71
76
|
"changelogen": "^0.6.2",
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
const bodySchema = z.object({ paymentId: z.string().min(1) });
|
|
3
|
-
export default defineEventHandler(async (event) => {
|
|
4
|
-
const user = await requireUser(event, { scopes: ["payments:write"] });
|
|
5
|
-
const { paymentId } = await readValidatedBody(event, bodySchema.parse);
|
|
6
|
-
return resumePayment(paymentId, user.id, event);
|
|
7
|
-
});
|
|
File without changes
|
|
File without changes
|
|
File without changes
|