import type { IResourceInventoryService } from "@metis/resources/services"; import type { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import { QueryTypes, type Sequelize, type Transaction } from "sequelize"; import { z } from "zod/v4"; import type { ITicketInventoryService, ITicketRepository, TicketContext, TicketLookups, TicketSaveContext, } from "../../application/ticket.repository"; import type { Ticket } from "../../domain/ticket.aggregate"; import { TicketError } from "../../domain/ticket-values"; import { TicketPersistenceMapper, ticketIdentity } from "./ticket-persistence.mapper"; export class SequelizeTicketRepository implements ITicketRepository { private readonly mapper = new TicketPersistenceMapper(); constructor( private readonly database: Sequelize, private readonly inventory: ITicketInventoryService, private readonly resources: IResourceInventoryService ) {} private async read( company: UniqueID | undefined, transaction: Transaction ): Promise> { const companies = await this.database.query<{ id: string; name: string; active: boolean }>( "SELECT id,name,active FROM companies ORDER BY name", { transaction, type: QueryTypes.SELECT } ); if (company && !companies.some((item) => item.id === company.toPrimitive())) { return Result.fail(new TicketError("COMPANY_NOT_FOUND")); } const users = await this.database.query<{ id: string; name: string; active: boolean }>( "SELECT id,name,active FROM users ORDER BY name", { transaction, type: QueryTypes.SELECT } ); const people = await this.database.query<{ id: string; companyId: string; name: string; active: boolean; }>("SELECT id,companyId,name,active FROM company_contacts", { transaction, type: QueryTypes.SELECT, }); const where = company ? " WHERE companyId=:companyId" : ""; const roots = await this.database.query>( "SELECT id,companyId,reference,requestKey,requesterId,assigneeId,`values` FROM tickets" + where, { transaction, type: QueryTypes.SELECT, replacements: { companyId: company?.toPrimitive() ?? null }, } ); const revisions = await this.database.query>( "SELECT r.id,r.ticketId,r.actorId,r.requesterId,r.assigneeId,r.`values`,r.recordedAt,r.position FROM ticket_revisions r JOIN tickets t ON t.id=r.ticketId" + (company ? " WHERE t.companyId=:companyId" : ""), { transaction, type: QueryTypes.SELECT, replacements: { companyId: company?.toPrimitive() ?? null }, } ); const option = (item: { id: string; name: string; active: boolean }) => ({ id: ticketIdentity(item.id), name: item.name, active: Boolean(item.active), }); return Result.ok({ companies: companies.map(option), technicians: users.map(option), people: people.map((item) => ({ ...option(item), companyId: ticketIdentity(item.companyId), })), actorNames: new Map(users.map((item) => [item.id, item.name])), tickets: roots.map((row) => this.mapper.toDomain( row, revisions.filter((item) => item.ticketId === row.id) ) ), }); } list(company?: UniqueID) { return this.database.transaction((transaction) => this.read(company, transaction)); } async lookups(company: UniqueID): Promise> { const rows = await this.database.query<{ id: string }>( "SELECT id FROM companies WHERE id=:id", { replacements: { id: company.toPrimitive() }, type: QueryTypes.SELECT } ); if (!rows.length) { return Result.fail(new TicketError("COMPANY_NOT_FOUND")); } return Result.ok(await this.readLookups(company)); } private async readLookups(company: UniqueID): Promise { const people = await this.database.query<{ id: string; name: string; active: boolean }>( "SELECT id,name,active FROM company_contacts WHERE companyId=:id", { replacements: { id: company.toPrimitive() }, type: QueryTypes.SELECT } ); const [assets, resources, workstations] = await Promise.all([ this.inventory.list(company), this.resources.list(company), this.inventory.workstations(company), ]); return { people: people.map((item) => ({ id: ticketIdentity(item.id), name: item.name, active: Boolean(item.active), })), assets: assets.map((item) => ({ id: item.id, name: item.name, active: item.available })), resources: resources.map((item) => ({ id: item.id, name: item.name, active: item.active })), workstations, }; } save( company: UniqueID, requestKey: UniqueID, action: (context: TicketSaveContext) => Result ) { return this.database.transaction(async (transaction) => { await this.database.query("SELECT id FROM companies WHERE id=:id FOR UPDATE", { replacements: { id: company.toPrimitive() }, transaction, type: QueryTypes.SELECT, }); await this.database.query("SELECT id FROM ticket_counter WHERE id=1 FOR UPDATE", { transaction, type: QueryTypes.SELECT, }); const context = await this.read(undefined, transaction); if (!context.isSuccess) { return Result.fail(context.error); } if (!context.data.companies.some((item) => item.id.equals(company))) { return Result.fail(new TicketError("COMPANY_NOT_FOUND")); } const sequence = await this.database.query<{ nextNumber: number }>( "SELECT nextNumber FROM ticket_counter WHERE id=1", { transaction, type: QueryTypes.SELECT } ); const nextNumber = z.number().int().positive().parse(sequence[0]?.nextNumber); const scoped = { ...context.data, tickets: context.data.tickets.filter((item) => item.companyId.equals(company)), lookups: await this.readLookups(company), nextNumber, requestTicket: context.data.tickets.find((item) => item.requestKey.equals(requestKey)), }; const result = action(scoped); if (!result.isSuccess) { return Result.fail(result.error); } const previous = context.data.tickets.find((item) => item.id.equals(result.data.id)); await this.write(result.data, previous, transaction); return Result.ok({ ticket: result.data, context: context.data }); }); } private async write(ticket: Ticket, previous: Ticket | undefined, transaction: Transaction) { const data = this.mapper.toPersistence(ticket); await this.database.query( previous ? "UPDATE tickets SET requesterId=:requesterId,assigneeId=:assigneeId,`values`=:values WHERE id=:id" : "INSERT INTO tickets (id,companyId,reference,requestKey,requesterId,assigneeId,`values`) VALUES (:id,:companyId,:reference,:requestKey,:requesterId,:assigneeId,:values)", { replacements: data.root, transaction } ); const existing = new Set(previous?.history.map((item) => item.id.toPrimitive()) ?? []); for (const row of data.revisions) { if (!existing.has(row.id)) { await this.database.query( "INSERT INTO ticket_revisions (id,ticketId,actorId,requesterId,assigneeId,`values`,recordedAt,position) VALUES (:id,:ticketId,:actorId,:requesterId,:assigneeId,:values,:recordedAt,:position)", { replacements: row, transaction } ); } } if (!previous) { await this.database.query("UPDATE ticket_counter SET nextNumber=nextNumber+1 WHERE id=1", { transaction, }); } } }