@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.
Files changed (29) hide show
  1. package/dist/module.d.mts +10 -1
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +14 -18
  4. package/dist/runtime/app/composables/usePayments.d.ts +5 -7
  5. package/dist/runtime/app/composables/usePayments.js +13 -6
  6. package/dist/runtime/app/tabs/Payments.vue +46 -16
  7. package/dist/runtime/app/widgets/AdminLastPayment.vue +2 -1
  8. package/dist/runtime/app/widgets/RecentTransactions.vue +2 -1
  9. package/dist/runtime/app/widgets/RevenueStat.vue +2 -1
  10. package/dist/runtime/i18n/lang/da.json +1 -0
  11. package/dist/runtime/i18n/lang/en.json +1 -0
  12. package/dist/runtime/server/plugins/staff-scopes.js +1 -0
  13. package/dist/runtime/server/routes/admin/recent.get.js +1 -1
  14. package/dist/runtime/server/routes/admin/user-payments.get.js +4 -2
  15. package/dist/runtime/server/routes/me/resume.post.d.ts +2 -0
  16. package/dist/runtime/server/routes/me/resume.post.js +6 -0
  17. package/dist/runtime/server/utils/auth.d.ts +5 -0
  18. package/dist/runtime/server/utils/auth.js +5 -0
  19. package/dist/runtime/server/utils/payments.js +17 -19
  20. package/dist/runtime/utils/api.d.ts +14 -0
  21. package/dist/runtime/utils/api.js +17 -0
  22. package/dist/runtime/utils/index.d.ts +2 -1
  23. package/dist/runtime/utils/index.js +1 -0
  24. package/dist/types.d.mts +1 -1
  25. package/package.json +16 -11
  26. package/dist/runtime/server/routes/payments/resume.post.js +0 -7
  27. /package/dist/runtime/server/{routes/payments/index.get.d.ts → plugins/staff-scopes.d.ts} +0 -0
  28. /package/dist/runtime/server/routes/{payments/resume.post.d.ts → me/payments.get.d.ts} +0 -0
  29. /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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@frigear-nu/payments",
3
3
  "configKey": "payments",
4
- "version": "0.1.1",
4
+ "version": "0.1.3",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.3",
7
7
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { defineNuxtModule, createResolver, addServerHandler, hasNuxtModule, addServerImportsDir, addServerPlugin, addImports } from '@nuxt/kit';
2
- import { registerModule, registerTailwindSource, registerLocales, registerDatabaseTable, moduleApiUrl, inlineModuleRuntime } from '@frigear-nu/module-kit/utils';
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
- addServerHandler({
56
- route: moduleApiUrl("payments"),
57
- method: "get",
58
- handler: resolver.resolve("./runtime/server/routes/payments/index.get")
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
- addServerHandler({
83
- route: moduleApiUrl("payments", "admin/recent"),
84
- method: "get",
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
- /** The signed-in user's payments, plus a helper to resume an unpaid one. */
2
- export declare function usePayments(): {
3
- payments: any;
4
- refresh: any;
5
- status: any;
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 { data, refresh, status } = useFetch("/api/payments", {
4
+ const fetched = useCachedFetch(paymentsApi.me.list, {
4
5
  key: "payments:mine",
5
- immediate: loggedIn.value,
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("/api/payments/resume", { method: "POST", body: { paymentId } });
16
+ const { url } = await $fetch(paymentsApi.me.resume(paymentId), { method: "POST" });
11
17
  await navigateTo(url, { external: true });
12
18
  }
13
- return { payments: data, refresh, status, resume };
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
- () => `/api/admin/users/${props.user.id}/payments`,
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
- <template #header>
27
- <h2 class="text-highlighted text-sm font-semibold">
28
- {{ t("title", "Payments") }}
29
- </h2>
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 class="whitespace-nowrap">{{ format.dateTime(row.original.paidAt ?? row.original.createdAt) || "\u2013" }}</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
- <UBadge
48
- variant="subtle"
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 class="whitespace-nowrap">{{ row.original.refundedAmount ? format.money(row.original.refundedAmount, row.original.currency) : "\u2013" }}</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 class="text-muted">{{ row.original.referenceType ? `${row.original.referenceType} ${row.original.referenceId ?? ""}`.trim() : "\u2013" }}</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
- () => `/api/admin/users/${props.user.id}/payments`,
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("/api/payments/admin/recent", { key: "admin-payments-recent" });
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("/api/payments/admin/recent", { key: "admin-payments-recent" });
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
 
@@ -14,6 +14,7 @@
14
14
  "title": "Betalinger",
15
15
  "lastPayment": "Seneste betaling",
16
16
  "empty": "Ingen betalinger endnu",
17
+ "loadFailed": "Betalingerne kunne ikke hentes",
17
18
  "date": "Dato",
18
19
  "description": "Beskrivelse",
19
20
  "amount": "Beløb",
@@ -14,6 +14,7 @@
14
14
  "title": "Payments",
15
15
  "lastPayment": "Last payment",
16
16
  "empty": "No payments yet",
17
+ "loadFailed": "The payments could not be loaded",
17
18
  "date": "Date",
18
19
  "description": "Description",
19
20
  "amount": "Amount",
@@ -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 authorizeUsersAdmin(event, "view");
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 = Number(getQuery(event).limit);
4
- return listUserPayments(target.id, { limit: Number.isInteger(limit) && limit > 0 ? Math.min(limit, 500) : 100 });
5
+ const { limit } = await getValidatedQuery(event, querySchema.parse);
6
+ return listUserPayments(target.id, { limit });
5
7
  });
@@ -0,0 +1,2 @@
1
+ declare const _default: any;
2
+ export default _default;
@@ -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
+ });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * The staff gate of `/api/admin/payments`. The host decides who is staff through the `payments:authorize` Nitro hook;
3
+ * denied by default. Bearer tokens need the `payments:manage` scope.
4
+ */
5
+ export declare const paymentsStaff: any;
@@ -0,0 +1,5 @@
1
+ export const paymentsStaff = defineStaffGuard({
2
+ hook: "payments:authorize",
3
+ scope: "payments:manage",
4
+ message: "You are not allowed to see payments."
5
+ });
@@ -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/stripe/utils";
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 createError({ statusCode: 400, statusMessage: `${name} must be an absolute http(s) URL.` });
24
+ if (!absolute) throw badRequest(`${name} must be an absolute http(s) URL.`);
25
25
  return input;
26
26
  }
27
- const resolved = resolveSameOriginUrl(input, getRequestURL(event).origin);
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 createError({ statusCode: 400, statusMessage: "A payment needs at least one item." });
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 createError({ statusCode: 400, statusMessage: "All items in a payment must share one currency." });
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 createError({ statusCode: 400, statusMessage: "The payment amount must be greater than zero." });
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 createError({ statusCode: 502, statusMessage: "Stripe did not return a checkout URL." });
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 createError({ statusCode: 404, statusMessage: "Payment not found." });
125
+ if (!row) throw notFound("Payment not found.");
128
126
  if (row.status === "paid" || row.status === "refunded" || row.status === "partially_refunded") {
129
- throw createError({ statusCode: 409, statusMessage: "This payment is already paid." });
127
+ throw conflict("This payment is already paid.");
130
128
  }
131
129
  const stripe = useStripe();
132
- if (!row.checkoutSessionId) throw createError({ statusCode: 409, statusMessage: "This payment cannot be resumed." });
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 createError({ statusCode: 409, statusMessage: "This payment is already paid." });
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 createError({ statusCode: 409, statusMessage: "Cannot resume this payment." });
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 createError({ statusCode: 502, statusMessage: "Stripe did not return a checkout URL." });
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 createError({ statusCode: 404, statusMessage: "Payment not found." });
160
+ if (!row) throw notFound("Payment not found.");
163
161
  if (!row.paymentIntentId || row.status !== "paid" && row.status !== "partially_refunded") {
164
- throw createError({ statusCode: 409, statusMessage: "Only paid payments can be refunded." });
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 createError({ statusCode: 400, statusMessage: "Invalid refund amount." });
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 createError({ statusCode: 500, statusMessage: "Could not record the invoice payment." });
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 type { CheckoutBeforeContext, CreatePaymentInput, CreatePaymentResult, ModuleOptions, PaymentDto, PaymentHookContext, PaymentItem, PaymentReference, PaymentStatus, } from '../../types.js';
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;
@@ -1,3 +1,4 @@
1
+ export * from "./api.js";
1
2
  export function sumItems(items) {
2
3
  return items.reduce((total, item) => total + item.amount * (item.quantity ?? 1), 0);
3
4
  }
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.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.2",
34
- "@frigear-nu/mcp": "0.1.1",
35
- "@frigear-nu/module-kit": "1.1.1",
36
- "@frigear-nu/stripe": "0.1.1",
37
- "@frigear-nu/ui": "0.1.1",
38
- "@frigear-nu/users": "1.1.2",
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.1",
50
- "@nuxt/ui": "^4.11.2",
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.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.11.2",
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
- });