import type { FastifyInstance } from 'fastify'; import { z } from 'zod'; import { STOCK_ALERT_TYPE_OPTIONS, createStockAlert, deleteStockAlert, listStockAlerts, sendCurrentPriceStockAlertWebPush, searchStockAlertCandidates, saveStockAlerts, updateStockAlert, updateStockAlertLayoutFeatureDescription, } from '../services/stock-alert-service.js'; const filterTypeSchema = z.enum(STOCK_ALERT_TYPE_OPTIONS.map((option) => option.value) as [string, ...string[]]).default('all'); const stockAlertMutationSchema = z .object({ id: z.string().trim().optional(), stockCode: z.string().trim().optional(), stockName: z.string().trim().optional(), alertType: z.string().trim().optional(), alertTypes: z.array(z.string().trim()).optional(), }) .transform((value) => ({ id: value.id, stockCode: value.stockCode, stockName: value.stockName, alertTypes: value.alertTypes?.length ? value.alertTypes : value.alertType?.trim() ? [value.alertType.trim()] : [], })); const stockAlertMutationBodySchema = z .object({ stockCode: z.string().trim().optional(), stockName: z.string().trim().optional(), alertType: z.string().trim().optional(), alertTypes: z.array(z.string().trim()).optional(), }) .transform((value) => ({ stockCode: value.stockCode, stockName: value.stockName, alertTypes: value.alertTypes?.length ? value.alertTypes : value.alertType?.trim() ? [value.alertType.trim()] : [], })); export async function registerStockAlertRoutes(app: FastifyInstance) { app.get('/api/stock-alerts/search', async (request) => { const query = z .object({ query: z.string().trim().min(1), limit: z.coerce.number().int().min(1).max(50).optional(), }) .parse(request.query ?? {}); const items = await searchStockAlertCandidates(query.query, query.limit ?? 20); return { ok: true, items, }; }); app.get('/api/stock-alerts', async (request) => { const query = z .object({ alertType: filterTypeSchema.optional(), }) .parse(request.query ?? {}); const alertType = (query.alertType ?? 'all') as 'all' | 'price' | 'top3'; await updateStockAlertLayoutFeatureDescription().catch(() => false); const items = await listStockAlerts(alertType); return { ok: true, items, }; }); app.post('/api/stock-alerts/notify-current-price', async () => { const result = await sendCurrentPriceStockAlertWebPush(); return { ok: result.ok, skipped: Boolean(result.web?.skipped), title: result.title, body: result.body, itemCount: result.itemCount, lines: result.lines, ios: result.ios, web: result.web, }; }); app.post('/api/stock-alerts', async (request) => { const payload = stockAlertMutationSchema.parse(request.body ?? {}); const item = await createStockAlert(payload); await updateStockAlertLayoutFeatureDescription().catch(() => false); return { ok: true, item, }; }); app.patch('/api/stock-alerts/:id', async (request) => { const params = z.object({ id: z.string().trim().min(1) }).parse(request.params ?? {}); const payload = stockAlertMutationBodySchema.parse(request.body ?? {}); const item = await updateStockAlert(params.id, payload); await updateStockAlertLayoutFeatureDescription().catch(() => false); return { ok: true, item, }; }); app.put('/api/stock-alerts/batch', async (request) => { const payload = z.object({ items: z.array(stockAlertMutationSchema).default([]) }).parse(request.body ?? {}); const items = await saveStockAlerts(payload.items); await updateStockAlertLayoutFeatureDescription().catch(() => false); return { ok: true, items, }; }); app.delete('/api/stock-alerts/:id', async (request) => { const params = z.object({ id: z.string().trim().min(1) }).parse(request.params ?? {}); await deleteStockAlert(params.id); await updateStockAlertLayoutFeatureDescription().catch(() => false); return { ok: true, id: params.id, }; }); }