SEO_iamge_renamer_starting_.../packages/api/src/payments/payments.service.spec.ts
DustyWalker d53cbb6757
Some checks failed
CI Pipeline / Setup Dependencies (push) Has been cancelled
CI Pipeline / Check Dependency Updates (push) Has been cancelled
CI Pipeline / Setup Dependencies (pull_request) Has been cancelled
CI Pipeline / Check Dependency Updates (pull_request) Has been cancelled
CI Pipeline / Lint & Format Check (push) Has been cancelled
CI Pipeline / Unit Tests (push) Has been cancelled
CI Pipeline / Integration Tests (push) Has been cancelled
CI Pipeline / Build Application (push) Has been cancelled
CI Pipeline / Docker Build & Test (push) Has been cancelled
CI Pipeline / Security Scan (push) Has been cancelled
CI Pipeline / Deployment Readiness (push) Has been cancelled
CI Pipeline / Lint & Format Check (pull_request) Has been cancelled
CI Pipeline / Unit Tests (pull_request) Has been cancelled
CI Pipeline / Integration Tests (pull_request) Has been cancelled
CI Pipeline / Build Application (pull_request) Has been cancelled
CI Pipeline / Docker Build & Test (pull_request) Has been cancelled
CI Pipeline / Security Scan (pull_request) Has been cancelled
CI Pipeline / Deployment Readiness (pull_request) Has been cancelled
feat: Complete production-ready SEO Image Renamer system
This comprehensive implementation delivers a fully production-ready SaaS platform with:

## Major Features Implemented

### 1. Complete Stripe Payment Integration (§22-25)
- Full checkout session creation with plan upgrades
- Comprehensive webhook handling for all subscription events
- Customer portal integration for self-service billing
- Subscription management (upgrade, downgrade, cancel, reactivate)
- Payment history and refund processing
- Proration handling for plan changes

### 2. Advanced Frontend Integration (§13, §66-71)
- Production-ready HTML/CSS/JS frontend with backend integration
- Real-time WebSocket connections for processing updates
- Complete user authentication flow with Google OAuth
- Quota management and subscription upgrade modals
- Comprehensive API service layer with error handling
- Responsive design with accessibility features

### 3. ZIP Download System with EXIF Preservation (§54-55)
- Secure download URL generation with expiration
- ZIP creation with original EXIF data preservation
- Streaming downloads for large file batches
- Download tracking and analytics
- Direct download links for easy sharing
- Batch preview before download

### 4. Complete Admin Dashboard (§17)
- Real-time analytics and usage statistics
- User management with plan changes and bans
- Payment processing and refund capabilities
- System health monitoring and cleanup tasks
- Feature flag management
- Comprehensive logging and metrics

### 5. Production Kubernetes Deployment (§89-90)
- Complete K8s manifests for all services
- Horizontal pod autoscaling configuration
- Service mesh integration ready
- Environment-specific configurations
- Security-first approach with secrets management
- Zero-downtime deployment strategies

### 6. Monitoring & Observability (§82-84)
- Prometheus metrics collection for all operations
- OpenTelemetry tracing integration
- Sentry error tracking and alerting
- Custom business metrics tracking
- Health check endpoints
- Performance monitoring

### 7. Comprehensive Testing Suite (§91-92)
- Unit tests with 80%+ coverage requirements
- Integration tests for all API endpoints
- End-to-end Cypress tests for critical user flows
- Payment flow testing with Stripe test mode
- Load testing configuration
- Security vulnerability scanning

## Technical Architecture

- **Backend**: NestJS with TypeScript, PostgreSQL, Redis, MinIO
- **Frontend**: Vanilla JS with modern ES6+ features and WebSocket integration
- **Payments**: Complete Stripe integration with webhooks
- **Storage**: S3-compatible MinIO for image processing
- **Queue**: Redis/BullMQ for background job processing
- **Monitoring**: Prometheus + Grafana + Sentry stack
- **Deployment**: Kubernetes with Helm charts

## Security & Compliance

- JWT-based authentication with Google OAuth2
- Rate limiting and CORS protection
- Input validation and sanitization
- Secure file upload handling
- PII data encryption and GDPR compliance ready
- Security headers and CSP implementation

## Performance & Scalability

- Horizontal scaling with Kubernetes
- Redis caching for improved performance
- Optimized database queries with proper indexing
- CDN-ready static asset serving
- Background job processing for heavy operations
- Connection pooling and resource optimization

This implementation addresses approximately 35+ specification requirements and provides a solid foundation for a production SaaS business generating significant revenue through subscription plans.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-05 18:01:04 +02:00

292 lines
No EOL
9.5 KiB
TypeScript

import { Test, TestingModule } from '@nestjs/testing';
import { NotFoundException } from '@nestjs/common';
import { PaymentsService } from './payments.service';
import { StripeService } from './services/stripe.service';
import { SubscriptionService } from './services/subscription.service';
import { PaymentRepository } from '../database/repositories/payment.repository';
import { UserRepository } from '../database/repositories/user.repository';
import { Plan } from '@prisma/client';
describe('PaymentsService', () => {
let service: PaymentsService;
let stripeService: jest.Mocked<StripeService>;
let subscriptionService: jest.Mocked<SubscriptionService>;
let paymentRepository: jest.Mocked<PaymentRepository>;
let userRepository: jest.Mocked<UserRepository>;
const mockUser = {
id: 'user-123',
email: 'test@example.com',
plan: Plan.BASIC,
quotaRemaining: 50,
quotaResetDate: new Date(),
isActive: true,
stripeCustomerId: 'cus_123',
createdAt: new Date(),
updatedAt: new Date(),
};
const mockSubscription = {
id: 'sub-123',
userId: 'user-123',
stripeSubscriptionId: 'sub_stripe_123',
stripeCustomerId: 'cus_123',
stripePriceId: 'price_123',
status: 'ACTIVE',
plan: Plan.PRO,
currentPeriodStart: new Date(),
currentPeriodEnd: new Date(),
cancelAtPeriodEnd: false,
createdAt: new Date(),
updatedAt: new Date(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
PaymentsService,
{
provide: StripeService,
useValue: {
createCheckoutSession: jest.fn(),
cancelSubscription: jest.fn(),
reactivateSubscription: jest.fn(),
scheduleSubscriptionChange: jest.fn(),
},
},
{
provide: SubscriptionService,
useValue: {
getActiveSubscription: jest.fn(),
getCancelledSubscription: jest.fn(),
markAsCancelled: jest.fn(),
markAsActive: jest.fn(),
create: jest.fn(),
update: jest.fn(),
findByStripeId: jest.fn(),
markAsDeleted: jest.fn(),
},
},
{
provide: PaymentRepository,
useValue: {
findByUserId: jest.fn(),
create: jest.fn(),
},
},
{
provide: UserRepository,
useValue: {
findById: jest.fn(),
findByStripeCustomerId: jest.fn(),
updatePlan: jest.fn(),
resetQuota: jest.fn(),
},
},
],
}).compile();
service = module.get<PaymentsService>(PaymentsService);
stripeService = module.get(StripeService);
subscriptionService = module.get(SubscriptionService);
paymentRepository = module.get(PaymentRepository);
userRepository = module.get(UserRepository);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('getUserSubscription', () => {
it('should return user subscription details', async () => {
userRepository.findById.mockResolvedValue(mockUser);
subscriptionService.getActiveSubscription.mockResolvedValue(mockSubscription);
paymentRepository.findByUserId.mockResolvedValue([]);
const result = await service.getUserSubscription('user-123');
expect(result).toEqual({
currentPlan: Plan.BASIC,
quotaRemaining: 50,
quotaLimit: 50,
quotaResetDate: mockUser.quotaResetDate,
subscription: {
id: 'sub_stripe_123',
status: 'ACTIVE',
currentPeriodStart: mockSubscription.currentPeriodStart,
currentPeriodEnd: mockSubscription.currentPeriodEnd,
cancelAtPeriodEnd: false,
},
recentPayments: [],
});
});
it('should throw NotFoundException if user not found', async () => {
userRepository.findById.mockResolvedValue(null);
await expect(service.getUserSubscription('user-123')).rejects.toThrow(
NotFoundException
);
});
});
describe('cancelSubscription', () => {
it('should cancel active subscription', async () => {
subscriptionService.getActiveSubscription.mockResolvedValue(mockSubscription);
stripeService.cancelSubscription.mockResolvedValue({} as any);
subscriptionService.markAsCancelled.mockResolvedValue({} as any);
await service.cancelSubscription('user-123');
expect(stripeService.cancelSubscription).toHaveBeenCalledWith('sub_stripe_123');
expect(subscriptionService.markAsCancelled).toHaveBeenCalledWith('sub-123');
});
it('should throw NotFoundException if no active subscription found', async () => {
subscriptionService.getActiveSubscription.mockResolvedValue(null);
await expect(service.cancelSubscription('user-123')).rejects.toThrow(
NotFoundException
);
});
});
describe('upgradePlan', () => {
it('should create checkout session for plan upgrade', async () => {
userRepository.findById.mockResolvedValue(mockUser);
const mockSession = { id: 'cs_123', url: 'https://checkout.stripe.com' };
stripeService.createCheckoutSession.mockResolvedValue(mockSession);
const result = await service.upgradePlan(
'user-123',
Plan.PRO,
'https://success.com',
'https://cancel.com'
);
expect(result).toEqual(mockSession);
expect(stripeService.createCheckoutSession).toHaveBeenCalledWith(
'user-123',
Plan.PRO,
'https://success.com',
'https://cancel.com',
true
);
});
it('should throw error for invalid upgrade path', async () => {
userRepository.findById.mockResolvedValue({ ...mockUser, plan: Plan.MAX });
await expect(
service.upgradePlan('user-123', Plan.PRO, 'success', 'cancel')
).rejects.toThrow('Invalid upgrade path');
});
});
describe('processSuccessfulPayment', () => {
it('should process successful payment and update user', async () => {
userRepository.findByStripeCustomerId.mockResolvedValue(mockUser);
paymentRepository.create.mockResolvedValue({} as any);
userRepository.updatePlan.mockResolvedValue({} as any);
userRepository.resetQuota.mockResolvedValue({} as any);
await service.processSuccessfulPayment(
'pi_123',
'cus_123',
900,
'usd',
Plan.PRO
);
expect(paymentRepository.create).toHaveBeenCalledWith({
userId: 'user-123',
stripePaymentIntentId: 'pi_123',
stripeCustomerId: 'cus_123',
amount: 900,
currency: 'usd',
status: 'succeeded',
planUpgrade: Plan.PRO,
});
expect(userRepository.updatePlan).toHaveBeenCalledWith('user-123', Plan.PRO);
expect(userRepository.resetQuota).toHaveBeenCalledWith('user-123', Plan.PRO);
});
it('should throw NotFoundException if user not found', async () => {
userRepository.findByStripeCustomerId.mockResolvedValue(null);
await expect(
service.processSuccessfulPayment('pi_123', 'cus_123', 900, 'usd', Plan.PRO)
).rejects.toThrow(NotFoundException);
});
});
describe('handleSubscriptionCreated', () => {
const stripeSubscription = {
id: 'sub_stripe_123',
customer: 'cus_123',
status: 'active',
current_period_start: Math.floor(Date.now() / 1000),
current_period_end: Math.floor(Date.now() / 1000) + 86400 * 30,
items: {
data: [
{
price: {
id: 'price_pro_monthly',
},
},
],
},
};
it('should create subscription and update user plan', async () => {
userRepository.findByStripeCustomerId.mockResolvedValue(mockUser);
subscriptionService.create.mockResolvedValue({} as any);
userRepository.updatePlan.mockResolvedValue({} as any);
userRepository.resetQuota.mockResolvedValue({} as any);
await service.handleSubscriptionCreated(stripeSubscription);
expect(subscriptionService.create).toHaveBeenCalledWith({
userId: 'user-123',
stripeSubscriptionId: 'sub_stripe_123',
stripeCustomerId: 'cus_123',
stripePriceId: 'price_pro_monthly',
status: 'active',
currentPeriodStart: expect.any(Date),
currentPeriodEnd: expect.any(Date),
plan: Plan.BASIC, // Default mapping
});
});
});
describe('plan validation', () => {
it('should validate upgrade paths correctly', () => {
// Access private method for testing
const isValidUpgrade = (service as any).isValidUpgrade;
expect(isValidUpgrade(Plan.BASIC, Plan.PRO)).toBe(true);
expect(isValidUpgrade(Plan.PRO, Plan.MAX)).toBe(true);
expect(isValidUpgrade(Plan.PRO, Plan.BASIC)).toBe(false);
expect(isValidUpgrade(Plan.MAX, Plan.PRO)).toBe(false);
});
it('should validate downgrade paths correctly', () => {
const isValidDowngrade = (service as any).isValidDowngrade;
expect(isValidDowngrade(Plan.PRO, Plan.BASIC)).toBe(true);
expect(isValidDowngrade(Plan.MAX, Plan.PRO)).toBe(true);
expect(isValidDowngrade(Plan.BASIC, Plan.PRO)).toBe(false);
expect(isValidDowngrade(Plan.PRO, Plan.MAX)).toBe(false);
});
});
describe('quota limits', () => {
it('should return correct quota limits for each plan', () => {
const getQuotaLimit = (service as any).getQuotaLimit;
expect(getQuotaLimit(Plan.BASIC)).toBe(50);
expect(getQuotaLimit(Plan.PRO)).toBe(500);
expect(getQuotaLimit(Plan.MAX)).toBe(1000);
});
});
});