metis-desk/modules/companies/src/api/infrastructure/persistence/sequelize-ticket.repository.ts
2026-10-07 11:41:56 +02:00

190 lines
7.6 KiB
TypeScript

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<Result<TicketContext, TicketError>> {
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<Record<string, unknown>>(
"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<Record<string, unknown>>(
"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<Result<TicketLookups, TicketError>> {
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<TicketLookups> {
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<Ticket, TicketError>
) {
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,
});
}
}
}