feat: implement ARCA integration architecture including credential, fiscal profile, and invoice models, services, and onboarding flows

This commit is contained in:
2026-08-06 19:36:53 -03:00
parent 3b56d00fcb
commit a93681357f
28 changed files with 2935 additions and 39 deletions
@@ -0,0 +1,83 @@
import { Document, FilterQuery, Model, Schema, model } from "mongoose";
import {
ARCA_CREDENTIAL_STATUS,
CreateArcaCredentialParams,
FindArcaCredentialsParams,
IArcaCredential,
IArcaCredentialsAdapter,
UpdateArcaCredentialParams,
} from "./ArcaCredentials.Interface";
export interface IArcaCredentialDocument extends Omit<IArcaCredential, "id">, Document {}
export class ArcaCredentialsAdapterMongoose implements IArcaCredentialsAdapter {
schema: Schema;
arcaCredentialList: Model<IArcaCredentialDocument>;
constructor() {
this.schema = new Schema({
companyId: {
type: Schema.Types.ObjectId,
required: true,
ref: "Company",
unique: true,
index: true,
},
csrPem: { type: String, required: false },
certificatePem: { type: String, required: false },
encryptedPrivateKey: { type: String, required: true },
privateKeyIv: { type: String, required: true },
privateKeyAuthTag: { type: String, required: true },
encryptionKeyVersion: { type: String, required: false, default: "v1" },
status: {
type: String,
required: true,
enum: Object.values(ARCA_CREDENTIAL_STATUS),
index: true,
},
certificateValidFrom: { type: Date, required: false },
certificateValidTo: { type: Date, required: false },
lastValidationError: { type: String, required: false },
createdAt: { type: Date, required: true, default: Date.now },
updatedAt: { type: Date, required: true, default: Date.now },
});
this.arcaCredentialList = model<IArcaCredentialDocument>("ArcaCredential", this.schema);
}
public async create(data: CreateArcaCredentialParams): Promise<IArcaCredential> {
return await this.arcaCredentialList.create(data);
}
private buildSearchCriteria(filters: FindArcaCredentialsParams): FilterQuery<IArcaCredential> {
const searchCriteria: FilterQuery<IArcaCredential> = {};
if (filters.id) searchCriteria._id = filters.id;
if (filters.companyId) searchCriteria.companyId = filters.companyId;
if (filters.status) searchCriteria.status = filters.status;
return searchCriteria;
}
public async find(filters: FindArcaCredentialsParams): Promise<IArcaCredential[]> {
const searchCriteria = this.buildSearchCriteria(filters);
return this.arcaCredentialList.find(searchCriteria).sort({ createdAt: -1 }).exec();
}
public async findOne(filters: FindArcaCredentialsParams): Promise<IArcaCredentialDocument | null> {
const searchCriteria = this.buildSearchCriteria(filters);
return this.arcaCredentialList.findOne(searchCriteria).exec();
}
public async update(data: UpdateArcaCredentialParams): Promise<IArcaCredentialDocument | null> {
const { id, companyId, sessionUser, ...updateData } = data;
if (!id && !companyId) {
throw new Error("Se requiere un identificador para actualizar las credenciales ARCA");
}
return this.arcaCredentialList
.findOneAndUpdate(this.buildSearchCriteria({ id, companyId, sessionUser }), updateData, { new: true })
.exec();
}
}
@@ -0,0 +1,159 @@
import { IArcaCredentialDocument } from "./ArcaCredentials.Adapter.Mongoose";
export enum ARCA_CREDENTIAL_STATUS {
CSR_GENERATED = "CSR_GENERATED",
CERTIFICATE_UPLOADED = "CERTIFICATE_UPLOADED",
READY = "READY",
INVALID = "INVALID",
DISABLED = "DISABLED",
}
export type GenerateArcaCredentialCsrParams = {
companyId: string;
sessionUser: string;
};
export type UploadArcaCredentialCertificateParams = {
companyId: string;
certificatePem: string;
sessionUser: string;
};
export type TestArcaWsaaLoginParams = {
companyId: string;
sessionUser: string;
};
export type ArcaWsaaLoginTestResult = {
status: "cached" | "created";
environment: "homologation";
service: "wsfe";
expirationTime: Date;
cached: boolean;
};
export type ArcaWsfeLastVoucherParams = {
companyId: string;
sessionUser: string;
};
export type ArcaWsfeLastVoucherResult = {
environment: "homologation";
service: "wsfe";
pointOfSale: number;
voucherType: 11;
lastVoucherNumber: number;
};
export type ArcaWsfeDiagnosticsParams = {
companyId: string;
sessionUser: string;
};
export type ArcaWsfeDiagnosticsResult = {
environment: "homologation";
service: "wsfe";
dummy: {
ok: boolean;
appServer?: string;
dbServer?: string;
authServer?: string;
error?: string;
};
voucherTypes: {
ok: boolean;
count?: number;
includesFacturaC?: boolean;
error?: string;
};
pointsOfSale: {
ok: boolean;
points?: number[];
includesConfiguredPointOfSale?: boolean;
configuredPointOfSale: number;
error?: string;
};
lastVoucher: {
ok: boolean;
pointOfSale: number;
voucherType: 11;
lastVoucherNumber?: number;
error?: string;
};
};
export type FindArcaCredentialsParams = {
id?: string;
companyId?: string;
status?: ARCA_CREDENTIAL_STATUS;
sessionUser?: string;
};
export type CreateArcaCredentialParams = {
companyId: string;
csrPem?: string;
certificatePem?: string;
encryptedPrivateKey: string;
privateKeyIv: string;
privateKeyAuthTag: string;
encryptionKeyVersion?: string;
status: ARCA_CREDENTIAL_STATUS;
certificateValidFrom?: Date;
certificateValidTo?: Date;
lastValidationError?: string;
createdAt: Date;
updatedAt: Date;
};
export type UpdateArcaCredentialParams = FindArcaCredentialsParams & {
csrPem?: string;
certificatePem?: string;
encryptedPrivateKey?: string;
privateKeyIv?: string;
privateKeyAuthTag?: string;
encryptionKeyVersion?: string;
status?: ARCA_CREDENTIAL_STATUS;
certificateValidFrom?: Date;
certificateValidTo?: Date;
lastValidationError?: string;
updatedAt: Date;
};
export interface IArcaCredential {
id?: string;
companyId: string;
csrPem?: string;
certificatePem?: string;
encryptedPrivateKey: string;
privateKeyIv: string;
privateKeyAuthTag: string;
encryptionKeyVersion?: string;
status: ARCA_CREDENTIAL_STATUS;
certificateValidFrom?: Date;
certificateValidTo?: Date;
lastValidationError?: string;
createdAt: Date;
updatedAt: Date;
}
export type ArcaCredentialView = Omit<
IArcaCredential,
"encryptedPrivateKey" | "privateKeyIv" | "privateKeyAuthTag"
>;
export interface IArcaCredentialsAdapter {
create(data: CreateArcaCredentialParams): Promise<IArcaCredential>;
find(filters: FindArcaCredentialsParams): Promise<IArcaCredential[]>;
findOne(filters: FindArcaCredentialsParams): Promise<IArcaCredentialDocument | null>;
update(data: UpdateArcaCredentialParams): Promise<IArcaCredentialDocument | null>;
}
export interface IArcaCredentialsManager {
arcaCredentials: IArcaCredentialsAdapter;
generateCsr(data: GenerateArcaCredentialCsrParams): Promise<ArcaCredentialView>;
uploadCertificate(data: UploadArcaCredentialCertificateParams): Promise<ArcaCredentialView>;
testWsaaLogin(data: TestArcaWsaaLoginParams): Promise<ArcaWsaaLoginTestResult>;
getWsfeLastVoucher(data: ArcaWsfeLastVoucherParams): Promise<ArcaWsfeLastVoucherResult>;
getWsfeDiagnostics(data: ArcaWsfeDiagnosticsParams): Promise<ArcaWsfeDiagnosticsResult>;
find(data: FindArcaCredentialsParams): Promise<ArcaCredentialView[]>;
}
@@ -0,0 +1,796 @@
import crypto from "crypto";
import axios from "axios";
import forge from "node-forge";
import { validatePermissionsByCompany, validateSessionUser } from "../../helpers/check";
import ArcaWsaaTokens from "../ArcaWsaaTokens/ArcaWsaaTokens";
import {
ARCA_WSAA_ENVIRONMENT,
ARCA_WSAA_SERVICE,
} from "../ArcaWsaaTokens/ArcaWsaaTokens.Interface";
import OrganizationFiscalProfiles from "../OrganizationFiscalProfiles/OrganizationFiscalProfiles";
import {
IOrganizationFiscalProfile,
ORGANIZATION_FISCAL_PROFILE_STATUS,
} from "../OrganizationFiscalProfiles/OrganizationFiscalProfiles.Interface";
import { ArcaCredentialsAdapterMongoose } from "./ArcaCredentials.Adapter.Mongoose";
import {
ARCA_CREDENTIAL_STATUS,
ArcaCredentialView,
ArcaWsfeDiagnosticsParams,
ArcaWsfeDiagnosticsResult,
ArcaWsfeLastVoucherParams,
ArcaWsfeLastVoucherResult,
FindArcaCredentialsParams,
GenerateArcaCredentialCsrParams,
ArcaWsaaLoginTestResult,
IArcaCredential,
IArcaCredentialsManager,
TestArcaWsaaLoginParams,
UploadArcaCredentialCertificateParams,
} from "./ArcaCredentials.Interface";
class ArcaCredentialsManager implements IArcaCredentialsManager {
arcaCredentials: ArcaCredentialsAdapterMongoose;
constructor() {
this.arcaCredentials = new ArcaCredentialsAdapterMongoose();
}
private getMasterKey(): Buffer {
const configuredKey = process.env.ARCA_CREDENTIALS_MASTER_KEY;
if (!configuredKey) {
throw new Error("ARCA_CREDENTIALS_MASTER_KEY no está configurada");
}
const key = /^[a-fA-F0-9]{64}$/.test(configuredKey)
? Buffer.from(configuredKey, "hex")
: Buffer.from(configuredKey, "base64");
if (key.length !== 32) {
throw new Error("ARCA_CREDENTIALS_MASTER_KEY debe tener 32 bytes en base64 o 64 caracteres hexadecimales");
}
return key;
}
private encryptPrivateKey(privateKeyPem: string): {
encryptedPrivateKey: string;
privateKeyIv: string;
privateKeyAuthTag: string;
} {
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv(
"aes-256-gcm",
crypto.createSecretKey(new Uint8Array(this.getMasterKey())),
new Uint8Array(iv)
);
const encryptedPrivateKey = cipher.update(privateKeyPem, "utf8", "base64") + cipher.final("base64");
return {
encryptedPrivateKey,
privateKeyIv: iv.toString("base64"),
privateKeyAuthTag: cipher.getAuthTag().toString("base64"),
};
}
private decryptPrivateKey(credential: IArcaCredential): string {
const decipher = crypto.createDecipheriv(
"aes-256-gcm",
crypto.createSecretKey(new Uint8Array(this.getMasterKey())),
new Uint8Array(Buffer.from(credential.privateKeyIv, "base64"))
);
decipher.setAuthTag(new Uint8Array(Buffer.from(credential.privateKeyAuthTag, "base64")));
return decipher.update(credential.encryptedPrivateKey, "base64", "utf8") + decipher.final("utf8");
}
private sanitizeCredential(credential: IArcaCredential): ArcaCredentialView {
const rawCredential = typeof (credential as any).toObject === "function"
? (credential as any).toObject()
: credential;
const { encryptedPrivateKey, privateKeyIv, privateKeyAuthTag, ...safeCredential } = rawCredential;
return safeCredential;
}
private async validateCompanyAdmin(companyId: string, sessionUser: string): Promise<void> {
await validateSessionUser({ sessionUser });
if (!companyId || !companyId.trim()) {
throw new Error("Se requiere una organización para operar credenciales ARCA");
}
await validatePermissionsByCompany({ companyId, sessionUser });
}
private async requireActiveFiscalProfile(
companyId: string,
sessionUser: string
): Promise<IOrganizationFiscalProfile> {
const profiles = await OrganizationFiscalProfiles.find({
companyId,
status: ORGANIZATION_FISCAL_PROFILE_STATUS.ACTIVE,
sessionUser,
});
if (!profiles.length) {
throw new Error("Se requiere un perfil fiscal activo para generar credenciales ARCA");
}
return profiles[0];
}
private parseCertificate(certificatePem: string): forge.pki.Certificate {
try {
return forge.pki.certificateFromPem(certificatePem);
} catch (error) {
const errorOccurred = error as Error;
throw new Error(`El certificado ARCA no es válido: ${errorOccurred.message}`);
}
}
private certificateMatchesPrivateKey(certificate: forge.pki.Certificate, privateKeyPem: string): boolean {
const privateKey = forge.pki.privateKeyFromPem(privateKeyPem);
const publicKeyFromPrivateKey = forge.pki.setRsaPublicKey(privateKey.n, privateKey.e);
return forge.pki.publicKeyToPem(certificate.publicKey) === forge.pki.publicKeyToPem(publicKeyFromPrivateKey);
}
private formatWsaaDate(date: Date): string {
// WSAA accepts ISO-8601 timestamps with an explicit offset; keep the Date instant in UTC.
return date.toISOString().replace(/\.\d{3}Z$/, "+00:00");
}
private buildTraXml(service: ARCA_WSAA_SERVICE): {
traXml: string;
generationTime: Date;
expirationTime: Date;
} {
const generationTime = new Date(Date.now() - 10 * 60 * 1000);
const expirationTime = new Date(Date.now() + 12 * 60 * 60 * 1000);
const uniqueId = Math.floor(Date.now() / 1000);
return {
generationTime,
expirationTime,
traXml: `<?xml version="1.0" encoding="UTF-8"?>\n<loginTicketRequest version="1.0">\n<header>\n<uniqueId>${uniqueId}</uniqueId>\n<generationTime>${this.formatWsaaDate(generationTime)}</generationTime>\n<expirationTime>${this.formatWsaaDate(expirationTime)}</expirationTime>\n</header>\n<service>${service}</service>\n</loginTicketRequest>`,
};
}
private signTraCms(traXml: string, certificatePem: string, privateKeyPem: string): string {
const p7 = forge.pkcs7.createSignedData();
p7.content = forge.util.createBuffer(traXml, "utf8");
p7.addCertificate(certificatePem);
p7.addSigner({
key: privateKeyPem,
certificate: certificatePem,
digestAlgorithm: forge.pki.oids.sha256,
authenticatedAttributes: [
{ type: forge.pki.oids.contentType, value: forge.pki.oids.data },
{ type: forge.pki.oids.messageDigest },
{ type: forge.pki.oids.signingTime, value: new Date() as any },
],
});
p7.sign();
const der = forge.asn1.toDer(p7.toAsn1()).getBytes();
return Buffer.from(der, "binary").toString("base64");
}
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>`;
}
private extractXmlValue(xml: string, tagName: string): string | undefined {
const match = xml.match(new RegExp(`<(?:\\w+:)?${tagName}[^>]*>([\\s\\S]*?)</(?:\\w+:)?${tagName}>`));
return match?.[1]?.trim();
}
private decodeXmlEntities(value: string): string {
return value
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, '"')
.replace(/&apos;/g, "'")
.replace(/&amp;/g, "&");
}
private parseLoginCmsReturn(xml: string): {
token: string;
sign: string;
generationTime?: Date;
expirationTime: Date;
} {
const loginCmsReturn = this.extractXmlValue(xml, "loginCmsReturn");
const responseXml = loginCmsReturn ? this.decodeXmlEntities(loginCmsReturn) : xml;
const token = this.extractXmlValue(responseXml, "token");
const sign = this.extractXmlValue(responseXml, "sign");
const expirationTime = this.extractXmlValue(responseXml, "expirationTime");
const generationTime = this.extractXmlValue(responseXml, "generationTime");
if (!token || !sign || !expirationTime) {
throw new Error("ARCA WSAA no devolvió token, firma o vencimiento válidos");
}
return {
token,
sign,
generationTime: generationTime ? new Date(generationTime) : undefined,
expirationTime: new Date(expirationTime),
};
}
private async getWsaaLoginTicket(companyId: string): Promise<{
token: string;
sign: string;
expirationTime: Date;
cached: boolean;
}> {
const service = ARCA_WSAA_SERVICE.WSFE;
const environment = ARCA_WSAA_ENVIRONMENT.HOMOLOGATION;
const cachedToken = await ArcaWsaaTokens.getValidToken({ companyId, service, environment });
if (cachedToken) {
return {
token: cachedToken.token,
sign: cachedToken.sign,
expirationTime: cachedToken.expirationTime,
cached: true,
};
}
const credential = await this.arcaCredentials.findOne({
companyId,
status: ARCA_CREDENTIAL_STATUS.READY,
});
if (!credential?.certificatePem) {
throw new Error("Se requieren credenciales ARCA READY con certificado cargado");
}
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;
if (!wsaaUrl) {
throw new Error("ARCA_WSAA_HOMOLOGATION_URL no está configurada");
}
let response;
try {
response = await axios.post(wsaaUrl, this.buildLoginCmsSoap(cms), {
timeout: 30000,
headers: {
"Content-Type": "text/xml; charset=utf-8",
SOAPAction: "loginCms",
},
transformResponse: [(body) => body],
});
} catch (error) {
throw new Error(this.buildWsaaErrorMessage(error));
}
const wsaaToken = this.parseLoginCmsReturn(String(response.data));
const savedToken = await ArcaWsaaTokens.upsertToken({
companyId,
service,
environment,
...wsaaToken,
});
return {
token: savedToken.token,
sign: savedToken.sign,
expirationTime: savedToken.expirationTime,
cached: false,
};
}
private buildWsfeLastVoucherSoap(data: {
token: string;
sign: string;
cuit: string;
pointOfSale: number;
voucherType: 11;
}): string {
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>
</Auth>
<PtoVta>${data.pointOfSale}</PtoVta>
<CbteTipo>${data.voucherType}</CbteTipo>
</FECompUltimoAutorizado>
</soapenv:Body>
</soapenv:Envelope>`;
}
private buildWsfeDummySoap(): string {
return `<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Header/>
<soapenv:Body>
<FEDummy xmlns="http://ar.gov.afip.dif.FEV1/" />
</soapenv:Body>
</soapenv:Envelope>`;
}
private buildWsfeParamGetTiposCbteSoap(data: { token: string; sign: string; cuit: string }): string {
return `<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Header/>
<soapenv:Body>
<FEParamGetTiposCbte xmlns="http://ar.gov.afip.dif.FEV1/">
<Auth>
<Token>${data.token}</Token>
<Sign>${data.sign}</Sign>
<Cuit>${data.cuit}</Cuit>
</Auth>
</FEParamGetTiposCbte>
</soapenv:Body>
</soapenv:Envelope>`;
}
private buildWsfeParamGetPtosVentaSoap(data: { token: string; sign: string; cuit: string }): string {
return `<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Header/>
<soapenv:Body>
<FEParamGetPtosVenta xmlns="http://ar.gov.afip.dif.FEV1/">
<Auth>
<Token>${data.token}</Token>
<Sign>${data.sign}</Sign>
<Cuit>${data.cuit}</Cuit>
</Auth>
</FEParamGetPtosVenta>
</soapenv:Body>
</soapenv:Envelope>`;
}
private async postWsfe(soapBody: string, soapAction: string): Promise<string> {
const wsfeUrl = process.env.ARCA_WSFE_HOMOLOGATION_URL;
if (!wsfeUrl) {
throw new Error("ARCA_WSFE_HOMOLOGATION_URL no está configurada");
}
const response = await axios.post(wsfeUrl, soapBody, {
timeout: 30000,
headers: {
"Content-Type": "text/xml; charset=utf-8",
SOAPAction: `"http://ar.gov.afip.dif.FEV1/${soapAction}"`,
},
transformResponse: [(body) => body],
});
return String(response.data);
}
private buildWsfeErrorMessage(error: unknown): string {
if (!axios.isAxiosError(error)) {
return error instanceof Error ? error.message : "No pudimos conectar con ARCA WSFE";
}
const responseBody = typeof error.response?.data === "string" ? error.response.data : "";
const faultString = responseBody ? this.extractXmlValue(responseBody, "faultstring") : undefined;
const decodedFault = faultString ? this.decodeXmlEntities(faultString) : undefined;
if (decodedFault) {
return `ARCA WSFE rechazó la consulta: ${decodedFault}`;
}
const htmlTitle = responseBody ? this.extractHtmlTitle(responseBody) : undefined;
if (htmlTitle && error.response?.status) {
return `ARCA WSFE respondió con estado ${error.response.status}: ${htmlTitle}`;
}
if (error.code === "ECONNABORTED") {
return "ARCA WSFE no respondió a tiempo. Probá nuevamente en unos minutos.";
}
if (error.response?.status) {
if (responseBody) {
const compactBody = responseBody.replace(/\s+/g, " ").trim().slice(0, 500);
return `ARCA WSFE respondió con estado ${error.response.status}: ${compactBody}`;
}
return `ARCA WSFE respondió con estado ${error.response.status}`;
}
return error.message || "No pudimos conectar con ARCA WSFE";
}
private extractHtmlTitle(html: string): string | undefined {
const title = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i)?.[1]?.replace(/\s+/g, " ").trim();
return title ? this.decodeXmlEntities(title) : undefined;
}
private parseWsfeLastVoucherReturn(xml: string): number {
const faultString = this.extractXmlValue(xml, "faultstring");
if (faultString) {
throw new Error(`ARCA WSFE rechazó la consulta: ${this.decodeXmlEntities(faultString)}`);
}
const voucherNumber = this.extractXmlValue(xml, "CbteNro");
if (!voucherNumber) {
const errorMessage = this.extractXmlValue(xml, "Msg");
if (errorMessage) {
throw new Error(`ARCA WSFE rechazó la consulta: ${this.decodeXmlEntities(errorMessage)}`);
}
throw new Error("ARCA WSFE no devolvió el último comprobante autorizado");
}
const parsedVoucherNumber = Number(voucherNumber);
if (!Number.isFinite(parsedVoucherNumber)) {
throw new Error("ARCA WSFE devolvió un número de comprobante inválido");
}
return parsedVoucherNumber;
}
private parseWsfeDummyReturn(xml: string): ArcaWsfeDiagnosticsResult["dummy"] {
const errorMessage = this.extractWsfeResponseError(xml);
if (errorMessage) {
return { ok: false, error: errorMessage };
}
return {
ok: true,
appServer: this.extractXmlValue(xml, "AppServer"),
dbServer: this.extractXmlValue(xml, "DbServer"),
authServer: this.extractXmlValue(xml, "AuthServer"),
};
}
private parseWsfeVoucherTypesReturn(xml: string): ArcaWsfeDiagnosticsResult["voucherTypes"] {
const errorMessage = this.extractWsfeResponseError(xml);
if (errorMessage) {
return { ok: false, error: errorMessage };
}
const ids = Array.from(xml.matchAll(/<(?:\w+:)?Id>(\d+)<\/(?:\w+:)?Id>/g)).map((match) => Number(match[1]));
return {
ok: true,
count: ids.length,
includesFacturaC: ids.includes(11),
};
}
private parseWsfePointsOfSaleReturn(
xml: string,
configuredPointOfSale: number
): ArcaWsfeDiagnosticsResult["pointsOfSale"] {
const errorMessage = this.extractWsfeResponseError(xml);
if (errorMessage) {
return { ok: false, configuredPointOfSale, error: errorMessage };
}
const points = Array.from(xml.matchAll(/<(?:\w+:)?PtoVenta>(\d+)<\/(?:\w+:)?PtoVenta>/g)).map((match) => Number(match[1]));
return {
ok: true,
points,
includesConfiguredPointOfSale: points.includes(configuredPointOfSale),
configuredPointOfSale,
};
}
private extractWsfeResponseError(xml: string): string | undefined {
const faultString = this.extractXmlValue(xml, "faultstring");
const msg = this.extractXmlValue(xml, "Msg");
const errorMessage = faultString || msg;
return errorMessage ? this.decodeXmlEntities(errorMessage) : undefined;
}
private buildWsaaErrorMessage(error: unknown): string {
if (!axios.isAxiosError(error)) {
return error instanceof Error ? error.message : "No pudimos conectar con ARCA WSAA";
}
const responseBody = typeof error.response?.data === "string" ? error.response.data : "";
const faultString = responseBody ? this.extractXmlValue(responseBody, "faultstring") : undefined;
const decodedFault = faultString ? this.decodeXmlEntities(faultString) : undefined;
if (decodedFault) {
return `ARCA WSAA rechazó la conexión: ${decodedFault}`;
}
if (error.code === "ECONNABORTED") {
return "ARCA WSAA no respondió a tiempo. Probá nuevamente en unos minutos.";
}
if (error.response?.status) {
return `ARCA WSAA respondió con estado ${error.response.status}`;
}
return error.message || "No pudimos conectar con ARCA WSAA";
}
public async generateCsr(data: GenerateArcaCredentialCsrParams): Promise<ArcaCredentialView> {
await this.validateCompanyAdmin(data.companyId, data.sessionUser);
const fiscalProfile = await this.requireActiveFiscalProfile(data.companyId, data.sessionUser);
const keys = forge.pki.rsa.generateKeyPair(2048);
const csr = forge.pki.createCertificationRequest();
csr.publicKey = keys.publicKey;
csr.setSubject([
{ name: "commonName", value: fiscalProfile.cuit },
{ name: "organizationName", value: fiscalProfile.legalName },
{ name: "serialNumber", value: `CUIT ${fiscalProfile.cuit}` },
{ name: "countryName", value: "AR" },
]);
csr.sign(keys.privateKey, forge.md.sha256.create());
if (!csr.verify()) {
throw new Error("No se pudo verificar el CSR generado para ARCA");
}
const csrPem = forge.pki.certificationRequestToPem(csr);
const privateKeyPem = forge.pki.privateKeyToPem(keys.privateKey);
const encryptedPrivateKey = this.encryptPrivateKey(privateKeyPem);
const now = new Date();
const existingCredential = await this.arcaCredentials.findOne({ companyId: data.companyId });
const credentialData = {
companyId: data.companyId,
csrPem,
...encryptedPrivateKey,
encryptionKeyVersion: process.env.ARCA_CREDENTIALS_KEY_VERSION || "v1",
status: ARCA_CREDENTIAL_STATUS.CSR_GENERATED,
updatedAt: now,
};
const credential = existingCredential
? await this.arcaCredentials.update(credentialData)
: await this.arcaCredentials.create({
...credentialData,
createdAt: now,
});
if (!credential) {
throw new Error("No se pudieron guardar las credenciales ARCA");
}
return this.sanitizeCredential(credential);
}
public async uploadCertificate(data: UploadArcaCredentialCertificateParams): Promise<ArcaCredentialView> {
await this.validateCompanyAdmin(data.companyId, data.sessionUser);
if (!data.certificatePem || !data.certificatePem.trim()) {
throw new Error("Se requiere el certificado ARCA");
}
const existingCredential = await this.arcaCredentials.findOne({ companyId: data.companyId });
if (
!existingCredential ||
!existingCredential.encryptedPrivateKey ||
!existingCredential.privateKeyIv ||
!existingCredential.privateKeyAuthTag
) {
throw new Error("Primero se deben generar las credenciales ARCA para la organización");
}
const certificatePem = data.certificatePem.trim();
let certificate: forge.pki.Certificate;
let privateKeyPem: string;
try {
certificate = this.parseCertificate(certificatePem);
privateKeyPem = this.decryptPrivateKey(existingCredential);
if (!this.certificateMatchesPrivateKey(certificate, privateKeyPem)) {
throw new Error("El certificado ARCA no corresponde a la clave privada generada para esta organización");
}
} catch (error) {
const errorOccurred = error as Error;
await this.arcaCredentials.update({
companyId: data.companyId,
lastValidationError: errorOccurred.message,
status: ARCA_CREDENTIAL_STATUS.INVALID,
updatedAt: new Date(),
});
throw errorOccurred;
}
const updatedCredential = await this.arcaCredentials.update({
companyId: data.companyId,
certificatePem,
certificateValidFrom: certificate.validity.notBefore,
certificateValidTo: certificate.validity.notAfter,
lastValidationError: undefined,
status: ARCA_CREDENTIAL_STATUS.READY,
updatedAt: new Date(),
});
if (!updatedCredential) {
throw new Error("No se pudieron actualizar las credenciales ARCA");
}
return this.sanitizeCredential(updatedCredential);
}
public async testWsaaLogin(data: TestArcaWsaaLoginParams): Promise<ArcaWsaaLoginTestResult> {
await this.validateCompanyAdmin(data.companyId, data.sessionUser);
const service = ARCA_WSAA_SERVICE.WSFE;
const environment = ARCA_WSAA_ENVIRONMENT.HOMOLOGATION;
const loginTicket = await this.getWsaaLoginTicket(data.companyId);
return {
status: loginTicket.cached ? "cached" : "created",
environment,
service,
expirationTime: loginTicket.expirationTime,
cached: loginTicket.cached,
};
}
public async getWsfeLastVoucher(data: ArcaWsfeLastVoucherParams): Promise<ArcaWsfeLastVoucherResult> {
await this.validateCompanyAdmin(data.companyId, data.sessionUser);
const fiscalProfile = await this.requireActiveFiscalProfile(data.companyId, data.sessionUser);
const loginTicket = await this.getWsaaLoginTicket(data.companyId);
const voucherType = 11;
let responseXml: string;
try {
responseXml = await this.postWsfe(
this.buildWsfeLastVoucherSoap({
token: loginTicket.token,
sign: loginTicket.sign,
cuit: fiscalProfile.cuit,
pointOfSale: fiscalProfile.pointOfSale,
voucherType,
}),
"FECompUltimoAutorizado"
);
} catch (error) {
throw new Error(this.buildWsfeErrorMessage(error));
}
return {
environment: ARCA_WSAA_ENVIRONMENT.HOMOLOGATION,
service: ARCA_WSAA_SERVICE.WSFE,
pointOfSale: fiscalProfile.pointOfSale,
voucherType,
lastVoucherNumber: this.parseWsfeLastVoucherReturn(responseXml),
};
}
public async getWsfeDiagnostics(data: ArcaWsfeDiagnosticsParams): Promise<ArcaWsfeDiagnosticsResult> {
await this.validateCompanyAdmin(data.companyId, data.sessionUser);
const fiscalProfile = await this.requireActiveFiscalProfile(data.companyId, data.sessionUser);
const loginTicket = await this.getWsaaLoginTicket(data.companyId);
const voucherType = 11;
const diagnostics: ArcaWsfeDiagnosticsResult = {
environment: ARCA_WSAA_ENVIRONMENT.HOMOLOGATION,
service: ARCA_WSAA_SERVICE.WSFE,
dummy: { ok: false },
voucherTypes: { ok: false },
pointsOfSale: {
ok: false,
configuredPointOfSale: fiscalProfile.pointOfSale,
},
lastVoucher: {
ok: false,
pointOfSale: fiscalProfile.pointOfSale,
voucherType,
},
};
try {
diagnostics.dummy = this.parseWsfeDummyReturn(
await this.postWsfe(this.buildWsfeDummySoap(), "FEDummy")
);
} catch (error) {
diagnostics.dummy = { ok: false, error: this.buildWsfeErrorMessage(error) };
}
try {
diagnostics.voucherTypes = this.parseWsfeVoucherTypesReturn(
await this.postWsfe(
this.buildWsfeParamGetTiposCbteSoap({
token: loginTicket.token,
sign: loginTicket.sign,
cuit: fiscalProfile.cuit,
}),
"FEParamGetTiposCbte"
)
);
} catch (error) {
diagnostics.voucherTypes = { ok: false, error: this.buildWsfeErrorMessage(error) };
}
try {
diagnostics.pointsOfSale = this.parseWsfePointsOfSaleReturn(
await this.postWsfe(
this.buildWsfeParamGetPtosVentaSoap({
token: loginTicket.token,
sign: loginTicket.sign,
cuit: fiscalProfile.cuit,
}),
"FEParamGetPtosVenta"
),
fiscalProfile.pointOfSale
);
} catch (error) {
diagnostics.pointsOfSale = {
ok: false,
configuredPointOfSale: fiscalProfile.pointOfSale,
error: this.buildWsfeErrorMessage(error),
};
}
try {
diagnostics.lastVoucher.lastVoucherNumber = this.parseWsfeLastVoucherReturn(
await this.postWsfe(
this.buildWsfeLastVoucherSoap({
token: loginTicket.token,
sign: loginTicket.sign,
cuit: fiscalProfile.cuit,
pointOfSale: fiscalProfile.pointOfSale,
voucherType,
}),
"FECompUltimoAutorizado"
)
);
diagnostics.lastVoucher.ok = true;
} catch (error) {
diagnostics.lastVoucher.error = this.buildWsfeErrorMessage(error);
}
return diagnostics;
}
public async find(data: FindArcaCredentialsParams): Promise<ArcaCredentialView[]> {
if (!data.sessionUser) {
throw new Error("Se requiere un usuario autenticado");
}
await validateSessionUser({ sessionUser: data.sessionUser });
if (data.companyId) {
await validatePermissionsByCompany({ companyId: data.companyId, sessionUser: data.sessionUser });
} else if (data.id) {
const credential = await this.arcaCredentials.findOne(data);
if (!credential) {
return [];
}
await validatePermissionsByCompany({
companyId: String(credential.companyId),
sessionUser: data.sessionUser,
});
return [this.sanitizeCredential(credential)];
} else {
throw new Error("Se requiere una organización para buscar credenciales ARCA");
}
const credentials = await this.arcaCredentials.find(data);
return credentials.map((credential) => this.sanitizeCredential(credential));
}
}
export default new ArcaCredentialsManager();
@@ -0,0 +1,97 @@
import { Document, FilterQuery, Model, Schema, model, models } from "mongoose";
import {
ARCA_WSAA_ENVIRONMENT,
ARCA_WSAA_SERVICE,
ArcaWsaaTokenLookupParams,
CreateArcaWsaaTokenParams,
IArcaWsaaToken,
IArcaWsaaTokensAdapter,
UpdateArcaWsaaTokenParams,
} from "./ArcaWsaaTokens.Interface";
export interface IArcaWsaaTokenDocument extends Omit<IArcaWsaaToken, "id">, Document {}
export class ArcaWsaaTokensAdapterMongoose implements IArcaWsaaTokensAdapter {
schema: Schema;
arcaWsaaTokenList: Model<IArcaWsaaTokenDocument>;
constructor() {
this.schema = new Schema({
companyId: {
type: Schema.Types.ObjectId,
required: true,
ref: "Company",
index: true,
},
service: {
type: String,
required: true,
enum: Object.values(ARCA_WSAA_SERVICE),
index: true,
},
environment: {
type: String,
required: true,
enum: Object.values(ARCA_WSAA_ENVIRONMENT),
index: true,
},
token: { type: String, required: true },
sign: { type: String, required: true },
generationTime: { type: Date, required: false },
expirationTime: { type: Date, required: true, index: true },
createdAt: { type: Date, required: true, default: Date.now },
updatedAt: { type: Date, required: true, default: Date.now },
});
this.schema.index({ companyId: 1, service: 1, environment: 1 }, { unique: true });
this.arcaWsaaTokenList =
models.ArcaWsaaToken || model<IArcaWsaaTokenDocument>("ArcaWsaaToken", this.schema);
}
public async create(data: CreateArcaWsaaTokenParams): Promise<IArcaWsaaToken> {
return await this.arcaWsaaTokenList.create(data);
}
private buildSearchCriteria(
filters: Partial<ArcaWsaaTokenLookupParams>
): FilterQuery<IArcaWsaaToken> {
const searchCriteria: FilterQuery<IArcaWsaaToken> = {};
if (filters.companyId) searchCriteria.companyId = filters.companyId;
if (filters.service) searchCriteria.service = filters.service;
if (filters.environment) searchCriteria.environment = filters.environment;
return searchCriteria;
}
public async find(filters: Partial<ArcaWsaaTokenLookupParams>): Promise<IArcaWsaaToken[]> {
return this.arcaWsaaTokenList.find(this.buildSearchCriteria(filters)).sort({ createdAt: -1 }).exec();
}
public async findOne(
filters: Partial<ArcaWsaaTokenLookupParams>
): Promise<IArcaWsaaTokenDocument | null> {
return this.arcaWsaaTokenList.findOne(this.buildSearchCriteria(filters)).exec();
}
public async update(data: UpdateArcaWsaaTokenParams): Promise<IArcaWsaaTokenDocument | null> {
const { companyId, service, environment, ...updateData } = data;
return this.arcaWsaaTokenList
.findOneAndUpdate(this.buildSearchCriteria({ companyId, service, environment }), updateData, {
new: true,
})
.exec();
}
public async upsert(data: CreateArcaWsaaTokenParams): Promise<IArcaWsaaTokenDocument | null> {
const { companyId, service, environment, createdAt, ...updateData } = data;
return this.arcaWsaaTokenList
.findOneAndUpdate(
this.buildSearchCriteria({ companyId, service, environment }),
{ $set: updateData, $setOnInsert: { companyId, service, environment, createdAt } },
{ new: true, upsert: true }
)
.exec();
}
}
@@ -0,0 +1,67 @@
import { IArcaWsaaTokenDocument } from "./ArcaWsaaTokens.Adapter.Mongoose";
export enum ARCA_WSAA_SERVICE {
WSFE = "wsfe",
}
export enum ARCA_WSAA_ENVIRONMENT {
HOMOLOGATION = "homologation",
}
export type ArcaWsaaTokenLookupParams = {
companyId: string;
service: ARCA_WSAA_SERVICE;
environment: ARCA_WSAA_ENVIRONMENT;
};
export type CreateArcaWsaaTokenParams = ArcaWsaaTokenLookupParams & {
token: string;
sign: string;
generationTime?: Date;
expirationTime: Date;
createdAt: Date;
updatedAt: Date;
};
export type UpdateArcaWsaaTokenParams = ArcaWsaaTokenLookupParams & {
token?: string;
sign?: string;
generationTime?: Date;
expirationTime?: Date;
updatedAt: Date;
};
export type UpsertArcaWsaaTokenParams = ArcaWsaaTokenLookupParams & {
token: string;
sign: string;
generationTime?: Date;
expirationTime: Date;
};
export interface IArcaWsaaToken {
id?: string;
companyId: string;
service: ARCA_WSAA_SERVICE;
environment: ARCA_WSAA_ENVIRONMENT;
token: string;
sign: string;
generationTime?: Date;
expirationTime: Date;
createdAt: Date;
updatedAt: Date;
}
export interface IArcaWsaaTokensAdapter {
create(data: CreateArcaWsaaTokenParams): Promise<IArcaWsaaToken>;
find(filters: Partial<ArcaWsaaTokenLookupParams>): Promise<IArcaWsaaToken[]>;
findOne(filters: Partial<ArcaWsaaTokenLookupParams>): Promise<IArcaWsaaTokenDocument | null>;
update(data: UpdateArcaWsaaTokenParams): Promise<IArcaWsaaTokenDocument | null>;
upsert(data: CreateArcaWsaaTokenParams): Promise<IArcaWsaaTokenDocument | null>;
}
export interface IArcaWsaaTokensManager {
arcaWsaaTokens: IArcaWsaaTokensAdapter;
getValidToken(data: ArcaWsaaTokenLookupParams): Promise<IArcaWsaaToken | null>;
saveToken(data: UpsertArcaWsaaTokenParams): Promise<IArcaWsaaToken>;
upsertToken(data: UpsertArcaWsaaTokenParams): Promise<IArcaWsaaToken>;
}
@@ -0,0 +1,58 @@
import { ArcaWsaaTokensAdapterMongoose } from "./ArcaWsaaTokens.Adapter.Mongoose";
import {
ArcaWsaaTokenLookupParams,
IArcaWsaaToken,
IArcaWsaaTokensManager,
UpsertArcaWsaaTokenParams,
} from "./ArcaWsaaTokens.Interface";
const TOKEN_REUSE_SKEW_MS = 10 * 60 * 1000;
class ArcaWsaaTokensManager implements IArcaWsaaTokensManager {
arcaWsaaTokens: ArcaWsaaTokensAdapterMongoose;
constructor() {
this.arcaWsaaTokens = new ArcaWsaaTokensAdapterMongoose();
}
public async getValidToken(data: ArcaWsaaTokenLookupParams): Promise<IArcaWsaaToken | null> {
const existingToken = await this.arcaWsaaTokens.findOne(data);
if (!existingToken) {
return null;
}
const reuseUntil = Date.now() + TOKEN_REUSE_SKEW_MS;
if (new Date(existingToken.expirationTime).getTime() <= reuseUntil) {
return null;
}
return existingToken;
}
public async saveToken(data: UpsertArcaWsaaTokenParams): Promise<IArcaWsaaToken> {
const now = new Date();
return await this.arcaWsaaTokens.create({
...data,
createdAt: now,
updatedAt: now,
});
}
public async upsertToken(data: UpsertArcaWsaaTokenParams): Promise<IArcaWsaaToken> {
const now = new Date();
const token = await this.arcaWsaaTokens.upsert({
...data,
createdAt: now,
updatedAt: now,
});
if (!token) {
throw new Error("No se pudo guardar el token WSAA de ARCA");
}
return token;
}
}
export default new ArcaWsaaTokensManager();
@@ -0,0 +1,82 @@
import { Document, FilterQuery, Model, Schema, model } from "mongoose";
import {
CreateInvoiceParams,
FindInvoicesParams,
IInvoice,
IInvoicesAdapter,
INVOICE_STATUS,
INVOICE_TYPE,
} from "./Invoices.Interface";
export interface IInvoiceDocument extends Omit<IInvoice, "id">, Document {}
export class InvoicesAdapterMongoose implements IInvoicesAdapter {
schema: Schema;
invoiceList: Model<IInvoiceDocument>;
constructor() {
this.schema = new Schema({
cashMovementId: {
type: Schema.Types.ObjectId,
required: true,
ref: "CashFlow",
unique: true,
index: true,
},
companyId: {
type: Schema.Types.ObjectId,
required: true,
ref: "Company",
index: true,
},
clientId: { type: Schema.Types.ObjectId, required: false, ref: "Client" },
paymentId: { type: Schema.Types.ObjectId, required: false, ref: "Payment" },
clientAccountMovementId: {
type: Schema.Types.ObjectId,
required: false,
ref: "ClientAccountMovement",
},
appointmentId: { type: Schema.Types.ObjectId, required: false, ref: "Appointment" },
amount: { type: Number, required: true },
type: { type: String, required: true, enum: Object.values(INVOICE_TYPE) },
status: { type: String, required: true, enum: Object.values(INVOICE_STATUS), index: true },
cae: { type: String, required: false },
caeExpiresAt: { type: Date, required: false },
pointOfSale: { type: Number, required: false },
voucherNumber: { type: Number, required: false },
voucherType: { type: String, required: false },
arcaErrorCode: { type: String, required: false },
arcaErrorMessage: { type: String, required: false },
createdAt: { type: Date, required: true, default: Date.now },
updatedAt: { type: Date, required: true, default: Date.now },
});
this.invoiceList = model<IInvoiceDocument>("Invoice", this.schema);
}
public async create(data: CreateInvoiceParams): Promise<IInvoice> {
return await this.invoiceList.create(data);
}
private buildSearchCriteria(filters: FindInvoicesParams): FilterQuery<IInvoice> {
const searchCriteria: FilterQuery<IInvoice> = {};
if (filters.id) searchCriteria._id = filters.id;
if (filters.cashMovementId) searchCriteria.cashMovementId = filters.cashMovementId;
if (filters.companyId) searchCriteria.companyId = filters.companyId;
if (filters.status) searchCriteria.status = filters.status;
return searchCriteria;
}
public async find(filters: FindInvoicesParams): Promise<IInvoice[]> {
const searchCriteria = this.buildSearchCriteria(filters);
return this.invoiceList.find(searchCriteria).sort({ createdAt: -1 }).exec();
}
public async findOne(filters: FindInvoicesParams): Promise<IInvoiceDocument | null> {
const searchCriteria = this.buildSearchCriteria(filters);
return this.invoiceList.findOne(searchCriteria).exec();
}
}
@@ -0,0 +1,81 @@
import { IInvoiceDocument } from "./Invoices.Adapter.Mongoose";
export enum INVOICE_TYPE {
FACTURA_C = "FACTURA_C",
}
export enum INVOICE_STATUS {
PENDING_ARCA = "PENDING_ARCA",
APPROVED = "APPROVED",
PENDING_VERIFICATION = "PENDING_VERIFICATION",
REJECTED = "REJECTED",
FAILED = "FAILED",
}
export type CreateInvoiceFromCashMovementParams = {
cashMovementId: string;
sessionUser: string;
};
export type FindInvoicesParams = {
id?: string;
cashMovementId?: string;
companyId?: string;
status?: INVOICE_STATUS;
sessionUser?: string;
};
export type CreateInvoiceParams = {
cashMovementId: string;
companyId: string;
clientId?: string;
paymentId?: string;
clientAccountMovementId?: string;
appointmentId?: string;
amount: number;
type: INVOICE_TYPE;
status: INVOICE_STATUS;
cae?: string;
caeExpiresAt?: Date;
pointOfSale?: number;
voucherNumber?: number;
voucherType?: string;
arcaErrorCode?: string;
arcaErrorMessage?: string;
createdAt: Date;
updatedAt: Date;
};
export interface IInvoice {
id?: string;
cashMovementId: string;
companyId: string;
clientId?: string;
paymentId?: string;
clientAccountMovementId?: string;
appointmentId?: string;
amount: number;
type: INVOICE_TYPE;
status: INVOICE_STATUS;
cae?: string;
caeExpiresAt?: Date;
pointOfSale?: number;
voucherNumber?: number;
voucherType?: string;
arcaErrorCode?: string;
arcaErrorMessage?: string;
createdAt: Date;
updatedAt: Date;
}
export interface IInvoicesAdapter {
create(data: CreateInvoiceParams): Promise<IInvoice>;
find(filters: FindInvoicesParams): Promise<IInvoice[]>;
findOne(filters: FindInvoicesParams): Promise<IInvoiceDocument | null>;
}
export interface IInvoicesManager {
invoices: IInvoicesAdapter;
createFromCashMovement(data: CreateInvoiceFromCashMovementParams): Promise<IInvoice>;
find(data: FindInvoicesParams): Promise<IInvoice[]>;
}
+132
View File
@@ -0,0 +1,132 @@
import CashFlow from "../CashFlow/CashFlow";
import { CLIENT_ACCOUNT_MOVEMENT_TYPES } from "../ClientAccountMovements/ClientAccountMovements.Interface";
import OrganizationFiscalProfiles from "../OrganizationFiscalProfiles/OrganizationFiscalProfiles";
import { ORGANIZATION_FISCAL_PROFILE_STATUS } from "../OrganizationFiscalProfiles/OrganizationFiscalProfiles.Interface";
import { validatePermissionsByCompany, validateSessionUser } from "../../helpers/check";
import { InvoicesAdapterMongoose } from "./Invoices.Adapter.Mongoose";
import {
CreateInvoiceFromCashMovementParams,
FindInvoicesParams,
IInvoice,
IInvoicesManager,
INVOICE_STATUS,
INVOICE_TYPE,
} from "./Invoices.Interface";
class InvoicesManager implements IInvoicesManager {
invoices: InvoicesAdapterMongoose;
constructor() {
this.invoices = new InvoicesAdapterMongoose();
}
private isDuplicateKeyError(error: unknown): boolean {
return typeof error === "object" && error !== null && "code" in error && error.code === 11000;
}
public async createFromCashMovement(data: CreateInvoiceFromCashMovementParams): Promise<IInvoice> {
await validateSessionUser({ sessionUser: data.sessionUser });
if (!data.cashMovementId || !data.cashMovementId.trim()) {
throw new Error("Se requiere un movimiento de caja para emitir la factura");
}
const movement = await CashFlow.movements.movementList.findOne({
_id: data.cashMovementId,
});
if (!movement) {
throw new Error("No se ha encontrado el movimiento de caja");
}
const companyId = String(movement.companyId);
await validatePermissionsByCompany({ companyId, sessionUser: data.sessionUser });
if (movement.type !== CLIENT_ACCOUNT_MOVEMENT_TYPES.DEPOSIT || movement.amount <= 0) {
throw new Error("El movimiento de caja no es facturable");
}
const fiscalProfiles = await OrganizationFiscalProfiles.find({
companyId,
status: ORGANIZATION_FISCAL_PROFILE_STATUS.ACTIVE,
sessionUser: data.sessionUser,
});
if (fiscalProfiles.length === 0) {
throw new Error("La organización debe tener un perfil fiscal activo para emitir facturas");
}
const existingInvoice = await this.invoices.findOne({
cashMovementId: data.cashMovementId,
});
if (existingInvoice) {
return existingInvoice;
}
try {
return await this.invoices.create({
cashMovementId: data.cashMovementId,
companyId,
clientId: movement.clientId ? String(movement.clientId) : undefined,
paymentId: movement.paymentId ? String(movement.paymentId) : undefined,
clientAccountMovementId: movement.clientAccountMovementId
? String(movement.clientAccountMovementId)
: undefined,
appointmentId: movement.appointmentId ? String(movement.appointmentId) : undefined,
amount: movement.amount,
type: INVOICE_TYPE.FACTURA_C,
status: INVOICE_STATUS.PENDING_ARCA,
createdAt: new Date(),
updatedAt: new Date(),
});
} catch (error) {
if (this.isDuplicateKeyError(error)) {
const duplicateInvoice = await this.invoices.findOne({
cashMovementId: data.cashMovementId,
});
if (duplicateInvoice) {
return duplicateInvoice;
}
}
throw error;
}
}
public async find(data: FindInvoicesParams): Promise<IInvoice[]> {
if (!data.sessionUser) {
throw new Error("Se requiere un usuario autenticado");
}
const sessionUser = data.sessionUser;
await validateSessionUser({ sessionUser });
if (data.companyId) {
await validatePermissionsByCompany({
companyId: data.companyId,
sessionUser,
});
} else if (data.id || data.cashMovementId) {
const invoice = await this.invoices.findOne(data);
if (!invoice) {
return [];
}
await validatePermissionsByCompany({
companyId: String(invoice.companyId),
sessionUser,
});
return [invoice];
} else {
throw new Error("Se requiere una organización para buscar facturas");
}
return await this.invoices.find(data);
}
}
export default new InvoicesManager();
@@ -0,0 +1,100 @@
import { Document, FilterQuery, Model, Schema, model } from "mongoose";
import {
CreateOrganizationFiscalProfileParams,
FindOrganizationFiscalProfilesParams,
IOrganizationFiscalProfile,
IOrganizationFiscalProfilesAdapter,
ORGANIZATION_FISCAL_PROFILE_STATUS,
ORGANIZATION_FISCAL_PROFILE_TAX_CONDITION,
UpdateOrganizationFiscalProfileParams,
} from "./OrganizationFiscalProfiles.Interface";
export interface IOrganizationFiscalProfileDocument
extends Omit<IOrganizationFiscalProfile, "id">,
Document {}
export class OrganizationFiscalProfilesAdapterMongoose implements IOrganizationFiscalProfilesAdapter {
schema: Schema;
organizationFiscalProfileList: Model<IOrganizationFiscalProfileDocument>;
constructor() {
this.schema = new Schema({
companyId: {
type: Schema.Types.ObjectId,
required: true,
ref: "Company",
unique: true,
index: true,
},
cuit: { type: String, required: true },
legalName: { type: String, required: true },
taxCondition: {
type: String,
required: true,
enum: Object.values(ORGANIZATION_FISCAL_PROFILE_TAX_CONDITION),
},
pointOfSale: { type: Number, required: true },
status: {
type: String,
required: true,
enum: Object.values(ORGANIZATION_FISCAL_PROFILE_STATUS),
index: true,
},
createdAt: { type: Date, required: true, default: Date.now },
updatedAt: { type: Date, required: true, default: Date.now },
});
this.organizationFiscalProfileList = model<IOrganizationFiscalProfileDocument>(
"OrganizationFiscalProfile",
this.schema
);
}
public async create(
data: CreateOrganizationFiscalProfileParams
): Promise<IOrganizationFiscalProfile> {
return await this.organizationFiscalProfileList.create(data);
}
private buildSearchCriteria(
filters: FindOrganizationFiscalProfilesParams
): FilterQuery<IOrganizationFiscalProfile> {
const searchCriteria: FilterQuery<IOrganizationFiscalProfile> = {};
if (filters.id) searchCriteria._id = filters.id;
if (filters.companyId) searchCriteria.companyId = filters.companyId;
if (filters.status) searchCriteria.status = filters.status;
return searchCriteria;
}
public async find(
filters: FindOrganizationFiscalProfilesParams
): Promise<IOrganizationFiscalProfile[]> {
const searchCriteria = this.buildSearchCriteria(filters);
return this.organizationFiscalProfileList.find(searchCriteria).sort({ createdAt: -1 }).exec();
}
public async findOne(
filters: FindOrganizationFiscalProfilesParams
): Promise<IOrganizationFiscalProfileDocument | null> {
const searchCriteria = this.buildSearchCriteria(filters);
return this.organizationFiscalProfileList.findOne(searchCriteria).exec();
}
public async update(
data: UpdateOrganizationFiscalProfileParams
): Promise<IOrganizationFiscalProfileDocument | null> {
const { id, companyId, sessionUser, ...updateData } = data;
if (!id && !companyId) {
throw new Error("Se requiere un identificador para actualizar el perfil fiscal");
}
return this.organizationFiscalProfileList
.findOneAndUpdate(this.buildSearchCriteria({ id, companyId, sessionUser }), updateData, {
new: true,
})
.exec();
}
}
@@ -0,0 +1,75 @@
import { IOrganizationFiscalProfileDocument } from "./OrganizationFiscalProfiles.Adapter.Mongoose";
export enum ORGANIZATION_FISCAL_PROFILE_TAX_CONDITION {
MONOTRIBUTO = "MONOTRIBUTO",
EXENTO = "EXENTO",
}
export enum ORGANIZATION_FISCAL_PROFILE_STATUS {
PENDING = "PENDING",
ACTIVE = "ACTIVE",
DISABLED = "DISABLED",
}
export type UpsertOrganizationFiscalProfileParams = {
companyId: string;
cuit: string;
legalName: string;
taxCondition: ORGANIZATION_FISCAL_PROFILE_TAX_CONDITION;
pointOfSale: number;
sessionUser: string;
};
export type FindOrganizationFiscalProfilesParams = {
id?: string;
companyId?: string;
status?: ORGANIZATION_FISCAL_PROFILE_STATUS;
sessionUser?: string;
};
export type CreateOrganizationFiscalProfileParams = {
companyId: string;
cuit: string;
legalName: string;
taxCondition: ORGANIZATION_FISCAL_PROFILE_TAX_CONDITION;
pointOfSale: number;
status: ORGANIZATION_FISCAL_PROFILE_STATUS;
createdAt: Date;
updatedAt: Date;
};
export type UpdateOrganizationFiscalProfileParams = FindOrganizationFiscalProfilesParams & {
cuit?: string;
legalName?: string;
taxCondition?: ORGANIZATION_FISCAL_PROFILE_TAX_CONDITION;
pointOfSale?: number;
status?: ORGANIZATION_FISCAL_PROFILE_STATUS;
updatedAt: Date;
};
export interface IOrganizationFiscalProfile {
id?: string;
companyId: string;
cuit: string;
legalName: string;
taxCondition: ORGANIZATION_FISCAL_PROFILE_TAX_CONDITION;
pointOfSale: number;
status: ORGANIZATION_FISCAL_PROFILE_STATUS;
createdAt: Date;
updatedAt: Date;
}
export interface IOrganizationFiscalProfilesAdapter {
create(data: CreateOrganizationFiscalProfileParams): Promise<IOrganizationFiscalProfile>;
find(filters: FindOrganizationFiscalProfilesParams): Promise<IOrganizationFiscalProfile[]>;
findOne(
filters: FindOrganizationFiscalProfilesParams
): Promise<IOrganizationFiscalProfileDocument | null>;
update(data: UpdateOrganizationFiscalProfileParams): Promise<IOrganizationFiscalProfileDocument | null>;
}
export interface IOrganizationFiscalProfilesManager {
organizationFiscalProfiles: IOrganizationFiscalProfilesAdapter;
upsert(data: UpsertOrganizationFiscalProfileParams): Promise<IOrganizationFiscalProfile>;
find(data: FindOrganizationFiscalProfilesParams): Promise<IOrganizationFiscalProfile[]>;
}
@@ -0,0 +1,149 @@
import { validatePermissionsByCompany, validateSessionUser } from "../../helpers/check";
import { OrganizationFiscalProfilesAdapterMongoose } from "./OrganizationFiscalProfiles.Adapter.Mongoose";
import {
FindOrganizationFiscalProfilesParams,
IOrganizationFiscalProfile,
IOrganizationFiscalProfilesManager,
ORGANIZATION_FISCAL_PROFILE_STATUS,
UpsertOrganizationFiscalProfileParams,
} from "./OrganizationFiscalProfiles.Interface";
class OrganizationFiscalProfilesManager implements IOrganizationFiscalProfilesManager {
organizationFiscalProfiles: OrganizationFiscalProfilesAdapterMongoose;
constructor() {
this.organizationFiscalProfiles = new OrganizationFiscalProfilesAdapterMongoose();
}
private isDuplicateKeyError(error: unknown): boolean {
return typeof error === "object" && error !== null && "code" in error && error.code === 11000;
}
private validateProfileFields(data: UpsertOrganizationFiscalProfileParams): void {
if (!/^\d{11}$/.test(data.cuit)) {
throw new Error("El CUIT debe contener 11 dígitos");
}
if (!data.legalName || !data.legalName.trim()) {
throw new Error("La razón social es requerida");
}
if (!data.pointOfSale || data.pointOfSale <= 0) {
throw new Error("El punto de venta debe ser mayor a 0");
}
}
public async upsert(
data: UpsertOrganizationFiscalProfileParams
): Promise<IOrganizationFiscalProfile> {
await validateSessionUser({ sessionUser: data.sessionUser });
if (!data.companyId || !data.companyId.trim()) {
throw new Error("Se requiere una organización para guardar el perfil fiscal");
}
await validatePermissionsByCompany({ companyId: data.companyId, sessionUser: data.sessionUser });
this.validateProfileFields(data);
const existingProfile = await this.organizationFiscalProfiles.findOne({
companyId: data.companyId,
});
const now = new Date();
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,
updatedAt: now,
});
if (!updatedProfile) {
throw new Error("No se pudo actualizar el perfil fiscal");
}
return updatedProfile;
}
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,
createdAt: now,
updatedAt: now,
});
} catch (error) {
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,
updatedAt: new Date(),
});
if (updatedProfile) {
return updatedProfile;
}
const existingProfile = await this.organizationFiscalProfiles.findOne({
companyId: data.companyId,
});
if (existingProfile) {
return existingProfile;
}
}
throw error;
}
}
public async find(
data: FindOrganizationFiscalProfilesParams
): Promise<IOrganizationFiscalProfile[]> {
if (!data.sessionUser) {
throw new Error("Se requiere un usuario autenticado");
}
const sessionUser = data.sessionUser;
await validateSessionUser({ sessionUser });
if (data.companyId) {
await validatePermissionsByCompany({
companyId: data.companyId,
sessionUser,
});
} else if (data.id) {
const profile = await this.organizationFiscalProfiles.findOne(data);
if (!profile) {
return [];
}
await validatePermissionsByCompany({
companyId: String(profile.companyId),
sessionUser,
});
return [profile];
} else {
throw new Error("Se requiere una organización para buscar perfiles fiscales");
}
return await this.organizationFiscalProfiles.find(data);
}
}
export default new OrganizationFiscalProfilesManager();
@@ -0,0 +1,142 @@
import { Body, Controller, Middlewares, Post, Response, Route, SuccessResponse } from "tsoa";
import { authenticateMiddleware } from "../../middleware/authentication";
import { ApiValidationError } from "../../Models/Server.Error.model";
import {
ArcaCredentialView,
ArcaWsfeDiagnosticsParams,
ArcaWsfeDiagnosticsResult,
ArcaWsfeLastVoucherParams,
ArcaWsfeLastVoucherResult,
ArcaWsaaLoginTestResult,
FindArcaCredentialsParams,
GenerateArcaCredentialCsrParams,
TestArcaWsaaLoginParams,
UploadArcaCredentialCertificateParams,
} from "../../Models/ArcaCredentials/ArcaCredentials.Interface";
import { ArcaCredentialsService } from "./ArcaCredentials.Service";
@Route("arca-credentials/generate-csr")
@Middlewares(authenticateMiddleware)
export class ArcaCredentialsGenerateCsrController extends Controller {
@SuccessResponse(200, "Done")
@Response<ApiValidationError>(500, "Ha ocurrido un error")
@Post()
public async generateCsr(
@Body() requestBody: GenerateArcaCredentialCsrParams
): Promise<ArcaCredentialView | ApiValidationError> {
try {
const credential = await new ArcaCredentialsService().generateCsr(requestBody);
this.setStatus(200);
return credential;
} catch (e) {
const errorOccurred: Error = e as Error;
this.setStatus(500);
return new ApiValidationError(500, errorOccurred.message);
}
}
}
@Route("arca-credentials/upload-certificate")
@Middlewares(authenticateMiddleware)
export class ArcaCredentialsUploadCertificateController extends Controller {
@SuccessResponse(200, "Done")
@Response<ApiValidationError>(500, "Ha ocurrido un error")
@Post()
public async uploadCertificate(
@Body() requestBody: UploadArcaCredentialCertificateParams
): Promise<ArcaCredentialView | ApiValidationError> {
try {
const credential = await new ArcaCredentialsService().uploadCertificate(requestBody);
this.setStatus(200);
return credential;
} catch (e) {
const errorOccurred: Error = e as Error;
this.setStatus(500);
return new ApiValidationError(500, errorOccurred.message);
}
}
}
@Route("arca-credentials/test-wsaa-login")
@Middlewares(authenticateMiddleware)
export class ArcaCredentialsTestWsaaLoginController extends Controller {
@SuccessResponse(200, "Done")
@Response<ApiValidationError>(500, "Ha ocurrido un error")
@Post()
public async testWsaaLogin(
@Body() requestBody: TestArcaWsaaLoginParams
): Promise<ArcaWsaaLoginTestResult | ApiValidationError> {
try {
const result = await new ArcaCredentialsService().testWsaaLogin(requestBody);
this.setStatus(200);
return result;
} catch (e) {
const errorOccurred: Error = e as Error;
this.setStatus(500);
return new ApiValidationError(500, errorOccurred.message);
}
}
}
@Route("arca-credentials/wsfe-last-voucher")
@Middlewares(authenticateMiddleware)
export class ArcaCredentialsWsfeLastVoucherController extends Controller {
@SuccessResponse(200, "Done")
@Response<ApiValidationError>(500, "Ha ocurrido un error")
@Post()
public async getWsfeLastVoucher(
@Body() requestBody: ArcaWsfeLastVoucherParams
): Promise<ArcaWsfeLastVoucherResult | ApiValidationError> {
try {
const result = await new ArcaCredentialsService().getWsfeLastVoucher(requestBody);
this.setStatus(200);
return result;
} catch (e) {
const errorOccurred: Error = e as Error;
this.setStatus(500);
return new ApiValidationError(500, errorOccurred.message);
}
}
}
@Route("arca-credentials/wsfe-diagnostics")
@Middlewares(authenticateMiddleware)
export class ArcaCredentialsWsfeDiagnosticsController extends Controller {
@SuccessResponse(200, "Done")
@Response<ApiValidationError>(500, "Ha ocurrido un error")
@Post()
public async getWsfeDiagnostics(
@Body() requestBody: ArcaWsfeDiagnosticsParams
): Promise<ArcaWsfeDiagnosticsResult | ApiValidationError> {
try {
const result = await new ArcaCredentialsService().getWsfeDiagnostics(requestBody);
this.setStatus(200);
return result;
} catch (e) {
const errorOccurred: Error = e as Error;
this.setStatus(500);
return new ApiValidationError(500, errorOccurred.message);
}
}
}
@Route("arca-credentials/find")
@Middlewares(authenticateMiddleware)
export class ArcaCredentialsFindController extends Controller {
@SuccessResponse(200, "Done")
@Response<ApiValidationError>(500, "Ha ocurrido un error")
@Post()
public async find(
@Body() requestBody: FindArcaCredentialsParams
): Promise<ArcaCredentialView[] | ApiValidationError> {
try {
const credentials = await new ArcaCredentialsService().find(requestBody);
this.setStatus(200);
return credentials;
} catch (e) {
const errorOccurred: Error = e as Error;
this.setStatus(500);
return new ApiValidationError(500, errorOccurred.message);
}
}
}
@@ -0,0 +1,46 @@
import { connect } from "mongoose";
import ArcaCredentials from "../../Models/ArcaCredentials/ArcaCredentials";
import {
ArcaCredentialView,
ArcaWsfeDiagnosticsParams,
ArcaWsfeDiagnosticsResult,
ArcaWsfeLastVoucherParams,
ArcaWsfeLastVoucherResult,
ArcaWsaaLoginTestResult,
FindArcaCredentialsParams,
GenerateArcaCredentialCsrParams,
TestArcaWsaaLoginParams,
UploadArcaCredentialCertificateParams,
} from "../../Models/ArcaCredentials/ArcaCredentials.Interface";
export class ArcaCredentialsService {
public async generateCsr(data: GenerateArcaCredentialCsrParams): Promise<ArcaCredentialView> {
await connect(`${process.env.DATABASE_CONNECTION}`);
return await ArcaCredentials.generateCsr(data);
}
public async uploadCertificate(data: UploadArcaCredentialCertificateParams): Promise<ArcaCredentialView> {
await connect(`${process.env.DATABASE_CONNECTION}`);
return await ArcaCredentials.uploadCertificate(data);
}
public async testWsaaLogin(data: TestArcaWsaaLoginParams): Promise<ArcaWsaaLoginTestResult> {
await connect(`${process.env.DATABASE_CONNECTION}`);
return await ArcaCredentials.testWsaaLogin(data);
}
public async getWsfeLastVoucher(data: ArcaWsfeLastVoucherParams): Promise<ArcaWsfeLastVoucherResult> {
await connect(`${process.env.DATABASE_CONNECTION}`);
return await ArcaCredentials.getWsfeLastVoucher(data);
}
public async getWsfeDiagnostics(data: ArcaWsfeDiagnosticsParams): Promise<ArcaWsfeDiagnosticsResult> {
await connect(`${process.env.DATABASE_CONNECTION}`);
return await ArcaCredentials.getWsfeDiagnostics(data);
}
public async find(data: FindArcaCredentialsParams): Promise<ArcaCredentialView[]> {
await connect(`${process.env.DATABASE_CONNECTION}`);
return await ArcaCredentials.find(data);
}
}
@@ -0,0 +1,49 @@
import { Body, Controller, Middlewares, Post, Response, Route, SuccessResponse } from "tsoa";
import { authenticateMiddleware } from "../../middleware/authentication";
import { ApiValidationError } from "../../Models/Server.Error.model";
import {
CreateInvoiceFromCashMovementParams,
FindInvoicesParams,
IInvoice,
} from "../../Models/Invoices/Invoices.Interface";
import { InvoicesService } from "./Invoices.Service";
@Route("invoices/create-from-cash-movement")
@Middlewares(authenticateMiddleware)
export class InvoicesCreateFromCashMovementController extends Controller {
@SuccessResponse(200, "Done")
@Response<ApiValidationError>(500, "Ha ocurrido un error")
@Post()
public async createFromCashMovement(
@Body() requestBody: CreateInvoiceFromCashMovementParams
): Promise<IInvoice | ApiValidationError> {
try {
const invoice = await new InvoicesService().createFromCashMovement(requestBody);
this.setStatus(200);
return invoice;
} catch (e) {
const errorOccurred: Error = e as Error;
this.setStatus(500);
return new ApiValidationError(500, errorOccurred.message);
}
}
}
@Route("invoices/find")
@Middlewares(authenticateMiddleware)
export class InvoicesFindController extends Controller {
@SuccessResponse(200, "Done")
@Response<ApiValidationError>(500, "Ha ocurrido un error")
@Post()
public async find(@Body() requestBody: FindInvoicesParams): Promise<IInvoice[] | ApiValidationError> {
try {
const invoices = await new InvoicesService().find(requestBody);
this.setStatus(200);
return invoices;
} catch (e) {
const errorOccurred: Error = e as Error;
this.setStatus(500);
return new ApiValidationError(500, errorOccurred.message);
}
}
}
@@ -0,0 +1,19 @@
import { connect } from "mongoose";
import Invoices from "../../Models/Invoices/Invoices";
import {
CreateInvoiceFromCashMovementParams,
FindInvoicesParams,
IInvoice,
} from "../../Models/Invoices/Invoices.Interface";
export class InvoicesService {
public async createFromCashMovement(data: CreateInvoiceFromCashMovementParams): Promise<IInvoice> {
await connect(`${process.env.DATABASE_CONNECTION}`);
return await Invoices.createFromCashMovement(data);
}
public async find(data: FindInvoicesParams): Promise<IInvoice[]> {
await connect(`${process.env.DATABASE_CONNECTION}`);
return await Invoices.find(data);
}
}
@@ -0,0 +1,51 @@
import { Body, Controller, Middlewares, Post, Response, Route, SuccessResponse } from "tsoa";
import { authenticateMiddleware } from "../../middleware/authentication";
import { ApiValidationError } from "../../Models/Server.Error.model";
import {
FindOrganizationFiscalProfilesParams,
IOrganizationFiscalProfile,
UpsertOrganizationFiscalProfileParams,
} from "../../Models/OrganizationFiscalProfiles/OrganizationFiscalProfiles.Interface";
import { OrganizationFiscalProfilesService } from "./OrganizationFiscalProfiles.Service";
@Route("organization-fiscal-profiles/upsert")
@Middlewares(authenticateMiddleware)
export class OrganizationFiscalProfilesUpsertController extends Controller {
@SuccessResponse(200, "Done")
@Response<ApiValidationError>(500, "Ha ocurrido un error")
@Post()
public async upsert(
@Body() requestBody: UpsertOrganizationFiscalProfileParams
): Promise<IOrganizationFiscalProfile | ApiValidationError> {
try {
const profile = await new OrganizationFiscalProfilesService().upsert(requestBody);
this.setStatus(200);
return profile;
} catch (e) {
const errorOccurred: Error = e as Error;
this.setStatus(500);
return new ApiValidationError(500, errorOccurred.message);
}
}
}
@Route("organization-fiscal-profiles/find")
@Middlewares(authenticateMiddleware)
export class OrganizationFiscalProfilesFindController extends Controller {
@SuccessResponse(200, "Done")
@Response<ApiValidationError>(500, "Ha ocurrido un error")
@Post()
public async find(
@Body() requestBody: FindOrganizationFiscalProfilesParams
): Promise<IOrganizationFiscalProfile[] | ApiValidationError> {
try {
const profiles = await new OrganizationFiscalProfilesService().find(requestBody);
this.setStatus(200);
return profiles;
} catch (e) {
const errorOccurred: Error = e as Error;
this.setStatus(500);
return new ApiValidationError(500, errorOccurred.message);
}
}
}
@@ -0,0 +1,23 @@
import { connect } from "mongoose";
import OrganizationFiscalProfiles from "../../Models/OrganizationFiscalProfiles/OrganizationFiscalProfiles";
import {
FindOrganizationFiscalProfilesParams,
IOrganizationFiscalProfile,
UpsertOrganizationFiscalProfileParams,
} from "../../Models/OrganizationFiscalProfiles/OrganizationFiscalProfiles.Interface";
export class OrganizationFiscalProfilesService {
public async upsert(
data: UpsertOrganizationFiscalProfileParams
): Promise<IOrganizationFiscalProfile> {
await connect(`${process.env.DATABASE_CONNECTION}`);
return await OrganizationFiscalProfiles.upsert(data);
}
public async find(
data: FindOrganizationFiscalProfilesParams
): Promise<IOrganizationFiscalProfile[]> {
await connect(`${process.env.DATABASE_CONNECTION}`);
return await OrganizationFiscalProfiles.find(data);
}
}