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
50 lines
1.7 KiB
TypeScript
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 };
|
|
});
|
|
}
|