import { CreateCountrySchema, CreateMetroCitySchema, UpdateCountrySchema, UpdateMetroCitySchema, } from "@capakraken/shared"; import type { Prisma } from "@capakraken/db"; import { z } from "zod"; import { findUniqueOrThrow } from "../db/helpers.js"; import { createAuditEntry } from "../lib/audit.js"; import { adminProcedure, createTRPCRouter, protectedProcedure, resourceOverviewProcedure, } from "../trpc.js"; import { assertCountryCodeAvailable, assertMetroCityDeletable, buildCountryCreateData, buildCountryListWhere, buildCountryUpdateData, buildMetroCityCreateData, buildMetroCityUpdateData, findCountryByIdentifier, } from "./country-support.js"; type CountryIdentifierReadModel = { id: string; code: string; name: string; isActive: boolean; dailyWorkingHours: number; }; type CountryDetailReadModel = CountryIdentifierReadModel & { scheduleRules?: Prisma.JsonValue | null; metroCities: Array<{ id: string; name: string; countryId: string }>; _count: { resources: number }; }; export const countryRouter = createTRPCRouter({ list: protectedProcedure .input(z.object({ isActive: z.boolean().optional() }).optional()) .query(async ({ ctx, input }) => { return ctx.db.country.findMany({ where: buildCountryListWhere(input ?? {}), include: { metroCities: { orderBy: { name: "asc" } } }, orderBy: { name: "asc" }, }); }), resolveByIdentifier: protectedProcedure .input(z.object({ identifier: z.string().trim().min(1) })) .query(async ({ ctx, input }) => { return findCountryByIdentifier(ctx.db, input.identifier, { select: { id: true, code: true, name: true, isActive: true, dailyWorkingHours: true, }, }); }), getByIdentifier: resourceOverviewProcedure .input(z.object({ identifier: z.string().trim().min(1) })) .query(async ({ ctx, input }) => { return findCountryByIdentifier(ctx.db, input.identifier, { include: { metroCities: { orderBy: { name: "asc" } }, _count: { select: { resources: true } }, }, }); }), getById: resourceOverviewProcedure .input(z.object({ id: z.string() })) .query(async ({ ctx, input }) => { const country = await findUniqueOrThrow( ctx.db.country.findUnique({ where: { id: input.id }, include: { metroCities: { orderBy: { name: "asc" } }, _count: { select: { resources: true } }, }, }), "Country", ); return country; }), getCityById: protectedProcedure .input(z.object({ id: z.string() })) .query(async ({ ctx, input }) => { const city = await findUniqueOrThrow( ctx.db.metroCity.findUnique({ where: { id: input.id }, select: { id: true, name: true, countryId: true }, }), "Metro city", ); return city; }), create: adminProcedure .input(CreateCountrySchema) .mutation(async ({ ctx, input }) => { await assertCountryCodeAvailable(ctx.db, input.code); const created = await ctx.db.country.create({ data: buildCountryCreateData(input), include: { metroCities: true }, }); void createAuditEntry({ db: ctx.db, entityType: "Country", entityId: created.id, entityName: created.name, action: "CREATE", userId: ctx.dbUser?.id, after: created as unknown as Record, source: "ui", }); return created; }), update: adminProcedure .input(z.object({ id: z.string(), data: UpdateCountrySchema })) .mutation(async ({ ctx, input }) => { const existing = await findUniqueOrThrow( ctx.db.country.findUnique({ where: { id: input.id } }), "Country", ); if (input.data.code && input.data.code !== existing.code) { await assertCountryCodeAvailable(ctx.db, input.data.code, existing.id); } const before = existing as unknown as Record; const updated = await ctx.db.country.update({ where: { id: input.id }, data: buildCountryUpdateData(input.data), include: { metroCities: true }, }); void createAuditEntry({ db: ctx.db, entityType: "Country", entityId: updated.id, entityName: updated.name, action: "UPDATE", userId: ctx.dbUser?.id, before, after: updated as unknown as Record, source: "ui", }); return updated; }), // ─── Metro City ───────────────────────────────────────────── createCity: adminProcedure .input(CreateMetroCitySchema) .mutation(async ({ ctx, input }) => { await findUniqueOrThrow( ctx.db.country.findUnique({ where: { id: input.countryId } }), "Country", ); const created = await ctx.db.metroCity.create({ data: buildMetroCityCreateData(input), }); void createAuditEntry({ db: ctx.db, entityType: "MetroCity", entityId: created.id, entityName: created.name, action: "CREATE", userId: ctx.dbUser?.id, after: created as unknown as Record, source: "ui", }); return created; }), updateCity: adminProcedure .input(z.object({ id: z.string(), data: UpdateMetroCitySchema })) .mutation(async ({ ctx, input }) => { const existing = await findUniqueOrThrow( ctx.db.metroCity.findUnique({ where: { id: input.id } }), "Metro city", ); const before = existing as unknown as Record; const updated = await ctx.db.metroCity.update({ where: { id: input.id }, data: buildMetroCityUpdateData(input.data), }); void createAuditEntry({ db: ctx.db, entityType: "MetroCity", entityId: updated.id, entityName: updated.name, action: "UPDATE", userId: ctx.dbUser?.id, before, after: updated as unknown as Record, source: "ui", }); return updated; }), deleteCity: adminProcedure .input(z.object({ id: z.string() })) .mutation(async ({ ctx, input }) => { const city = await findUniqueOrThrow( ctx.db.metroCity.findUnique({ where: { id: input.id }, include: { _count: { select: { resources: true } } }, }), "Metro city", ); assertMetroCityDeletable(city); await ctx.db.metroCity.delete({ where: { id: input.id } }); void createAuditEntry({ db: ctx.db, entityType: "MetroCity", entityId: city.id, entityName: city.name, action: "DELETE", userId: ctx.dbUser?.id, before: city as unknown as Record, source: "ui", }); return { success: true, id: city.id, name: city.name }; }), });