import { Injectable, NotFoundException } from "@nestjs/common"; import { auditRepo, fiscalRepo, productsRepo, type FiscalCfop, type FiscalMunicipality, type FiscalNcm, type FiscalOperationNature, type ProductFiscalProfile, } from "@eden/database"; import type { EdenAuthenticatedUser } from "../auth/jwt-auth.guard.js"; import type { CreateNcmDto } from "./dto/create-ncm.dto.js"; import type { UpdateNcmDto } from "./dto/update-ncm.dto.js"; import type { CreateCfopDto } from "./dto/create-cfop.dto.js"; import type { UpdateCfopDto } from "./dto/update-cfop.dto.js"; import type { CreateMunicipalityDto } from "./dto/create-municipality.dto.js"; import type { UpdateMunicipalityDto } from "./dto/update-municipality.dto.js"; import type { CreateProductFiscalProfileDto } from "./dto/create-product-fiscal-profile.dto.js"; /** * Fiscal catalogs (eden.md módulo 5) — see PENDING_fiscal.cjs migration * header for the MVP scope. Read (`fiscal:view`) is open to any * authenticated user holding that grant; write (`fiscal:edit`) creates or * PATCHes a single catalog row — no bulk/mass operations in this MVP. */ @Injectable() export class FiscalService { // --------------------------------------------------------------------- // fiscal_ncm // --------------------------------------------------------------------- async listNcm(q?: string, active?: boolean): Promise { return fiscalRepo.listNcm({ q, active }); } async getNcm(id: string): Promise { const row = await fiscalRepo.getNcmById(id); if (!row) throw new NotFoundException("NCM não encontrado"); return row; } async createNcm(dto: CreateNcmDto, user: EdenAuthenticatedUser): Promise { const row = await fiscalRepo.createNcm(dto); await auditRepo.appendAuditEvent({ actorType: "user", actorId: user.id, action: "fiscal.ncm.created", resourceType: "fiscal_ncm", resourceId: row.id, metadata: { code: row.code }, }); return row; } async updateNcm(id: string, dto: UpdateNcmDto, user: EdenAuthenticatedUser): Promise { const row = await fiscalRepo.updateNcm(id, dto); if (!row) throw new NotFoundException("NCM não encontrado"); await auditRepo.appendAuditEvent({ actorType: "user", actorId: user.id, action: "fiscal.ncm.updated", resourceType: "fiscal_ncm", resourceId: row.id, metadata: { ...dto }, }); return row; } // --------------------------------------------------------------------- // fiscal_cfop // --------------------------------------------------------------------- async listCfop(q?: string, active?: boolean): Promise { return fiscalRepo.listCfop({ q, active }); } async getCfop(id: string): Promise { const row = await fiscalRepo.getCfopById(id); if (!row) throw new NotFoundException("CFOP não encontrado"); return row; } async createCfop(dto: CreateCfopDto, user: EdenAuthenticatedUser): Promise { const row = await fiscalRepo.createCfop(dto); await auditRepo.appendAuditEvent({ actorType: "user", actorId: user.id, action: "fiscal.cfop.created", resourceType: "fiscal_cfop", resourceId: row.id, metadata: { code: row.code }, }); return row; } async updateCfop(id: string, dto: UpdateCfopDto, user: EdenAuthenticatedUser): Promise { const row = await fiscalRepo.updateCfop(id, dto); if (!row) throw new NotFoundException("CFOP não encontrado"); await auditRepo.appendAuditEvent({ actorType: "user", actorId: user.id, action: "fiscal.cfop.updated", resourceType: "fiscal_cfop", resourceId: row.id, metadata: { ...dto }, }); return row; } // --------------------------------------------------------------------- // fiscal_municipality // --------------------------------------------------------------------- async listMunicipalities(q?: string, active?: boolean): Promise { return fiscalRepo.listMunicipalities({ q, active }); } async getMunicipality(id: string): Promise { const row = await fiscalRepo.getMunicipalityById(id); if (!row) throw new NotFoundException("Município não encontrado"); return row; } async createMunicipality( dto: CreateMunicipalityDto, user: EdenAuthenticatedUser, ): Promise { const row = await fiscalRepo.createMunicipality(dto); await auditRepo.appendAuditEvent({ actorType: "user", actorId: user.id, action: "fiscal.municipality.created", resourceType: "fiscal_municipality", resourceId: row.id, metadata: { code: row.code }, }); return row; } async updateMunicipality( id: string, dto: UpdateMunicipalityDto, user: EdenAuthenticatedUser, ): Promise { const row = await fiscalRepo.updateMunicipality(id, dto); if (!row) throw new NotFoundException("Município não encontrado"); await auditRepo.appendAuditEvent({ actorType: "user", actorId: user.id, action: "fiscal.municipality.updated", resourceType: "fiscal_municipality", resourceId: row.id, metadata: { ...dto }, }); return row; } // --------------------------------------------------------------------- // fiscal_operation_nature — read-only in this MVP (fixed business enum, // seeded by the migration) // --------------------------------------------------------------------- async listOperationNatures(): Promise { return fiscalRepo.listOperationNatures(); } // --------------------------------------------------------------------- // product_fiscal_profiles // --------------------------------------------------------------------- async listProfiles(productId: string): Promise { await this.assertProductExists(productId); return fiscalRepo.listProductFiscalProfiles(productId); } /** * Creates a new ACTIVE profile for (productId, billingComponent). If one * is already active, the repository layer inactivates it and creates the * new one in a single transaction — never a destructive update of the * old profile (eden.md §2.5). */ async createProfile( productId: string, dto: CreateProductFiscalProfileDto, user: EdenAuthenticatedUser, ): Promise { await this.assertProductExists(productId); const created = await fiscalRepo.createActiveProductFiscalProfile({ productId, billingComponent: dto.billingComponent, documentType: dto.documentType, ncmId: dto.ncmId ?? null, cfopId: dto.cfopId ?? null, municipalityId: dto.municipalityId ?? null, operationNatureId: dto.operationNatureId ?? null, validFrom: dto.validFrom ?? null, validUntil: dto.validUntil ?? null, createdBy: user.id, }); await auditRepo.appendAuditEvent({ actorType: "user", actorId: user.id, action: "fiscal.product_profile.created", resourceType: "product_fiscal_profile", resourceId: created.id, metadata: { productId, billingComponent: dto.billingComponent, documentType: created.document_type }, }); return created; } private async assertProductExists(productId: string): Promise { const product = await productsRepo.getProductById(productId); if (!product) throw new NotFoundException("Produto não encontrado"); } }