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: {
enableCredits: true,
creditExpireDays: 30,
registerGiftCredits: {
enable: true,
credits: 100,
expireDays: 30,
},
freeMonthlyCredits: {
enable: true,
credits: 50,
expireDays: 30,
},
packages: {
basic: {
id: 'basic',
popular: false,
credits: 100,
expireDays: 30,
price: {
priceId: process.env.NEXT_PUBLIC_STRIPE_PRICE_CREDITS_BASIC!,
amount: 990,
@ -156,6 +158,7 @@ export const websiteConfig: WebsiteConfig = {
id: 'standard',
popular: true,
credits: 200,
expireDays: 60,
price: {
priceId: process.env.NEXT_PUBLIC_STRIPE_PRICE_CREDITS_STANDARD!,
amount: 1490,
@ -167,6 +170,7 @@ export const websiteConfig: WebsiteConfig = {
id: 'premium',
popular: false,
credits: 500,
expireDays: 90,
price: {
priceId: process.env.NEXT_PUBLIC_STRIPE_PRICE_CREDITS_PREMIUM!,
amount: 3990,
@ -178,6 +182,7 @@ export const websiteConfig: WebsiteConfig = {
id: 'enterprise',
popular: false,
credits: 1000,
expireDays: 180,
price: {
priceId: process.env.NEXT_PUBLIC_STRIPE_PRICE_CREDITS_ENTERPRISE!,
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,
description,
paymentId,
expireDays = websiteConfig.credits.creditExpireDays,
expireDays,
}: {
userId: string;
amount: number;
@ -113,7 +113,10 @@ export async function addCredits({
console.error('addCredits, invalid amount', userId, 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);
throw new Error('Invalid expire days');
}
@ -159,7 +162,7 @@ export async function addCredits({
paymentId,
// NOTE: there is no expiration date for PURCHASE type
expirationDate:
type === CREDIT_TRANSACTION_TYPE.PURCHASE
type === CREDIT_TRANSACTION_TYPE.PURCHASE || expireDays === undefined
? undefined
: 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
if (record.length === 0) {
const credits = websiteConfig.credits.registerGiftCredits.credits;
const expireDays = websiteConfig.credits.registerGiftCredits.expireDays;
await addCredits({
userId,
amount: credits,
type: CREDIT_TRANSACTION_TYPE.REGISTER_GIFT,
description: `Register gift credits: ${credits}`,
expireDays,
});
}
}
@ -394,11 +399,13 @@ export async function addMonthlyFreeCredits(userId: string) {
// add credits if it's a new month
if (canAdd) {
const credits = websiteConfig.credits.freeMonthlyCredits.credits;
const expireDays = websiteConfig.credits.freeMonthlyCredits.expireDays;
await addCredits({
userId,
amount: credits,
type: CREDIT_TRANSACTION_TYPE.MONTHLY_REFRESH,
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
name?: string; // Display name 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 {
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
expireDays?: number; // The number of days to expire the credits, undefined means no expire
};
freeMonthlyCredits: {
enable: boolean; // Whether to enable free monthly credits
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
}