Migración desde trading-platform/apps/mcp-vip - Estándar multi-repo v2
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
commit
41952f8985
25
.env.example
Normal file
25
.env.example
Normal file
@ -0,0 +1,25 @@
|
||||
# MCP VIP SERVER CONFIGURATION
|
||||
|
||||
PORT=3092
|
||||
NODE_ENV=development
|
||||
LOG_LEVEL=info
|
||||
|
||||
# Database
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=trading_platform
|
||||
DB_USER=trading_app
|
||||
DB_PASSWORD=your_password
|
||||
DB_SSL=false
|
||||
DB_POOL_MAX=20
|
||||
|
||||
# VIP Config
|
||||
VIP_TRIAL_DAYS=7
|
||||
VIP_GRACE_PERIOD_DAYS=3
|
||||
|
||||
# Wallet Service
|
||||
WALLET_SERVICE_URL=http://localhost:3090
|
||||
|
||||
# Stripe
|
||||
STRIPE_SECRET_KEY=sk_test_...
|
||||
STRIPE_WEBHOOK_SECRET=whsec_...
|
||||
18
Dockerfile
Normal file
18
Dockerfile
Normal file
@ -0,0 +1,18 @@
|
||||
FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY tsconfig.json ./
|
||||
COPY src ./src
|
||||
RUN npm run build && npm prune --production
|
||||
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
RUN addgroup -g 1001 -S nodejs && adduser -S vip -u 1001
|
||||
COPY --from=builder --chown=vip:nodejs /app/node_modules ./node_modules
|
||||
COPY --from=builder --chown=vip:nodejs /app/dist ./dist
|
||||
COPY --from=builder --chown=vip:nodejs /app/package.json ./
|
||||
USER vip
|
||||
EXPOSE 3092
|
||||
HEALTHCHECK --interval=30s --timeout=10s CMD wget -q --spider http://localhost:3092/health || exit 1
|
||||
CMD ["node", "dist/index.js"]
|
||||
68
README.md
Normal file
68
README.md
Normal file
@ -0,0 +1,68 @@
|
||||
# MCP VIP Server
|
||||
|
||||
VIP Subscription MCP Server for the Trading Platform. Manages Gold, Platinum, Diamond tiers with exclusive ML model access.
|
||||
|
||||
## VIP Tiers
|
||||
|
||||
| Tier | Price/mo | Predictions | Models |
|
||||
|------|----------|-------------|--------|
|
||||
| **Gold** | $199 | 50/mo | AMD, Range |
|
||||
| **Platinum** | $399 | 150/mo | + TPSL, ICT/SMC |
|
||||
| **Diamond** | $999 | Unlimited | + Strategy Ensemble |
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
npm install
|
||||
cp .env.example .env
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Tiers
|
||||
- `GET /api/v1/tiers` - List all tiers
|
||||
- `GET /api/v1/tiers/:tier` - Get tier details
|
||||
|
||||
### Subscriptions
|
||||
- `POST /api/v1/subscriptions` - Create subscription
|
||||
- `GET /api/v1/subscriptions/:id` - Get subscription
|
||||
- `GET /api/v1/users/:userId/subscription` - Get user subscription
|
||||
- `POST /api/v1/subscriptions/:id/cancel` - Cancel
|
||||
- `POST /api/v1/subscriptions/:id/reactivate` - Reactivate
|
||||
- `POST /api/v1/subscriptions/:id/upgrade` - Upgrade tier
|
||||
- `GET /api/v1/subscriptions/:id/usage` - Usage stats
|
||||
- `GET /api/v1/subscriptions/:id/models` - Model access
|
||||
|
||||
### Model Access
|
||||
- `GET /api/v1/users/:userId/access/:modelId` - Check access
|
||||
|
||||
## MCP Tools (13)
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `vip_get_tiers` | List tiers |
|
||||
| `vip_get_tier` | Get tier |
|
||||
| `vip_create_subscription` | Create sub |
|
||||
| `vip_get_subscription` | Get sub |
|
||||
| `vip_cancel_subscription` | Cancel |
|
||||
| `vip_reactivate_subscription` | Reactivate |
|
||||
| `vip_upgrade_tier` | Upgrade |
|
||||
| `vip_check_model_access` | Check access |
|
||||
| `vip_get_model_access` | List access |
|
||||
| `vip_record_model_usage` | Record usage |
|
||||
| `vip_get_usage_stats` | Usage stats |
|
||||
| `vip_update_status` | Update status |
|
||||
| `vip_renew_period` | Renew period |
|
||||
|
||||
## Subscription Status
|
||||
|
||||
- `trialing` - In trial period
|
||||
- `active` - Active and paying
|
||||
- `past_due` - Payment failed
|
||||
- `cancelled` - Cancelled
|
||||
- `expired` - Expired
|
||||
|
||||
## License
|
||||
|
||||
UNLICENSED - Private
|
||||
42
package.json
Normal file
42
package.json
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "@trading-platform/mcp-vip",
|
||||
"version": "1.0.0",
|
||||
"description": "MCP Server for VIP Subscriptions - Gold, Platinum, Diamond tiers",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"lint": "eslint src --ext .ts",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.0",
|
||||
"express": "^4.18.2",
|
||||
"pg": "^8.11.3",
|
||||
"zod": "^3.22.4",
|
||||
"winston": "^3.11.0",
|
||||
"decimal.js": "^10.4.3",
|
||||
"uuid": "^9.0.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"helmet": "^7.1.0",
|
||||
"cors": "^2.8.5",
|
||||
"jsonwebtoken": "^9.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.10.0",
|
||||
"@types/pg": "^8.10.9",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/jsonwebtoken": "^9.0.5",
|
||||
"typescript": "^5.3.2",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"@types/jest": "^29.5.11",
|
||||
"ts-jest": "^29.1.1"
|
||||
},
|
||||
"engines": { "node": ">=18.0.0" },
|
||||
"private": true
|
||||
}
|
||||
55
src/config.ts
Normal file
55
src/config.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { Pool, PoolConfig } from 'pg';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
export const serverConfig = {
|
||||
port: parseInt(process.env.PORT || '3092', 10),
|
||||
nodeEnv: process.env.NODE_ENV || 'development',
|
||||
logLevel: process.env.LOG_LEVEL || 'info',
|
||||
};
|
||||
|
||||
export const dbConfig: PoolConfig = {
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: parseInt(process.env.DB_PORT || '5432', 10),
|
||||
database: process.env.DB_NAME || 'trading_platform',
|
||||
user: process.env.DB_USER || 'trading_app',
|
||||
password: process.env.DB_PASSWORD || '',
|
||||
ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false,
|
||||
max: parseInt(process.env.DB_POOL_MAX || '20', 10),
|
||||
};
|
||||
|
||||
export const vipConfig = {
|
||||
trialDays: parseInt(process.env.VIP_TRIAL_DAYS || '7', 10),
|
||||
gracePeriodDays: parseInt(process.env.VIP_GRACE_PERIOD_DAYS || '3', 10),
|
||||
};
|
||||
|
||||
export const walletServiceConfig = {
|
||||
baseUrl: process.env.WALLET_SERVICE_URL || 'http://localhost:3090',
|
||||
};
|
||||
|
||||
export const stripeConfig = {
|
||||
secretKey: process.env.STRIPE_SECRET_KEY || '',
|
||||
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET || '',
|
||||
};
|
||||
|
||||
let pool: Pool | null = null;
|
||||
|
||||
export function getPool(): Pool {
|
||||
if (!pool) {
|
||||
pool = new Pool(dbConfig);
|
||||
pool.on('error', (err) => console.error('DB pool error', err));
|
||||
}
|
||||
return pool;
|
||||
}
|
||||
|
||||
export async function closePool(): Promise<void> {
|
||||
if (pool) { await pool.end(); pool = null; }
|
||||
}
|
||||
|
||||
export async function setTenantContext(
|
||||
client: ReturnType<Pool['connect']> extends Promise<infer T> ? T : never,
|
||||
tenantId: string
|
||||
): Promise<void> {
|
||||
await client.query(`SET app.current_tenant_id = $1`, [tenantId]);
|
||||
}
|
||||
292
src/index.ts
Normal file
292
src/index.ts
Normal file
@ -0,0 +1,292 @@
|
||||
/**
|
||||
* MCP VIP Server
|
||||
*
|
||||
* VIP Subscription system for trading platform.
|
||||
* Manages Gold, Platinum, Diamond tiers and exclusive model access.
|
||||
*/
|
||||
|
||||
import express, { Request, Response, NextFunction } from 'express';
|
||||
import helmet from 'helmet';
|
||||
import cors from 'cors';
|
||||
import { ZodError } from 'zod';
|
||||
|
||||
import { serverConfig, closePool, getPool } from './config';
|
||||
import { logger } from './utils/logger';
|
||||
import { allToolSchemas, toolHandlers, toolNames } from './tools';
|
||||
import { authMiddleware } from './middleware';
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(helmet());
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: '1mb' }));
|
||||
|
||||
// ============================================================================
|
||||
// HEALTH & INFO
|
||||
// ============================================================================
|
||||
|
||||
app.get('/health', async (_req: Request, res: Response) => {
|
||||
try {
|
||||
await getPool().query('SELECT 1');
|
||||
res.json({ status: 'healthy', service: 'mcp-vip', database: 'connected' });
|
||||
} catch {
|
||||
res.status(503).json({ status: 'unhealthy', service: 'mcp-vip', database: 'disconnected' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/info', (_req: Request, res: Response) => {
|
||||
res.json({ name: 'mcp-vip', version: '1.0.0', tools: toolNames, toolCount: toolNames.length });
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// MCP ENDPOINTS
|
||||
// ============================================================================
|
||||
|
||||
app.get('/mcp/tools', (_req: Request, res: Response) => {
|
||||
res.json({ tools: Object.values(allToolSchemas) });
|
||||
});
|
||||
|
||||
app.post('/mcp/tools/:toolName', async (req: Request, res: Response, next: NextFunction) => {
|
||||
const handler = toolHandlers[req.params.toolName];
|
||||
if (!handler) {
|
||||
res.status(404).json({ error: `Tool not found: ${req.params.toolName}` });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
res.json(await handler(req.body));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/mcp/call', async (req: Request, res: Response, next: NextFunction) => {
|
||||
const { name, arguments: args } = req.body;
|
||||
const handler = toolHandlers[name];
|
||||
if (!handler) {
|
||||
res.status(404).json({ error: `Tool not found: ${name}` });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
res.json(await handler(args || {}));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// REST API ENDPOINTS
|
||||
// ============================================================================
|
||||
|
||||
// Tiers
|
||||
app.get('/api/v1/tiers', async (_req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
res.json(await toolHandlers.vip_get_tiers({}));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/v1/tiers/:tier', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
res.json(await toolHandlers.vip_get_tier({ tier: req.params.tier }));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Subscriptions - Protected routes
|
||||
const subscriptionsRouter = express.Router();
|
||||
subscriptionsRouter.use(authMiddleware);
|
||||
|
||||
// Create subscription
|
||||
subscriptionsRouter.post('/', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
res.status(201).json(await toolHandlers.vip_create_subscription({
|
||||
tenantId: req.tenantId,
|
||||
userId: req.userId,
|
||||
...req.body,
|
||||
}));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Get subscription by ID
|
||||
subscriptionsRouter.get('/:subscriptionId', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
res.json(await toolHandlers.vip_get_subscription({
|
||||
tenantId: req.tenantId,
|
||||
subscriptionId: req.params.subscriptionId,
|
||||
}));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Cancel subscription
|
||||
subscriptionsRouter.post('/:subscriptionId/cancel', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
res.json(await toolHandlers.vip_cancel_subscription({
|
||||
tenantId: req.tenantId,
|
||||
subscriptionId: req.params.subscriptionId,
|
||||
cancelledBy: req.userId,
|
||||
...req.body,
|
||||
}));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Reactivate subscription
|
||||
subscriptionsRouter.post('/:subscriptionId/reactivate', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
res.json(await toolHandlers.vip_reactivate_subscription({
|
||||
tenantId: req.tenantId,
|
||||
subscriptionId: req.params.subscriptionId,
|
||||
}));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Upgrade tier
|
||||
subscriptionsRouter.post('/:subscriptionId/upgrade', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
res.json(await toolHandlers.vip_upgrade_tier({
|
||||
tenantId: req.tenantId,
|
||||
subscriptionId: req.params.subscriptionId,
|
||||
newTier: req.body.newTier,
|
||||
}));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Get usage stats
|
||||
subscriptionsRouter.get('/:subscriptionId/usage', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
res.json(await toolHandlers.vip_get_usage_stats({
|
||||
tenantId: req.tenantId,
|
||||
subscriptionId: req.params.subscriptionId,
|
||||
}));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Get model access
|
||||
subscriptionsRouter.get('/:subscriptionId/models', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
res.json(await toolHandlers.vip_get_model_access({
|
||||
tenantId: req.tenantId,
|
||||
subscriptionId: req.params.subscriptionId,
|
||||
}));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.use('/api/v1/subscriptions', subscriptionsRouter);
|
||||
|
||||
// User-specific endpoints - Protected
|
||||
const userRouter = express.Router();
|
||||
userRouter.use(authMiddleware);
|
||||
|
||||
// Get user's subscription
|
||||
userRouter.get('/:userId/subscription', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
res.json(await toolHandlers.vip_get_subscription({
|
||||
tenantId: req.tenantId,
|
||||
userId: req.params.userId,
|
||||
}));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Check model access
|
||||
userRouter.get('/:userId/access/:modelId', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
res.json(await toolHandlers.vip_check_model_access({
|
||||
tenantId: req.tenantId,
|
||||
userId: req.params.userId,
|
||||
modelId: req.params.modelId,
|
||||
}));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Get current user's subscription (convenience)
|
||||
userRouter.get('/me/subscription', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
res.json(await toolHandlers.vip_get_subscription({
|
||||
tenantId: req.tenantId,
|
||||
userId: req.userId,
|
||||
}));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Check current user's model access
|
||||
userRouter.get('/me/access/:modelId', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
res.json(await toolHandlers.vip_check_model_access({
|
||||
tenantId: req.tenantId,
|
||||
userId: req.userId,
|
||||
modelId: req.params.modelId,
|
||||
}));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.use('/api/v1/users', userRouter);
|
||||
|
||||
// ============================================================================
|
||||
// ERROR HANDLING
|
||||
// ============================================================================
|
||||
|
||||
app.use((_req: Request, res: Response) => {
|
||||
res.status(404).json({ error: 'Not found' });
|
||||
});
|
||||
|
||||
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
|
||||
if (err instanceof ZodError) {
|
||||
res.status(400).json({ error: 'Validation error', details: err.errors });
|
||||
return;
|
||||
}
|
||||
logger.error('Unhandled error', { error: err.message, stack: err.stack });
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// SERVER STARTUP
|
||||
// ============================================================================
|
||||
|
||||
const server = app.listen(serverConfig.port, () => {
|
||||
logger.info(`MCP VIP Server started`, { port: serverConfig.port });
|
||||
console.log(`
|
||||
╔════════════════════════════════════════════════════════════╗
|
||||
║ MCP VIP SERVER ║
|
||||
╠════════════════════════════════════════════════════════════╣
|
||||
║ Port: ${serverConfig.port} ║
|
||||
║ Tools: ${String(toolNames.length).padEnd(12)} ║
|
||||
║ Tiers: GOLD | PLATINUM | DIAMOND ║
|
||||
╠════════════════════════════════════════════════════════════╣
|
||||
║ /api/v1/tiers, /api/v1/subscriptions/* ║
|
||||
╚════════════════════════════════════════════════════════════╝
|
||||
`);
|
||||
});
|
||||
|
||||
const shutdown = async (signal: string) => {
|
||||
logger.info(`Received ${signal}, shutting down...`);
|
||||
server.close(async () => { await closePool(); process.exit(0); });
|
||||
setTimeout(() => process.exit(1), 30000);
|
||||
};
|
||||
|
||||
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
||||
process.on('SIGINT', () => shutdown('SIGINT'));
|
||||
|
||||
export { app };
|
||||
99
src/middleware/auth.middleware.ts
Normal file
99
src/middleware/auth.middleware.ts
Normal file
@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Auth Middleware for MCP VIP
|
||||
* Verifies JWT tokens and sets user context
|
||||
*/
|
||||
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'dev-jwt-secret-change-in-production-min-256-bits';
|
||||
|
||||
export interface JWTPayload {
|
||||
sub: string;
|
||||
email: string;
|
||||
tenantId: string;
|
||||
isOwner: boolean;
|
||||
iat: number;
|
||||
exp: number;
|
||||
}
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
userId?: string;
|
||||
tenantId?: string;
|
||||
userEmail?: string;
|
||||
isOwner?: boolean;
|
||||
isAuthenticated?: boolean;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function authMiddleware(req: Request, res: Response, next: NextFunction): void {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
res.status(401).json({
|
||||
error: 'Unauthorized',
|
||||
code: 'MISSING_TOKEN',
|
||||
message: 'No authentication token provided',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET) as JWTPayload;
|
||||
|
||||
req.userId = decoded.sub;
|
||||
req.tenantId = decoded.tenantId;
|
||||
req.userEmail = decoded.email;
|
||||
req.isOwner = decoded.isOwner;
|
||||
req.isAuthenticated = true;
|
||||
|
||||
req.headers['x-tenant-id'] = decoded.tenantId;
|
||||
req.headers['x-user-id'] = decoded.sub;
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
if (error instanceof jwt.TokenExpiredError) {
|
||||
res.status(401).json({ error: 'Unauthorized', code: 'TOKEN_EXPIRED' });
|
||||
return;
|
||||
}
|
||||
if (error instanceof jwt.JsonWebTokenError) {
|
||||
res.status(401).json({ error: 'Unauthorized', code: 'INVALID_TOKEN' });
|
||||
return;
|
||||
}
|
||||
logger.error('Auth middleware error', { error });
|
||||
res.status(500).json({ error: 'Internal server error', code: 'AUTH_ERROR' });
|
||||
}
|
||||
}
|
||||
|
||||
export function optionalAuthMiddleware(req: Request, _res: Response, next: NextFunction): void {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
req.isAuthenticated = false;
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET) as JWTPayload;
|
||||
req.userId = decoded.sub;
|
||||
req.tenantId = decoded.tenantId;
|
||||
req.userEmail = decoded.email;
|
||||
req.isOwner = decoded.isOwner;
|
||||
req.isAuthenticated = true;
|
||||
req.headers['x-tenant-id'] = decoded.tenantId;
|
||||
req.headers['x-user-id'] = decoded.sub;
|
||||
} catch {
|
||||
req.isAuthenticated = false;
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
1
src/middleware/index.ts
Normal file
1
src/middleware/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { authMiddleware, optionalAuthMiddleware } from './auth.middleware';
|
||||
541
src/services/vip.service.ts
Normal file
541
src/services/vip.service.ts
Normal file
@ -0,0 +1,541 @@
|
||||
import { Pool } from 'pg';
|
||||
import Decimal from 'decimal.js';
|
||||
import { getPool, setTenantContext, vipConfig, walletServiceConfig } from '../config';
|
||||
import {
|
||||
VipTier,
|
||||
VipSubscription,
|
||||
SubscriptionWithTier,
|
||||
ModelAccess,
|
||||
VipTierType,
|
||||
SubscriptionStatus,
|
||||
CreateSubscriptionInput,
|
||||
UsageStats,
|
||||
mapRowToTier,
|
||||
mapRowToSubscription,
|
||||
mapRowToModelAccess,
|
||||
} from '../types/vip.types';
|
||||
|
||||
export class VipService {
|
||||
private pool: Pool;
|
||||
|
||||
constructor() {
|
||||
this.pool = getPool();
|
||||
}
|
||||
|
||||
// ==================== TIERS ====================
|
||||
|
||||
async getAllTiers(): Promise<VipTier[]> {
|
||||
const result = await this.pool.query(
|
||||
'SELECT * FROM vip.tiers WHERE is_active = TRUE ORDER BY sort_order ASC'
|
||||
);
|
||||
return result.rows.map(mapRowToTier);
|
||||
}
|
||||
|
||||
async getTierById(tierId: string): Promise<VipTier | null> {
|
||||
const result = await this.pool.query('SELECT * FROM vip.tiers WHERE id = $1', [tierId]);
|
||||
return result.rows.length > 0 ? mapRowToTier(result.rows[0]) : null;
|
||||
}
|
||||
|
||||
async getTierByType(tier: VipTierType): Promise<VipTier | null> {
|
||||
const result = await this.pool.query(
|
||||
'SELECT * FROM vip.tiers WHERE tier = $1 AND is_active = TRUE',
|
||||
[tier]
|
||||
);
|
||||
return result.rows.length > 0 ? mapRowToTier(result.rows[0]) : null;
|
||||
}
|
||||
|
||||
// ==================== SUBSCRIPTIONS ====================
|
||||
|
||||
async createSubscription(input: CreateSubscriptionInput): Promise<SubscriptionWithTier> {
|
||||
const client = await this.pool.connect();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
await setTenantContext(client, input.tenantId);
|
||||
|
||||
const tier = await this.getTierByType(input.tier);
|
||||
if (!tier) {
|
||||
throw new Error(`Tier not found: ${input.tier}`);
|
||||
}
|
||||
|
||||
// Check for existing active subscription
|
||||
const existing = await client.query(
|
||||
`SELECT id FROM vip.subscriptions
|
||||
WHERE tenant_id = $1 AND user_id = $2 AND status IN ('active', 'trialing')`,
|
||||
[input.tenantId, input.userId]
|
||||
);
|
||||
|
||||
if (existing.rows.length > 0) {
|
||||
throw new Error('User already has an active VIP subscription');
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const trialEnd = input.startTrial
|
||||
? new Date(now.getTime() + vipConfig.trialDays * 24 * 60 * 60 * 1000)
|
||||
: null;
|
||||
const periodEnd = new Date(
|
||||
now.getTime() + (input.interval === 'year' ? 365 : 30) * 24 * 60 * 60 * 1000
|
||||
);
|
||||
|
||||
const result = await client.query(
|
||||
`INSERT INTO vip.subscriptions (
|
||||
tenant_id, user_id, tier_id,
|
||||
stripe_subscription_id, stripe_customer_id,
|
||||
status, interval,
|
||||
current_period_start, current_period_end,
|
||||
trial_start, trial_end
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
RETURNING *`,
|
||||
[
|
||||
input.tenantId,
|
||||
input.userId,
|
||||
tier.id,
|
||||
input.stripeSubscriptionId || null,
|
||||
input.stripeCustomerId || null,
|
||||
input.startTrial ? 'trialing' : 'active',
|
||||
input.interval || 'month',
|
||||
now,
|
||||
periodEnd,
|
||||
input.startTrial ? now : null,
|
||||
trialEnd,
|
||||
]
|
||||
);
|
||||
|
||||
const subscription = mapRowToSubscription(result.rows[0]);
|
||||
|
||||
// Grant model access
|
||||
for (const modelId of tier.includedModels) {
|
||||
await client.query(
|
||||
`INSERT INTO vip.model_access (subscription_id, model_id, model_name)
|
||||
VALUES ($1, $2, $3)`,
|
||||
[subscription.id, modelId, modelId]
|
||||
);
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
return { ...subscription, tier };
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async getSubscription(userId: string, tenantId: string): Promise<SubscriptionWithTier | null> {
|
||||
const client = await this.pool.connect();
|
||||
|
||||
try {
|
||||
await setTenantContext(client, tenantId);
|
||||
|
||||
const result = await client.query(
|
||||
`SELECT s.*, t.*,
|
||||
s.id as subscription_id, t.id as tier_id
|
||||
FROM vip.subscriptions s
|
||||
JOIN vip.tiers t ON s.tier_id = t.id
|
||||
WHERE s.user_id = $1 AND s.status IN ('active', 'trialing', 'past_due')
|
||||
LIMIT 1`,
|
||||
[userId]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) return null;
|
||||
|
||||
const row = result.rows[0];
|
||||
const subscription = mapRowToSubscription({ ...row, id: row.subscription_id });
|
||||
const tier = mapRowToTier({ ...row, id: row.tier_id });
|
||||
|
||||
return { ...subscription, tier };
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async getSubscriptionById(subscriptionId: string, tenantId: string): Promise<SubscriptionWithTier | null> {
|
||||
const client = await this.pool.connect();
|
||||
|
||||
try {
|
||||
await setTenantContext(client, tenantId);
|
||||
|
||||
const result = await client.query(
|
||||
`SELECT s.*, t.*,
|
||||
s.id as subscription_id, t.id as tier_id
|
||||
FROM vip.subscriptions s
|
||||
JOIN vip.tiers t ON s.tier_id = t.id
|
||||
WHERE s.id = $1`,
|
||||
[subscriptionId]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) return null;
|
||||
|
||||
const row = result.rows[0];
|
||||
const subscription = mapRowToSubscription({ ...row, id: row.subscription_id });
|
||||
const tier = mapRowToTier({ ...row, id: row.tier_id });
|
||||
|
||||
return { ...subscription, tier };
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async cancelSubscription(
|
||||
subscriptionId: string,
|
||||
tenantId: string,
|
||||
reason?: string,
|
||||
cancelImmediately: boolean = false
|
||||
): Promise<VipSubscription> {
|
||||
const client = await this.pool.connect();
|
||||
|
||||
try {
|
||||
await setTenantContext(client, tenantId);
|
||||
|
||||
const cancelAt = cancelImmediately ? new Date() : null;
|
||||
const newStatus = cancelImmediately ? 'cancelled' : 'active';
|
||||
|
||||
const result = await client.query(
|
||||
`UPDATE vip.subscriptions
|
||||
SET status = $1,
|
||||
cancel_at = COALESCE($2, current_period_end),
|
||||
cancelled_at = NOW(),
|
||||
cancel_reason = $3
|
||||
WHERE id = $4
|
||||
RETURNING *`,
|
||||
[newStatus, cancelAt, reason || null, subscriptionId]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
throw new Error('Subscription not found');
|
||||
}
|
||||
|
||||
return mapRowToSubscription(result.rows[0]);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async reactivateSubscription(subscriptionId: string, tenantId: string): Promise<VipSubscription> {
|
||||
const client = await this.pool.connect();
|
||||
|
||||
try {
|
||||
await setTenantContext(client, tenantId);
|
||||
|
||||
const result = await client.query(
|
||||
`UPDATE vip.subscriptions
|
||||
SET status = 'active',
|
||||
cancel_at = NULL,
|
||||
cancelled_at = NULL,
|
||||
cancel_reason = NULL
|
||||
WHERE id = $1 AND status IN ('cancelled', 'past_due')
|
||||
RETURNING *`,
|
||||
[subscriptionId]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
throw new Error('Subscription not found or cannot be reactivated');
|
||||
}
|
||||
|
||||
return mapRowToSubscription(result.rows[0]);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async upgradeTier(
|
||||
subscriptionId: string,
|
||||
newTier: VipTierType,
|
||||
tenantId: string
|
||||
): Promise<SubscriptionWithTier> {
|
||||
const client = await this.pool.connect();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
await setTenantContext(client, tenantId);
|
||||
|
||||
const tier = await this.getTierByType(newTier);
|
||||
if (!tier) {
|
||||
throw new Error(`Tier not found: ${newTier}`);
|
||||
}
|
||||
|
||||
// Update subscription
|
||||
const result = await client.query(
|
||||
`UPDATE vip.subscriptions SET tier_id = $1 WHERE id = $2 RETURNING *`,
|
||||
[tier.id, subscriptionId]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
throw new Error('Subscription not found');
|
||||
}
|
||||
|
||||
const subscription = mapRowToSubscription(result.rows[0]);
|
||||
|
||||
// Update model access
|
||||
await client.query(
|
||||
'DELETE FROM vip.model_access WHERE subscription_id = $1',
|
||||
[subscriptionId]
|
||||
);
|
||||
|
||||
for (const modelId of tier.includedModels) {
|
||||
await client.query(
|
||||
`INSERT INTO vip.model_access (subscription_id, model_id, model_name)
|
||||
VALUES ($1, $2, $3)`,
|
||||
[subscriptionId, modelId, modelId]
|
||||
);
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
return { ...subscription, tier };
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== MODEL ACCESS ====================
|
||||
|
||||
async checkModelAccess(userId: string, modelId: string, tenantId: string): Promise<boolean> {
|
||||
const client = await this.pool.connect();
|
||||
|
||||
try {
|
||||
await setTenantContext(client, tenantId);
|
||||
|
||||
const result = await client.query(
|
||||
`SELECT vip.check_vip_access($1, $2) as has_access`,
|
||||
[userId, modelId]
|
||||
);
|
||||
|
||||
return result.rows[0]?.has_access || false;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async getModelAccess(subscriptionId: string, tenantId: string): Promise<ModelAccess[]> {
|
||||
const client = await this.pool.connect();
|
||||
|
||||
try {
|
||||
await setTenantContext(client, tenantId);
|
||||
|
||||
const result = await client.query(
|
||||
`SELECT * FROM vip.model_access WHERE subscription_id = $1`,
|
||||
[subscriptionId]
|
||||
);
|
||||
|
||||
return result.rows.map(mapRowToModelAccess);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async recordModelUsage(
|
||||
subscriptionId: string,
|
||||
modelId: string,
|
||||
tenantId: string
|
||||
): Promise<ModelAccess> {
|
||||
const client = await this.pool.connect();
|
||||
|
||||
try {
|
||||
await setTenantContext(client, tenantId);
|
||||
|
||||
const result = await client.query(
|
||||
`UPDATE vip.model_access
|
||||
SET predictions_generated = predictions_generated + 1,
|
||||
daily_used = daily_used + 1,
|
||||
monthly_used = monthly_used + 1,
|
||||
last_used_at = NOW()
|
||||
WHERE subscription_id = $1 AND model_id = $2
|
||||
RETURNING *`,
|
||||
[subscriptionId, modelId]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
throw new Error('Model access not found');
|
||||
}
|
||||
|
||||
// Also update subscription usage
|
||||
await client.query(
|
||||
`UPDATE vip.subscriptions
|
||||
SET vip_predictions_used = vip_predictions_used + 1
|
||||
WHERE id = $1`,
|
||||
[subscriptionId]
|
||||
);
|
||||
|
||||
return mapRowToModelAccess(result.rows[0]);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== USAGE ====================
|
||||
|
||||
async getUsageStats(subscriptionId: string, tenantId: string): Promise<UsageStats | null> {
|
||||
const client = await this.pool.connect();
|
||||
|
||||
try {
|
||||
await setTenantContext(client, tenantId);
|
||||
|
||||
const result = await client.query(
|
||||
`SELECT s.*, t.limits
|
||||
FROM vip.subscriptions s
|
||||
JOIN vip.tiers t ON s.tier_id = t.id
|
||||
WHERE s.id = $1`,
|
||||
[subscriptionId]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) return null;
|
||||
|
||||
const row = result.rows[0];
|
||||
const limits = row.limits as { vip_predictions_per_month: number; api_calls_per_month: number };
|
||||
const periodEnd = new Date(row.current_period_end);
|
||||
const now = new Date();
|
||||
const daysRemaining = Math.max(0, Math.ceil((periodEnd.getTime() - now.getTime()) / (24 * 60 * 60 * 1000)));
|
||||
|
||||
const predictionsLimit = limits.vip_predictions_per_month === -1 ? Infinity : limits.vip_predictions_per_month;
|
||||
const apiLimit = limits.api_calls_per_month === -1 ? Infinity : limits.api_calls_per_month;
|
||||
|
||||
return {
|
||||
predictionsUsed: row.vip_predictions_used,
|
||||
predictionsLimit,
|
||||
predictionsRemaining: Math.max(0, predictionsLimit - row.vip_predictions_used),
|
||||
apiCallsUsed: row.api_calls_this_period,
|
||||
apiCallsLimit: apiLimit,
|
||||
apiCallsRemaining: Math.max(0, apiLimit - row.api_calls_this_period),
|
||||
periodStart: new Date(row.current_period_start),
|
||||
periodEnd,
|
||||
daysRemaining,
|
||||
};
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async incrementApiCalls(subscriptionId: string, tenantId: string): Promise<number> {
|
||||
const client = await this.pool.connect();
|
||||
|
||||
try {
|
||||
await setTenantContext(client, tenantId);
|
||||
|
||||
const result = await client.query(
|
||||
`UPDATE vip.subscriptions
|
||||
SET api_calls_this_period = api_calls_this_period + 1
|
||||
WHERE id = $1
|
||||
RETURNING api_calls_this_period`,
|
||||
[subscriptionId]
|
||||
);
|
||||
|
||||
return result.rows[0]?.api_calls_this_period || 0;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async resetUsage(subscriptionId: string, tenantId: string): Promise<void> {
|
||||
const client = await this.pool.connect();
|
||||
|
||||
try {
|
||||
await setTenantContext(client, tenantId);
|
||||
|
||||
await client.query(
|
||||
`UPDATE vip.subscriptions
|
||||
SET vip_predictions_used = 0,
|
||||
api_calls_this_period = 0,
|
||||
last_usage_reset = NOW()
|
||||
WHERE id = $1`,
|
||||
[subscriptionId]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`UPDATE vip.model_access
|
||||
SET daily_used = 0, monthly_used = 0
|
||||
WHERE subscription_id = $1`,
|
||||
[subscriptionId]
|
||||
);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== STATUS MANAGEMENT ====================
|
||||
|
||||
async updateStatus(
|
||||
subscriptionId: string,
|
||||
status: SubscriptionStatus,
|
||||
tenantId: string
|
||||
): Promise<VipSubscription> {
|
||||
const client = await this.pool.connect();
|
||||
|
||||
try {
|
||||
await setTenantContext(client, tenantId);
|
||||
|
||||
const result = await client.query(
|
||||
`UPDATE vip.subscriptions SET status = $1 WHERE id = $2 RETURNING *`,
|
||||
[status, subscriptionId]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
throw new Error('Subscription not found');
|
||||
}
|
||||
|
||||
return mapRowToSubscription(result.rows[0]);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async renewPeriod(
|
||||
subscriptionId: string,
|
||||
tenantId: string,
|
||||
interval: 'month' | 'year' = 'month'
|
||||
): Promise<VipSubscription> {
|
||||
const client = await this.pool.connect();
|
||||
|
||||
try {
|
||||
await setTenantContext(client, tenantId);
|
||||
|
||||
const now = new Date();
|
||||
const periodEnd = new Date(
|
||||
now.getTime() + (interval === 'year' ? 365 : 30) * 24 * 60 * 60 * 1000
|
||||
);
|
||||
|
||||
const result = await client.query(
|
||||
`UPDATE vip.subscriptions
|
||||
SET status = 'active',
|
||||
current_period_start = NOW(),
|
||||
current_period_end = $1,
|
||||
vip_predictions_used = 0,
|
||||
api_calls_this_period = 0,
|
||||
last_usage_reset = NOW()
|
||||
WHERE id = $2
|
||||
RETURNING *`,
|
||||
[periodEnd, subscriptionId]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
throw new Error('Subscription not found');
|
||||
}
|
||||
|
||||
// Reset model access usage
|
||||
await client.query(
|
||||
`UPDATE vip.model_access
|
||||
SET daily_used = 0, monthly_used = 0
|
||||
WHERE subscription_id = $1`,
|
||||
[subscriptionId]
|
||||
);
|
||||
|
||||
return mapRowToSubscription(result.rows[0]);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let vipServiceInstance: VipService | null = null;
|
||||
|
||||
export function getVipService(): VipService {
|
||||
if (!vipServiceInstance) {
|
||||
vipServiceInstance = new VipService();
|
||||
}
|
||||
return vipServiceInstance;
|
||||
}
|
||||
66
src/tools/index.ts
Normal file
66
src/tools/index.ts
Normal file
@ -0,0 +1,66 @@
|
||||
export {
|
||||
vip_get_tiers,
|
||||
vip_get_tier,
|
||||
vip_create_subscription,
|
||||
vip_get_subscription,
|
||||
vip_cancel_subscription,
|
||||
vip_reactivate_subscription,
|
||||
vip_upgrade_tier,
|
||||
vip_check_model_access,
|
||||
vip_get_model_access,
|
||||
vip_record_model_usage,
|
||||
vip_get_usage_stats,
|
||||
vip_update_status,
|
||||
vip_renew_period,
|
||||
handleVipGetTiers,
|
||||
handleVipGetTier,
|
||||
handleVipCreateSubscription,
|
||||
handleVipGetSubscription,
|
||||
handleVipCancelSubscription,
|
||||
handleVipReactivateSubscription,
|
||||
handleVipUpgradeTier,
|
||||
handleVipCheckModelAccess,
|
||||
handleVipGetModelAccess,
|
||||
handleVipRecordModelUsage,
|
||||
handleVipGetUsageStats,
|
||||
handleVipUpdateStatus,
|
||||
handleVipRenewPeriod,
|
||||
vipToolSchemas,
|
||||
} from './vip';
|
||||
|
||||
import { vipToolSchemas } from './vip';
|
||||
import {
|
||||
handleVipGetTiers,
|
||||
handleVipGetTier,
|
||||
handleVipCreateSubscription,
|
||||
handleVipGetSubscription,
|
||||
handleVipCancelSubscription,
|
||||
handleVipReactivateSubscription,
|
||||
handleVipUpgradeTier,
|
||||
handleVipCheckModelAccess,
|
||||
handleVipGetModelAccess,
|
||||
handleVipRecordModelUsage,
|
||||
handleVipGetUsageStats,
|
||||
handleVipUpdateStatus,
|
||||
handleVipRenewPeriod,
|
||||
} from './vip';
|
||||
|
||||
export const allToolSchemas = vipToolSchemas;
|
||||
|
||||
export const toolHandlers: Record<string, (params: unknown) => Promise<{ content: Array<{ type: string; text: string }> }>> = {
|
||||
vip_get_tiers: handleVipGetTiers,
|
||||
vip_get_tier: handleVipGetTier,
|
||||
vip_create_subscription: handleVipCreateSubscription,
|
||||
vip_get_subscription: handleVipGetSubscription,
|
||||
vip_cancel_subscription: handleVipCancelSubscription,
|
||||
vip_reactivate_subscription: handleVipReactivateSubscription,
|
||||
vip_upgrade_tier: handleVipUpgradeTier,
|
||||
vip_check_model_access: handleVipCheckModelAccess,
|
||||
vip_get_model_access: handleVipGetModelAccess,
|
||||
vip_record_model_usage: handleVipRecordModelUsage,
|
||||
vip_get_usage_stats: handleVipGetUsageStats,
|
||||
vip_update_status: handleVipUpdateStatus,
|
||||
vip_renew_period: handleVipRenewPeriod,
|
||||
};
|
||||
|
||||
export const toolNames = Object.keys(allToolSchemas);
|
||||
511
src/tools/vip.ts
Normal file
511
src/tools/vip.ts
Normal file
@ -0,0 +1,511 @@
|
||||
import { z } from 'zod';
|
||||
import { getVipService } from '../services/vip.service';
|
||||
import { logger } from '../utils/logger';
|
||||
import {
|
||||
VipTier,
|
||||
VipSubscription,
|
||||
SubscriptionWithTier,
|
||||
ModelAccess,
|
||||
UsageStats,
|
||||
VipTierType,
|
||||
SubscriptionStatus,
|
||||
} from '../types/vip.types';
|
||||
|
||||
// ============================================================================
|
||||
// INPUT SCHEMAS
|
||||
// ============================================================================
|
||||
|
||||
export const GetTiersInputSchema = z.object({});
|
||||
|
||||
export const GetTierInputSchema = z.object({
|
||||
tierId: z.string().uuid().optional(),
|
||||
tier: z.enum(['GOLD', 'PLATINUM', 'DIAMOND'] as const).optional(),
|
||||
}).refine(data => data.tierId || data.tier, { message: 'Either tierId or tier required' });
|
||||
|
||||
export const CreateSubscriptionInputSchema = z.object({
|
||||
tenantId: z.string().uuid(),
|
||||
userId: z.string().uuid(),
|
||||
tier: z.enum(['GOLD', 'PLATINUM', 'DIAMOND'] as const),
|
||||
interval: z.enum(['month', 'year'] as const).default('month'),
|
||||
stripeSubscriptionId: z.string().optional(),
|
||||
stripeCustomerId: z.string().optional(),
|
||||
startTrial: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export const GetSubscriptionInputSchema = z.object({
|
||||
tenantId: z.string().uuid(),
|
||||
userId: z.string().uuid().optional(),
|
||||
subscriptionId: z.string().uuid().optional(),
|
||||
}).refine(data => data.userId || data.subscriptionId, { message: 'Either userId or subscriptionId required' });
|
||||
|
||||
export const CancelSubscriptionInputSchema = z.object({
|
||||
tenantId: z.string().uuid(),
|
||||
subscriptionId: z.string().uuid(),
|
||||
reason: z.string().optional(),
|
||||
cancelImmediately: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export const ReactivateSubscriptionInputSchema = z.object({
|
||||
tenantId: z.string().uuid(),
|
||||
subscriptionId: z.string().uuid(),
|
||||
});
|
||||
|
||||
export const UpgradeTierInputSchema = z.object({
|
||||
tenantId: z.string().uuid(),
|
||||
subscriptionId: z.string().uuid(),
|
||||
newTier: z.enum(['GOLD', 'PLATINUM', 'DIAMOND'] as const),
|
||||
});
|
||||
|
||||
export const CheckModelAccessInputSchema = z.object({
|
||||
tenantId: z.string().uuid(),
|
||||
userId: z.string().uuid(),
|
||||
modelId: z.string(),
|
||||
});
|
||||
|
||||
export const GetModelAccessInputSchema = z.object({
|
||||
tenantId: z.string().uuid(),
|
||||
subscriptionId: z.string().uuid(),
|
||||
});
|
||||
|
||||
export const RecordModelUsageInputSchema = z.object({
|
||||
tenantId: z.string().uuid(),
|
||||
subscriptionId: z.string().uuid(),
|
||||
modelId: z.string(),
|
||||
});
|
||||
|
||||
export const GetUsageStatsInputSchema = z.object({
|
||||
tenantId: z.string().uuid(),
|
||||
subscriptionId: z.string().uuid(),
|
||||
});
|
||||
|
||||
export const UpdateStatusInputSchema = z.object({
|
||||
tenantId: z.string().uuid(),
|
||||
subscriptionId: z.string().uuid(),
|
||||
status: z.enum(['trialing', 'active', 'past_due', 'cancelled', 'expired'] as const),
|
||||
});
|
||||
|
||||
export const RenewPeriodInputSchema = z.object({
|
||||
tenantId: z.string().uuid(),
|
||||
subscriptionId: z.string().uuid(),
|
||||
interval: z.enum(['month', 'year'] as const).default('month'),
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// RESULT TYPE
|
||||
// ============================================================================
|
||||
|
||||
interface ToolResult<T = unknown> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TOOL IMPLEMENTATIONS
|
||||
// ============================================================================
|
||||
|
||||
export async function vip_get_tiers(): Promise<ToolResult<VipTier[]>> {
|
||||
try {
|
||||
const service = getVipService();
|
||||
const tiers = await service.getAllTiers();
|
||||
return { success: true, data: tiers };
|
||||
} catch (error) {
|
||||
logger.error('vip_get_tiers failed', { error });
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function vip_get_tier(params: z.infer<typeof GetTierInputSchema>): Promise<ToolResult<VipTier>> {
|
||||
try {
|
||||
const service = getVipService();
|
||||
let tier: VipTier | null = null;
|
||||
|
||||
if (params.tierId) {
|
||||
tier = await service.getTierById(params.tierId);
|
||||
} else if (params.tier) {
|
||||
tier = await service.getTierByType(params.tier);
|
||||
}
|
||||
|
||||
if (!tier) return { success: false, error: 'Tier not found' };
|
||||
return { success: true, data: tier };
|
||||
} catch (error) {
|
||||
logger.error('vip_get_tier failed', { error, params });
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function vip_create_subscription(
|
||||
params: z.infer<typeof CreateSubscriptionInputSchema>
|
||||
): Promise<ToolResult<SubscriptionWithTier>> {
|
||||
try {
|
||||
const service = getVipService();
|
||||
const subscription = await service.createSubscription(params);
|
||||
return { success: true, data: subscription };
|
||||
} catch (error) {
|
||||
logger.error('vip_create_subscription failed', { error, params });
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function vip_get_subscription(
|
||||
params: z.infer<typeof GetSubscriptionInputSchema>
|
||||
): Promise<ToolResult<SubscriptionWithTier>> {
|
||||
try {
|
||||
const service = getVipService();
|
||||
let subscription: SubscriptionWithTier | null = null;
|
||||
|
||||
if (params.subscriptionId) {
|
||||
subscription = await service.getSubscriptionById(params.subscriptionId, params.tenantId);
|
||||
} else if (params.userId) {
|
||||
subscription = await service.getSubscription(params.userId, params.tenantId);
|
||||
}
|
||||
|
||||
if (!subscription) return { success: false, error: 'Subscription not found' };
|
||||
return { success: true, data: subscription };
|
||||
} catch (error) {
|
||||
logger.error('vip_get_subscription failed', { error, params });
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function vip_cancel_subscription(
|
||||
params: z.infer<typeof CancelSubscriptionInputSchema>
|
||||
): Promise<ToolResult<VipSubscription>> {
|
||||
try {
|
||||
const service = getVipService();
|
||||
const subscription = await service.cancelSubscription(
|
||||
params.subscriptionId,
|
||||
params.tenantId,
|
||||
params.reason,
|
||||
params.cancelImmediately
|
||||
);
|
||||
return { success: true, data: subscription };
|
||||
} catch (error) {
|
||||
logger.error('vip_cancel_subscription failed', { error, params });
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function vip_reactivate_subscription(
|
||||
params: z.infer<typeof ReactivateSubscriptionInputSchema>
|
||||
): Promise<ToolResult<VipSubscription>> {
|
||||
try {
|
||||
const service = getVipService();
|
||||
const subscription = await service.reactivateSubscription(params.subscriptionId, params.tenantId);
|
||||
return { success: true, data: subscription };
|
||||
} catch (error) {
|
||||
logger.error('vip_reactivate_subscription failed', { error, params });
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function vip_upgrade_tier(
|
||||
params: z.infer<typeof UpgradeTierInputSchema>
|
||||
): Promise<ToolResult<SubscriptionWithTier>> {
|
||||
try {
|
||||
const service = getVipService();
|
||||
const subscription = await service.upgradeTier(
|
||||
params.subscriptionId,
|
||||
params.newTier,
|
||||
params.tenantId
|
||||
);
|
||||
return { success: true, data: subscription };
|
||||
} catch (error) {
|
||||
logger.error('vip_upgrade_tier failed', { error, params });
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function vip_check_model_access(
|
||||
params: z.infer<typeof CheckModelAccessInputSchema>
|
||||
): Promise<ToolResult<{ hasAccess: boolean }>> {
|
||||
try {
|
||||
const service = getVipService();
|
||||
const hasAccess = await service.checkModelAccess(params.userId, params.modelId, params.tenantId);
|
||||
return { success: true, data: { hasAccess } };
|
||||
} catch (error) {
|
||||
logger.error('vip_check_model_access failed', { error, params });
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function vip_get_model_access(
|
||||
params: z.infer<typeof GetModelAccessInputSchema>
|
||||
): Promise<ToolResult<ModelAccess[]>> {
|
||||
try {
|
||||
const service = getVipService();
|
||||
const access = await service.getModelAccess(params.subscriptionId, params.tenantId);
|
||||
return { success: true, data: access };
|
||||
} catch (error) {
|
||||
logger.error('vip_get_model_access failed', { error, params });
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function vip_record_model_usage(
|
||||
params: z.infer<typeof RecordModelUsageInputSchema>
|
||||
): Promise<ToolResult<ModelAccess>> {
|
||||
try {
|
||||
const service = getVipService();
|
||||
const access = await service.recordModelUsage(
|
||||
params.subscriptionId,
|
||||
params.modelId,
|
||||
params.tenantId
|
||||
);
|
||||
return { success: true, data: access };
|
||||
} catch (error) {
|
||||
logger.error('vip_record_model_usage failed', { error, params });
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function vip_get_usage_stats(
|
||||
params: z.infer<typeof GetUsageStatsInputSchema>
|
||||
): Promise<ToolResult<UsageStats>> {
|
||||
try {
|
||||
const service = getVipService();
|
||||
const stats = await service.getUsageStats(params.subscriptionId, params.tenantId);
|
||||
if (!stats) return { success: false, error: 'Subscription not found' };
|
||||
return { success: true, data: stats };
|
||||
} catch (error) {
|
||||
logger.error('vip_get_usage_stats failed', { error, params });
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function vip_update_status(
|
||||
params: z.infer<typeof UpdateStatusInputSchema>
|
||||
): Promise<ToolResult<VipSubscription>> {
|
||||
try {
|
||||
const service = getVipService();
|
||||
const subscription = await service.updateStatus(
|
||||
params.subscriptionId,
|
||||
params.status,
|
||||
params.tenantId
|
||||
);
|
||||
return { success: true, data: subscription };
|
||||
} catch (error) {
|
||||
logger.error('vip_update_status failed', { error, params });
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function vip_renew_period(
|
||||
params: z.infer<typeof RenewPeriodInputSchema>
|
||||
): Promise<ToolResult<VipSubscription>> {
|
||||
try {
|
||||
const service = getVipService();
|
||||
const subscription = await service.renewPeriod(
|
||||
params.subscriptionId,
|
||||
params.tenantId,
|
||||
params.interval
|
||||
);
|
||||
return { success: true, data: subscription };
|
||||
} catch (error) {
|
||||
logger.error('vip_renew_period failed', { error, params });
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MCP TOOL SCHEMAS
|
||||
// ============================================================================
|
||||
|
||||
export const vipToolSchemas = {
|
||||
vip_get_tiers: {
|
||||
name: 'vip_get_tiers',
|
||||
description: 'Get all available VIP tiers (Gold, Platinum, Diamond)',
|
||||
inputSchema: { type: 'object', properties: {}, required: [] },
|
||||
riskLevel: 'LOW',
|
||||
},
|
||||
vip_get_tier: {
|
||||
name: 'vip_get_tier',
|
||||
description: 'Get a specific VIP tier by ID or type',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tierId: { type: 'string' },
|
||||
tier: { type: 'string', enum: ['GOLD', 'PLATINUM', 'DIAMOND'] },
|
||||
},
|
||||
},
|
||||
riskLevel: 'LOW',
|
||||
},
|
||||
vip_create_subscription: {
|
||||
name: 'vip_create_subscription',
|
||||
description: 'Create a new VIP subscription for a user',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tenantId: { type: 'string' },
|
||||
userId: { type: 'string' },
|
||||
tier: { type: 'string', enum: ['GOLD', 'PLATINUM', 'DIAMOND'] },
|
||||
interval: { type: 'string', enum: ['month', 'year'] },
|
||||
stripeSubscriptionId: { type: 'string' },
|
||||
stripeCustomerId: { type: 'string' },
|
||||
startTrial: { type: 'boolean' },
|
||||
},
|
||||
required: ['tenantId', 'userId', 'tier'],
|
||||
},
|
||||
riskLevel: 'HIGH',
|
||||
},
|
||||
vip_get_subscription: {
|
||||
name: 'vip_get_subscription',
|
||||
description: 'Get user VIP subscription by user ID or subscription ID',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tenantId: { type: 'string' },
|
||||
userId: { type: 'string' },
|
||||
subscriptionId: { type: 'string' },
|
||||
},
|
||||
required: ['tenantId'],
|
||||
},
|
||||
riskLevel: 'LOW',
|
||||
},
|
||||
vip_cancel_subscription: {
|
||||
name: 'vip_cancel_subscription',
|
||||
description: 'Cancel a VIP subscription',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tenantId: { type: 'string' },
|
||||
subscriptionId: { type: 'string' },
|
||||
reason: { type: 'string' },
|
||||
cancelImmediately: { type: 'boolean' },
|
||||
},
|
||||
required: ['tenantId', 'subscriptionId'],
|
||||
},
|
||||
riskLevel: 'HIGH',
|
||||
},
|
||||
vip_reactivate_subscription: {
|
||||
name: 'vip_reactivate_subscription',
|
||||
description: 'Reactivate a cancelled VIP subscription',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tenantId: { type: 'string' },
|
||||
subscriptionId: { type: 'string' },
|
||||
},
|
||||
required: ['tenantId', 'subscriptionId'],
|
||||
},
|
||||
riskLevel: 'HIGH',
|
||||
},
|
||||
vip_upgrade_tier: {
|
||||
name: 'vip_upgrade_tier',
|
||||
description: 'Upgrade subscription to a higher tier',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tenantId: { type: 'string' },
|
||||
subscriptionId: { type: 'string' },
|
||||
newTier: { type: 'string', enum: ['GOLD', 'PLATINUM', 'DIAMOND'] },
|
||||
},
|
||||
required: ['tenantId', 'subscriptionId', 'newTier'],
|
||||
},
|
||||
riskLevel: 'HIGH',
|
||||
},
|
||||
vip_check_model_access: {
|
||||
name: 'vip_check_model_access',
|
||||
description: 'Check if user has VIP access to a specific ML model',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tenantId: { type: 'string' },
|
||||
userId: { type: 'string' },
|
||||
modelId: { type: 'string' },
|
||||
},
|
||||
required: ['tenantId', 'userId', 'modelId'],
|
||||
},
|
||||
riskLevel: 'LOW',
|
||||
},
|
||||
vip_get_model_access: {
|
||||
name: 'vip_get_model_access',
|
||||
description: 'Get all model access records for a subscription',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tenantId: { type: 'string' },
|
||||
subscriptionId: { type: 'string' },
|
||||
},
|
||||
required: ['tenantId', 'subscriptionId'],
|
||||
},
|
||||
riskLevel: 'LOW',
|
||||
},
|
||||
vip_record_model_usage: {
|
||||
name: 'vip_record_model_usage',
|
||||
description: 'Record model usage for a VIP subscription',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tenantId: { type: 'string' },
|
||||
subscriptionId: { type: 'string' },
|
||||
modelId: { type: 'string' },
|
||||
},
|
||||
required: ['tenantId', 'subscriptionId', 'modelId'],
|
||||
},
|
||||
riskLevel: 'MEDIUM',
|
||||
},
|
||||
vip_get_usage_stats: {
|
||||
name: 'vip_get_usage_stats',
|
||||
description: 'Get usage statistics for a VIP subscription',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tenantId: { type: 'string' },
|
||||
subscriptionId: { type: 'string' },
|
||||
},
|
||||
required: ['tenantId', 'subscriptionId'],
|
||||
},
|
||||
riskLevel: 'LOW',
|
||||
},
|
||||
vip_update_status: {
|
||||
name: 'vip_update_status',
|
||||
description: 'Update subscription status',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tenantId: { type: 'string' },
|
||||
subscriptionId: { type: 'string' },
|
||||
status: { type: 'string', enum: ['trialing', 'active', 'past_due', 'cancelled', 'expired'] },
|
||||
},
|
||||
required: ['tenantId', 'subscriptionId', 'status'],
|
||||
},
|
||||
riskLevel: 'HIGH',
|
||||
},
|
||||
vip_renew_period: {
|
||||
name: 'vip_renew_period',
|
||||
description: 'Renew subscription period and reset usage',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tenantId: { type: 'string' },
|
||||
subscriptionId: { type: 'string' },
|
||||
interval: { type: 'string', enum: ['month', 'year'] },
|
||||
},
|
||||
required: ['tenantId', 'subscriptionId'],
|
||||
},
|
||||
riskLevel: 'HIGH',
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// MCP HANDLERS
|
||||
// ============================================================================
|
||||
|
||||
function formatMcpResponse(result: ToolResult): { content: Array<{ type: string; text: string }> } {
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
|
||||
export const handleVipGetTiers = async () => formatMcpResponse(await vip_get_tiers());
|
||||
export const handleVipGetTier = async (p: unknown) => formatMcpResponse(await vip_get_tier(GetTierInputSchema.parse(p)));
|
||||
export const handleVipCreateSubscription = async (p: unknown) => formatMcpResponse(await vip_create_subscription(CreateSubscriptionInputSchema.parse(p)));
|
||||
export const handleVipGetSubscription = async (p: unknown) => formatMcpResponse(await vip_get_subscription(GetSubscriptionInputSchema.parse(p)));
|
||||
export const handleVipCancelSubscription = async (p: unknown) => formatMcpResponse(await vip_cancel_subscription(CancelSubscriptionInputSchema.parse(p)));
|
||||
export const handleVipReactivateSubscription = async (p: unknown) => formatMcpResponse(await vip_reactivate_subscription(ReactivateSubscriptionInputSchema.parse(p)));
|
||||
export const handleVipUpgradeTier = async (p: unknown) => formatMcpResponse(await vip_upgrade_tier(UpgradeTierInputSchema.parse(p)));
|
||||
export const handleVipCheckModelAccess = async (p: unknown) => formatMcpResponse(await vip_check_model_access(CheckModelAccessInputSchema.parse(p)));
|
||||
export const handleVipGetModelAccess = async (p: unknown) => formatMcpResponse(await vip_get_model_access(GetModelAccessInputSchema.parse(p)));
|
||||
export const handleVipRecordModelUsage = async (p: unknown) => formatMcpResponse(await vip_record_model_usage(RecordModelUsageInputSchema.parse(p)));
|
||||
export const handleVipGetUsageStats = async (p: unknown) => formatMcpResponse(await vip_get_usage_stats(GetUsageStatsInputSchema.parse(p)));
|
||||
export const handleVipUpdateStatus = async (p: unknown) => formatMcpResponse(await vip_update_status(UpdateStatusInputSchema.parse(p)));
|
||||
export const handleVipRenewPeriod = async (p: unknown) => formatMcpResponse(await vip_renew_period(RenewPeriodInputSchema.parse(p)));
|
||||
178
src/types/vip.types.ts
Normal file
178
src/types/vip.types.ts
Normal file
@ -0,0 +1,178 @@
|
||||
import Decimal from 'decimal.js';
|
||||
|
||||
export type VipTierType = 'GOLD' | 'PLATINUM' | 'DIAMOND';
|
||||
|
||||
export type SubscriptionStatus = 'trialing' | 'active' | 'past_due' | 'cancelled' | 'expired';
|
||||
|
||||
export interface TierBenefit {
|
||||
name: string;
|
||||
included?: boolean;
|
||||
value?: string | number;
|
||||
}
|
||||
|
||||
export interface TierLimits {
|
||||
vip_predictions_per_month: number;
|
||||
api_calls_per_month: number;
|
||||
exclusive_models: string[];
|
||||
priority_support?: boolean;
|
||||
custom_alerts?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface VipTier {
|
||||
id: string;
|
||||
tier: VipTierType;
|
||||
name: string;
|
||||
description: string | null;
|
||||
tagline: string | null;
|
||||
priceMonthly: Decimal;
|
||||
priceYearly: Decimal | null;
|
||||
currency: string;
|
||||
stripeProductId: string | null;
|
||||
stripePriceMonthlyId: string | null;
|
||||
stripePriceYearlyId: string | null;
|
||||
benefits: TierBenefit[];
|
||||
limits: TierLimits;
|
||||
includedModels: string[];
|
||||
color: string | null;
|
||||
icon: string | null;
|
||||
sortOrder: number;
|
||||
isPopular: boolean;
|
||||
isActive: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface VipSubscription {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
userId: string;
|
||||
tierId: string;
|
||||
stripeSubscriptionId: string | null;
|
||||
stripeCustomerId: string | null;
|
||||
status: SubscriptionStatus;
|
||||
interval: string;
|
||||
currentPeriodStart: Date | null;
|
||||
currentPeriodEnd: Date | null;
|
||||
trialStart: Date | null;
|
||||
trialEnd: Date | null;
|
||||
cancelAt: Date | null;
|
||||
cancelledAt: Date | null;
|
||||
cancelReason: string | null;
|
||||
vipPredictionsUsed: number;
|
||||
apiCallsThisPeriod: number;
|
||||
lastUsageReset: Date;
|
||||
metadata: Record<string, unknown>;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface SubscriptionWithTier extends VipSubscription {
|
||||
tier: VipTier;
|
||||
}
|
||||
|
||||
export interface ModelAccess {
|
||||
id: string;
|
||||
subscriptionId: string;
|
||||
modelId: string;
|
||||
modelName: string | null;
|
||||
predictionsGenerated: number;
|
||||
lastUsedAt: Date | null;
|
||||
dailyLimit: number | null;
|
||||
monthlyLimit: number | null;
|
||||
dailyUsed: number;
|
||||
monthlyUsed: number;
|
||||
grantedAt: Date;
|
||||
expiresAt: Date | null;
|
||||
}
|
||||
|
||||
export interface CreateSubscriptionInput {
|
||||
tenantId: string;
|
||||
userId: string;
|
||||
tier: VipTierType;
|
||||
interval?: 'month' | 'year';
|
||||
stripeSubscriptionId?: string;
|
||||
stripeCustomerId?: string;
|
||||
startTrial?: boolean;
|
||||
}
|
||||
|
||||
export interface UsageStats {
|
||||
predictionsUsed: number;
|
||||
predictionsLimit: number;
|
||||
predictionsRemaining: number;
|
||||
apiCallsUsed: number;
|
||||
apiCallsLimit: number;
|
||||
apiCallsRemaining: number;
|
||||
periodStart: Date;
|
||||
periodEnd: Date;
|
||||
daysRemaining: number;
|
||||
}
|
||||
|
||||
export function mapRowToTier(row: Record<string, unknown>): VipTier {
|
||||
return {
|
||||
id: row.id as string,
|
||||
tier: row.tier as VipTierType,
|
||||
name: row.name as string,
|
||||
description: row.description as string | null,
|
||||
tagline: row.tagline as string | null,
|
||||
priceMonthly: new Decimal(row.price_monthly as string),
|
||||
priceYearly: row.price_yearly ? new Decimal(row.price_yearly as string) : null,
|
||||
currency: row.currency as string,
|
||||
stripeProductId: row.stripe_product_id as string | null,
|
||||
stripePriceMonthlyId: row.stripe_price_monthly_id as string | null,
|
||||
stripePriceYearlyId: row.stripe_price_yearly_id as string | null,
|
||||
benefits: (row.benefits as TierBenefit[]) || [],
|
||||
limits: (row.limits as TierLimits) || { vip_predictions_per_month: 0, api_calls_per_month: 0, exclusive_models: [] },
|
||||
includedModels: (row.included_models as string[]) || [],
|
||||
color: row.color as string | null,
|
||||
icon: row.icon as string | null,
|
||||
sortOrder: row.sort_order as number,
|
||||
isPopular: row.is_popular as boolean,
|
||||
isActive: row.is_active as boolean,
|
||||
createdAt: new Date(row.created_at as string),
|
||||
updatedAt: new Date(row.updated_at as string),
|
||||
};
|
||||
}
|
||||
|
||||
export function mapRowToSubscription(row: Record<string, unknown>): VipSubscription {
|
||||
return {
|
||||
id: row.id as string,
|
||||
tenantId: row.tenant_id as string,
|
||||
userId: row.user_id as string,
|
||||
tierId: row.tier_id as string,
|
||||
stripeSubscriptionId: row.stripe_subscription_id as string | null,
|
||||
stripeCustomerId: row.stripe_customer_id as string | null,
|
||||
status: row.status as SubscriptionStatus,
|
||||
interval: row.interval as string,
|
||||
currentPeriodStart: row.current_period_start ? new Date(row.current_period_start as string) : null,
|
||||
currentPeriodEnd: row.current_period_end ? new Date(row.current_period_end as string) : null,
|
||||
trialStart: row.trial_start ? new Date(row.trial_start as string) : null,
|
||||
trialEnd: row.trial_end ? new Date(row.trial_end as string) : null,
|
||||
cancelAt: row.cancel_at ? new Date(row.cancel_at as string) : null,
|
||||
cancelledAt: row.cancelled_at ? new Date(row.cancelled_at as string) : null,
|
||||
cancelReason: row.cancel_reason as string | null,
|
||||
vipPredictionsUsed: row.vip_predictions_used as number,
|
||||
apiCallsThisPeriod: row.api_calls_this_period as number,
|
||||
lastUsageReset: new Date(row.last_usage_reset as string),
|
||||
metadata: (row.metadata as Record<string, unknown>) || {},
|
||||
createdAt: new Date(row.created_at as string),
|
||||
updatedAt: new Date(row.updated_at as string),
|
||||
};
|
||||
}
|
||||
|
||||
export function mapRowToModelAccess(row: Record<string, unknown>): ModelAccess {
|
||||
return {
|
||||
id: row.id as string,
|
||||
subscriptionId: row.subscription_id as string,
|
||||
modelId: row.model_id as string,
|
||||
modelName: row.model_name as string | null,
|
||||
predictionsGenerated: row.predictions_generated as number,
|
||||
lastUsedAt: row.last_used_at ? new Date(row.last_used_at as string) : null,
|
||||
dailyLimit: row.daily_limit as number | null,
|
||||
monthlyLimit: row.monthly_limit as number | null,
|
||||
dailyUsed: row.daily_used as number,
|
||||
monthlyUsed: row.monthly_used as number,
|
||||
grantedAt: new Date(row.granted_at as string),
|
||||
expiresAt: row.expires_at ? new Date(row.expires_at as string) : null,
|
||||
};
|
||||
}
|
||||
28
src/utils/logger.ts
Normal file
28
src/utils/logger.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import winston from 'winston';
|
||||
import { serverConfig } from '../config';
|
||||
|
||||
const { combine, timestamp, printf, colorize, errors } = winston.format;
|
||||
|
||||
const consoleFormat = printf(({ level, message, timestamp, ...meta }) => {
|
||||
let msg = `${timestamp} [${level}]: ${message}`;
|
||||
if (Object.keys(meta).length > 0) msg += ` ${JSON.stringify(meta)}`;
|
||||
return msg;
|
||||
});
|
||||
|
||||
export const logger = winston.createLogger({
|
||||
level: serverConfig.logLevel,
|
||||
format: combine(errors({ stack: true }), timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' })),
|
||||
defaultMeta: { service: 'mcp-vip' },
|
||||
transports: [
|
||||
new winston.transports.Console({
|
||||
format: combine(
|
||||
serverConfig.nodeEnv === 'development' ? colorize() : winston.format.uncolorize(),
|
||||
consoleFormat
|
||||
),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
export function logError(context: string, error: Error, details: Record<string, unknown> = {}): void {
|
||||
logger.error(`Error in ${context}`, { context, error: error.message, stack: error.stack, ...details });
|
||||
}
|
||||
19
tsconfig.json
Normal file
19
tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user