feat: implement organization fiscal profile management including ARCA activity search and fiscal data configuration
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
import { TextObjectFilterResult } from "../TextObjectFilter.model";
|
||||
|
||||
export type ArcaActivity = {
|
||||
code: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export const ARCA_ACTIVITIES: ArcaActivity[] = [
|
||||
{ code: "011111", description: "Cultivo de arroz" },
|
||||
{ code: "011112", description: "Cultivo de trigo" },
|
||||
{ code: "011119", description: "Cultivo de cereales n.c.p., excepto los de uso forrajero" },
|
||||
{ code: "011121", description: "Cultivo de maíz" },
|
||||
{ code: "011129", description: "Cultivo de cereales de uso forrajero n.c.p." },
|
||||
{ code: "011130", description: "Cultivo de oleaginosas, excepto soja" },
|
||||
{ code: "011140", description: "Cultivo de pastos de uso forrajero" },
|
||||
{ code: "011211", description: "Cultivo de papa, batata y mandioca" },
|
||||
{ code: "011221", description: "Cultivo de tomate" },
|
||||
{ code: "011229", description: "Cultivo de bulbos, brotes, raíces y hortalizas de fruto n.c.p." },
|
||||
{ code: "011240", description: "Cultivo de legumbres" },
|
||||
{ code: "012110", description: "Cultivo de vid para vinificar" },
|
||||
{ code: "012121", description: "Cultivo de uva de mesa" },
|
||||
{ code: "012200", description: "Cultivo de frutas cítricas" },
|
||||
{ code: "012311", description: "Cultivo de manzana y pera" },
|
||||
{ code: "012320", description: "Cultivo de frutas de carozo" },
|
||||
{ code: "012410", description: "Cultivo de frutas tropicales y subtropicales" },
|
||||
{ code: "012510", description: "Cultivo de frutas secas" },
|
||||
{ code: "013011", description: "Producción de semillas híbridas de cereales y oleaginosas" },
|
||||
{ code: "013019", description: "Producción de semillas varietales o autofecundadas de cereales, oleaginosas, y forrajeras" },
|
||||
{ code: "014113", description: "Cría de ganado bovino, excepto la realizada en cabañas y para la producción de leche" },
|
||||
{ code: "014114", description: "Invernada de ganado bovino excepto el engorde en corrales (feed-lot)" },
|
||||
{ code: "014115", description: "Engorde en corrales (feed-lot)" },
|
||||
{ code: "014211", description: "Cría de ganado equino, excepto la realizada en haras" },
|
||||
{ code: "014300", description: "Cría de ganado camélido" },
|
||||
{ code: "014410", description: "Cría de ganado ovino, excepto en cabañas y para la producción de lana" },
|
||||
{ code: "014420", description: "Cría de ganado ovino realizada en cabañas" },
|
||||
{ code: "014430", description: "Cría de ganado ovino para la producción de lana" },
|
||||
{ code: "014510", description: "Cría de ganado porcino, excepto la realizada en cabañas" },
|
||||
{ code: "014520", description: "Cría de ganado porcino realizada en cabañas" },
|
||||
{ code: "014610", description: "Producción de leche bovina" },
|
||||
{ code: "014620", description: "Producción de leche de oveja y de cabra" },
|
||||
{ code: "014710", description: "Producción de huevos" },
|
||||
{ code: "014720", description: "Cría de aves para producción de carnes" },
|
||||
{ code: "014740", description: "Producción de pollitos BB" },
|
||||
{ code: "014810", description: "Cría de abejas" },
|
||||
{ code: "014820", description: "Producción de pelos de ganado" },
|
||||
{ code: "014910", description: "Cría de animales para la obtención de pieles y cueros" },
|
||||
{ code: "014990", description: "Cría de animales y obtención de productos de origen animal n.c.p." },
|
||||
{ code: "016111", description: "Servicios de labranza, siembra, transplante y cuidados culturales" },
|
||||
{ code: "016112", description: "Servicios de pulverización, desinfección y fumigación terrestre" },
|
||||
{ code: "016113", description: "Servicios de pulverización, desinfección y fumigación aérea" },
|
||||
{ code: "016119", description: "Servicios de maquinaria agrícola n.c.p., excepto los de cosecha mecánica" },
|
||||
{ code: "016120", description: "Servicios de cosecha mecánica" },
|
||||
{ code: "016130", description: "Servicios de contratistas de mano de obra agrícola" },
|
||||
{ code: "016140", description: "Servicios de post cosecha" },
|
||||
{ code: "016150", description: "Servicios de procesamiento de semillas para su siembra" },
|
||||
{ code: "016190", description: "Servicios de apoyo agrícolas n.c.p." },
|
||||
{ code: "016210", description: "Inseminación artificial y servicios n.c.p. para mejorar la reproducción de los animales y el rendimiento de sus productos" },
|
||||
{ code: "016220", description: "Servicios de contratistas de mano de obra pecuaria" },
|
||||
{ code: "016230", description: "Servicios de esquila de animales" },
|
||||
{ code: "016291", description: "Servicios para el control de plagas, baños parasiticidas, etc." },
|
||||
{ code: "016292", description: "Albergue y cuidado de animales de terceros" },
|
||||
{ code: "016299", description: "Servicios de apoyo pecuarios n.c.p." },
|
||||
{ code: "551010", description: "Servicios de alojamiento por hora" },
|
||||
{ code: "551021", description: "Servicios de alojamiento en pensiones" },
|
||||
{ code: "551022", description: "Servicios de alojamiento en hoteles, hosterías y residenciales similares, excepto por hora, que incluyen servicio de restaurante al público" },
|
||||
{ code: "551023", description: "Servicios de alojamiento en hoteles, hosterías y residenciales similares, excepto por hora, que no incluyen servicio de restaurante al público" },
|
||||
{ code: "561011", description: "Servicios de restaurantes y cantinas sin espectáculo" },
|
||||
{ code: "561012", description: "Servicios de restaurantes y cantinas con espectáculo" },
|
||||
{ code: "561013", description: "Servicios de fast food y locales de venta de comidas y bebidas al paso" },
|
||||
{ code: "561014", description: "Servicios de expendio de bebidas en bares" },
|
||||
{ code: "561019", description: "Servicios de expendio de comidas y bebidas en establecimientos con servicio de mesa y/o en mostrador n.c.p." },
|
||||
{ code: "620100", description: "Servicios de consultores en informática y suministros de programas de informática" },
|
||||
{ code: "620200", description: "Servicios de consultores en equipo de informática" },
|
||||
{ code: "620300", description: "Servicios de consultores en tecnología de la información" },
|
||||
{ code: "620900", description: "Servicios de informática n.c.p." },
|
||||
{ code: "681098", description: "Servicios inmobiliarios realizados por cuenta propia, con bienes urbanos propios o arrendados n.c.p." },
|
||||
{ code: "681099", description: "Servicios inmobiliarios realizados por cuenta propia, con bienes rurales propios o arrendados n.c.p." },
|
||||
{ code: "691001", description: "Servicios jurídicos" },
|
||||
{ code: "692000", description: "Servicios de contabilidad, auditoría y asesoría fiscal" },
|
||||
{ code: "702091", description: "Servicios de asesoramiento, dirección y gestión empresarial realizados por integrantes de los órganos de administración y/o fiscalización en sociedades anónimas" },
|
||||
{ code: "702092", description: "Servicios de asesoramiento, dirección y gestión empresarial realizados por integrantes de cuerpos de dirección en sociedades excepto las anónimas" },
|
||||
{ code: "702099", description: "Servicios de asesoramiento, dirección y gestión empresarial n.c.p." },
|
||||
{ code: "749009", description: "Actividades profesionales, científicas y técnicas n.c.p." },
|
||||
{ code: "851010", description: "Guarderías y jardines maternales" },
|
||||
{ code: "851020", description: "Enseñanza inicial, jardín de infantes y primaria" },
|
||||
{ code: "852100", description: "Enseñanza secundaria de formación general" },
|
||||
{ code: "852200", description: "Enseñanza secundaria de formación técnica y profesional" },
|
||||
{ code: "853100", description: "Enseñanza terciaria" },
|
||||
{ code: "853201", description: "Enseñanza universitaria excepto formación de posgrado" },
|
||||
{ code: "853300", description: "Formación de posgrado" },
|
||||
{ code: "854910", description: "Enseñanza de idiomas" },
|
||||
{ code: "854920", description: "Enseñanza de cursos relacionados con informática" },
|
||||
{ code: "854930", description: "Enseñanza para adultos, excepto discapacitados" },
|
||||
{ code: "854940", description: "Enseñanza especial y para discapacitados" },
|
||||
{ code: "854950", description: "Enseñanza de gimnasia, deportes y actividades físicas" },
|
||||
{ code: "854960", description: "Enseñanza artística" },
|
||||
{ code: "854990", description: "Servicios de enseñanza n.c.p." },
|
||||
{ code: "855000", description: "Servicios de apoyo a la educación" },
|
||||
{ code: "861010", description: "Servicios de internación" },
|
||||
{ code: "862110", description: "Servicios de consulta médica" },
|
||||
{ code: "862120", description: "Servicios de atención médica en dispensarios, salitas, vacunatorios y otros locales de atención primaria de la salud" },
|
||||
{ code: "862130", description: "Servicios de atención médica en otros establecimientos n.c.p." },
|
||||
{ code: "862200", description: "Servicios odontológicos" },
|
||||
{ code: "869010", description: "Servicios de prácticas de diagnóstico en laboratorios" },
|
||||
{ code: "869090", description: "Servicios relacionados con la salud humana n.c.p." },
|
||||
{ code: "900011", description: "Producción de espectáculos teatrales y musicales" },
|
||||
{ code: "900021", description: "Composición y representación de obras teatrales, musicales y artísticas" },
|
||||
{ code: "931010", description: "Servicios de organización, dirección y gestión de prácticas deportivas y explotación de las instalaciones" },
|
||||
{ code: "931020", description: "Servicios prestados por profesionales y técnicos para la realización de prácticas deportivas" },
|
||||
{ code: "960201", description: "Servicios de peluquería" },
|
||||
{ code: "960202", description: "Servicios de tratamiento de belleza, excepto los de peluquería" },
|
||||
{ code: "960990", description: "Servicios personales n.c.p." },
|
||||
];
|
||||
|
||||
export const findArcaActivityByCode = (code: string): ArcaActivity | undefined => {
|
||||
return ARCA_ACTIVITIES.find((activity) => activity.code === code.trim());
|
||||
};
|
||||
|
||||
export const filterArcaActivities = (name?: string): TextObjectFilterResult[] => {
|
||||
const normalizedFilter = (name || "").trim().toLowerCase();
|
||||
|
||||
return ARCA_ACTIVITIES.filter((activity) => {
|
||||
if (!normalizedFilter) return true;
|
||||
|
||||
return (
|
||||
activity.code.includes(normalizedFilter) ||
|
||||
activity.description.toLowerCase().includes(normalizedFilter)
|
||||
);
|
||||
}).map((activity) => ({
|
||||
id: activity.code,
|
||||
name: activity.description,
|
||||
description: activity.code,
|
||||
}));
|
||||
};
|
||||
@@ -49,6 +49,11 @@ export type ArcaWsfeIssueFacturaCParams = {
|
||||
companyId: string;
|
||||
amount: number;
|
||||
billingDate: Date;
|
||||
servicePeriodFrom: Date;
|
||||
servicePeriodTo: Date;
|
||||
paymentDueDate: Date;
|
||||
receiverDocumentType?: number | string;
|
||||
receiverDocumentNumber?: number | string;
|
||||
sessionUser: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -177,6 +177,29 @@ class ArcaCredentialsManager implements IArcaCredentialsManager {
|
||||
};
|
||||
}
|
||||
|
||||
private escapeXml(value: string): string {
|
||||
return value
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
private parseXmlInteger(value: number | string, fieldName: string): number {
|
||||
if (typeof value === "string" && value.trim() === "") {
|
||||
throw new Error(`${fieldName} debe ser un número entero válido`);
|
||||
}
|
||||
|
||||
const parsedValue = typeof value === "string" ? Number(value.trim()) : value;
|
||||
|
||||
if (!Number.isInteger(parsedValue) || !Number.isFinite(parsedValue) || parsedValue < 0) {
|
||||
throw new Error(`${fieldName} debe ser un número entero válido`);
|
||||
}
|
||||
|
||||
return parsedValue;
|
||||
}
|
||||
|
||||
private signTraCms(traXml: string, certificatePem: string, privateKeyPem: string): string {
|
||||
const p7 = forge.pkcs7.createSignedData();
|
||||
p7.content = forge.util.createBuffer(traXml, "utf8");
|
||||
@@ -198,7 +221,7 @@ class ArcaCredentialsManager implements IArcaCredentialsManager {
|
||||
}
|
||||
|
||||
private buildLoginCmsSoap(cms: string): string {
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>\n<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:wsaa="http://wsaa.view.sua.dvadac.desein.afip.gov">\n<soapenv:Header/>\n<soapenv:Body>\n<wsaa:loginCms>\n<wsaa:in0>${cms}</wsaa:in0>\n</wsaa:loginCms>\n</soapenv:Body>\n</soapenv:Envelope>`;
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>\n<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:wsaa="http://wsaa.view.sua.dvadac.desein.afip.gov">\n<soapenv:Header/>\n<soapenv:Body>\n<wsaa:loginCms>\n<wsaa:in0>${this.escapeXml(cms)}</wsaa:in0>\n</wsaa:loginCms>\n</soapenv:Body>\n</soapenv:Envelope>`;
|
||||
}
|
||||
|
||||
private extractXmlValue(xml: string, tagName: string): string | undefined {
|
||||
@@ -271,10 +294,10 @@ class ArcaCredentialsManager implements IArcaCredentialsManager {
|
||||
const privateKeyPem = this.decryptPrivateKey(credential);
|
||||
const { traXml } = this.buildTraXml(service);
|
||||
const cms = this.signTraCms(traXml, credential.certificatePem, privateKeyPem);
|
||||
const wsaaUrl = process.env.ARCA_WSAA_HOMOLOGATION_URL;
|
||||
const wsaaUrl = process.env.ARCA_WSAA_URL;
|
||||
|
||||
if (!wsaaUrl) {
|
||||
throw new Error("ARCA_WSAA_HOMOLOGATION_URL no está configurada");
|
||||
throw new Error("ARCA_WSAA_URL no está configurada");
|
||||
}
|
||||
|
||||
let response;
|
||||
@@ -315,18 +338,21 @@ class ArcaCredentialsManager implements IArcaCredentialsManager {
|
||||
pointOfSale: number;
|
||||
voucherType: 11;
|
||||
}): string {
|
||||
const pointOfSale = this.parseXmlInteger(data.pointOfSale, "PtoVta");
|
||||
const voucherType = this.parseXmlInteger(data.voucherType, "CbteTipo");
|
||||
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
<soapenv:Header/>
|
||||
<soapenv:Body>
|
||||
<FECompUltimoAutorizado xmlns="http://ar.gov.afip.dif.FEV1/">
|
||||
<Auth>
|
||||
<Token>${data.token}</Token>
|
||||
<Sign>${data.sign}</Sign>
|
||||
<Cuit>${data.cuit}</Cuit>
|
||||
<Token>${this.escapeXml(data.token)}</Token>
|
||||
<Sign>${this.escapeXml(data.sign)}</Sign>
|
||||
<Cuit>${this.escapeXml(data.cuit)}</Cuit>
|
||||
</Auth>
|
||||
<PtoVta>${data.pointOfSale}</PtoVta>
|
||||
<CbteTipo>${data.voucherType}</CbteTipo>
|
||||
<PtoVta>${pointOfSale}</PtoVta>
|
||||
<CbteTipo>${voucherType}</CbteTipo>
|
||||
</FECompUltimoAutorizado>
|
||||
</soapenv:Body>
|
||||
</soapenv:Envelope>`;
|
||||
@@ -347,9 +373,9 @@ class ArcaCredentialsManager implements IArcaCredentialsManager {
|
||||
|
||||
private parseArcaDate(value: string): Date {
|
||||
const year = Number(value.slice(0, 4));
|
||||
const month = Number(value.slice(4, 6)) - 1;
|
||||
const month = Number(value.slice(4, 6));
|
||||
const day = Number(value.slice(6, 8));
|
||||
return new Date(year, month, day);
|
||||
return new Date(Date.UTC(year, month - 1, day, 12));
|
||||
}
|
||||
|
||||
private buildWsfeIssueFacturaCSoap(data: {
|
||||
@@ -360,8 +386,17 @@ class ArcaCredentialsManager implements IArcaCredentialsManager {
|
||||
voucherNumber: number;
|
||||
amount: number;
|
||||
voucherDate: string;
|
||||
servicePeriodFrom: string;
|
||||
servicePeriodTo: string;
|
||||
paymentDueDate: string;
|
||||
receiverDocumentType: number | string;
|
||||
receiverDocumentNumber: number | string;
|
||||
}): string {
|
||||
const amount = data.amount.toFixed(2);
|
||||
const pointOfSale = this.parseXmlInteger(data.pointOfSale, "PtoVta");
|
||||
const voucherNumber = this.parseXmlInteger(data.voucherNumber, "CbteDesde");
|
||||
const receiverDocumentType = this.parseXmlInteger(data.receiverDocumentType, "DocTipo");
|
||||
const receiverDocumentNumber = this.parseXmlInteger(data.receiverDocumentNumber, "DocNro");
|
||||
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
@@ -369,23 +404,23 @@ class ArcaCredentialsManager implements IArcaCredentialsManager {
|
||||
<soapenv:Body>
|
||||
<FECAESolicitar xmlns="http://ar.gov.afip.dif.FEV1/">
|
||||
<Auth>
|
||||
<Token>${data.token}</Token>
|
||||
<Sign>${data.sign}</Sign>
|
||||
<Cuit>${data.cuit}</Cuit>
|
||||
<Token>${this.escapeXml(data.token)}</Token>
|
||||
<Sign>${this.escapeXml(data.sign)}</Sign>
|
||||
<Cuit>${this.escapeXml(data.cuit)}</Cuit>
|
||||
</Auth>
|
||||
<FeCAEReq>
|
||||
<FeCabReq>
|
||||
<CantReg>1</CantReg>
|
||||
<PtoVta>${data.pointOfSale}</PtoVta>
|
||||
<PtoVta>${pointOfSale}</PtoVta>
|
||||
<CbteTipo>11</CbteTipo>
|
||||
</FeCabReq>
|
||||
<FeDetReq>
|
||||
<FECAEDetRequest>
|
||||
<Concepto>1</Concepto>
|
||||
<DocTipo>${FINAL_CONSUMER_DOCUMENT_TYPE}</DocTipo>
|
||||
<DocNro>${FINAL_CONSUMER_DOCUMENT_NUMBER}</DocNro>
|
||||
<CbteDesde>${data.voucherNumber}</CbteDesde>
|
||||
<CbteHasta>${data.voucherNumber}</CbteHasta>
|
||||
<Concepto>3</Concepto>
|
||||
<DocTipo>${receiverDocumentType}</DocTipo>
|
||||
<DocNro>${receiverDocumentNumber}</DocNro>
|
||||
<CbteDesde>${voucherNumber}</CbteDesde>
|
||||
<CbteHasta>${voucherNumber}</CbteHasta>
|
||||
<CbteFch>${data.voucherDate}</CbteFch>
|
||||
<ImpTotal>${amount}</ImpTotal>
|
||||
<ImpTotConc>0.00</ImpTotConc>
|
||||
@@ -393,6 +428,9 @@ class ArcaCredentialsManager implements IArcaCredentialsManager {
|
||||
<ImpOpEx>0.00</ImpOpEx>
|
||||
<ImpTrib>0.00</ImpTrib>
|
||||
<ImpIVA>0.00</ImpIVA>
|
||||
<FchServDesde>${data.servicePeriodFrom}</FchServDesde>
|
||||
<FchServHasta>${data.servicePeriodTo}</FchServHasta>
|
||||
<FchVtoPago>${data.paymentDueDate}</FchVtoPago>
|
||||
<MonId>PES</MonId>
|
||||
<MonCotiz>1</MonCotiz>
|
||||
<CondicionIVAReceptorId>${FINAL_CONSUMER_IVA_CONDITION}</CondicionIVAReceptorId>
|
||||
@@ -421,9 +459,9 @@ class ArcaCredentialsManager implements IArcaCredentialsManager {
|
||||
<soapenv:Body>
|
||||
<FEParamGetTiposCbte xmlns="http://ar.gov.afip.dif.FEV1/">
|
||||
<Auth>
|
||||
<Token>${data.token}</Token>
|
||||
<Sign>${data.sign}</Sign>
|
||||
<Cuit>${data.cuit}</Cuit>
|
||||
<Token>${this.escapeXml(data.token)}</Token>
|
||||
<Sign>${this.escapeXml(data.sign)}</Sign>
|
||||
<Cuit>${this.escapeXml(data.cuit)}</Cuit>
|
||||
</Auth>
|
||||
</FEParamGetTiposCbte>
|
||||
</soapenv:Body>
|
||||
@@ -437,9 +475,9 @@ class ArcaCredentialsManager implements IArcaCredentialsManager {
|
||||
<soapenv:Body>
|
||||
<FEParamGetPtosVenta xmlns="http://ar.gov.afip.dif.FEV1/">
|
||||
<Auth>
|
||||
<Token>${data.token}</Token>
|
||||
<Sign>${data.sign}</Sign>
|
||||
<Cuit>${data.cuit}</Cuit>
|
||||
<Token>${this.escapeXml(data.token)}</Token>
|
||||
<Sign>${this.escapeXml(data.sign)}</Sign>
|
||||
<Cuit>${this.escapeXml(data.cuit)}</Cuit>
|
||||
</Auth>
|
||||
</FEParamGetPtosVenta>
|
||||
</soapenv:Body>
|
||||
@@ -447,10 +485,10 @@ class ArcaCredentialsManager implements IArcaCredentialsManager {
|
||||
}
|
||||
|
||||
private async postWsfe(soapBody: string, soapAction: string): Promise<string> {
|
||||
const wsfeUrl = process.env.ARCA_WSFE_HOMOLOGATION_URL;
|
||||
const wsfeUrl = process.env.ARCA_WSFE_URL;
|
||||
|
||||
if (!wsfeUrl) {
|
||||
throw new Error("ARCA_WSFE_HOMOLOGATION_URL no está configurada");
|
||||
throw new Error("ARCA_WSFE_URL no está configurada");
|
||||
}
|
||||
|
||||
const response = await axios.post(wsfeUrl, soapBody, {
|
||||
@@ -613,7 +651,12 @@ class ArcaCredentialsManager implements IArcaCredentialsManager {
|
||||
return { ok: false, configuredPointOfSale, error: errorMessage };
|
||||
}
|
||||
|
||||
const points = Array.from(xml.matchAll(/<(?:\w+:)?PtoVenta>(\d+)<\/(?:\w+:)?PtoVenta>/g)).map((match) => Number(match[1]));
|
||||
const points = Array.from(xml.matchAll(/<(?:\w+:)?PtoVenta(?:\s[^>]*)?>([\s\S]*?)<\/(?:\w+:)?PtoVenta>/g))
|
||||
.map((match) => {
|
||||
const nestedNumber = this.extractXmlValue(match[1], "Nro");
|
||||
return Number(nestedNumber || match[1].trim());
|
||||
})
|
||||
.filter((point) => Number.isInteger(point));
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
@@ -817,6 +860,14 @@ class ArcaCredentialsManager implements IArcaCredentialsManager {
|
||||
const fiscalProfile = await this.requireActiveFiscalProfile(data.companyId, data.sessionUser);
|
||||
const loginTicket = await this.getWsaaLoginTicket(data.companyId);
|
||||
const voucherType = 11;
|
||||
const receiverDocumentType = this.parseXmlInteger(
|
||||
data.receiverDocumentType ?? FINAL_CONSUMER_DOCUMENT_TYPE,
|
||||
"DocTipo"
|
||||
);
|
||||
const receiverDocumentNumber = this.parseXmlInteger(
|
||||
data.receiverDocumentNumber ?? FINAL_CONSUMER_DOCUMENT_NUMBER,
|
||||
"DocNro"
|
||||
);
|
||||
let lastVoucherNumber: number;
|
||||
|
||||
try {
|
||||
@@ -848,6 +899,11 @@ class ArcaCredentialsManager implements IArcaCredentialsManager {
|
||||
voucherNumber,
|
||||
amount: data.amount,
|
||||
voucherDate: this.formatArcaDate(data.billingDate),
|
||||
servicePeriodFrom: this.formatArcaDate(data.servicePeriodFrom),
|
||||
servicePeriodTo: this.formatArcaDate(data.servicePeriodTo),
|
||||
paymentDueDate: this.formatArcaDate(data.paymentDueDate),
|
||||
receiverDocumentType,
|
||||
receiverDocumentNumber,
|
||||
}),
|
||||
"FECAESolicitar"
|
||||
),
|
||||
|
||||
@@ -42,8 +42,25 @@ export class InvoicesAdapterMongoose implements IInvoicesAdapter {
|
||||
type: { type: String, required: true, enum: Object.values(INVOICE_TYPE) },
|
||||
status: { type: String, required: true, enum: Object.values(INVOICE_STATUS), index: true },
|
||||
billingDate: { type: Date, required: true },
|
||||
paymentMethod: { type: String, required: true },
|
||||
description: { type: String, required: true },
|
||||
servicePeriodFrom: { type: Date, required: true },
|
||||
servicePeriodTo: { type: Date, required: true },
|
||||
paymentDueDate: { type: Date, required: true },
|
||||
activityCode: { type: String, required: true },
|
||||
activityDescription: { type: String, required: true },
|
||||
receiverIvaCondition: { type: Number, required: true, default: 5 },
|
||||
receiverDocumentType: { type: Schema.Types.Mixed, required: false },
|
||||
receiverDocumentNumber: { type: Schema.Types.Mixed, required: false },
|
||||
receiverName: { type: String, required: false },
|
||||
receiverAddress: { type: String, required: false },
|
||||
receiverEmail: { type: String, required: false },
|
||||
saleCondition: { type: String, required: true },
|
||||
itemDescription: { type: String, required: true },
|
||||
itemQuantity: { type: Number, required: true, default: 1 },
|
||||
itemUnit: { type: String, required: false },
|
||||
itemUnitPrice: { type: Number, required: true },
|
||||
itemDiscountPercent: { type: Number, required: true, default: 0 },
|
||||
itemDiscountAmount: { type: Number, required: true, default: 0 },
|
||||
itemSubtotal: { type: Number, required: true },
|
||||
cae: { type: String, required: false },
|
||||
caeExpiresAt: { type: Date, required: false },
|
||||
pointOfSale: { type: Number, required: false },
|
||||
|
||||
@@ -12,19 +12,42 @@ export enum INVOICE_STATUS {
|
||||
FAILED = "FAILED",
|
||||
}
|
||||
|
||||
export enum INVOICE_PAYMENT_METHOD {
|
||||
export enum INVOICE_SALE_CONDITION {
|
||||
CASH = "cash",
|
||||
CONTADO = "contado",
|
||||
BANK_TRANSFER = "bank_transfer",
|
||||
CREDIT_CARD = "credit_card",
|
||||
DEBIT_CARD = "debit_card",
|
||||
CURRENT_ACCOUNT = "current_account",
|
||||
CHECK = "check",
|
||||
OTHER = "other",
|
||||
ELECTRONIC_PAYMENT = "electronic_payment",
|
||||
}
|
||||
|
||||
export const FINAL_CONSUMER_IVA_CONDITION_ID = 5;
|
||||
|
||||
export type CreateInvoiceFromCashMovementParams = {
|
||||
cashMovementId: string;
|
||||
billingDate: Date | string;
|
||||
paymentMethod: INVOICE_PAYMENT_METHOD | string;
|
||||
description: string;
|
||||
servicePeriodFrom: Date | string;
|
||||
servicePeriodTo: Date | string;
|
||||
paymentDueDate: Date | string;
|
||||
activityCode: string;
|
||||
activityDescription: string;
|
||||
receiverIvaCondition?: 5;
|
||||
receiverDocumentType?: number | string;
|
||||
receiverDocumentNumber?: number | string;
|
||||
receiverName?: string;
|
||||
receiverAddress?: string;
|
||||
receiverEmail?: string;
|
||||
saleCondition: INVOICE_SALE_CONDITION | string;
|
||||
itemDescription: string;
|
||||
itemQuantity?: number;
|
||||
itemUnit?: string;
|
||||
itemUnitPrice: number;
|
||||
itemDiscountPercent?: number;
|
||||
itemDiscountAmount?: number;
|
||||
itemSubtotal: number;
|
||||
sessionUser: string;
|
||||
};
|
||||
|
||||
@@ -48,8 +71,25 @@ export type CreateInvoiceParams = {
|
||||
type: INVOICE_TYPE;
|
||||
status: INVOICE_STATUS;
|
||||
billingDate: Date;
|
||||
paymentMethod: INVOICE_PAYMENT_METHOD | string;
|
||||
description: string;
|
||||
servicePeriodFrom: Date;
|
||||
servicePeriodTo: Date;
|
||||
paymentDueDate: Date;
|
||||
activityCode: string;
|
||||
activityDescription: string;
|
||||
receiverIvaCondition: 5;
|
||||
receiverDocumentType?: number | string;
|
||||
receiverDocumentNumber?: number | string;
|
||||
receiverName?: string;
|
||||
receiverAddress?: string;
|
||||
receiverEmail?: string;
|
||||
saleCondition: INVOICE_SALE_CONDITION | string;
|
||||
itemDescription: string;
|
||||
itemQuantity: number;
|
||||
itemUnit?: string;
|
||||
itemUnitPrice: number;
|
||||
itemDiscountPercent: number;
|
||||
itemDiscountAmount: number;
|
||||
itemSubtotal: number;
|
||||
cae?: string;
|
||||
caeExpiresAt?: Date;
|
||||
pointOfSale?: number;
|
||||
@@ -75,8 +115,25 @@ export interface IInvoice {
|
||||
type: INVOICE_TYPE;
|
||||
status: INVOICE_STATUS;
|
||||
billingDate: Date;
|
||||
paymentMethod: INVOICE_PAYMENT_METHOD | string;
|
||||
description: string;
|
||||
servicePeriodFrom: Date;
|
||||
servicePeriodTo: Date;
|
||||
paymentDueDate: Date;
|
||||
activityCode: string;
|
||||
activityDescription: string;
|
||||
receiverIvaCondition: 5;
|
||||
receiverDocumentType?: number | string;
|
||||
receiverDocumentNumber?: number | string;
|
||||
receiverName?: string;
|
||||
receiverAddress?: string;
|
||||
receiverEmail?: string;
|
||||
saleCondition: INVOICE_SALE_CONDITION | string;
|
||||
itemDescription: string;
|
||||
itemQuantity: number;
|
||||
itemUnit?: string;
|
||||
itemUnitPrice: number;
|
||||
itemDiscountPercent: number;
|
||||
itemDiscountAmount: number;
|
||||
itemSubtotal: number;
|
||||
cae?: string;
|
||||
caeExpiresAt?: Date;
|
||||
pointOfSale?: number;
|
||||
|
||||
@@ -12,17 +12,24 @@ import {
|
||||
CreateInvoiceFromCashMovementParams,
|
||||
CreateInvoiceParams,
|
||||
FindInvoicesParams,
|
||||
FINAL_CONSUMER_IVA_CONDITION_ID,
|
||||
IInvoice,
|
||||
IInvoicesManager,
|
||||
INVOICE_PAYMENT_METHOD,
|
||||
INVOICE_SALE_CONDITION,
|
||||
INVOICE_STATUS,
|
||||
INVOICE_TYPE,
|
||||
} from "./Invoices.Interface";
|
||||
|
||||
const FACTURA_C_VOUCHER_TYPE = "11";
|
||||
const ARGENTINA_TIME_ZONE = "America/Argentina/Buenos_Aires";
|
||||
const DESCRIPTION_MAX_LENGTH = 500;
|
||||
const TEXT_MAX_LENGTH = 500;
|
||||
const RECEIVER_TEXT_MAX_LENGTH = 200;
|
||||
const CENTS_TOLERANCE = 0.01;
|
||||
const YYYY_MM_DD_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
|
||||
const DNI_DOCUMENT_TYPE = 96;
|
||||
const FINAL_CONSUMER_DOCUMENT_TYPE = 99;
|
||||
const FINAL_CONSUMER_DOCUMENT_NUMBER = 0;
|
||||
const FINAL_CONSUMER_COMPATIBLE_DOCUMENT_TYPES = new Set([DNI_DOCUMENT_TYPE, FINAL_CONSUMER_DOCUMENT_TYPE]);
|
||||
|
||||
class InvoicesManager implements IInvoicesManager {
|
||||
invoices: InvoicesAdapterMongoose;
|
||||
@@ -65,30 +72,51 @@ class InvoicesManager implements IInvoicesManager {
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
private parseBillingDate(billingDate: Date | string): Date {
|
||||
if (!billingDate) {
|
||||
throw new Error("Se requiere la fecha del comprobante");
|
||||
private parseArgentinaCalendarDate(dateValue: Date | string, requiredMessage: string, invalidMessage: string): Date {
|
||||
if (!dateValue) {
|
||||
throw new Error(requiredMessage);
|
||||
}
|
||||
|
||||
let parsedDate: Date;
|
||||
|
||||
if (typeof billingDate === "string") {
|
||||
const localDateMatch = billingDate.match(YYYY_MM_DD_PATTERN);
|
||||
if (typeof dateValue === "string") {
|
||||
const localDateMatch = dateValue.match(YYYY_MM_DD_PATTERN);
|
||||
|
||||
if (localDateMatch) {
|
||||
const [, year, month, day] = localDateMatch;
|
||||
parsedDate = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day), 12));
|
||||
} else {
|
||||
parsedDate = new Date(billingDate);
|
||||
if (!localDateMatch) {
|
||||
throw new Error(invalidMessage);
|
||||
}
|
||||
|
||||
const [, year, month, day] = localDateMatch;
|
||||
const yearNumber = Number(year);
|
||||
const monthNumber = Number(month);
|
||||
const dayNumber = Number(day);
|
||||
parsedDate = new Date(Date.UTC(yearNumber, monthNumber - 1, dayNumber, 12));
|
||||
|
||||
if (
|
||||
parsedDate.getUTCFullYear() !== yearNumber ||
|
||||
parsedDate.getUTCMonth() !== monthNumber - 1 ||
|
||||
parsedDate.getUTCDate() !== dayNumber
|
||||
) {
|
||||
throw new Error(invalidMessage);
|
||||
}
|
||||
} else {
|
||||
parsedDate = billingDate;
|
||||
parsedDate = dateValue;
|
||||
}
|
||||
|
||||
if (Number.isNaN(parsedDate.getTime())) {
|
||||
throw new Error("La fecha del comprobante no es válida");
|
||||
throw new Error(invalidMessage);
|
||||
}
|
||||
|
||||
return parsedDate;
|
||||
}
|
||||
|
||||
private parseBillingDate(billingDate: Date | string): Date {
|
||||
const parsedDate = this.parseArgentinaCalendarDate(
|
||||
billingDate,
|
||||
"Se requiere la fecha del comprobante",
|
||||
"La fecha del comprobante no es válida"
|
||||
);
|
||||
|
||||
const selectedDate = this.formatArgentinaCalendarDate(parsedDate);
|
||||
const todayDate = this.formatArgentinaCalendarDate(new Date());
|
||||
|
||||
@@ -99,12 +127,66 @@ class InvoicesManager implements IInvoicesManager {
|
||||
return parsedDate;
|
||||
}
|
||||
|
||||
private validatePaymentMethod(paymentMethod: string): INVOICE_PAYMENT_METHOD {
|
||||
if (!Object.values(INVOICE_PAYMENT_METHOD).includes(paymentMethod as INVOICE_PAYMENT_METHOD)) {
|
||||
throw new Error("El medio de pago de la factura no es válido");
|
||||
private validateSaleCondition(saleCondition: string): INVOICE_SALE_CONDITION {
|
||||
if (!Object.values(INVOICE_SALE_CONDITION).includes(saleCondition as INVOICE_SALE_CONDITION)) {
|
||||
throw new Error("La condición de venta de la factura no es válida");
|
||||
}
|
||||
|
||||
return paymentMethod as INVOICE_PAYMENT_METHOD;
|
||||
return saleCondition as INVOICE_SALE_CONDITION;
|
||||
}
|
||||
|
||||
private trimOptional(value?: string): string | undefined {
|
||||
const trimmed = value?.trim();
|
||||
return trimmed || undefined;
|
||||
}
|
||||
|
||||
private parseReceiverDocumentCode(value: number | string, fieldName: string): number {
|
||||
if (typeof value === "string" && value.trim() === "") {
|
||||
throw new Error(`${fieldName} del receptor no es válido`);
|
||||
}
|
||||
|
||||
const parsedValue = typeof value === "string" ? Number(value.trim()) : value;
|
||||
|
||||
if (!Number.isInteger(parsedValue) || !Number.isFinite(parsedValue) || parsedValue < 0) {
|
||||
throw new Error(`${fieldName} del receptor no es válido`);
|
||||
}
|
||||
|
||||
return parsedValue;
|
||||
}
|
||||
|
||||
private validateReceiverDocumentPair(data: CreateInvoiceFromCashMovementParams): {
|
||||
receiverDocumentType: number;
|
||||
receiverDocumentNumber: number;
|
||||
} {
|
||||
const hasDocumentType = data.receiverDocumentType !== undefined && data.receiverDocumentType !== null && data.receiverDocumentType !== "";
|
||||
const hasDocumentNumber = data.receiverDocumentNumber !== undefined && data.receiverDocumentNumber !== null && data.receiverDocumentNumber !== "";
|
||||
|
||||
if (hasDocumentType !== hasDocumentNumber) {
|
||||
throw new Error("Tipo y número de documento del receptor deben informarse juntos");
|
||||
}
|
||||
|
||||
if (!hasDocumentType && !hasDocumentNumber) {
|
||||
return {
|
||||
receiverDocumentType: FINAL_CONSUMER_DOCUMENT_TYPE,
|
||||
receiverDocumentNumber: FINAL_CONSUMER_DOCUMENT_NUMBER,
|
||||
};
|
||||
}
|
||||
|
||||
const receiverDocumentType = this.parseReceiverDocumentCode(data.receiverDocumentType as number | string, "El tipo de documento");
|
||||
const receiverDocumentNumber = this.parseReceiverDocumentCode(data.receiverDocumentNumber as number | string, "El número de documento");
|
||||
|
||||
if (!FINAL_CONSUMER_COMPATIBLE_DOCUMENT_TYPES.has(receiverDocumentType)) {
|
||||
throw new Error("El tipo de documento del receptor no es compatible con Consumidor Final");
|
||||
}
|
||||
|
||||
if (receiverDocumentType === FINAL_CONSUMER_DOCUMENT_TYPE && receiverDocumentNumber !== FINAL_CONSUMER_DOCUMENT_NUMBER) {
|
||||
throw new Error("El número de documento debe ser 0 cuando el tipo es Consumidor Final");
|
||||
}
|
||||
|
||||
return {
|
||||
receiverDocumentType,
|
||||
receiverDocumentNumber,
|
||||
};
|
||||
}
|
||||
|
||||
public async createFromCashMovement(data: CreateInvoiceFromCashMovementParams): Promise<IInvoice> {
|
||||
@@ -115,21 +197,71 @@ class InvoicesManager implements IInvoicesManager {
|
||||
}
|
||||
|
||||
const billingDate = this.parseBillingDate(data.billingDate);
|
||||
const description = data.description?.trim();
|
||||
const servicePeriodFrom = this.parseArgentinaCalendarDate(
|
||||
data.servicePeriodFrom,
|
||||
"Se requiere la fecha de inicio del período facturado",
|
||||
"La fecha de inicio del período facturado no es válida"
|
||||
);
|
||||
const servicePeriodTo = this.parseArgentinaCalendarDate(
|
||||
data.servicePeriodTo,
|
||||
"Se requiere la fecha de fin del período facturado",
|
||||
"La fecha de fin del período facturado no es válida"
|
||||
);
|
||||
const paymentDueDate = this.parseArgentinaCalendarDate(
|
||||
data.paymentDueDate,
|
||||
"Se requiere el vencimiento de pago",
|
||||
"El vencimiento de pago no es válido"
|
||||
);
|
||||
const servicePeriodFromValue = this.formatArgentinaCalendarDate(servicePeriodFrom);
|
||||
const servicePeriodToValue = this.formatArgentinaCalendarDate(servicePeriodTo);
|
||||
const billingDateValue = this.formatArgentinaCalendarDate(billingDate);
|
||||
const paymentDueDateValue = this.formatArgentinaCalendarDate(paymentDueDate);
|
||||
|
||||
if (!description) {
|
||||
throw new Error("Se requiere el detalle de la factura");
|
||||
if (servicePeriodFromValue > servicePeriodToValue) {
|
||||
throw new Error("El inicio del período facturado no puede ser posterior al fin");
|
||||
}
|
||||
|
||||
if (description.length > DESCRIPTION_MAX_LENGTH) {
|
||||
throw new Error(`El detalle de la factura no puede superar los ${DESCRIPTION_MAX_LENGTH} caracteres`);
|
||||
if (paymentDueDateValue < billingDateValue) {
|
||||
throw new Error("El vencimiento de pago no puede ser anterior a la fecha del comprobante");
|
||||
}
|
||||
|
||||
if (!data.paymentMethod || !data.paymentMethod.trim()) {
|
||||
throw new Error("Se requiere el medio de pago de la factura");
|
||||
const activityCode = data.activityCode?.trim();
|
||||
const activityDescription = data.activityDescription?.trim();
|
||||
const itemDescription = data.itemDescription?.trim();
|
||||
|
||||
if (!activityCode) {
|
||||
throw new Error("Se requiere el código de actividad");
|
||||
}
|
||||
|
||||
const paymentMethod = this.validatePaymentMethod(data.paymentMethod.trim());
|
||||
if (!activityDescription) {
|
||||
throw new Error("Se requiere la descripción de la actividad");
|
||||
}
|
||||
|
||||
if (!itemDescription) {
|
||||
throw new Error("Se requiere la descripción del ítem facturado");
|
||||
}
|
||||
|
||||
if ([activityCode, activityDescription, itemDescription].some((value) => value.length > TEXT_MAX_LENGTH)) {
|
||||
throw new Error(`Los datos de actividad e ítem no pueden superar los ${TEXT_MAX_LENGTH} caracteres`);
|
||||
}
|
||||
|
||||
if (!data.saleCondition || !data.saleCondition.trim()) {
|
||||
throw new Error("Se requiere la condición de venta");
|
||||
}
|
||||
|
||||
const saleCondition = this.validateSaleCondition(data.saleCondition.trim());
|
||||
const receiverDocument = this.validateReceiverDocumentPair(data);
|
||||
const itemQuantity = data.itemQuantity ?? 1;
|
||||
const itemDiscountPercent = data.itemDiscountPercent ?? 0;
|
||||
const itemDiscountAmount = data.itemDiscountAmount ?? 0;
|
||||
|
||||
if (!Number.isFinite(data.itemUnitPrice) || !Number.isFinite(data.itemSubtotal)) {
|
||||
throw new Error("El importe del ítem no es válido");
|
||||
}
|
||||
|
||||
if (itemQuantity !== 1 || data.itemUnitPrice <= 0 || itemDiscountPercent !== 0 || itemDiscountAmount !== 0) {
|
||||
throw new Error("El MVP permite un único ítem de servicio sin descuentos por el total del movimiento");
|
||||
}
|
||||
|
||||
const movement = await CashFlow.movements.movementList.findOne({
|
||||
_id: data.cashMovementId,
|
||||
@@ -147,6 +279,17 @@ class InvoicesManager implements IInvoicesManager {
|
||||
throw new Error("El movimiento de caja no es facturable");
|
||||
}
|
||||
|
||||
const expectedSubtotal = Number((itemQuantity * data.itemUnitPrice - itemDiscountAmount).toFixed(2));
|
||||
const sentSubtotal = Number(data.itemSubtotal.toFixed(2));
|
||||
const movementAmount = Number(movement.amount.toFixed(2));
|
||||
|
||||
if (
|
||||
Math.abs(expectedSubtotal - sentSubtotal) > CENTS_TOLERANCE ||
|
||||
Math.abs(sentSubtotal - movementAmount) > CENTS_TOLERANCE
|
||||
) {
|
||||
throw new Error("El total del ítem debe coincidir con el importe del movimiento");
|
||||
}
|
||||
|
||||
const fiscalProfiles = await OrganizationFiscalProfiles.find({
|
||||
companyId,
|
||||
status: ORGANIZATION_FISCAL_PROFILE_STATUS.ACTIVE,
|
||||
@@ -195,8 +338,25 @@ class InvoicesManager implements IInvoicesManager {
|
||||
type: INVOICE_TYPE.FACTURA_C,
|
||||
status: INVOICE_STATUS.PENDING_ARCA,
|
||||
billingDate,
|
||||
paymentMethod,
|
||||
description,
|
||||
servicePeriodFrom,
|
||||
servicePeriodTo,
|
||||
paymentDueDate,
|
||||
activityCode,
|
||||
activityDescription,
|
||||
receiverIvaCondition: FINAL_CONSUMER_IVA_CONDITION_ID,
|
||||
receiverDocumentType: receiverDocument.receiverDocumentType,
|
||||
receiverDocumentNumber: receiverDocument.receiverDocumentNumber,
|
||||
receiverName: this.trimOptional(data.receiverName)?.slice(0, RECEIVER_TEXT_MAX_LENGTH),
|
||||
receiverAddress: this.trimOptional(data.receiverAddress)?.slice(0, RECEIVER_TEXT_MAX_LENGTH),
|
||||
receiverEmail: this.trimOptional(data.receiverEmail)?.slice(0, RECEIVER_TEXT_MAX_LENGTH),
|
||||
saleCondition,
|
||||
itemDescription,
|
||||
itemQuantity,
|
||||
itemUnit: this.trimOptional(data.itemUnit),
|
||||
itemUnitPrice: data.itemUnitPrice,
|
||||
itemDiscountPercent,
|
||||
itemDiscountAmount,
|
||||
itemSubtotal: sentSubtotal,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
@@ -233,8 +393,25 @@ class InvoicesManager implements IInvoicesManager {
|
||||
invoice = await this.invoices.update({
|
||||
cashMovementId: data.cashMovementId,
|
||||
billingDate,
|
||||
paymentMethod,
|
||||
description,
|
||||
servicePeriodFrom,
|
||||
servicePeriodTo,
|
||||
paymentDueDate,
|
||||
activityCode,
|
||||
activityDescription,
|
||||
receiverIvaCondition: FINAL_CONSUMER_IVA_CONDITION_ID,
|
||||
receiverDocumentType: receiverDocument.receiverDocumentType,
|
||||
receiverDocumentNumber: receiverDocument.receiverDocumentNumber,
|
||||
receiverName: this.trimOptional(data.receiverName)?.slice(0, RECEIVER_TEXT_MAX_LENGTH),
|
||||
receiverAddress: this.trimOptional(data.receiverAddress)?.slice(0, RECEIVER_TEXT_MAX_LENGTH),
|
||||
receiverEmail: this.trimOptional(data.receiverEmail)?.slice(0, RECEIVER_TEXT_MAX_LENGTH),
|
||||
saleCondition,
|
||||
itemDescription,
|
||||
itemQuantity,
|
||||
itemUnit: this.trimOptional(data.itemUnit),
|
||||
itemUnitPrice: data.itemUnitPrice,
|
||||
itemDiscountPercent,
|
||||
itemDiscountAmount,
|
||||
itemSubtotal: sentSubtotal,
|
||||
updatedAt: new Date(),
|
||||
}) || invoice;
|
||||
}
|
||||
@@ -255,6 +432,11 @@ class InvoicesManager implements IInvoicesManager {
|
||||
companyId,
|
||||
amount: movement.amount,
|
||||
billingDate,
|
||||
servicePeriodFrom,
|
||||
servicePeriodTo,
|
||||
paymentDueDate,
|
||||
receiverDocumentType: receiverDocument.receiverDocumentType,
|
||||
receiverDocumentNumber: receiverDocument.receiverDocumentNumber,
|
||||
sessionUser: data.sessionUser,
|
||||
});
|
||||
|
||||
|
||||
+2
@@ -34,6 +34,8 @@ export class OrganizationFiscalProfilesAdapterMongoose implements IOrganizationF
|
||||
enum: Object.values(ORGANIZATION_FISCAL_PROFILE_TAX_CONDITION),
|
||||
},
|
||||
pointOfSale: { type: Number, required: true },
|
||||
activityCode: { type: String, required: false },
|
||||
activityDescription: { type: String, required: false },
|
||||
status: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
||||
@@ -17,6 +17,8 @@ export type UpsertOrganizationFiscalProfileParams = {
|
||||
legalName: string;
|
||||
taxCondition: ORGANIZATION_FISCAL_PROFILE_TAX_CONDITION;
|
||||
pointOfSale: number;
|
||||
activityCode?: string;
|
||||
activityDescription?: string;
|
||||
sessionUser: string;
|
||||
};
|
||||
|
||||
@@ -33,6 +35,8 @@ export type CreateOrganizationFiscalProfileParams = {
|
||||
legalName: string;
|
||||
taxCondition: ORGANIZATION_FISCAL_PROFILE_TAX_CONDITION;
|
||||
pointOfSale: number;
|
||||
activityCode?: string;
|
||||
activityDescription?: string;
|
||||
status: ORGANIZATION_FISCAL_PROFILE_STATUS;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
@@ -43,6 +47,8 @@ export type UpdateOrganizationFiscalProfileParams = FindOrganizationFiscalProfil
|
||||
legalName?: string;
|
||||
taxCondition?: ORGANIZATION_FISCAL_PROFILE_TAX_CONDITION;
|
||||
pointOfSale?: number;
|
||||
activityCode?: string;
|
||||
activityDescription?: string;
|
||||
status?: ORGANIZATION_FISCAL_PROFILE_STATUS;
|
||||
updatedAt: Date;
|
||||
};
|
||||
@@ -54,6 +60,8 @@ export interface IOrganizationFiscalProfile {
|
||||
legalName: string;
|
||||
taxCondition: ORGANIZATION_FISCAL_PROFILE_TAX_CONDITION;
|
||||
pointOfSale: number;
|
||||
activityCode?: string;
|
||||
activityDescription?: string;
|
||||
status: ORGANIZATION_FISCAL_PROFILE_STATUS;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { validatePermissionsByCompany, validateSessionUser } from "../../helpers/check";
|
||||
import { findArcaActivityByCode } from "../ArcaActivities/ArcaActivities.Catalog";
|
||||
import { OrganizationFiscalProfilesAdapterMongoose } from "./OrganizationFiscalProfiles.Adapter.Mongoose";
|
||||
import {
|
||||
FindOrganizationFiscalProfilesParams,
|
||||
@@ -31,6 +32,31 @@ class OrganizationFiscalProfilesManager implements IOrganizationFiscalProfilesMa
|
||||
if (!data.pointOfSale || data.pointOfSale <= 0) {
|
||||
throw new Error("El punto de venta debe ser mayor a 0");
|
||||
}
|
||||
|
||||
const activityCode = data.activityCode?.trim();
|
||||
|
||||
if (activityCode && !/^\d{6}$/.test(activityCode)) {
|
||||
throw new Error("El código de actividad debe contener 6 dígitos");
|
||||
}
|
||||
|
||||
if (activityCode && !findArcaActivityByCode(activityCode)) {
|
||||
throw new Error("La actividad ARCA/CLAE seleccionada no existe en el catálogo");
|
||||
}
|
||||
}
|
||||
|
||||
private buildProfileData(data: UpsertOrganizationFiscalProfileParams) {
|
||||
const activityCode = data.activityCode?.trim();
|
||||
const activity = activityCode ? findArcaActivityByCode(activityCode) : undefined;
|
||||
|
||||
return {
|
||||
cuit: data.cuit,
|
||||
legalName: data.legalName.trim(),
|
||||
taxCondition: data.taxCondition,
|
||||
pointOfSale: data.pointOfSale,
|
||||
activityCode: activity?.code || "",
|
||||
activityDescription: activity?.description || "",
|
||||
status: ORGANIZATION_FISCAL_PROFILE_STATUS.ACTIVE,
|
||||
};
|
||||
}
|
||||
|
||||
public async upsert(
|
||||
@@ -51,15 +77,12 @@ class OrganizationFiscalProfilesManager implements IOrganizationFiscalProfilesMa
|
||||
});
|
||||
|
||||
const now = new Date();
|
||||
const profileData = this.buildProfileData(data);
|
||||
|
||||
if (existingProfile) {
|
||||
const updatedProfile = await this.organizationFiscalProfiles.update({
|
||||
companyId: data.companyId,
|
||||
cuit: data.cuit,
|
||||
legalName: data.legalName.trim(),
|
||||
taxCondition: data.taxCondition,
|
||||
pointOfSale: data.pointOfSale,
|
||||
status: ORGANIZATION_FISCAL_PROFILE_STATUS.ACTIVE,
|
||||
...profileData,
|
||||
updatedAt: now,
|
||||
});
|
||||
|
||||
@@ -73,11 +96,7 @@ class OrganizationFiscalProfilesManager implements IOrganizationFiscalProfilesMa
|
||||
try {
|
||||
return await this.organizationFiscalProfiles.create({
|
||||
companyId: data.companyId,
|
||||
cuit: data.cuit,
|
||||
legalName: data.legalName.trim(),
|
||||
taxCondition: data.taxCondition,
|
||||
pointOfSale: data.pointOfSale,
|
||||
status: ORGANIZATION_FISCAL_PROFILE_STATUS.ACTIVE,
|
||||
...profileData,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
@@ -85,11 +104,7 @@ class OrganizationFiscalProfilesManager implements IOrganizationFiscalProfilesMa
|
||||
if (this.isDuplicateKeyError(error)) {
|
||||
const updatedProfile = await this.organizationFiscalProfiles.update({
|
||||
companyId: data.companyId,
|
||||
cuit: data.cuit,
|
||||
legalName: data.legalName.trim(),
|
||||
taxCondition: data.taxCondition,
|
||||
pointOfSale: data.pointOfSale,
|
||||
status: ORGANIZATION_FISCAL_PROFILE_STATUS.ACTIVE,
|
||||
...profileData,
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user