feat: add daily credit distribution function and integrate with existing credits logic

This commit is contained in:
javayhu 2025-07-12 00:37:49 +08:00
parent 997c362ac9
commit 9f3c5e80c2
3 changed files with 102 additions and 49 deletions

View File

@ -1,6 +1,6 @@
import { serve } from 'inngest/next';
import { inngest } from '../../../inngest/client';
import { helloWorld } from '../../../inngest/functions';
import { distributeCreditsDaily, helloWorld } from '../../../inngest/functions';
/**
* Inngest route
@ -9,5 +9,5 @@ import { helloWorld } from '../../../inngest/functions';
*/
export const { GET, POST, PUT } = serve({
client: inngest,
functions: [helloWorld],
functions: [helloWorld, distributeCreditsDaily],
});

View File

@ -347,9 +347,52 @@ export async function processExpiredCredits(userId: string) {
amount: -expiredTotal,
description: `Expire credits: ${expiredTotal}`,
});
console.log(
`processExpiredCredits, ${expiredTotal} credits expired for user ${userId}`
);
}
}
/**
* Add subscription renewal credits
* @param userId - User ID
* @param priceId - Price ID
*/
export async function addSubscriptionRenewalCredits(
userId: string,
priceId: string
) {
const pricePlan = findPlanByPriceId(priceId);
if (
!pricePlan ||
pricePlan.isFree ||
!pricePlan.credits ||
!pricePlan.credits.enable
) {
console.log(
`addSubscriptionRenewalCredits, no credits configured for plan ${priceId}`
);
return;
}
const credits = pricePlan.credits.amount;
const expireDays = pricePlan.credits.expireDays;
const now = new Date();
await addCredits({
userId,
amount: credits,
type: CREDIT_TRANSACTION_TYPE.SUBSCRIPTION_RENEWAL,
description: `Subscription renewal credits: ${credits} for ${now.getFullYear()}-${now.getMonth() + 1}`,
expireDays,
});
console.log(
`addSubscriptionRenewalCredits, ${credits} credits for user ${userId}, priceId: ${priceId}`
);
}
/**
* Add register gift credits
* @param userId - User ID
@ -382,6 +425,10 @@ export async function addRegisterGiftCredits(userId: string) {
description: `Register gift credits: ${credits}`,
expireDays,
});
console.log(
`addRegisterGiftCredits, ${credits} credits for user ${userId}`
);
}
}
@ -389,17 +436,21 @@ export async function addRegisterGiftCredits(userId: string) {
* Add free monthly credits
* @param userId - User ID
*/
export async function addMonthlyFreeCredits(userId: string) {
export async function addMonthlyFreeCreditsIfNeed(userId: string) {
const freePlan = Object.values(websiteConfig.price.plans).find(
(plan) => plan.isFree
);
if (!freePlan) {
console.log('addMonthlyFreeCredits, no free plan found');
console.log('addMonthlyFreeCreditsIfNeed, no free plan found');
return;
}
if (freePlan.disabled || !freePlan.credits?.enable) {
if (
freePlan.disabled ||
!freePlan.credits?.enable ||
!freePlan.credits?.amount
) {
console.log(
'addMonthlyFreeCredits, plan disabled or credits disabled',
'addMonthlyFreeCreditsIfNeed, plan disabled or credits disabled',
freePlan.id
);
return;
@ -434,52 +485,18 @@ export async function addMonthlyFreeCredits(userId: string) {
description: `Free monthly credits: ${credits} for ${now.getFullYear()}-${now.getMonth() + 1}`,
expireDays,
});
}
}
/**
* Add subscription renewal credits
* @param userId - User ID
* @param priceId - Price ID
*/
export async function addSubscriptionRenewalCredits(
userId: string,
priceId: string
) {
const pricePlan = findPlanByPriceId(priceId);
if (
!pricePlan ||
pricePlan.isFree ||
!pricePlan.credits ||
!pricePlan.credits.enable
) {
console.log(
`addSubscriptionRenewalCredits, no credits configured for plan ${priceId}`
`addMonthlyFreeCreditsIfNeed, ${credits} credits for user ${userId}, date: ${now.getFullYear()}-${now.getMonth() + 1}`
);
return;
}
const credits = pricePlan.credits.amount;
const expireDays = pricePlan.credits.expireDays;
await addCredits({
userId,
amount: credits,
type: CREDIT_TRANSACTION_TYPE.SUBSCRIPTION_RENEWAL,
description: `Subscription renewal credits for ${priceId}: ${credits}`,
expireDays,
});
console.log(
`Added ${credits} subscription renewal credits for user ${userId}, priceId: ${priceId}`
);
}
/**
* Add lifetime monthly credits
* @param userId - User ID
*/
export async function addLifetimeMonthlyCredits(userId: string) {
export async function addLifetimeMonthlyCreditsIfNeed(userId: string) {
const lifetimePlan = Object.values(websiteConfig.price.plans).find(
(plan) => plan.isLifetime
);
@ -534,7 +551,9 @@ export async function addLifetimeMonthlyCredits(userId: string) {
// Update last refresh time for lifetime credits
await updateUserLastRefreshAt(userId, now);
console.log(`Added ${credits} lifetime monthly credits for user ${userId}`);
console.log(
`addLifetimeMonthlyCredits, ${credits} credits for user ${userId}, date: ${now.getFullYear()}-${now.getMonth() + 1}`
);
}
}
@ -543,7 +562,7 @@ export async function addLifetimeMonthlyCredits(userId: string) {
* This function is designed to be called by a cron job
*/
export async function distributeCreditsToAllUsers() {
console.log('Starting credit distribution to all users...');
console.log('distributing credits to all users start');
const db = await getDb();
@ -569,7 +588,7 @@ export async function distributeCreditsToAllUsers() {
.where(
and(
eq(payment.userId, userRecord.userId),
eq(payment.status, 'active')
or(eq(payment.status, 'active'), eq(payment.status, 'trialing'))
)
)
.orderBy(desc(payment.createdAt));
@ -581,18 +600,18 @@ export async function distributeCreditsToAllUsers() {
if (pricePlan?.isLifetime) {
// Lifetime user - add monthly credits
await addLifetimeMonthlyCredits(userRecord.userId);
await addLifetimeMonthlyCreditsIfNeed(userRecord.userId);
}
// Note: Subscription renewals are handled by Stripe webhooks, not here
} else {
// User has no active subscription - add free monthly credits if enabled
await addMonthlyFreeCredits(userRecord.userId);
await addMonthlyFreeCreditsIfNeed(userRecord.userId);
}
processedCount++;
} catch (error) {
console.error(
`Error processing credits for user ${userRecord.userId}:`,
`distributing credits to all users error, user: ${userRecord.userId}, error:`,
error
);
errorCount++;
@ -600,7 +619,7 @@ export async function distributeCreditsToAllUsers() {
}
console.log(
`Credit distribution completed. Processed: ${processedCount}, Errors: ${errorCount}`
`distributing credits to all users end, processed: ${processedCount}, errors: ${errorCount}`
);
return { processedCount, errorCount };
}

View File

@ -1,5 +1,39 @@
import { distributeCreditsToAllUsers } from '@/credits/credits';
import { inngest } from './client';
/**
* Distribute credits to all users daily
*
* https://www.inngest.com/docs/guides/scheduled-functions
*/
export const distributeCreditsDaily = inngest.createFunction(
{ id: 'distribute-credits-daily' },
{ cron: 'TZ=Asia/Shanghai 0 1 * * *' },
async ({ step }) => {
// You should use step.run for any async or long-running logic.
// This allows Inngest to track, retry, and visualize each step in your workflow.
await step.run('distribute-credits-to-all-users', async () => {
console.log('distributing credits to all users start');
const { processedCount, errorCount } =
await distributeCreditsToAllUsers();
console.log(
`distributing credits to all users end, processed: ${processedCount}, errors: ${errorCount}`
);
return {
message: `credits distributed, processed: ${processedCount}, errors: ${errorCount}`,
processedCount,
errorCount,
};
});
// you can add new steps here, for example, send email to admin
}
);
/**
* Hello World function, for testing inngest
*
* https://www.inngest.com/docs/guides/scheduled-functions
*/
export const helloWorld = inngest.createFunction(
{ id: 'hello-world' },
{ event: 'test/hello.world' },