chore: update credits related functions (2)

This commit is contained in:
javayhu 2025-05-28 00:54:27 +08:00
parent 443f01769c
commit 4374f118b4
4 changed files with 70 additions and 53 deletions

View File

@ -1,10 +1,6 @@
import {
CREDIT_TRANSACTION_DESCRIPTION,
CREDIT_TRANSACTION_TYPE,
} from '@/lib/constants';
import {
addCredits,
addMonthlyFreeCredits,
addRegisterGiftCredits,
consumeCredits,
getUserCredits,
} from '@/lib/credits';
@ -22,6 +18,22 @@ export const getCreditsAction = actionClient.action(async () => {
return { success: true, credits };
});
// add register gift credits (for testing)
export const addRegisterCreditsAction = actionClient.action(async () => {
const session = await getSession();
if (!session) return { success: false, error: 'Unauthorized' };
await addRegisterGiftCredits(session.user.id);
return { success: true };
});
// add monthly free credits (for testing)
export const addMonthlyCreditsAction = actionClient.action(async () => {
const session = await getSession();
if (!session) return { success: false, error: 'Unauthorized' };
await addMonthlyFreeCredits(session.user.id);
return { success: true };
});
// consume credits (simulate button)
const consumeSchema = z.object({
amount: z.number().min(1),
@ -38,31 +50,10 @@ export const consumeCreditsAction = actionClient
userId: session.user.id,
amount: parsedInput.amount,
description:
parsedInput.description || CREDIT_TRANSACTION_DESCRIPTION.USAGE,
parsedInput.description || `Consume credits: ${parsedInput.amount}`,
});
return { success: true };
} catch (e) {
return { success: false, error: (e as Error).message };
}
});
// add register credits (for testing)
export const addRegisterCreditsAction = actionClient.action(async () => {
const session = await getSession();
if (!session) return { success: false, error: 'Unauthorized' };
await addCredits({
userId: session.user.id,
amount: 100,
type: CREDIT_TRANSACTION_TYPE.REGISTER_GIFT,
description: CREDIT_TRANSACTION_DESCRIPTION.REGISTER_GIFT,
});
return { success: true };
});
// add monthly free credits (for testing)
export const addMonthlyCreditsAction = actionClient.action(async () => {
const session = await getSession();
if (!session) return { success: false, error: 'Unauthorized' };
await addMonthlyFreeCredits(session.user.id);
return { success: true };
});

View File

@ -85,7 +85,7 @@ 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_GIFT, MONTHLY_REFRESH, PURCHASE, USAGE, EXPIRE
description: text("description"), // description, e.g. REGISTER_GIFT, MONTHLY_REFRESH, USAGE, EXPIRE
description: text("description"), // description, e.g. "Register gift credits: 100"
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

View File

@ -3,14 +3,17 @@ export const PLACEHOLDER_IMAGE =
// credit package definition (example)
export const CREDIT_PACKAGES = [
{ id: 'package-1', credits: 500, price: 5 },
{ id: 'package-2', credits: 1200, price: 10 },
{ id: 'package-3', credits: 3000, price: 20 },
{ id: 'package-1', credits: 1000, price: 10 },
{ id: 'package-2', credits: 2500, price: 20 },
{ id: 'package-3', credits: 5000, price: 30 },
];
// free monthly credits (10% of the smallest package)
export const FREE_MONTHLY_CREDITS = 50;
// register gift credits (for new user registration)
export const REGISTER_GIFT_CREDITS = 100;
// default credit expiration days
export const CREDIT_EXPIRE_DAYS = 30;
@ -22,12 +25,3 @@ export const CREDIT_TRANSACTION_TYPE = {
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
};

View File

@ -4,9 +4,9 @@ import { addDays, isAfter } from 'date-fns';
import { and, asc, eq, or } from 'drizzle-orm';
import {
CREDIT_EXPIRE_DAYS,
CREDIT_TRANSACTION_DESCRIPTION,
CREDIT_TRANSACTION_TYPE,
FREE_MONTHLY_CREDITS,
REGISTER_GIFT_CREDITS,
} from './constants';
// Get user's current credit balance
@ -60,7 +60,7 @@ export async function addCredits({
}) {
// Process expired credits first
await processExpiredCredits(userId);
// Update balance
// Update user credit balance
const current = await db
.select()
.from(userCredit)
@ -72,7 +72,11 @@ export async function addCredits({
if (current.length > 0) {
await db
.update(userCredit)
.set({ balance: newBalance, updatedAt: new Date() })
.set({
balance: newBalance,
lastRefresh: new Date(),
updatedAt: new Date(),
})
.where(eq(userCredit.userId, userId));
} else {
await db.insert(userCredit).values({
@ -84,7 +88,7 @@ export async function addCredits({
updatedAt: new Date(),
});
}
// Write transaction record
// Write credit transaction record
await logCreditTransaction({
userId,
type,
@ -230,12 +234,36 @@ export async function processExpiredCredits(userId: string) {
userId,
type: CREDIT_TRANSACTION_TYPE.EXPIRE,
amount: -expiredTotal,
description: CREDIT_TRANSACTION_DESCRIPTION.EXPIRE,
description: `Expire credits: ${expiredTotal}`,
});
}
}
// Add free monthly credits (can be called by scheduler)
// Add register gift credits
export async function addRegisterGiftCredits(userId: string) {
// Check if user has already received register gift credits
const record = await db
.select()
.from(creditTransaction)
.where(
and(
eq(creditTransaction.userId, userId),
eq(creditTransaction.type, CREDIT_TRANSACTION_TYPE.REGISTER_GIFT)
)
)
.limit(1);
// add register gift credits if user has not received them yet
if (record.length === 0) {
await addCredits({
userId,
amount: REGISTER_GIFT_CREDITS,
type: CREDIT_TRANSACTION_TYPE.REGISTER_GIFT,
description: `Register gift credits: ${REGISTER_GIFT_CREDITS}`,
});
}
}
// Add free monthly credits
export async function addMonthlyFreeCredits(userId: string) {
// Check last refresh time
const record = await db
@ -245,23 +273,27 @@ export async function addMonthlyFreeCredits(userId: string) {
.limit(1);
const now = new Date();
let canAdd = false;
if (!record[0]?.lastRefresh) canAdd = true;
else {
// never added credits before
if (!record[0]?.lastRefresh) {
canAdd = true;
} else {
const last = new Date(record[0].lastRefresh);
canAdd =
now.getMonth() !== last.getMonth() ||
now.getFullYear() !== last.getFullYear();
}
// add credits if it's a new month
if (canAdd) {
await addCredits({
userId,
amount: FREE_MONTHLY_CREDITS,
type: CREDIT_TRANSACTION_TYPE.MONTHLY_REFRESH,
description: CREDIT_TRANSACTION_DESCRIPTION.MONTHLY_REFRESH,
description: `Free monthly credits: ${FREE_MONTHLY_CREDITS} for ${now.getFullYear()}-${now.getMonth() + 1}`,
});
await db
.update(userCredit)
.set({ lastRefresh: now, updatedAt: now })
.where(eq(userCredit.userId, userId));
// update last refresh time ? addCredits has already updated it
// await db
// .update(userCredit)
// .set({ lastRefresh: now, updatedAt: now })
// .where(eq(userCredit.userId, userId));
}
}