import type { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import { QueryTypes, type Sequelize, type Transaction } from "sequelize"; import type { IMailConfigurationRepository, IMailInventoryService, MailContext, } from "../../application/mail-configuration.repository"; import type { MailboxContext } from "../../application/mailbox.repository"; import { MailConfigurationError } from "../../domain/mail-configuration-values"; import type { MailConfigurationValues } from "../../domain/value-objects/mail-configuration-values"; import { MailConfigurationPersistenceMapper, type MailRevisionRow, type MailRow, mailIdentity, } from "./mail-configuration-persistence.mapper"; import type { SequelizeMailboxRepository } from "./sequelize-mailbox.repository"; import { type ProfileRevisionRow, type ProfileRow, WorkProfilePersistenceMapper, } from "./work-profile-persistence.mapper"; function validateLink( values: MailConfigurationValues, legacy: MailConfigurationValues["legacy"], context: MailContext ) { if (!legacy) { return; } const profile = context.profiles.find((item) => item.id.equals(values.profileId.getOrUndefined()) ); if ( !profile || profile.values.personId.toPrimitive() !== legacy.personId || profile.values.assetId.toPrimitive() !== legacy.assetId || values.address.toPrimitive() !== legacy.address.toLowerCase() ) { throw new Error("Vinculación histórica de correo inconsistente."); } } function checkScope( values: MailConfigurationValues, context: MailContext, accounts: MailboxContext["items"] ) { const legacy = values.legacy; if (legacy) { return ( context.people.some((item) => item.id.toPrimitive() === legacy.personId) && context.assets.some((item) => item.id.toPrimitive() === legacy.assetId) && accounts.some( (item) => item.id.toPrimitive() === legacy.accountId && item.values.kind === "account" ) ); } return ( context.profiles.some((item) => item.id.equals(values.profileId.getOrUndefined())) && context.mailboxes.some((item) => item.id.equals(values.mailboxId.getOrUndefined())) ); } function validateReferences(context: MailContext, accounts: MailboxContext["items"]) { for (const profile of context.profiles) { if ( !( context.people.some((item) => item.id.equals(profile.values.personId)) && context.assets.some((item) => item.id.equals(profile.values.assetId)) ) ) { throw new Error("Pertenencia de perfil persistida inconsistente."); } } for (const configuration of context.configurations) { let previousLegacy: MailConfigurationValues["legacy"]; for (const revision of configuration.history) { if (!revision.values.legacy) { validateLink(revision.values, previousLegacy, context); } previousLegacy = revision.values.legacy; if (!checkScope(revision.values, context, accounts)) { throw new Error("Pertenencia de correo persistida inconsistente."); } } } } export class SequelizeMailConfigurationRepository implements IMailConfigurationRepository { private readonly mapper = new MailConfigurationPersistenceMapper(); constructor( private readonly database: Sequelize, private readonly inventory: IMailInventoryService, private readonly mailboxRepository: SequelizeMailboxRepository ) {} private async load( company: UniqueID, transaction: Transaction, write: boolean ): Promise> { const companyId = company.toPrimitive(); // El bloqueo de compañía precede a las lecturas: los escritores de personas, cuentas y activos usan el mismo bloqueo. const companies = await this.database.query<{ id: string }>( `SELECT id FROM companies WHERE id=:companyId${write ? " FOR UPDATE" : ""}`, { replacements: { companyId }, transaction, type: QueryTypes.SELECT } ); if (!companies.length) { return Result.fail(new MailConfigurationError("COMPANY_NOT_FOUND")); } const rows = await this.database.query( "SELECT * FROM mail_configurations WHERE companyId=:companyId", { replacements: { companyId }, transaction, type: QueryTypes.SELECT } ); const revisions = await this.database.query( "SELECT r.* FROM mail_configuration_revisions r JOIN mail_configurations c ON c.id=r.configurationId WHERE c.companyId=:companyId", { replacements: { companyId }, transaction, type: QueryTypes.SELECT } ); const people = await this.database.query<{ id: string; active: boolean }>( "SELECT id,active FROM company_contacts WHERE companyId=:companyId", { replacements: { companyId }, transaction, type: QueryTypes.SELECT } ); const mailboxContext = await this.mailboxRepository.load(company, transaction, false); if (!mailboxContext.isSuccess) { return Result.fail(new MailConfigurationError(mailboxContext.error.code)); } const profiles = await this.database.query( "SELECT * FROM work_profiles WHERE companyId=:companyId", { replacements: { companyId }, transaction, type: QueryTypes.SELECT } ); const profileRevisions = await this.database.query( "SELECT r.* FROM work_profile_revisions r JOIN work_profiles p ON p.id=r.profileId WHERE p.companyId=:companyId", { replacements: { companyId }, transaction, type: QueryTypes.SELECT } ); const actors = await this.database.query<{ id: string; name: string }>( "SELECT id,name FROM users", { transaction, type: QueryTypes.SELECT } ); const context: MailContext = { configurations: rows.map((row) => this.mapper.toDomain( row, revisions.filter((revision) => revision.configurationId === row.id) ) ), people: people.map((person) => ({ id: mailIdentity(person.id), active: Boolean(person.active), })), mailboxes: mailboxContext.data.mailboxes, uses: mailboxContext.data.uses, profiles: profiles.map((row) => new WorkProfilePersistenceMapper().toDomain( row, profileRevisions.filter((revision) => revision.profileId === row.id) ) ), assets: await this.inventory.list(company), actors: new Map(actors.map((actor) => [actor.id, actor.name])), today: new Date().toISOString().slice(0, 10), }; validateReferences(context, mailboxContext.data.items); return Result.ok(context); } get(companyId: UniqueID) { return this.database.transaction((transaction) => this.load(companyId, transaction, false)); } save(companyId: UniqueID, action: Parameters[1]) { return this.database.transaction(async (transaction) => { const context = await this.load(companyId, transaction, true); if (!context.isSuccess) { return context; } const candidate = action(context.data); if (!candidate.isSuccess) { return candidate; } const previous = context.data.configurations.find((item) => item.id.equals(candidate.data.id) ); if (previous !== candidate.data) { const record = this.mapper.toPersistence(candidate.data); await this.database.query( "INSERT INTO mail_configurations (id,companyId,personId,accountId,assetId,profileId,mailboxId,`values`,currentKey) VALUES (:id,:companyId,:personId,:accountId,:assetId,:profileId,:mailboxId,:values,:currentKey) ON DUPLICATE KEY UPDATE `values`=VALUES(`values`),currentKey=VALUES(currentKey),personId=VALUES(personId),accountId=VALUES(accountId),assetId=VALUES(assetId),profileId=VALUES(profileId),mailboxId=VALUES(mailboxId)", { replacements: record.root, transaction } ); const stored = new Set( previous?.history.map((revision) => revision.id.toPrimitive()) ?? [] ); for (const revision of record.revisions.filter((item) => !stored.has(item.id))) { await this.database.query( "INSERT INTO mail_configuration_revisions (id,configurationId,position,actorId,recordedAt,`values`) VALUES (:id,:configurationId,:position,:actorId,:recordedAt,:values)", { replacements: revision, transaction } ); } } return Result.ok({ configuration: candidate.data, context: context.data }); }); } }