@frigear-nu/payments 0.1.2 → 0.2.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.
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.2",
4
+ "version": "0.2.0",
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';
@@ -32,7 +32,8 @@ const module$1 = defineNuxtModule({
32
32
  addAuthScope(
33
33
  nuxt,
34
34
  { name: "payments:read", description: "Read your payments" },
35
- { 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)" }
36
37
  );
37
38
  const resolver = createResolver(import.meta.url);
38
39
  registerTailwindSource(nuxt, resolver.resolve("./runtime/app"));
@@ -54,16 +55,10 @@ const module$1 = defineNuxtModule({
54
55
  ],
55
56
  handler: resolver.resolve("./runtime/server/stripe/payment-events")
56
57
  });
57
- addServerHandler({
58
- route: moduleApiUrl("payments"),
59
- method: "get",
60
- handler: resolver.resolve("./runtime/server/routes/payments/index.get")
61
- });
62
- addServerHandler({
63
- route: moduleApiUrl("payments", "resume"),
64
- method: "post",
65
- handler: resolver.resolve("./runtime/server/routes/payments/resume.post")
66
- });
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" });
67
62
  await extendUsersAdmin(nuxt, {
68
63
  routes: [{ method: "get", path: "payments", handler: resolver.resolve("./runtime/server/routes/admin/user-payments.get") }],
69
64
  tab: {
@@ -81,11 +76,9 @@ const module$1 = defineNuxtModule({
81
76
  span: 2
82
77
  }
83
78
  });
84
- addServerHandler({
85
- route: moduleApiUrl("payments", "admin/recent"),
86
- method: "get",
87
- handler: resolver.resolve("./runtime/server/routes/admin/recent.get")
88
- });
79
+ addModuleRoutes(resolver, [
80
+ { method: "get", path: "recent", file: "admin/recent.get" }
81
+ ], { base: "/api/admin/payments" });
89
82
  if (hasNuxtModule("@frigear-nu/admin") && hasNuxtModule("@frigear-nu/widget")) {
90
83
  const { extendWidgets } = await import('@frigear-nu/widget/utils');
91
84
  extendWidgets(nuxt, "admin.dashboard.top", { id: "payments-revenue-stat", component: resolver.resolve("./runtime/app/widgets/RevenueStat.vue"), order: 30 });
@@ -101,6 +94,7 @@ const module$1 = defineNuxtModule({
101
94
  addServerImportsDir(resolver.resolve("./runtime/server/utils"));
102
95
  inlineModuleRuntime(nuxt, resolver);
103
96
  addServerPlugin(resolver.resolve("./runtime/server/plugins/invoice-payments"));
97
+ addServerPlugin(resolver.resolve("./runtime/server/plugins/staff-scopes"));
104
98
  if (options.settings?.mcp === true) addMcpDefinitions(nuxt, resolver.resolve("./runtime/server/mcp"));
105
99
  addImports([
106
100
  { name: "usePayments", from: resolver.resolve("./runtime/app/composables/usePayments") }
@@ -1,6 +1,8 @@
1
+ import { paymentsApi } from "../../utils/index.js";
2
+ import { $fetch, computed, navigateTo, useCachedFetch, useUserSession, watch } from "#imports";
1
3
  export function usePayments() {
2
4
  const { loggedIn } = useUserSession();
3
- const fetched = useCachedFetch("/api/payments", {
5
+ const fetched = useCachedFetch(paymentsApi.me.list, {
4
6
  key: "payments:mine",
5
7
  ttl: 60 * 1e3,
6
8
  watch: false
@@ -12,7 +14,7 @@ export function usePayments() {
12
14
  });
13
15
  const payments = computed(() => fetched.data.value ?? []);
14
16
  async function resume(paymentId) {
15
- const { url } = await $fetch("/api/payments/resume", { method: "POST", body: { paymentId } });
17
+ const { url } = await $fetch(paymentsApi.me.resume(paymentId), { method: "POST" });
16
18
  await navigateTo(url, { external: true });
17
19
  }
18
20
  const api = { payments, refresh, status, error, loading, resume };
@@ -1,4 +1,6 @@
1
1
  <script setup>
2
+ import { paymentsApi } from "../../utils";
3
+ import { computed, useFormat, useLazyFetch, useModuleT } from "#imports";
2
4
  defineOptions({ name: "UsersAdminPayments" });
3
5
  const props = defineProps({
4
6
  user: { type: Object, required: true }
@@ -7,11 +9,12 @@ const t = useModuleT("payments.admin");
7
9
  const status = useModuleT("payments.status");
8
10
  const format = useFormat();
9
11
  const { data: payments, status: fetchStatus, error } = useLazyFetch(
10
- () => `/api/admin/users/${props.user.id}/payments`,
12
+ () => paymentsApi.admin.userPayments(props.user.id),
11
13
  { key: () => `admin-payments-${props.user.id}` }
12
14
  );
13
15
  const loading = computed(() => fetchStatus.value === "pending" && !payments.value);
14
16
  const statusLabel = (value) => status(value, value);
17
+ const paymentDate = (payment) => format.dateTime(payment.paidAt ?? payment.createdAt) || "";
15
18
  const columns = computed(() => [
16
19
  { accessorKey: "createdAt", header: t("date", "Date") },
17
20
  { accessorKey: "description", header: t("description", "Description") },
@@ -24,40 +27,57 @@ const columns = computed(() => [
24
27
 
25
28
  <template>
26
29
  <TableCard
30
+ :title="t('title', 'Payments')"
27
31
  :loading="loading"
28
32
  :refreshing="fetchStatus === 'pending' && !loading"
29
33
  >
30
- <template #header>
31
- <h2 class="text-highlighted text-sm font-semibold">
32
- {{ t("title", "Payments") }}
33
- </h2>
34
- </template>
35
34
  <UTable
36
35
  :data="payments ?? []"
37
36
  :columns="columns"
38
37
  :ui="tableUi({ tr: '' })"
39
38
  >
40
39
  <template #createdAt-cell="{ row }">
41
- <span class="whitespace-nowrap">{{ format.dateTime(row.original.paidAt ?? row.original.createdAt) || "\u2013" }}</span>
40
+ <span
41
+ v-if="paymentDate(row.original)"
42
+ class="whitespace-nowrap tabular-nums"
43
+ >{{ paymentDate(row.original) }}</span>
44
+ <span
45
+ v-else
46
+ class="text-dimmed"
47
+ >–</span>
42
48
  </template>
43
49
  <template #description-cell="{ row }">
44
50
  {{ row.original.description ?? t("defaultDescription", "Payment") }}
45
51
  </template>
46
52
  <template #amount-cell="{ row }">
47
- <span class="whitespace-nowrap">{{ format.money(row.original.amount, row.original.currency) }}</span>
53
+ <span class="whitespace-nowrap tabular-nums">{{ format.money(row.original.amount, row.original.currency) }}</span>
48
54
  </template>
49
55
  <template #status-cell="{ row }">
50
- <UBadge
51
- variant="subtle"
52
- :color="statusColor(row.original.status, { partially_refunded: 'warning' })"
56
+ <StatusBadge
57
+ :status="row.original.status"
53
58
  :label="statusLabel(row.original.status)"
59
+ :colors="{ partially_refunded: 'warning' }"
54
60
  />
55
61
  </template>
56
62
  <template #refundedAmount-cell="{ row }">
57
- <span class="whitespace-nowrap">{{ row.original.refundedAmount ? format.money(row.original.refundedAmount, row.original.currency) : "\u2013" }}</span>
63
+ <span
64
+ v-if="row.original.refundedAmount"
65
+ class="whitespace-nowrap tabular-nums"
66
+ >{{ format.money(row.original.refundedAmount, row.original.currency) }}</span>
67
+ <span
68
+ v-else
69
+ class="text-dimmed"
70
+ >–</span>
58
71
  </template>
59
72
  <template #referenceType-cell="{ row }">
60
- <span class="text-muted">{{ row.original.referenceType ? `${row.original.referenceType} ${row.original.referenceId ?? ""}`.trim() : "\u2013" }}</span>
73
+ <span
74
+ v-if="row.original.referenceType"
75
+ class="text-muted"
76
+ >{{ `${row.original.referenceType} ${row.original.referenceId ?? ""}`.trim() }}</span>
77
+ <span
78
+ v-else
79
+ class="text-dimmed"
80
+ >–</span>
61
81
  </template>
62
82
  <template #empty>
63
83
  <UEmpty
@@ -1,4 +1,6 @@
1
1
  <script setup>
2
+ import { paymentsApi } from "../../utils";
3
+ import { computed, useFormat, useLazyFetch, useModuleT } from "#imports";
2
4
  defineOptions({ name: "UsersAdminLastPayment" });
3
5
  const props = defineProps({
4
6
  user: { type: Object, required: true }
@@ -7,7 +9,7 @@ const t = useModuleT("payments.admin");
7
9
  const status = useModuleT("payments.status");
8
10
  const format = useFormat();
9
11
  const { data: payments, status: fetchStatus } = useLazyFetch(
10
- () => `/api/admin/users/${props.user.id}/payments`,
12
+ () => paymentsApi.admin.userPayments(props.user.id),
11
13
  { key: () => `admin-last-payment-${props.user.id}`, query: { limit: 1 } }
12
14
  );
13
15
  const last = computed(() => payments.value?.[0]);
@@ -1,4 +1,6 @@
1
1
  <script setup>
2
+ import { paymentsApi } from "../../utils";
3
+ import { useFormat, useLazyFetch, useLocaleLink, useModuleT, useRuntimeConfig } from "#imports";
2
4
  defineOptions({ name: "PaymentsRecentTransactions" });
3
5
  const props = defineProps({
4
6
  tabId: { type: String, required: false, default: "payments" }
@@ -9,7 +11,7 @@ const format = useFormat();
9
11
  const { localize } = useLocaleLink();
10
12
  const usersPage = useRuntimeConfig().public.users?.pages?.admin;
11
13
  const usersPath = usersPage ? localize(usersPage.replace(/\/+$/, "")) : void 0;
12
- const { data, status: fetchStatus } = useLazyFetch("/api/payments/admin/recent", { key: "admin-payments-recent" });
14
+ const { data, status: fetchStatus } = useLazyFetch(paymentsApi.admin.recent, { key: "admin-payments-recent" });
13
15
  const userTo = (id) => usersPath ? { path: `${usersPath}/${id}`, query: { tab: props.tabId } } : void 0;
14
16
  </script>
15
17
 
@@ -1,8 +1,10 @@
1
1
  <script setup>
2
+ import { paymentsApi } from "../../utils";
3
+ import { computed, useFormat, useLazyFetch, useModuleT } from "#imports";
2
4
  defineOptions({ name: "PaymentsRevenueStat" });
3
5
  const t = useModuleT("payments.admin");
4
6
  const format = useFormat();
5
- const { data, status } = useLazyFetch("/api/payments/admin/recent", { key: "admin-payments-recent" });
7
+ const { data, status } = useLazyFetch(paymentsApi.admin.recent, { key: "admin-payments-recent" });
6
8
  const main = computed(() => data.value?.revenue[0]);
7
9
  </script>
8
10
 
@@ -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.2",
3
+ "version": "0.2.0",
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.3",
34
- "@frigear-nu/mcp": "0.1.2",
35
- "@frigear-nu/module-kit": "1.1.2",
36
- "@frigear-nu/stripe": "0.1.2",
37
- "@frigear-nu/ui": "0.1.2",
38
- "@frigear-nu/users": "1.1.3",
38
+ "@frigear-nu/auth": "0.2.0",
39
+ "@frigear-nu/mcp": "0.2.0",
40
+ "@frigear-nu/module-kit": "1.2.0",
41
+ "@frigear-nu/stripe": "0.2.0",
42
+ "@frigear-nu/ui": "0.2.0",
43
+ "@frigear-nu/users": "1.2.0",
39
44
  "@libsql/client": "^0.18.0",
40
45
  "@nuxt/kit": "^4.6.1",
41
46
  "drizzle-kit": "^0.31.11",
@@ -46,7 +51,7 @@
46
51
  "zod": "^4.6.5"
47
52
  },
48
53
  "peerDependencies": {
49
- "@frigear-nu/widget": "0.1.2",
54
+ "@frigear-nu/widget": "0.2.0",
50
55
  "@nuxt/ui": "^4.12.0",
51
56
  "@nuxthub/core": "^0.10.8"
52
57
  },
@@ -59,7 +64,7 @@
59
64
  }
60
65
  },
61
66
  "devDependencies": {
62
- "@frigear-nu/widget": "0.1.2",
67
+ "@frigear-nu/widget": "0.2.0",
63
68
  "@nuxt/devtools": "^3.4.2",
64
69
  "@nuxt/eslint-config": "^1.17.0",
65
70
  "@nuxt/module-builder": "^1.0.3",
@@ -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
- });