import type { FastifyInstance } from 'fastify'; import { z } from 'zod'; import { requireEditor } from '../auth/guard.js'; import { badRequest } from '../errors.js'; import { applyMapping, commitImport, createImport, discardImport, getImport, listImports, parseUpload, bookRows, dismissRows, } from '../services/imports.js'; import { contextOf, idParam, parseWith } from './helpers.js'; const mappingBody = z.object({ playerKey: z.string().min(1, 'Spalte für die Spieler-Kennung fehlt'), playerName: z.string().optional(), product: z.string().min(1, 'Spalte für den Produktnamen fehlt'), orderId: z.string().optional(), purchasedAt: z.string().optional(), createMissingPlayers: z.boolean().default(false), }); export async function importRoutes(app: FastifyInstance): Promise { // Der Import verändert Guthaben in großer Zahl und bleibt deshalb Admins vorbehalten. app.post('/api/imports', { preHandler: requireEditor }, async (request, reply) => { const file = await request.file(); if (!file) throw badRequest('NO_FILE', 'Es wurde keine Datei hochgeladen'); const content = await file.toBuffer(); const parsed = parseUpload(file.filename, content); const importId = await createImport(file.filename, parsed, contextOf(request)); reply.code(201); return { importId, columns: parsed.columns, rowCount: parsed.rows.length, sample: parsed.rows.slice(0, 5), }; }); app.get('/api/imports', { preHandler: requireEditor }, async () => { return { items: await listImports() }; }); app.get('/api/imports/:id', { preHandler: requireEditor }, async (request) => { const { id } = parseWith(idParam, request.params); return getImport(id); }); app.post('/api/imports/:id/mapping', { preHandler: requireEditor }, async (request) => { const { id } = parseWith(idParam, request.params); const body = parseWith(mappingBody, request.body); const summary = await applyMapping(id, body, contextOf(request)); const record = await getImport(id); return { summary, import: record }; }); app.post('/api/imports/:id/commit', { preHandler: requireEditor }, async (request) => { const { id } = parseWith(idParam, request.params); return commitImport(id, contextOf(request)); }); /** * Einzelne Zeilen sofort buchen, statt auf den ganzen Import zu warten. * Gedacht für den Weg über die Spielerseite: einen Spieler an einem Stück * abarbeiten. Gebuchtes verschwindet danach aus der Vorschau. */ app.post('/api/imports/rows/book', { preHandler: requireEditor }, async (request) => { const body = parseWith( z.object({ rowIds: z.array(z.number().int().positive()).min(1).max(500) }), request.body, ); return bookRows(body.rowIds, contextOf(request)); }); /** Zeilen aus der Vorschau nehmen, ohne sie zu buchen. */ app.post('/api/imports/rows/dismiss', { preHandler: requireEditor }, async (request) => { const body = parseWith( z.object({ rowIds: z.array(z.number().int().positive()).min(1).max(500) }), request.body, ); return dismissRows(body.rowIds, contextOf(request)); }); app.post('/api/imports/:id/discard', { preHandler: requireEditor }, async (request) => { const { id } = parseWith(idParam, request.params); await discardImport(id, contextOf(request)); return { ok: true }; }); }