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:
parent
3e0861f883
commit
04f7f891a4
@ -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,
|
||||||
|
@ -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
|
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
3
src/types/index.d.ts
vendored
3
src/types/index.d.ts
vendored
@ -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
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user