chore: update credits related functions
This commit is contained in:
parent
e1b0e2f44c
commit
e0c0ff9518
@ -1,4 +1,7 @@
|
||||
import { CREDIT_TRANSACTION_TYPE } from '@/lib/constants';
|
||||
import {
|
||||
CREDIT_TRANSACTION_DESCRIPTION,
|
||||
CREDIT_TRANSACTION_TYPE,
|
||||
} from '@/lib/constants';
|
||||
import {
|
||||
addCredits,
|
||||
addMonthlyFreeCredits,
|
||||
@ -22,8 +25,9 @@ export const getCreditsAction = actionClient.action(async () => {
|
||||
// consume credits (simulate button)
|
||||
const consumeSchema = z.object({
|
||||
amount: z.number().min(1),
|
||||
reason: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
export const consumeCreditsAction = actionClient
|
||||
.schema(consumeSchema)
|
||||
.action(async ({ parsedInput }) => {
|
||||
@ -33,7 +37,8 @@ export const consumeCreditsAction = actionClient
|
||||
await consumeCredits({
|
||||
userId: session.user.id,
|
||||
amount: parsedInput.amount,
|
||||
reason: parsedInput.reason || 'SIMULATE_USE',
|
||||
description:
|
||||
parsedInput.description || CREDIT_TRANSACTION_DESCRIPTION.USAGE,
|
||||
});
|
||||
return { success: true };
|
||||
} catch (e) {
|
||||
@ -48,8 +53,8 @@ export const addRegisterCreditsAction = actionClient.action(async () => {
|
||||
await addCredits({
|
||||
userId: session.user.id,
|
||||
amount: 100,
|
||||
type: CREDIT_TRANSACTION_TYPE.REGISTER,
|
||||
reason: 'REGISTER',
|
||||
type: CREDIT_TRANSACTION_TYPE.REGISTER_GIFT,
|
||||
description: CREDIT_TRANSACTION_DESCRIPTION.REGISTER_GIFT,
|
||||
});
|
||||
return { success: true };
|
||||
});
|
||||
|
@ -84,12 +84,12 @@ export const userCredit = pgTable("user_credit", {
|
||||
export const creditTransaction = pgTable("credit_transaction", {
|
||||
id: text("id").primaryKey(),
|
||||
userId: text("user_id").notNull().references(() => user.id, { onDelete: 'cascade' }),
|
||||
type: text("type").notNull(), // main type, e.g. REGISTER, MONTHLY_REFRESH, PURCHASE, USAGE, EXPIRE
|
||||
reason: text("reason"), // sub reason, e.g. REGISTER, MONTHLY_REFRESH, FEATURE_USE
|
||||
type: text("type").notNull(), // main type, e.g. REGISTER_GIFT, MONTHLY_REFRESH, PURCHASE, USAGE, EXPIRE
|
||||
description: text("description"), // description, e.g. REGISTER_GIFT, MONTHLY_REFRESH, USAGE, EXPIRE
|
||||
amount: text("amount").notNull(), // positive for earn, negative for spend
|
||||
remainingAmount: text("remaining_amount"), // for FIFO consumption
|
||||
paymentId: text("payment_id"), // associated payment order, can be null, only has value when purchasing credits
|
||||
expirationDate: timestamp("expiration_date"), // when these credits expire
|
||||
expirationDate: timestamp("expiration_date"), // when these credits expire, null for no expiration
|
||||
expirationDateProcessedAt: timestamp("expiration_date_processed_at"), // when expired credits were processed
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
||||
|
@ -16,9 +16,18 @@ export const CREDIT_EXPIRE_DAYS = 30;
|
||||
|
||||
// credit transaction type
|
||||
export const CREDIT_TRANSACTION_TYPE = {
|
||||
MONTHLY_REFRESH: 'MONTHLY_REFRESH',
|
||||
REGISTER: 'REGISTER',
|
||||
PURCHASE: 'PURCHASE',
|
||||
USAGE: 'USAGE',
|
||||
EXPIRE: 'EXPIRE',
|
||||
MONTHLY_REFRESH: 'MONTHLY_REFRESH', // credits earned by monthly refresh
|
||||
REGISTER_GIFT: 'REGISTER_GIFT', // credits earned by register gift
|
||||
PURCHASE: 'PURCHASE', // credits earned by purchase
|
||||
USAGE: 'USAGE', // credits spent by usage
|
||||
EXPIRE: 'EXPIRE', // credits expired
|
||||
};
|
||||
|
||||
// credit transaction description
|
||||
export const CREDIT_TRANSACTION_DESCRIPTION = {
|
||||
MONTHLY_REFRESH: 'MONTHLY_REFRESH', // credits earned by monthly refresh
|
||||
REGISTER_GIFT: 'REGISTER_GIFT', // credits earned by register gift
|
||||
PURCHASE: 'PURCHASE', // credits earned by purchase
|
||||
USAGE: 'USAGE', // credits spent by usage
|
||||
EXPIRE: 'EXPIRE', // credits expired
|
||||
};
|
||||
|
@ -1,9 +1,10 @@
|
||||
import db from '@/db';
|
||||
import { creditTransaction, payment, userCredit } from '@/db/schema';
|
||||
import { creditTransaction, userCredit } from '@/db/schema';
|
||||
import { addDays, isAfter } from 'date-fns';
|
||||
import { and, asc, eq } from 'drizzle-orm';
|
||||
import { and, asc, eq, or } from 'drizzle-orm';
|
||||
import {
|
||||
CREDIT_EXPIRE_DAYS,
|
||||
CREDIT_TRANSACTION_DESCRIPTION,
|
||||
CREDIT_TRANSACTION_TYPE,
|
||||
FREE_MONTHLY_CREDITS,
|
||||
} from './constants';
|
||||
@ -23,7 +24,7 @@ async function logCreditTransaction(params: {
|
||||
userId: string;
|
||||
type: string;
|
||||
amount: number;
|
||||
reason: string;
|
||||
description: string;
|
||||
paymentId?: string;
|
||||
expirationDate?: Date;
|
||||
}) {
|
||||
@ -33,7 +34,7 @@ async function logCreditTransaction(params: {
|
||||
type: params.type,
|
||||
amount: params.amount.toString(),
|
||||
remainingAmount: params.amount > 0 ? params.amount.toString() : undefined,
|
||||
reason: params.reason,
|
||||
description: params.description,
|
||||
paymentId: params.paymentId,
|
||||
expirationDate: params.expirationDate,
|
||||
createdAt: new Date(),
|
||||
@ -46,14 +47,14 @@ export async function addCredits({
|
||||
userId,
|
||||
amount,
|
||||
type,
|
||||
reason,
|
||||
description,
|
||||
paymentId,
|
||||
expireDays = CREDIT_EXPIRE_DAYS,
|
||||
}: {
|
||||
userId: string;
|
||||
amount: number;
|
||||
type: string;
|
||||
reason: string;
|
||||
description: string;
|
||||
paymentId?: string;
|
||||
expireDays?: number;
|
||||
}) {
|
||||
@ -88,7 +89,7 @@ export async function addCredits({
|
||||
userId,
|
||||
type,
|
||||
amount,
|
||||
reason,
|
||||
description,
|
||||
paymentId,
|
||||
expirationDate: addDays(new Date(), expireDays),
|
||||
});
|
||||
@ -100,13 +101,15 @@ export async function addCredits({
|
||||
export async function consumeCredits({
|
||||
userId,
|
||||
amount,
|
||||
reason,
|
||||
description,
|
||||
}: {
|
||||
userId: string;
|
||||
amount: number;
|
||||
reason: string;
|
||||
description: string;
|
||||
}) {
|
||||
// Process expired credits first
|
||||
await processExpiredCredits(userId);
|
||||
// Check balance
|
||||
const balance = await getUserCredits(userId);
|
||||
if (balance < amount) throw new Error('Insufficient credits');
|
||||
// FIFO consumption: consume from the earliest unexpired credits first
|
||||
@ -116,18 +119,24 @@ export async function consumeCredits({
|
||||
.where(
|
||||
and(
|
||||
eq(creditTransaction.userId, userId),
|
||||
eq(creditTransaction.type, CREDIT_TRANSACTION_TYPE.PURCHASE)
|
||||
or(
|
||||
eq(creditTransaction.type, CREDIT_TRANSACTION_TYPE.PURCHASE),
|
||||
eq(creditTransaction.type, CREDIT_TRANSACTION_TYPE.MONTHLY_REFRESH),
|
||||
eq(creditTransaction.type, CREDIT_TRANSACTION_TYPE.REGISTER_GIFT)
|
||||
)
|
||||
)
|
||||
)
|
||||
.orderBy(
|
||||
asc(creditTransaction.expirationDate),
|
||||
asc(creditTransaction.createdAt)
|
||||
);
|
||||
// Consume credits
|
||||
let left = amount;
|
||||
for (const tx of txs) {
|
||||
if (left <= 0) break;
|
||||
const remain = Number.parseInt(tx.remainingAmount || '0', 10);
|
||||
if (remain <= 0) continue;
|
||||
// credits to consume at most in this transaction
|
||||
const consume = Math.min(remain, left);
|
||||
await db
|
||||
.update(creditTransaction)
|
||||
@ -156,7 +165,7 @@ export async function consumeCredits({
|
||||
userId,
|
||||
type: CREDIT_TRANSACTION_TYPE.USAGE,
|
||||
amount: -amount,
|
||||
reason,
|
||||
description,
|
||||
});
|
||||
// Refresh session if needed
|
||||
// await refreshUserSession(userId);
|
||||
@ -165,16 +174,22 @@ export async function consumeCredits({
|
||||
// Process expired credits
|
||||
export async function processExpiredCredits(userId: string) {
|
||||
const now = new Date();
|
||||
// Get all credit transactions without type EXPIRE
|
||||
const txs = await db
|
||||
.select()
|
||||
.from(creditTransaction)
|
||||
.where(
|
||||
and(
|
||||
eq(creditTransaction.userId, userId),
|
||||
eq(creditTransaction.type, CREDIT_TRANSACTION_TYPE.PURCHASE)
|
||||
or(
|
||||
eq(creditTransaction.type, CREDIT_TRANSACTION_TYPE.PURCHASE),
|
||||
eq(creditTransaction.type, CREDIT_TRANSACTION_TYPE.MONTHLY_REFRESH),
|
||||
eq(creditTransaction.type, CREDIT_TRANSACTION_TYPE.REGISTER_GIFT)
|
||||
)
|
||||
)
|
||||
);
|
||||
let expiredTotal = 0;
|
||||
// Process expired credit transactions
|
||||
for (const tx of txs) {
|
||||
if (
|
||||
tx.expirationDate &&
|
||||
@ -202,7 +217,8 @@ export async function processExpiredCredits(userId: string) {
|
||||
.from(userCredit)
|
||||
.where(eq(userCredit.userId, userId))
|
||||
.limit(1);
|
||||
const newBalance = (
|
||||
const newBalance = Math.max(
|
||||
0,
|
||||
Number.parseInt(current[0]?.balance || '0', 10) - expiredTotal
|
||||
).toString();
|
||||
await db
|
||||
@ -214,7 +230,7 @@ export async function processExpiredCredits(userId: string) {
|
||||
userId,
|
||||
type: CREDIT_TRANSACTION_TYPE.EXPIRE,
|
||||
amount: -expiredTotal,
|
||||
reason: 'EXPIRE',
|
||||
description: CREDIT_TRANSACTION_DESCRIPTION.EXPIRE,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -241,7 +257,7 @@ export async function addMonthlyFreeCredits(userId: string) {
|
||||
userId,
|
||||
amount: FREE_MONTHLY_CREDITS,
|
||||
type: CREDIT_TRANSACTION_TYPE.MONTHLY_REFRESH,
|
||||
reason: 'MONTHLY_FREE',
|
||||
description: CREDIT_TRANSACTION_DESCRIPTION.MONTHLY_REFRESH,
|
||||
});
|
||||
await db
|
||||
.update(userCredit)
|
||||
|
Loading…
Reference in New Issue
Block a user