duoqi-api/src/routes/payment.ts
Wang Zhuoxuan b872b1cad9 feat: implement Phase 1b core features and Phase 1c commercialization
Phase 1b — Core Features:
- Huawei ID Kit login (token exchange + user info) with guest mode
- Quiz engine: randomized questions, distractor shuffling, answer verification
- XP service with combo bonuses (3/5/10-hit streaks), daily reset
- Streak service: >=3 correct/day, freeze, UTC date handling
- Hearts service: 5/day, 30min auto-restore, Pro unlimited
- 50 quiz questions across 3 categories (history/drama/crosstalk)
- 13 skill tree chapters with linear progression
- Idempotent seed import script (categories → skill tree → questions)
- 7 admin CRUD services (questions, categories, knowledge cards,
  skill tree, users, stats, feedback) with Zod validation
- All routes use Zod schema validation, /auth/me endpoint

Phase 1c — Commercialization:
- Leaderboard with live XP ranking, 10 tiers, weekly settlement
- Achievement system with 15 seed achievements and condition checking
- Huawei IAP receipt verification + subscription management
- Differentiated rate limiting (auth 10/min, quiz 60/min)
- Admin audit logging middleware

Infrastructure:
- Vitest test framework with DB mock utilities (19 tests passing)
- 12 DB tables (5 new: question_ratings, user_feedback, achievements,
  user_achievements, leaderboard_snapshots, subscriptions, admin_audit_log)
- TypeScript strict mode: zero errors
2026-04-09 00:12:12 +08:00

50 lines
1.7 KiB
TypeScript

import { FastifyInstance } from 'fastify';
import { z } from 'zod';
import { verifyReceipt } from '../services/payment/huawei-iap.js';
import { activateSubscription, getSubscriptionStatus } from '../services/payment/subscription-service.js';
const verifyHuaweiSchema = z.object({
purchaseToken: z.string().min(1),
productId: z.string().min(1),
tier: z.enum(['pro', 'proplus']),
});
export async function paymentRoutes(app: FastifyInstance): Promise<void> {
app.post('/payment/verify-huawei', async (request) => {
const parsed = verifyHuaweiSchema.safeParse(request.body);
if (!parsed.success) {
return { success: false, data: null, error: { code: 'VALIDATION_ERROR', message: parsed.error.issues[0]?.message } };
}
const result = await verifyReceipt(parsed.data.purchaseToken);
if (!result.valid) {
return { success: false, data: null, error: { code: 'INVALID_RECEIPT', message: 'Purchase verification failed' } };
}
const userId = (request.user as { userId: string }).userId;
// Calculate expiry (1 month from now as default)
const expiresAt = result.expiryTime
? new Date(result.expiryTime)
: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
await activateSubscription(
userId,
'huawei',
parsed.data.purchaseToken,
parsed.data.tier,
expiresAt,
);
const subscription = await getSubscriptionStatus(userId);
return { success: true, data: subscription, error: null };
});
app.get('/payment/subscription', async (request) => {
const userId = (request.user as { userId: string }).userId;
const data = await getSubscriptionStatus(userId);
return { success: true, data, error: null };
});
}