feat: enhance credits management with new configurations

- Added credit expiration days, register gift credits, and free monthly credits options in website configuration.
- Updated credits handling functions to utilize the new configuration settings for improved flexibility and maintainability.
- Removed obsolete constants related to credits from constants.ts to streamline the codebase.
- Enhanced type definitions for credits configuration in index.d.ts for better clarity.
This commit is contained in:
javayhu 2025-07-08 00:48:17 +08:00
parent b94fd34be5
commit 2e8f70dc76
4 changed files with 53 additions and 35 deletions

View File

@ -131,34 +131,39 @@ export const websiteConfig: WebsiteConfig = {
},
credits: {
enableCredits: true,
creditExpireDays: 30,
registerGiftCredits: {
enable: true,
credits: 100,
},
freeMonthlyCredits: {
enable: true,
credits: 50,
},
packages: {
basic: {
id: 'basic',
credits: 100,
price: 990,
popular: false,
description: 'Perfect for getting started',
},
standard: {
id: 'standard',
credits: 200,
price: 1490,
popular: true,
description: 'Most popular package',
},
premium: {
id: 'premium',
credits: 500,
price: 3990,
popular: false,
description: 'Best value for heavy users',
},
enterprise: {
id: 'enterprise',
credits: 1000,
price: 6990,
popular: false,
description: 'Tailored for enterprises',
},
},
},

View File

@ -1,13 +1,9 @@
import { randomUUID } from 'crypto';
import { websiteConfig } from '@/config/website';
import { getDb } from '@/db';
import { creditTransaction, userCredit } from '@/db/schema';
import { addDays, isAfter } from 'date-fns';
import { and, asc, eq, or } from 'drizzle-orm';
import {
CREDIT_EXPIRE_DAYS,
FREE_MONTHLY_CREDITS,
REGISTER_GIFT_CREDITS,
} from '../lib/constants';
import { CREDIT_TRANSACTION_TYPE } from './types';
/**
@ -45,7 +41,7 @@ export async function updateUserLastRefreshAt(userId: string, date: Date) {
* Write a credit transaction record
* @param params - Credit transaction parameters
*/
export async function logCreditTransaction({
export async function saveCreditTransaction({
userId,
type,
amount,
@ -61,11 +57,16 @@ export async function logCreditTransaction({
expirationDate?: Date;
}) {
if (!userId || !type || !description) {
console.error('Invalid params', userId, type, description);
console.error(
'saveCreditTransaction, invalid params',
userId,
type,
description
);
throw new Error('Invalid params');
}
if (!Number.isFinite(amount) || amount === 0) {
console.error('Invalid amount', userId, amount);
console.error('saveCreditTransaction, invalid amount', userId, amount);
throw new Error('Invalid amount');
}
const db = await getDb();
@ -95,7 +96,7 @@ export async function addCredits({
type,
description,
paymentId,
expireDays = CREDIT_EXPIRE_DAYS,
expireDays = websiteConfig.credits.creditExpireDays.days,
}: {
userId: string;
amount: number;
@ -105,15 +106,15 @@ export async function addCredits({
expireDays?: number;
}) {
if (!userId || !type || !description) {
console.error('Invalid params', userId, type, description);
console.error('addCredits, invalid params', userId, type, description);
throw new Error('Invalid params');
}
if (!Number.isFinite(amount) || amount <= 0) {
console.error('Invalid amount', userId, amount);
console.error('addCredits, invalid amount', userId, amount);
throw new Error('Invalid amount');
}
if (!Number.isFinite(expireDays) || expireDays <= 0) {
console.error('Invalid expire days', userId, expireDays);
console.error('addCredits, invalid expire days', userId, expireDays);
throw new Error('Invalid expire days');
}
// Process expired credits first
@ -150,7 +151,7 @@ export async function addCredits({
});
}
// Write credit transaction record
await logCreditTransaction({
await saveCreditTransaction({
userId,
type,
amount,
@ -189,18 +190,20 @@ export async function consumeCredits({
description: string;
}) {
if (!userId || !description) {
console.error('Invalid params', userId, description);
console.error('consumeCredits, invalid params', userId, description);
throw new Error('Invalid params');
}
if (!Number.isFinite(amount) || amount <= 0) {
console.error('Invalid amount', userId, amount);
console.error('consumeCredits, invalid amount', userId, amount);
throw new Error('Invalid amount');
}
// Process expired credits first
await processExpiredCredits(userId);
// Check balance
if (!(await hasEnoughCredits({ userId, requiredCredits: amount }))) {
console.error( `Insufficient credits for user ${userId}, required: ${amount}` );
console.error(
`Insufficient credits for user ${userId}, required: ${amount}`
);
throw new Error('Insufficient credits');
}
// FIFO consumption: consume from the earliest unexpired credits first
@ -251,7 +254,7 @@ export async function consumeCredits({
.set({ currentCredits: newBalance, updatedAt: new Date() })
.where(eq(userCredit.userId, userId));
// Write usage record
await logCreditTransaction({
await saveCreditTransaction({
userId,
type: CREDIT_TRANSACTION_TYPE.USAGE,
amount: -amount,
@ -319,7 +322,7 @@ export async function processExpiredCredits(userId: string) {
.set({ currentCredits: newBalance, updatedAt: now })
.where(eq(userCredit.userId, userId));
// Write expire record
await logCreditTransaction({
await saveCreditTransaction({
userId,
type: CREDIT_TRANSACTION_TYPE.EXPIRE,
amount: -expiredTotal,
@ -333,6 +336,10 @@ export async function processExpiredCredits(userId: string) {
* @param userId - User ID
*/
export async function addRegisterGiftCredits(userId: string) {
if (!websiteConfig.credits.registerGiftCredits.enable) {
console.log('addRegisterGiftCredits, disabled');
return;
}
// Check if user has already received register gift credits
const db = await getDb();
const record = await db
@ -347,11 +354,12 @@ export async function addRegisterGiftCredits(userId: string) {
.limit(1);
// add register gift credits if user has not received them yet
if (record.length === 0) {
const credits = websiteConfig.credits.registerGiftCredits.credits;
await addCredits({
userId,
amount: REGISTER_GIFT_CREDITS,
amount: credits,
type: CREDIT_TRANSACTION_TYPE.REGISTER_GIFT,
description: `Register gift credits: ${REGISTER_GIFT_CREDITS}`,
description: `Register gift credits: ${credits}`,
});
}
}
@ -361,6 +369,10 @@ export async function addRegisterGiftCredits(userId: string) {
* @param userId - User ID
*/
export async function addMonthlyFreeCredits(userId: string) {
if (!websiteConfig.credits.freeMonthlyCredits.enable) {
console.log('addMonthlyFreeCredits, disabled');
return;
}
// Check last refresh time
const db = await getDb();
const record = await db
@ -381,11 +393,12 @@ export async function addMonthlyFreeCredits(userId: string) {
}
// add credits if it's a new month
if (canAdd) {
const credits = websiteConfig.credits.freeMonthlyCredits.credits;
await addCredits({
userId,
amount: FREE_MONTHLY_CREDITS,
amount: credits,
type: CREDIT_TRANSACTION_TYPE.MONTHLY_REFRESH,
description: `Free monthly credits: ${FREE_MONTHLY_CREDITS} for ${now.getFullYear()}-${now.getMonth() + 1}`,
description: `Free monthly credits: ${credits} for ${now.getFullYear()}-${now.getMonth() + 1}`,
});
}
}

View File

@ -1,11 +1,2 @@
export const PLACEHOLDER_IMAGE =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAoJJREFUWEfFl4lu4zAMRO3cx/9/au6reMaOdkxTTl0grQFCRoqaT+SQotq2bV9N8rRt28xms87m83l553eZ/9vr9Wpkz+ezkT0ej+6dv1X81AFw7M4FBACPVn2c1Z3zLgDeJwHgeLFYdAARYioAEAKJEG2WAjl3gCwNYymQQ9b7/V4spmIAwO6Wy2VnAMikBWlDURBELf8CuN1uHQSrPwMAHK5WqwFELQ01AIXdAa7XawfAb3p6AOwK5+v1ugAoEq4FRSFLgavfQ49jAGQpAE5wjgGCeRrGdBArwHOPcwFcLpcGU1X0IsBuN5tNgYhaiFFwHTiAwq8I+O5xfj6fOz38K+X/fYAdb7fbAgFAjIJ6Aav3AYlQ6nfnDoDz0+lUxNiLALvf7XaDNGQ6GANQBKR85V27B4D3QQRw7hGIYlQKWGM79hSweyCUe1blXhEAogfABwHAXAcqSYkxCtHLUK3XBajSc4Dj8dilAeiSAgD2+30BAEKV4GKcAuDqB4TdYwBgPQByCgApUBoE4EJUGvxUjF3Q69/zLw3g/HA45ABKgdIQu+JPIyDnisCfAxAFNFM0EFNQ64gfS0EUoQP8ighrZSjn3oziZEQpauyKbfjbZchHUL/3AS/Dd30gAkxuRACgfO+EWQW8qwI1o+wseNuKcQiESjALvwNoMI0TcRzD4lFcPYwIM+JTF5x6HOs8yI7jeB5oKhpMRFH9UwaSCDB2Jmg4rc6E2TT0biIaG0rQhNqyhpHBcayTTSXH6vcDL7/sdqRK8LkwTsU499E8vRcAojHcZ4AxABdilgrp4lsXk8oVqgwh7+6H3phqd8J0Kk4vbx/+sZqCD/vNLya/5dT9fAH8g1WdNGgwbQAAAABJRU5ErkJggg==';
// 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;

View File

@ -155,6 +155,15 @@ export interface PriceConfig {
*/
export interface CreditsConfig {
enableCredits: boolean; // Whether to enable credits
creditExpireDays: number; // The number of days to expire the credits
registerGiftCredits: {
enable: boolean; // Whether to enable register gift credits
credits: number; // The number of credits to give to the user
};
freeMonthlyCredits: {
enable: boolean; // Whether to enable free monthly credits
credits: number; // The number of credits to give to the user
};
packages: Record<string, CreditPackage>; // Packages indexed by ID
}