159 lines
4.5 KiB
TypeScript
159 lines
4.5 KiB
TypeScript
import { getApproximateQuotaBucket, type QuotaBucket } from "@nproxy/domain";
|
|
import type { PrismaClient, SubscriptionStatus } from "@prisma/client";
|
|
import { Prisma } from "@prisma/client";
|
|
import { prisma as defaultPrisma } from "./prisma-client.js";
|
|
import { reconcileElapsedSubscription } from "./subscription-lifecycle.js";
|
|
|
|
export interface UserAccountOverview {
|
|
user: {
|
|
id: string;
|
|
email: string;
|
|
isAdmin: boolean;
|
|
createdAt: Date;
|
|
};
|
|
subscription: {
|
|
id: string;
|
|
status: SubscriptionStatus;
|
|
renewsManually: boolean;
|
|
activatedAt?: Date;
|
|
currentPeriodStart?: Date;
|
|
currentPeriodEnd?: Date;
|
|
canceledAt?: Date;
|
|
plan: {
|
|
id: string;
|
|
code: string;
|
|
displayName: string;
|
|
monthlyPriceUsd: number;
|
|
billingCurrency: string;
|
|
isActive: boolean;
|
|
};
|
|
} | null;
|
|
quota: {
|
|
approximateBucket: QuotaBucket;
|
|
} | null;
|
|
}
|
|
|
|
export function createPrismaAccountStore(database: PrismaClient = defaultPrisma) {
|
|
return {
|
|
async getUserAccountOverview(userId: string): Promise<UserAccountOverview | null> {
|
|
const user = await database.user.findUnique({
|
|
where: {
|
|
id: userId,
|
|
},
|
|
});
|
|
|
|
if (!user) {
|
|
return null;
|
|
}
|
|
|
|
const subscription = await database.subscription.findFirst({
|
|
where: {
|
|
userId,
|
|
},
|
|
include: {
|
|
plan: true,
|
|
},
|
|
orderBy: [
|
|
{ currentPeriodEnd: "desc" },
|
|
{ createdAt: "desc" },
|
|
],
|
|
});
|
|
|
|
const currentSubscription = await reconcileElapsedSubscription(database, subscription, {
|
|
reload: async () =>
|
|
database.subscription.findFirst({
|
|
where: {
|
|
userId,
|
|
},
|
|
include: {
|
|
plan: true,
|
|
},
|
|
orderBy: [{ currentPeriodEnd: "desc" }, { createdAt: "desc" }],
|
|
}),
|
|
});
|
|
|
|
const quota = currentSubscription?.status === "active"
|
|
? await buildQuotaSnapshot(database, userId, {
|
|
monthlyRequestLimit: currentSubscription.plan.monthlyRequestLimit,
|
|
cycleStart:
|
|
currentSubscription.currentPeriodStart ??
|
|
currentSubscription.activatedAt ??
|
|
currentSubscription.createdAt,
|
|
})
|
|
: null;
|
|
|
|
return {
|
|
user: {
|
|
id: user.id,
|
|
email: user.email,
|
|
isAdmin: user.isAdmin,
|
|
createdAt: user.createdAt,
|
|
},
|
|
subscription: currentSubscription
|
|
? {
|
|
id: currentSubscription.id,
|
|
status: currentSubscription.status,
|
|
renewsManually: currentSubscription.renewsManually,
|
|
...(currentSubscription.activatedAt
|
|
? { activatedAt: currentSubscription.activatedAt }
|
|
: {}),
|
|
...(currentSubscription.currentPeriodStart
|
|
? { currentPeriodStart: currentSubscription.currentPeriodStart }
|
|
: {}),
|
|
...(currentSubscription.currentPeriodEnd
|
|
? { currentPeriodEnd: currentSubscription.currentPeriodEnd }
|
|
: {}),
|
|
...(currentSubscription.canceledAt
|
|
? { canceledAt: currentSubscription.canceledAt }
|
|
: {}),
|
|
plan: {
|
|
id: currentSubscription.plan.id,
|
|
code: currentSubscription.plan.code,
|
|
displayName: currentSubscription.plan.displayName,
|
|
monthlyPriceUsd: decimalToNumber(currentSubscription.plan.monthlyPriceUsd),
|
|
billingCurrency: currentSubscription.plan.billingCurrency,
|
|
isActive: currentSubscription.plan.isActive,
|
|
},
|
|
}
|
|
: null,
|
|
quota,
|
|
};
|
|
},
|
|
};
|
|
}
|
|
|
|
function decimalToNumber(value: Prisma.Decimal | { toNumber(): number }): number {
|
|
return value.toNumber();
|
|
}
|
|
|
|
async function buildQuotaSnapshot(
|
|
database: PrismaClient,
|
|
userId: string,
|
|
input: {
|
|
monthlyRequestLimit: number;
|
|
cycleStart: Date;
|
|
},
|
|
): Promise<UserAccountOverview["quota"]> {
|
|
const usageAggregation = await database.usageLedgerEntry.aggregate({
|
|
where: {
|
|
userId,
|
|
entryType: "generation_success",
|
|
createdAt: {
|
|
gte: input.cycleStart,
|
|
},
|
|
},
|
|
_sum: {
|
|
deltaRequests: true,
|
|
},
|
|
});
|
|
|
|
const usedSuccessfulRequests = usageAggregation._sum.deltaRequests ?? 0;
|
|
|
|
return {
|
|
approximateBucket: getApproximateQuotaBucket({
|
|
used: usedSuccessfulRequests,
|
|
limit: input.monthlyRequestLimit,
|
|
}),
|
|
};
|
|
}
|