Files
nroxy/packages/db/src/account-store.ts
2026-03-10 18:00:26 +03:00

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,
}),
};
}