v1.3.27-c: Add adjustment/summary action for meter event
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@bowong/better-auth-stripe",
|
||||
"author": "Bowong",
|
||||
"version": "1.3.27-a",
|
||||
"version": "1.3.27-c",
|
||||
"main": "dist/index.cjs",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
180
src/index.ts
180
src/index.ts
@@ -3,9 +3,9 @@ import {
|
||||
type BetterAuthPlugin,
|
||||
logger,
|
||||
} from "better-auth";
|
||||
import { createAuthEndpoint, createAuthMiddleware } from "better-auth/plugins";
|
||||
import {createAuthEndpoint, createAuthMiddleware} from "better-auth/plugins";
|
||||
import Stripe from "stripe";
|
||||
import { type Stripe as StripeType } from "stripe";
|
||||
import {type Stripe as StripeType} from "stripe";
|
||||
import * as z from "zod/v4";
|
||||
import {
|
||||
sessionMiddleware,
|
||||
@@ -19,14 +19,15 @@ import {
|
||||
onSubscriptionUpdated,
|
||||
} from "./hooks";
|
||||
import type {
|
||||
actionType,
|
||||
InputSubscription,
|
||||
StripeOptions,
|
||||
StripePlan,
|
||||
Subscription,
|
||||
} from "./types";
|
||||
import { getPlanByName, getPlanByPriceInfo, getPlans } from "./utils";
|
||||
import { getSchema } from "./schema";
|
||||
import { defu } from "defu";
|
||||
import {getPlanByName, getPlanByPriceInfo, getPlans} from "./utils";
|
||||
import {getSchema} from "./schema";
|
||||
import {defu} from "defu";
|
||||
|
||||
const STRIPE_ERROR_CODES = {
|
||||
SUBSCRIPTION_NOT_FOUND: "Subscription not found",
|
||||
@@ -67,13 +68,7 @@ export const stripe = <O extends StripeOptions>(options: O) => {
|
||||
const client = options.stripeClient;
|
||||
|
||||
const referenceMiddleware = (
|
||||
action:
|
||||
| "upgrade-subscription"
|
||||
| "list-subscription"
|
||||
| "cancel-subscription"
|
||||
| "restore-subscription"
|
||||
| "meter-event"
|
||||
| "billing-portal",
|
||||
action: actionType,
|
||||
) =>
|
||||
createAuthMiddleware(async (ctx) => {
|
||||
const session = ctx.context.session;
|
||||
@@ -235,7 +230,7 @@ export const stripe = <O extends StripeOptions>(options: O) => {
|
||||
],
|
||||
},
|
||||
async (ctx) => {
|
||||
const { user, session } = ctx.context.session;
|
||||
const {user, session} = ctx.context.session;
|
||||
if (
|
||||
!user.emailVerified &&
|
||||
options.subscription?.requireEmailVerification
|
||||
@@ -270,7 +265,7 @@ export const stripe = <O extends StripeOptions>(options: O) => {
|
||||
: referenceId
|
||||
? await ctx.context.adapter.findOne<Subscription>({
|
||||
model: "subscription",
|
||||
where: [{ field: "referenceId", value: referenceId }],
|
||||
where: [{field: "referenceId", value: referenceId}],
|
||||
})
|
||||
: null;
|
||||
|
||||
@@ -443,7 +438,7 @@ export const stripe = <O extends StripeOptions>(options: O) => {
|
||||
});
|
||||
}
|
||||
|
||||
const { url } = await client.billingPortal.sessions
|
||||
const {url} = await client.billingPortal.sessions
|
||||
.create({
|
||||
customer: customerId,
|
||||
return_url: getUrl(ctx, ctx.body.returnUrl || "/"),
|
||||
@@ -543,7 +538,7 @@ export const stripe = <O extends StripeOptions>(options: O) => {
|
||||
|
||||
const freeTrial =
|
||||
!hasEverTrialed && plan.freeTrial
|
||||
? { trial_period_days: plan.freeTrial.days }
|
||||
? {trial_period_days: plan.freeTrial.days}
|
||||
: undefined;
|
||||
|
||||
let priceIdToUse: string | undefined = undefined;
|
||||
@@ -637,8 +632,8 @@ export const stripe = <O extends StripeOptions>(options: O) => {
|
||||
if (!session) {
|
||||
throw ctx.redirect(getUrl(ctx, ctx.query?.callbackURL || "/"));
|
||||
}
|
||||
const { user } = session;
|
||||
const { callbackURL, subscriptionId } = ctx.query;
|
||||
const {user} = session;
|
||||
const {callbackURL, subscriptionId} = ctx.query;
|
||||
|
||||
if (user?.stripeCustomerId) {
|
||||
try {
|
||||
@@ -759,7 +754,7 @@ export const stripe = <O extends StripeOptions>(options: O) => {
|
||||
: await ctx.context.adapter
|
||||
.findMany<Subscription>({
|
||||
model: "subscription",
|
||||
where: [{ field: "referenceId", value: referenceId }],
|
||||
where: [{field: "referenceId", value: referenceId}],
|
||||
})
|
||||
.then((subs) =>
|
||||
subs.find(
|
||||
@@ -807,7 +802,7 @@ export const stripe = <O extends StripeOptions>(options: O) => {
|
||||
message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
const { url } = await client.billingPortal.sessions
|
||||
const {url} = await client.billingPortal.sessions
|
||||
.create({
|
||||
customer: subscription.stripeCustomerId,
|
||||
return_url: getUrl(
|
||||
@@ -1058,8 +1053,8 @@ export const stripe = <O extends StripeOptions>(options: O) => {
|
||||
if (!session) {
|
||||
throw ctx.redirect(getUrl(ctx, ctx.query?.callbackURL || "/"));
|
||||
}
|
||||
const { user } = session;
|
||||
const { callbackURL, subscriptionId } = ctx.query;
|
||||
const {user} = session;
|
||||
const {callbackURL, subscriptionId} = ctx.query;
|
||||
|
||||
const subscription = await ctx.context.adapter.findOne<Subscription>({
|
||||
model: "subscription",
|
||||
@@ -1163,7 +1158,7 @@ export const stripe = <O extends StripeOptions>(options: O) => {
|
||||
],
|
||||
},
|
||||
async (ctx) => {
|
||||
const { user } = ctx.context.session;
|
||||
const {user} = ctx.context.session;
|
||||
const referenceId = ctx.body.referenceId || user.id;
|
||||
|
||||
let customerId = user.stripeCustomerId;
|
||||
@@ -1195,7 +1190,7 @@ export const stripe = <O extends StripeOptions>(options: O) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const { url } = await client.billingPortal.sessions.create({
|
||||
const {url} = await client.billingPortal.sessions.create({
|
||||
locale: ctx.body.locale,
|
||||
customer: customerId,
|
||||
return_url: getUrl(ctx, ctx.body.returnUrl),
|
||||
@@ -1221,7 +1216,6 @@ export const stripe = <O extends StripeOptions>(options: O) => {
|
||||
{
|
||||
method: "POST",
|
||||
body: z.object({
|
||||
meter_id: z.string(),
|
||||
event_name: z.string(),
|
||||
payload: z.object({
|
||||
value: z.string(),
|
||||
@@ -1235,7 +1229,7 @@ export const stripe = <O extends StripeOptions>(options: O) => {
|
||||
]
|
||||
},
|
||||
async (ctx) => {
|
||||
const { user } = ctx.context.session;
|
||||
const {user} = ctx.context.session;
|
||||
let customerId = user.stripeCustomerId;
|
||||
const referenceId = ctx.body.referenceId || user.id;
|
||||
|
||||
@@ -1265,17 +1259,17 @@ export const stripe = <O extends StripeOptions>(options: O) => {
|
||||
});
|
||||
}
|
||||
|
||||
const submitPayload = {
|
||||
try {
|
||||
const bodyPayload = {
|
||||
event_name: ctx.body.event_name,
|
||||
payload: {
|
||||
value: ctx.body.payload.value,
|
||||
stripe_customer_id: customerId,
|
||||
},
|
||||
identifier: ctx.body.identifier
|
||||
}
|
||||
|
||||
try {
|
||||
const {identifier, payload, timestamp} = await client.billing.meterEvents.create({
|
||||
event_name: ctx.body.event_name,
|
||||
payload: submitPayload,
|
||||
identifier: ctx.body.identifier
|
||||
});
|
||||
const {identifier, payload, timestamp} = await client.v2.billing.meterEvents.create(bodyPayload);
|
||||
|
||||
return ctx.json({
|
||||
identifier,
|
||||
@@ -1293,6 +1287,122 @@ export const stripe = <O extends StripeOptions>(options: O) => {
|
||||
}
|
||||
}
|
||||
),
|
||||
adjustBillingMeterEvent: createAuthEndpoint(
|
||||
"/subscription/meter-event/adjust", // https://docs.stripe.com/api/v2/billing-meter-adjustment
|
||||
{
|
||||
method: "POST",
|
||||
body: z.object({
|
||||
event_name: z.string(),
|
||||
type: z.enum(["cancel"]),
|
||||
cancel: z.object({
|
||||
identifier: z.string(),
|
||||
}),
|
||||
}),
|
||||
use: [sessionMiddleware, referenceMiddleware("adjust-meter-event")],
|
||||
},
|
||||
async (ctx) => {
|
||||
const bodyPayload = ctx.body
|
||||
try {
|
||||
const result = await client.v2.billing.meterEventAdjustments.create(bodyPayload);
|
||||
|
||||
return ctx.json(result);
|
||||
} catch (error: any) {
|
||||
ctx.context.logger.error(
|
||||
"Error submitting meter event",
|
||||
error,
|
||||
);
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
),
|
||||
summaryBillingMeterEvent: createAuthEndpoint(
|
||||
"/subscription/meter-event/summary", // https://docs.stripe.com/api/billing/meter-event-summary
|
||||
{
|
||||
method: "GET",
|
||||
query: z.object({
|
||||
id: z.string().meta({
|
||||
description: "The id of the meter event, ie: 'mtr_test_61Q8nQMqIFK9fRQmr41CMAXJrFdZ5MnA'"
|
||||
}),
|
||||
start_time: z.number().meta({
|
||||
description: "Unix timestamp in seconds"
|
||||
}),
|
||||
end_time: z.number().meta({
|
||||
description: "Unix timestamp in seconds"
|
||||
}),
|
||||
value_grouping_window: z.enum(['day', 'hour']),
|
||||
ending_before: z.string().optional().meta({
|
||||
description: "A cursor for use in pagination"
|
||||
}),
|
||||
starting_after: z.string().optional().meta({
|
||||
description: "A cursor for use in pagination"
|
||||
}),
|
||||
limit: z.number().optional().meta({
|
||||
description: "A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 10."
|
||||
})
|
||||
}),
|
||||
use: [
|
||||
sessionMiddleware,
|
||||
referenceMiddleware("meter-event-summary"),
|
||||
]
|
||||
},
|
||||
async (ctx) => {
|
||||
const {user} = ctx.context.session;
|
||||
let customerId = user.stripeCustomerId;
|
||||
const referenceId = ctx.body.referenceId || user.id;
|
||||
|
||||
if (!customerId) {
|
||||
const subscription = await ctx.context.adapter
|
||||
.findMany<Subscription>({
|
||||
model: "subscription",
|
||||
where: [
|
||||
{
|
||||
field: "referenceId",
|
||||
value: referenceId,
|
||||
},
|
||||
],
|
||||
})
|
||||
.then((subs) =>
|
||||
subs.find(
|
||||
(sub) => sub.status === "active" || sub.status === "trialing",
|
||||
),
|
||||
);
|
||||
|
||||
customerId = subscription?.stripeCustomerId;
|
||||
}
|
||||
|
||||
if (!customerId) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "No Stripe customer found for this user",
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await client.billing.meters.listEventSummaries(
|
||||
ctx.query.id,
|
||||
{
|
||||
customer: customerId,
|
||||
start_time: ctx.query.start_time,
|
||||
end_time: ctx.query.end_time,
|
||||
limit: ctx.query.limit,
|
||||
starting_after: ctx.query.starting_after,
|
||||
ending_before: ctx.query.ending_before,
|
||||
}
|
||||
);
|
||||
|
||||
return ctx.json(result);
|
||||
} catch (error: any) {
|
||||
ctx.context.logger.error(
|
||||
"Error submitting meter event",
|
||||
error,
|
||||
);
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
)
|
||||
} as const;
|
||||
return {
|
||||
id: "stripe",
|
||||
@@ -1364,7 +1474,7 @@ export const stripe = <O extends StripeOptions>(options: O) => {
|
||||
message: "Webhook error: See server logs for more information.",
|
||||
});
|
||||
}
|
||||
return ctx.json({ success: true });
|
||||
return ctx.json({success: true});
|
||||
},
|
||||
),
|
||||
...((options.subscription?.enabled
|
||||
@@ -1479,4 +1589,4 @@ export const stripe = <O extends StripeOptions>(options: O) => {
|
||||
} satisfies BetterAuthPlugin;
|
||||
};
|
||||
|
||||
export type { Subscription, StripePlan };
|
||||
export type {Subscription, StripePlan};
|
||||
|
||||
23
src/types.ts
23
src/types.ts
@@ -5,7 +5,7 @@ import type {
|
||||
User,
|
||||
} from "better-auth";
|
||||
import type Stripe from "stripe";
|
||||
import type { subscriptions, user } from "./schema";
|
||||
import type {subscriptions, user} from "./schema";
|
||||
|
||||
export type StripePlan = {
|
||||
/**
|
||||
@@ -90,6 +90,16 @@ export type StripePlan = {
|
||||
};
|
||||
};
|
||||
|
||||
export type actionType =
|
||||
| "upgrade-subscription"
|
||||
| "list-subscription"
|
||||
| "cancel-subscription"
|
||||
| "restore-subscription"
|
||||
| "meter-event"
|
||||
| "adjust-meter-event"
|
||||
| "meter-event-summary"
|
||||
| "billing-portal";
|
||||
|
||||
export interface Subscription {
|
||||
/**
|
||||
* Database identifier
|
||||
@@ -267,13 +277,7 @@ export interface StripeOptions {
|
||||
user: User & Record<string, any>;
|
||||
session: Session & Record<string, any>;
|
||||
referenceId: string;
|
||||
action:
|
||||
| "upgrade-subscription"
|
||||
| "list-subscription"
|
||||
| "cancel-subscription"
|
||||
| "restore-subscription"
|
||||
| "meter-event"
|
||||
| "billing-portal";
|
||||
action: actionType;
|
||||
},
|
||||
ctx: GenericEndpointContext,
|
||||
) => Promise<boolean>;
|
||||
@@ -328,4 +332,5 @@ export interface StripeOptions {
|
||||
schema?: InferOptionSchema<typeof subscriptions & typeof user>;
|
||||
}
|
||||
|
||||
export interface InputSubscription extends Omit<Subscription, "id"> {}
|
||||
export interface InputSubscription extends Omit<Subscription, "id"> {
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { StripeOptions } from "./types";
|
||||
import type {StripeOptions} from "./types";
|
||||
|
||||
export async function getPlans(options: StripeOptions) {
|
||||
return typeof options?.subscription?.plans === "function"
|
||||
|
||||
Reference in New Issue
Block a user