feat: update credit expiration handling and configuration

- Added expireDays property to credit packages and related configurations in website.tsx for better management of credit expiration.
- Modified addCredits function to handle expireDays more flexibly, allowing for undefined values.
- Updated functions for adding register gift and monthly free credits to utilize the new expireDays configuration.
- Enhanced type definitions for credits to include optional expireDays for improved clarity.
- Removed obsolete creditExpireDays from the credits configuration to streamline the codebase.
This commit is contained in:
javayhu 2025-07-10 01:12:11 +08:00
parent 3e0861f883
commit 04f7f891a4
5 changed files with 19 additions and 178 deletions

View File

@ -131,20 +131,22 @@ export const websiteConfig: WebsiteConfig = {
}, },
credits: { credits: {
enableCredits: true, enableCredits: true,
creditExpireDays: 30,
registerGiftCredits: { registerGiftCredits: {
enable: true, enable: true,
credits: 100, credits: 100,
expireDays: 30,
}, },
freeMonthlyCredits: { freeMonthlyCredits: {
enable: true, enable: true,
credits: 50, credits: 50,
expireDays: 30,
}, },
packages: { packages: {
basic: { basic: {
id: 'basic', id: 'basic',
popular: false, popular: false,
credits: 100, credits: 100,
expireDays: 30,
price: { price: {
priceId: process.env.NEXT_PUBLIC_STRIPE_PRICE_CREDITS_BASIC!, priceId: process.env.NEXT_PUBLIC_STRIPE_PRICE_CREDITS_BASIC!,
amount: 990, amount: 990,
@ -156,6 +158,7 @@ export const websiteConfig: WebsiteConfig = {
id: 'standard', id: 'standard',
popular: true, popular: true,
credits: 200, credits: 200,
expireDays: 60,
price: { price: {
priceId: process.env.NEXT_PUBLIC_STRIPE_PRICE_CREDITS_STANDARD!, priceId: process.env.NEXT_PUBLIC_STRIPE_PRICE_CREDITS_STANDARD!,
amount: 1490, amount: 1490,
@ -167,6 +170,7 @@ export const websiteConfig: WebsiteConfig = {
id: 'premium', id: 'premium',
popular: false, popular: false,
credits: 500, credits: 500,
expireDays: 90,
price: { price: {
priceId: process.env.NEXT_PUBLIC_STRIPE_PRICE_CREDITS_PREMIUM!, priceId: process.env.NEXT_PUBLIC_STRIPE_PRICE_CREDITS_PREMIUM!,
amount: 3990, amount: 3990,
@ -178,6 +182,7 @@ export const websiteConfig: WebsiteConfig = {
id: 'enterprise', id: 'enterprise',
popular: false, popular: false,
credits: 1000, credits: 1000,
expireDays: 180,
price: { price: {
priceId: process.env.NEXT_PUBLIC_STRIPE_PRICE_CREDITS_ENTERPRISE!, priceId: process.env.NEXT_PUBLIC_STRIPE_PRICE_CREDITS_ENTERPRISE!,
amount: 6990, amount: 6990,

View File

@ -1,173 +0,0 @@
# Credit Management System Implementation
## Overview
This document describes the credit management system implementation for the mksaas-template project, which allows users to purchase credits using Stripe payments.
## Features Implemented
### 1. Credit Packages
- Defined credit packages with different tiers (Basic, Standard, Premium, Enterprise)
- Each package includes credits amount, price, and description
- Popular package highlighting
### 2. Payment Integration
- Stripe PaymentIntent integration for credit purchases
- Secure payment processing with webhook verification
- Automatic credit addition upon successful payment
### 3. UI Components
- Credit balance display with refresh functionality
- Credit packages selection interface
- Stripe payment form integration
- Modern, responsive design
### 4. Database Integration
- Credit transaction recording
- User credit balance management
- Proper error handling and validation
## Files Created/Modified
### Core Components
- `src/components/dashboard/credit-packages.tsx` - Main credit packages interface
- `src/components/dashboard/credit-balance.tsx` - Credit balance display
- `src/components/dashboard/stripe-payment-form.tsx` - Stripe payment integration
### Actions & API
- `src/actions/credits.action.ts` - Credit-related server actions
- `src/app/api/webhooks/stripe/route.ts` - Stripe webhook handler
- `src/payment/index.ts` - Payment provider interface (updated)
- `src/payment/types.ts` - Payment types (updated)
### Configuration
- `src/config/website.tsx` - Credit packages configuration
- `env.example` - Environment variables template
### Pages
- `src/app/[locale]/(protected)/settings/credits/page.tsx` - Credits management page
## Environment Variables Required
Add these to your `.env.local` file:
```env
# Stripe Configuration
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_test_..."
STRIPE_SECRET_KEY="sk_test_..."
STRIPE_WEBHOOK_SECRET="whsec_..."
```
## Setup Instructions
### 1. Stripe Configuration
1. Create a Stripe account at https://dashboard.stripe.com
2. Get your API keys from the Stripe dashboard
3. Set up a webhook endpoint pointing to `/api/webhooks/stripe`
4. Copy the webhook secret and add it to your environment variables
### 2. Database Setup
Make sure your database schema includes the required credit tables as defined in `src/db/schema.ts`.
### 3. Environment Variables
Copy the required environment variables from `env.example` to your `.env.local` file and fill in the values.
## Usage
### For Users
1. Navigate to `/settings/credits`
2. View current credit balance
3. Select a credit package
4. Complete payment using Stripe
5. Credits are automatically added to account
### For Developers
```typescript
// Get user credits
const result = await getCreditsAction();
// Create payment intent
const paymentIntent = await createCreditPaymentIntent({
packageId: 'standard'
});
// Add credits manually
await addCredits({
userId: 'user-id',
amount: 100,
type: 'PURCHASE',
description: 'Credit purchase'
});
// Access credit packages from config
import { websiteConfig } from '@/config/website';
const creditPackages = Object.values(websiteConfig.credits.packages);
```
## Credit Packages Configuration
Edit `src/config/website.tsx` to modify available credit packages:
```typescript
export const websiteConfig: WebsiteConfig = {
// ... other config
credits: {
enableCredits: true,
packages: {
basic: {
id: 'basic',
credits: 100,
price: 990, // Price in cents
popular: false,
description: 'Perfect for getting started',
},
// ... more packages
},
},
};
```
## Webhook Events
The system handles these Stripe webhook events:
- `payment_intent.succeeded` - Adds credits to user account upon successful payment
## Security Features
1. **Webhook Verification**: All webhook requests are verified using Stripe signatures
2. **Payment Validation**: Amount and package validation before processing
3. **User Authentication**: All credit operations require authenticated users
4. **Metadata Validation**: Payment metadata is validated before processing
## Error Handling
The system includes comprehensive error handling for:
- Invalid payment attempts
- Network failures
- Database errors
- Authentication issues
- Webhook verification failures
## Testing
To test the credit purchase flow:
1. Use Stripe test cards (e.g., `4242424242424242`)
2. Monitor webhook events in Stripe dashboard
3. Check credit balance updates in the application
## Integration Notes
This implementation:
- Uses Next.js server actions for secure server-side operations
- Integrates with existing Drizzle ORM schema
- Follows the existing payment provider pattern
- Maintains consistency with the existing codebase architecture
## Future Enhancements
Potential improvements:
- Credit transaction history display
- Credit expiration management
- Bulk credit operations
- Credit usage analytics
- Subscription-based credit allocation

View File

@ -96,7 +96,7 @@ export async function addCredits({
type, type,
description, description,
paymentId, paymentId,
expireDays = websiteConfig.credits.creditExpireDays, expireDays,
}: { }: {
userId: string; userId: string;
amount: number; amount: number;
@ -113,7 +113,10 @@ export async function addCredits({
console.error('addCredits, invalid amount', userId, amount); console.error('addCredits, invalid amount', userId, amount);
throw new Error('Invalid amount'); throw new Error('Invalid amount');
} }
if (!Number.isFinite(expireDays) || expireDays <= 0) { if (
expireDays !== undefined &&
(!Number.isFinite(expireDays) || expireDays <= 0)
) {
console.error('addCredits, invalid expire days', userId, expireDays); console.error('addCredits, invalid expire days', userId, expireDays);
throw new Error('Invalid expire days'); throw new Error('Invalid expire days');
} }
@ -159,7 +162,7 @@ export async function addCredits({
paymentId, paymentId,
// NOTE: there is no expiration date for PURCHASE type // NOTE: there is no expiration date for PURCHASE type
expirationDate: expirationDate:
type === CREDIT_TRANSACTION_TYPE.PURCHASE type === CREDIT_TRANSACTION_TYPE.PURCHASE || expireDays === undefined
? undefined ? undefined
: addDays(new Date(), expireDays), : addDays(new Date(), expireDays),
}); });
@ -355,11 +358,13 @@ export async function addRegisterGiftCredits(userId: string) {
// add register gift credits if user has not received them yet // add register gift credits if user has not received them yet
if (record.length === 0) { if (record.length === 0) {
const credits = websiteConfig.credits.registerGiftCredits.credits; const credits = websiteConfig.credits.registerGiftCredits.credits;
const expireDays = websiteConfig.credits.registerGiftCredits.expireDays;
await addCredits({ await addCredits({
userId, userId,
amount: credits, amount: credits,
type: CREDIT_TRANSACTION_TYPE.REGISTER_GIFT, type: CREDIT_TRANSACTION_TYPE.REGISTER_GIFT,
description: `Register gift credits: ${credits}`, description: `Register gift credits: ${credits}`,
expireDays,
}); });
} }
} }
@ -394,11 +399,13 @@ export async function addMonthlyFreeCredits(userId: string) {
// add credits if it's a new month // add credits if it's a new month
if (canAdd) { if (canAdd) {
const credits = websiteConfig.credits.freeMonthlyCredits.credits; const credits = websiteConfig.credits.freeMonthlyCredits.credits;
const expireDays = websiteConfig.credits.freeMonthlyCredits.expireDays;
await addCredits({ await addCredits({
userId, userId,
amount: credits, amount: credits,
type: CREDIT_TRANSACTION_TYPE.MONTHLY_REFRESH, type: CREDIT_TRANSACTION_TYPE.MONTHLY_REFRESH,
description: `Free monthly credits: ${credits} for ${now.getFullYear()}-${now.getMonth() + 1}`, description: `Free monthly credits: ${credits} for ${now.getFullYear()}-${now.getMonth() + 1}`,
expireDays,
}); });
} }
} }

View File

@ -26,4 +26,5 @@ export interface CreditPackage {
popular: boolean; // Whether the package is popular popular: boolean; // Whether the package is popular
name?: string; // Display name of the package name?: string; // Display name of the package
description?: string; // Description of the package description?: string; // Description of the package
expireDays?: number; // Number of days to expire the credits, undefined means no expire
} }

View File

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