From 04f7f891a444b48057eb68875c15bbce0a6d61da Mon Sep 17 00:00:00 2001 From: javayhu Date: Thu, 10 Jul 2025 01:12:11 +0800 Subject: [PATCH] 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. --- src/config/website.tsx | 7 +- src/credits/README.md | 173 ----------------------------------------- src/credits/credits.ts | 13 +++- src/credits/types.ts | 1 + src/types/index.d.ts | 3 +- 5 files changed, 19 insertions(+), 178 deletions(-) delete mode 100644 src/credits/README.md diff --git a/src/config/website.tsx b/src/config/website.tsx index 7504a7f..9ac824a 100644 --- a/src/config/website.tsx +++ b/src/config/website.tsx @@ -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, diff --git a/src/credits/README.md b/src/credits/README.md deleted file mode 100644 index aa9f2db..0000000 --- a/src/credits/README.md +++ /dev/null @@ -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 diff --git a/src/credits/credits.ts b/src/credits/credits.ts index c167f5a..4f16e47 100644 --- a/src/credits/credits.ts +++ b/src/credits/credits.ts @@ -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, }); } } diff --git a/src/credits/types.ts b/src/credits/types.ts index 9618a01..0b2fab4 100644 --- a/src/credits/types.ts +++ b/src/credits/types.ts @@ -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 } diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 8f88b35..e8cf263 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -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; // Packages indexed by ID }