fix(contracts): enforce the edit-lock ADR-0006 promises but never wired
ADR-0006 explicitly commits to preserving the legacy's "trava" once a contract leaves its editable draft state, overridable only by an audited super_admin correction — but ContractsService.addItem/addParty had no status or role check at all, letting anyone with contracts:edit add items/parties to a contract in any status, including a signed active one. Adds assertEditable() (same shape as Quotes' assertNotLocked): draft is the only free-editing state; past it, only super_admin can still write, and that write is tagged distinctly in the audit log (contract.item_added_override) rather than looking like an ordinary edit. No frontend change needed — ContractDetailPage never called the add-item/party endpoints in the first place. Verified live: super_admin override still works past draft, and the audit_log row for it carries overrode:true and the contract's status at the time — confirmed directly in Postgres. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BcWWuKfykGtUweXmHD6cKx
This commit is contained in:
@@ -22,6 +22,8 @@ import type { ContractPartyInputDto } from "./dto/contract-party-input.dto.js";
|
||||
|
||||
const { ContractNotFoundError, InvalidContractTransitionError } = contractsRepo;
|
||||
|
||||
const SUPER_ADMIN = "super_admin";
|
||||
|
||||
@Injectable()
|
||||
export class ContractsService {
|
||||
async list(user: EdenAuthenticatedUser): Promise<Contract[]> {
|
||||
@@ -113,37 +115,63 @@ export class ContractsService {
|
||||
|
||||
async addItem(id: string, dto: ContractItemInputDto, user: EdenAuthenticatedUser) {
|
||||
const contract = await this.get(id, user);
|
||||
const overrode = this.assertEditable(contract, user);
|
||||
const [item] = await this.buildItems([dto], contract.contract_period);
|
||||
const created = await contractsRepo.addContractItem(id, { ...item!, sortOrder: contract.items.length });
|
||||
|
||||
await auditRepo.appendAuditEvent({
|
||||
actorType: "user",
|
||||
actorId: user.id,
|
||||
action: "contract.item_added",
|
||||
action: overrode ? "contract.item_added_override" : "contract.item_added",
|
||||
resourceType: "contract",
|
||||
resourceId: id,
|
||||
metadata: { productId: dto.productId },
|
||||
metadata: { productId: dto.productId, contractStatus: contract.status, overrode },
|
||||
});
|
||||
|
||||
return created;
|
||||
}
|
||||
|
||||
async addParty(id: string, dto: ContractPartyInputDto, user: EdenAuthenticatedUser) {
|
||||
await this.get(id, user);
|
||||
const contract = await this.get(id, user);
|
||||
const overrode = this.assertEditable(contract, user);
|
||||
const created = await contractsRepo.addContractParty(id, this.mapParty(dto));
|
||||
|
||||
await auditRepo.appendAuditEvent({
|
||||
actorType: "user",
|
||||
actorId: user.id,
|
||||
action: "contract.party_added",
|
||||
action: overrode ? "contract.party_added_override" : "contract.party_added",
|
||||
resourceType: "contract",
|
||||
resourceId: id,
|
||||
metadata: { partyRole: dto.partyRole },
|
||||
metadata: { partyRole: dto.partyRole, contractStatus: contract.status, overrode },
|
||||
});
|
||||
|
||||
return created;
|
||||
}
|
||||
|
||||
/**
|
||||
* ADR-0006's own stated invariant: "fluxo de fechamento de oferta →
|
||||
* geração de contrato preserva a trava equivalente [...] exceto por
|
||||
* papel com permissão de correção auditada equivalente ao super_admin
|
||||
* do legado" — ported here from Quotes' assertNotLocked (same shape:
|
||||
* hard lock once the aggregate has moved past its editable draft state,
|
||||
* super_admin-only override). `draft` is the only status items/parties
|
||||
* can be freely added to; anything past it means signing has started or
|
||||
* the contract is already live — an edit past that point is either a
|
||||
* correction (audited, super_admin-only) or should go through a future
|
||||
* amendment instead (contract_amendments, not yet built — ADR-0006).
|
||||
* Returns whether this call IS an override, so the caller can tag the
|
||||
* audit event distinctly.
|
||||
*/
|
||||
private assertEditable(contract: Contract, user: EdenAuthenticatedUser): boolean {
|
||||
if (contract.status === "draft") return false;
|
||||
if (user.roleKey !== SUPER_ADMIN) {
|
||||
throw new ConflictException(
|
||||
`Este contrato não está mais em rascunho (status atual: ${contract.status}) — itens/partes só podem ser adicionados enquanto o contrato está em rascunho.`,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private mapParty(dto: ContractPartyInputDto) {
|
||||
return {
|
||||
partyRole: dto.partyRole,
|
||||
|
||||
Reference in New Issue
Block a user