From 4ffb6a281f4738883b240640b2f121723b0a1143 Mon Sep 17 00:00:00 2001 From: Horacio Daniel Ros Date: Wed, 12 Aug 2026 09:04:06 -0300 Subject: [PATCH] feat: support ARCA production environment with dynamic endpoint configuration and environment selection --- server/.env | 1 + server/.env.prod | 1 + .../ArcaCredentials.Interface.ts | 26 +++++++--- .../Models/ArcaCredentials/ArcaCredentials.ts | 52 ++++++++++++++++--- .../ArcaWsaaTokens.Interface.ts | 1 + .../org/profile/[id]/cash/Cash.Service.tsx | 6 ++- .../flows/ArcaTestConnectionFlow.tsx | 13 ++++- 7 files changed, 82 insertions(+), 18 deletions(-) diff --git a/server/.env b/server/.env index 2d6e7b7..a9a1163 100644 --- a/server/.env +++ b/server/.env @@ -69,6 +69,7 @@ FILE_SERVER_URL = http://localhost:4000/ FILE_SERVER_URL_prod = https://files.turnosxpress.com.ar/ SYS_ADMIN_API_PUBLIC_KEY = keys/tx-sysadmin-public.key +ARCA_ENVIRONMENT=homologation ARCA_WSAA_URL=https://wsaahomo.afip.gov.ar/ws/services/LoginCms ARCA_WSFE_URL=https://wswhomo.afip.gov.ar/wsfev1/service.asmx diff --git a/server/.env.prod b/server/.env.prod index 0635964..85a305b 100644 --- a/server/.env.prod +++ b/server/.env.prod @@ -67,6 +67,7 @@ SYSTEM_KEY = ad7c956a-76bf-45hdr60-8a50-2ca7b2180997 FILE_SERVER_URL = https://files.turnosxpress.com.ar/ SYS_ADMIN_API_PUBLIC_KEY = keys/tx-sysadmin-public.key +ARCA_ENVIRONMENT=production ARCA_WSAA_URL=https://wsaa.afip.gov.ar/ws/services/LoginCms ARCA_WSFE_URL=https://servicios1.afip.gov.ar/wsfev1/service.asmx diff --git a/server/src/Models/ArcaCredentials/ArcaCredentials.Interface.ts b/server/src/Models/ArcaCredentials/ArcaCredentials.Interface.ts index 98cc8da..7b3f6bb 100644 --- a/server/src/Models/ArcaCredentials/ArcaCredentials.Interface.ts +++ b/server/src/Models/ArcaCredentials/ArcaCredentials.Interface.ts @@ -1,4 +1,5 @@ import { IArcaCredentialDocument } from "./ArcaCredentials.Adapter.Mongoose"; +import { ARCA_WSAA_ENVIRONMENT, ARCA_WSAA_SERVICE } from "../ArcaWsaaTokens/ArcaWsaaTokens.Interface"; export enum ARCA_CREDENTIAL_STATUS { CSR_GENERATED = "CSR_GENERATED", @@ -26,8 +27,9 @@ export type TestArcaWsaaLoginParams = { export type ArcaWsaaLoginTestResult = { status: "cached" | "created"; - environment: "homologation"; - service: "wsfe"; + environment: ARCA_WSAA_ENVIRONMENT; + service: ARCA_WSAA_SERVICE; + endpoints: ArcaEndpointMetadata; expirationTime: Date; cached: boolean; }; @@ -38,8 +40,9 @@ export type ArcaWsfeLastVoucherParams = { }; export type ArcaWsfeLastVoucherResult = { - environment: "homologation"; - service: "wsfe"; + environment: ARCA_WSAA_ENVIRONMENT; + service: ARCA_WSAA_SERVICE; + endpoints: ArcaEndpointMetadata; pointOfSale: number; voucherType: 11; lastVoucherNumber: number; @@ -58,8 +61,9 @@ export type ArcaWsfeIssueFacturaCParams = { }; export type ArcaWsfeIssueFacturaCResult = { - environment: "homologation"; - service: "wsfe"; + environment: ARCA_WSAA_ENVIRONMENT; + service: ARCA_WSAA_SERVICE; + endpoints: ArcaEndpointMetadata; pointOfSale: number; voucherType: 11; voucherNumber: number; @@ -78,9 +82,15 @@ export type ArcaWsfeDiagnosticsParams = { sessionUser: string; }; +export type ArcaEndpointMetadata = { + wsaaHost?: string; + wsfeHost?: string; +}; + export type ArcaWsfeDiagnosticsResult = { - environment: "homologation"; - service: "wsfe"; + environment: ARCA_WSAA_ENVIRONMENT; + service: ARCA_WSAA_SERVICE; + endpoints: ArcaEndpointMetadata; dummy: { ok: boolean; appServer?: string; diff --git a/server/src/Models/ArcaCredentials/ArcaCredentials.ts b/server/src/Models/ArcaCredentials/ArcaCredentials.ts index 058cb2e..738edbc 100644 --- a/server/src/Models/ArcaCredentials/ArcaCredentials.ts +++ b/server/src/Models/ArcaCredentials/ArcaCredentials.ts @@ -200,6 +200,35 @@ class ArcaCredentialsManager implements IArcaCredentialsManager { return parsedValue; } + private getArcaEnvironment(): ARCA_WSAA_ENVIRONMENT { + const environment = (process.env.ARCA_ENVIRONMENT || ARCA_WSAA_ENVIRONMENT.HOMOLOGATION).trim().toLowerCase(); + + if (!Object.values(ARCA_WSAA_ENVIRONMENT).includes(environment as ARCA_WSAA_ENVIRONMENT)) { + throw new Error("ARCA_ENVIRONMENT debe ser 'homologation' o 'production'"); + } + + return environment as ARCA_WSAA_ENVIRONMENT; + } + + private getEndpointHost(value?: string): string | undefined { + if (!value) { + return undefined; + } + + try { + return new URL(value).host; + } catch { + return undefined; + } + } + + private getArcaEndpointMetadata() { + return { + wsaaHost: this.getEndpointHost(process.env.ARCA_WSAA_URL), + wsfeHost: this.getEndpointHost(process.env.ARCA_WSFE_URL), + }; + } + private signTraCms(traXml: string, certificatePem: string, privateKeyPem: string): string { const p7 = forge.pkcs7.createSignedData(); p7.content = forge.util.createBuffer(traXml, "utf8"); @@ -270,7 +299,7 @@ class ArcaCredentialsManager implements IArcaCredentialsManager { cached: boolean; }> { const service = ARCA_WSAA_SERVICE.WSFE; - const environment = ARCA_WSAA_ENVIRONMENT.HOMOLOGATION; + const environment = this.getArcaEnvironment(); const cachedToken = await ArcaWsaaTokens.getValidToken({ companyId, service, environment }); if (cachedToken) { @@ -582,7 +611,8 @@ class ArcaCredentialsManager implements IArcaCredentialsManager { private parseWsfeIssueFacturaCReturn( xml: string, pointOfSale: number, - voucherNumber: number + voucherNumber: number, + environment: ARCA_WSAA_ENVIRONMENT ): ArcaWsfeIssueFacturaCResult { const faultString = this.extractXmlValue(xml, "faultstring"); @@ -600,8 +630,9 @@ class ArcaCredentialsManager implements IArcaCredentialsManager { } return { - environment: ARCA_WSAA_ENVIRONMENT.HOMOLOGATION, + environment, service: ARCA_WSAA_SERVICE.WSFE, + endpoints: this.getArcaEndpointMetadata(), pointOfSale, voucherType: FACTURA_C_VOUCHER_TYPE, voucherNumber, @@ -807,13 +838,14 @@ class ArcaCredentialsManager implements IArcaCredentialsManager { await this.validateCompanyAdmin(data.companyId, data.sessionUser); const service = ARCA_WSAA_SERVICE.WSFE; - const environment = ARCA_WSAA_ENVIRONMENT.HOMOLOGATION; + const environment = this.getArcaEnvironment(); const loginTicket = await this.getWsaaLoginTicket(data.companyId); return { status: loginTicket.cached ? "cached" : "created", environment, service, + endpoints: this.getArcaEndpointMetadata(), expirationTime: loginTicket.expirationTime, cached: loginTicket.cached, }; @@ -823,6 +855,7 @@ class ArcaCredentialsManager implements IArcaCredentialsManager { await this.validateCompanyAdmin(data.companyId, data.sessionUser); const fiscalProfile = await this.requireActiveFiscalProfile(data.companyId, data.sessionUser); const loginTicket = await this.getWsaaLoginTicket(data.companyId); + const environment = this.getArcaEnvironment(); const voucherType = FACTURA_C_VOUCHER_TYPE; let responseXml: string; @@ -842,8 +875,9 @@ class ArcaCredentialsManager implements IArcaCredentialsManager { } return { - environment: ARCA_WSAA_ENVIRONMENT.HOMOLOGATION, + environment, service: ARCA_WSAA_SERVICE.WSFE, + endpoints: this.getArcaEndpointMetadata(), pointOfSale: fiscalProfile.pointOfSale, voucherType, lastVoucherNumber: this.parseWsfeLastVoucherReturn(responseXml), @@ -859,6 +893,7 @@ class ArcaCredentialsManager implements IArcaCredentialsManager { const fiscalProfile = await this.requireActiveFiscalProfile(data.companyId, data.sessionUser); const loginTicket = await this.getWsaaLoginTicket(data.companyId); + const environment = this.getArcaEnvironment(); const voucherType = 11; const receiverDocumentType = this.parseXmlInteger( data.receiverDocumentType ?? FINAL_CONSUMER_DOCUMENT_TYPE, @@ -908,7 +943,8 @@ class ArcaCredentialsManager implements IArcaCredentialsManager { "FECAESolicitar" ), fiscalProfile.pointOfSale, - voucherNumber + voucherNumber, + environment ); } catch (error) { throw new ArcaWsfeIssueFacturaCError( @@ -927,11 +963,13 @@ class ArcaCredentialsManager implements IArcaCredentialsManager { await this.validateCompanyAdmin(data.companyId, data.sessionUser); const fiscalProfile = await this.requireActiveFiscalProfile(data.companyId, data.sessionUser); const loginTicket = await this.getWsaaLoginTicket(data.companyId); + const environment = this.getArcaEnvironment(); const voucherType = 11; const diagnostics: ArcaWsfeDiagnosticsResult = { - environment: ARCA_WSAA_ENVIRONMENT.HOMOLOGATION, + environment, service: ARCA_WSAA_SERVICE.WSFE, + endpoints: this.getArcaEndpointMetadata(), dummy: { ok: false }, voucherTypes: { ok: false }, pointsOfSale: { diff --git a/server/src/Models/ArcaWsaaTokens/ArcaWsaaTokens.Interface.ts b/server/src/Models/ArcaWsaaTokens/ArcaWsaaTokens.Interface.ts index f5feb6d..adf972d 100644 --- a/server/src/Models/ArcaWsaaTokens/ArcaWsaaTokens.Interface.ts +++ b/server/src/Models/ArcaWsaaTokens/ArcaWsaaTokens.Interface.ts @@ -6,6 +6,7 @@ export enum ARCA_WSAA_SERVICE { export enum ARCA_WSAA_ENVIRONMENT { HOMOLOGATION = "homologation", + PRODUCTION = "production", } export type ArcaWsaaTokenLookupParams = { diff --git a/txclient/src/app/admin/(organization-profile)/org/profile/[id]/cash/Cash.Service.tsx b/txclient/src/app/admin/(organization-profile)/org/profile/[id]/cash/Cash.Service.tsx index d058aaf..6037649 100644 --- a/txclient/src/app/admin/(organization-profile)/org/profile/[id]/cash/Cash.Service.tsx +++ b/txclient/src/app/admin/(organization-profile)/org/profile/[id]/cash/Cash.Service.tsx @@ -19,8 +19,12 @@ export type ArcaWsfeLastVoucherParams = { }; export type ArcaWsfeLastVoucherResult = { - environment: "homologation"; + environment: "homologation" | "production"; service: "wsfe"; + endpoints?: { + wsaaHost?: string; + wsfeHost?: string; + }; pointOfSale: number; voucherType: 11; lastVoucherNumber: number; diff --git a/txclient/src/app/admin/assistant/components/StepEngine/flows/ArcaTestConnectionFlow.tsx b/txclient/src/app/admin/assistant/components/StepEngine/flows/ArcaTestConnectionFlow.tsx index 76593f5..93c25ad 100644 --- a/txclient/src/app/admin/assistant/components/StepEngine/flows/ArcaTestConnectionFlow.tsx +++ b/txclient/src/app/admin/assistant/components/StepEngine/flows/ArcaTestConnectionFlow.tsx @@ -4,15 +4,22 @@ import QuestionCard from "../../QuestionCard/QuestionCard"; export type ArcaWsaaTestResult = { status?: string; - environment?: string; + environment?: "homologation" | "production"; service?: string; + endpoints?: ArcaEndpointMetadata; expirationTime?: string; cached?: boolean; }; +type ArcaEndpointMetadata = { + wsaaHost?: string; + wsfeHost?: string; +}; + export type ArcaWsfeDiagnosticsResult = { - environment?: string; + environment?: "homologation" | "production"; service?: string; + endpoints?: ArcaEndpointMetadata; dummy?: { ok: boolean; appServer?: string; @@ -84,6 +91,7 @@ export default function ArcaTestConnectionFlow({ currentStepIndex, loading, diag WSAA está OK. El diagnóstico WSFE no emite comprobantes: solo consulta salud, puntos de venta, tipos de comprobante y último número autorizado. + {result?.endpoints?.wsaaHost ? : null} {diagnosticsLoading && Consultando FEDummy, puntos de venta, tipos de comprobante y último comprobante autorizado...} @@ -125,6 +133,7 @@ function WsfeDiagnosticsPanel({ diagnostics }: { diagnostics: ArcaWsfeDiagnostic return (
+ {diagnostics.endpoints?.wsfeHost ? : null} {diagnostics.pointsOfSale?.points?.length ? : null} {diagnostics.pointsOfSale?.error ? : null}