refactor: improve credit transaction filtering in processExpiredCredits and consumeCredits functions

This commit is contained in:
javayhu 2025-07-10 22:53:04 +08:00
parent 59c7c807db
commit 6cf9d4db9c

View File

@ -4,7 +4,7 @@ import { getDb } from '@/db';
import { creditTransaction, payment, user, userCredit } from '@/db/schema'; import { creditTransaction, payment, user, userCredit } from '@/db/schema';
import { findPlanByPriceId } from '@/lib/price-plan'; import { findPlanByPriceId } from '@/lib/price-plan';
import { addDays, isAfter } from 'date-fns'; import { addDays, isAfter } from 'date-fns';
import { and, asc, desc, eq, or } from 'drizzle-orm'; import { and, asc, desc, eq, gt, isNull, not, or } from 'drizzle-orm';
import { CREDIT_TRANSACTION_TYPE } from './types'; import { CREDIT_TRANSACTION_TYPE } from './types';
/** /**
@ -143,7 +143,7 @@ export async function addCredits({
// const newBalance = (current[0]?.currentCredits || 0) + amount; // const newBalance = (current[0]?.currentCredits || 0) + amount;
if (current.length > 0) { if (current.length > 0) {
const newBalance = (current[0]?.currentCredits || 0) + amount; const newBalance = (current[0]?.currentCredits || 0) + amount;
console.log('update user credit', userId, newBalance); console.log('addCredits, update user credit', userId, newBalance);
await db await db
.update(userCredit) .update(userCredit)
.set({ .set({
@ -154,7 +154,7 @@ export async function addCredits({
.where(eq(userCredit.userId, userId)); .where(eq(userCredit.userId, userId));
} else { } else {
const newBalance = amount; const newBalance = amount;
console.log('insert user credit', userId, newBalance); console.log('addCredits, insert user credit', userId, newBalance);
await db.insert(userCredit).values({ await db.insert(userCredit).values({
id: randomUUID(), id: randomUUID(),
userId, userId,
@ -171,11 +171,7 @@ export async function addCredits({
amount, amount,
description, description,
paymentId, paymentId,
// NOTE: there is no expiration date for PURCHASE type expirationDate: expireDays ? addDays(new Date(), expireDays) : undefined,
expirationDate:
type === CREDIT_TRANSACTION_TYPE.PURCHASE || expireDays === undefined
? undefined
: addDays(new Date(), expireDays),
}); });
} }
@ -216,22 +212,28 @@ export async function consumeCredits({
// Check balance // Check balance
if (!(await hasEnoughCredits({ userId, requiredCredits: amount }))) { if (!(await hasEnoughCredits({ userId, requiredCredits: amount }))) {
console.error( console.error(
`Insufficient credits for user ${userId}, required: ${amount}` `consumeCredits, insufficient credits for user ${userId}, required: ${amount}`
); );
throw new Error('Insufficient credits'); throw new Error('Insufficient credits');
} }
// FIFO consumption: consume from the earliest unexpired credits first // FIFO consumption: consume from the earliest unexpired credits first
const db = await getDb(); const db = await getDb();
const now = new Date();
const transactions = await db const transactions = await db
.select() .select()
.from(creditTransaction) .from(creditTransaction)
.where( .where(
and( and(
eq(creditTransaction.userId, userId), eq(creditTransaction.userId, userId),
// Exclude usage and expire records (these are consumption/expiration logs)
not(eq(creditTransaction.type, CREDIT_TRANSACTION_TYPE.USAGE)),
not(eq(creditTransaction.type, CREDIT_TRANSACTION_TYPE.EXPIRE)),
// Only include transactions with remaining amount > 0
gt(creditTransaction.remainingAmount, 0),
// Only include unexpired credits (either no expiration date or not yet expired)
or( or(
eq(creditTransaction.type, CREDIT_TRANSACTION_TYPE.PURCHASE), isNull(creditTransaction.expirationDate),
eq(creditTransaction.type, CREDIT_TRANSACTION_TYPE.MONTHLY_REFRESH), gt(creditTransaction.expirationDate, now)
eq(creditTransaction.type, CREDIT_TRANSACTION_TYPE.REGISTER_GIFT)
) )
) )
) )
@ -282,7 +284,7 @@ export async function consumeCredits({
*/ */
export async function processExpiredCredits(userId: string) { export async function processExpiredCredits(userId: string) {
const now = new Date(); const now = new Date();
// Get all credit transactions without type EXPIRE // Get all credit transactions that can expire (have expirationDate and not yet processed)
const db = await getDb(); const db = await getDb();
const transactions = await db const transactions = await db
.select() .select()
@ -290,12 +292,15 @@ export async function processExpiredCredits(userId: string) {
.where( .where(
and( and(
eq(creditTransaction.userId, userId), eq(creditTransaction.userId, userId),
or( // Exclude usage and expire records (these are consumption/expiration logs)
// NOTE: credits with PURCHASE type can not be expired not(eq(creditTransaction.type, CREDIT_TRANSACTION_TYPE.USAGE)),
// eq(creditTransaction.type, CREDIT_TRANSACTION_TYPE.PURCHASE), not(eq(creditTransaction.type, CREDIT_TRANSACTION_TYPE.EXPIRE)),
eq(creditTransaction.type, CREDIT_TRANSACTION_TYPE.MONTHLY_REFRESH), // Only include transactions with expirationDate set
eq(creditTransaction.type, CREDIT_TRANSACTION_TYPE.REGISTER_GIFT) not(isNull(creditTransaction.expirationDate)),
) // Only include transactions not yet processed for expiration
isNull(creditTransaction.expirationDateProcessedAt),
// Only include transactions with remaining amount > 0
gt(creditTransaction.remainingAmount, 0)
) )
); );
let expiredTotal = 0; let expiredTotal = 0;