feat: agregar validaciones para el módulo ARCA según el plan de la organización

This commit is contained in:
2026-09-15 11:32:11 -03:00
parent 4460d66f39
commit 322f1d3f15
2 changed files with 95 additions and 4 deletions
@@ -2,6 +2,9 @@ import crypto from "crypto";
import axios from "axios"; import axios from "axios";
import forge from "node-forge"; import forge from "node-forge";
import { validatePermissionsByCompany, validateSessionUser } from "../../helpers/check"; import { validatePermissionsByCompany, validateSessionUser } from "../../helpers/check";
import CompaniesList from "../Companies/Companies";
import PlanSubscriptionsList from "../PlanSubscriptions/PlanSubscriptons";
import { PlanFeatures } from "../Plans/Plans.interface";
import ArcaWsaaTokens from "../ArcaWsaaTokens/ArcaWsaaTokens"; import ArcaWsaaTokens from "../ArcaWsaaTokens/ArcaWsaaTokens";
import { import {
ARCA_WSAA_ENVIRONMENT, ARCA_WSAA_ENVIRONMENT,
@@ -123,6 +126,23 @@ class ArcaCredentialsManager implements IArcaCredentialsManager {
await validatePermissionsByCompany({ companyId, sessionUser }); await validatePermissionsByCompany({ companyId, sessionUser });
} }
private async requirePaymentsFeature(companyId: string): Promise<void> {
const company = await CompaniesList.companies.findOne({ _id: companyId });
if (!company) {
throw new Error("No se ha encontrado la organización");
}
const canAccessPayments = await PlanSubscriptionsList.checkFeature({
userId: String(company.ownerId),
feature: PlanFeatures.PAYMENTS,
});
if (!canAccessPayments) {
throw new Error("La organización no tiene un plan que soporte el módulo de pagos");
}
}
private async requireActiveFiscalProfile( private async requireActiveFiscalProfile(
companyId: string, companyId: string,
sessionUser: string sessionUser: string
@@ -730,6 +750,7 @@ class ArcaCredentialsManager implements IArcaCredentialsManager {
public async generateCsr(data: GenerateArcaCredentialCsrParams): Promise<ArcaCredentialView> { public async generateCsr(data: GenerateArcaCredentialCsrParams): Promise<ArcaCredentialView> {
await this.validateCompanyAdmin(data.companyId, data.sessionUser); await this.validateCompanyAdmin(data.companyId, data.sessionUser);
await this.requirePaymentsFeature(data.companyId);
const fiscalProfile = await this.requireActiveFiscalProfile(data.companyId, data.sessionUser); const fiscalProfile = await this.requireActiveFiscalProfile(data.companyId, data.sessionUser);
const keys = forge.pki.rsa.generateKeyPair(2048); const keys = forge.pki.rsa.generateKeyPair(2048);
@@ -778,6 +799,7 @@ class ArcaCredentialsManager implements IArcaCredentialsManager {
public async uploadCertificate(data: UploadArcaCredentialCertificateParams): Promise<ArcaCredentialView> { public async uploadCertificate(data: UploadArcaCredentialCertificateParams): Promise<ArcaCredentialView> {
await this.validateCompanyAdmin(data.companyId, data.sessionUser); await this.validateCompanyAdmin(data.companyId, data.sessionUser);
await this.requirePaymentsFeature(data.companyId);
if (!data.certificatePem || !data.certificatePem.trim()) { if (!data.certificatePem || !data.certificatePem.trim()) {
throw new Error("Se requiere el certificado ARCA"); throw new Error("Se requiere el certificado ARCA");
@@ -836,6 +858,7 @@ class ArcaCredentialsManager implements IArcaCredentialsManager {
public async testWsaaLogin(data: TestArcaWsaaLoginParams): Promise<ArcaWsaaLoginTestResult> { public async testWsaaLogin(data: TestArcaWsaaLoginParams): Promise<ArcaWsaaLoginTestResult> {
await this.validateCompanyAdmin(data.companyId, data.sessionUser); await this.validateCompanyAdmin(data.companyId, data.sessionUser);
await this.requirePaymentsFeature(data.companyId);
const service = ARCA_WSAA_SERVICE.WSFE; const service = ARCA_WSAA_SERVICE.WSFE;
const environment = this.getArcaEnvironment(); const environment = this.getArcaEnvironment();
@@ -853,6 +876,7 @@ class ArcaCredentialsManager implements IArcaCredentialsManager {
public async getWsfeLastVoucher(data: ArcaWsfeLastVoucherParams): Promise<ArcaWsfeLastVoucherResult> { public async getWsfeLastVoucher(data: ArcaWsfeLastVoucherParams): Promise<ArcaWsfeLastVoucherResult> {
await this.validateCompanyAdmin(data.companyId, data.sessionUser); await this.validateCompanyAdmin(data.companyId, data.sessionUser);
await this.requirePaymentsFeature(data.companyId);
const fiscalProfile = await this.requireActiveFiscalProfile(data.companyId, data.sessionUser); const fiscalProfile = await this.requireActiveFiscalProfile(data.companyId, data.sessionUser);
const loginTicket = await this.getWsaaLoginTicket(data.companyId); const loginTicket = await this.getWsaaLoginTicket(data.companyId);
const environment = this.getArcaEnvironment(); const environment = this.getArcaEnvironment();
@@ -886,6 +910,7 @@ class ArcaCredentialsManager implements IArcaCredentialsManager {
public async issueFacturaC(data: ArcaWsfeIssueFacturaCParams): Promise<ArcaWsfeIssueFacturaCResult> { public async issueFacturaC(data: ArcaWsfeIssueFacturaCParams): Promise<ArcaWsfeIssueFacturaCResult> {
await this.validateCompanyAdmin(data.companyId, data.sessionUser); await this.validateCompanyAdmin(data.companyId, data.sessionUser);
await this.requirePaymentsFeature(data.companyId);
if (data.amount <= 0) { if (data.amount <= 0) {
throw new Error("El importe de la factura debe ser mayor a cero"); throw new Error("El importe de la factura debe ser mayor a cero");
@@ -961,6 +986,7 @@ class ArcaCredentialsManager implements IArcaCredentialsManager {
public async getWsfeDiagnostics(data: ArcaWsfeDiagnosticsParams): Promise<ArcaWsfeDiagnosticsResult> { public async getWsfeDiagnostics(data: ArcaWsfeDiagnosticsParams): Promise<ArcaWsfeDiagnosticsResult> {
await this.validateCompanyAdmin(data.companyId, data.sessionUser); await this.validateCompanyAdmin(data.companyId, data.sessionUser);
await this.requirePaymentsFeature(data.companyId);
const fiscalProfile = await this.requireActiveFiscalProfile(data.companyId, data.sessionUser); const fiscalProfile = await this.requireActiveFiscalProfile(data.companyId, data.sessionUser);
const loginTicket = await this.getWsaaLoginTicket(data.companyId); const loginTicket = await this.getWsaaLoginTicket(data.companyId);
const environment = this.getArcaEnvironment(); const environment = this.getArcaEnvironment();
@@ -1054,8 +1080,11 @@ class ArcaCredentialsManager implements IArcaCredentialsManager {
await validateSessionUser({ sessionUser: data.sessionUser }); await validateSessionUser({ sessionUser: data.sessionUser });
let companyId: string | undefined;
if (data.companyId) { if (data.companyId) {
await validatePermissionsByCompany({ companyId: data.companyId, sessionUser: data.sessionUser }); await validatePermissionsByCompany({ companyId: data.companyId, sessionUser: data.sessionUser });
companyId = data.companyId;
} else if (data.id) { } else if (data.id) {
const credential = await this.arcaCredentials.findOne(data); const credential = await this.arcaCredentials.findOne(data);
@@ -1067,12 +1096,13 @@ class ArcaCredentialsManager implements IArcaCredentialsManager {
companyId: String(credential.companyId), companyId: String(credential.companyId),
sessionUser: data.sessionUser, sessionUser: data.sessionUser,
}); });
companyId = String(credential.companyId);
return [this.sanitizeCredential(credential)];
} else { } else {
throw new Error("Se requiere una organización para buscar credenciales ARCA"); throw new Error("Se requiere una organización para buscar credenciales ARCA");
} }
await this.requirePaymentsFeature(companyId);
const credentials = await this.arcaCredentials.find(data); const credentials = await this.arcaCredentials.find(data);
return credentials.map((credential) => this.sanitizeCredential(credential)); return credentials.map((credential) => this.sanitizeCredential(credential));
} }
@@ -206,6 +206,7 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
const [wapError, setWapError] = useState<string | null>(null); const [wapError, setWapError] = useState<string | null>(null);
// State for ARCA onboarding flow // State for ARCA onboarding flow
const [arcaIsBlocked, setArcaIsBlocked] = useState(false);
const [arcaCuit, setArcaCuit] = useState(""); const [arcaCuit, setArcaCuit] = useState("");
const [arcaLegalName, setArcaLegalName] = useState(""); const [arcaLegalName, setArcaLegalName] = useState("");
const [arcaTaxCondition, setArcaTaxCondition] = useState<"MONOTRIBUTO" | "EXENTO">("MONOTRIBUTO"); const [arcaTaxCondition, setArcaTaxCondition] = useState<"MONOTRIBUTO" | "EXENTO">("MONOTRIBUTO");
@@ -277,6 +278,7 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
}; };
const loadOrganizationContext = (org: any) => { const loadOrganizationContext = (org: any) => {
setArcaIsBlocked(false);
setCreatedCompanyId(org.id || org._id); setCreatedCompanyId(org.id || org._id);
setSelectedOrganization(org); setSelectedOrganization(org);
setOrgName(org.name || ""); setOrgName(org.name || "");
@@ -499,6 +501,18 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
setWapQr(null); setWapQr(null);
setWapError(null); setWapError(null);
} else if (flowId === "arca-onboarding") { } else if (flowId === "arca-onboarding") {
const orgSub = SessionInfo.organizationSubscriptions[org.id || org._id];
const planSupportsPayments = orgSub ? orgSub.plan?.payments : false;
if (!planSupportsPayments) {
setArcaIsBlocked(true);
setCurrentStepIndex(0);
setArcaError(null);
setArcaLoading(false);
return;
}
setArcaIsBlocked(false);
setCurrentStepIndex(0); setCurrentStepIndex(0);
setArcaError(null); setArcaError(null);
setArcaLoading(false); setArcaLoading(false);
@@ -527,6 +541,19 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
setArcaStatusLabel(getArcaStatusLabel(credential.status)); setArcaStatusLabel(getArcaStatusLabel(credential.status));
}).catch(console.error); }).catch(console.error);
} else if (flowId === "arca-test-connection") { } else if (flowId === "arca-test-connection") {
const orgSub = SessionInfo.organizationSubscriptions[org.id || org._id];
const planSupportsPayments = orgSub ? orgSub.plan?.payments : false;
if (!planSupportsPayments) {
setArcaIsBlocked(true);
setCurrentStepIndex(0);
setArcaError(null);
setArcaLoading(false);
setArcaTestResult(null);
return;
}
setArcaIsBlocked(false);
setCurrentStepIndex(0); setCurrentStepIndex(0);
setArcaError(null); setArcaError(null);
setArcaLoading(false); setArcaLoading(false);
@@ -2415,8 +2442,42 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
if (flowId === "public-link") return renderPublicLinkFlow(); if (flowId === "public-link") return renderPublicLinkFlow();
if (flowId === "notifications") return renderNotificationsFlow(); if (flowId === "notifications") return renderNotificationsFlow();
if (flowId === "whatsapp-bot") return renderWhatsAppBotFlow(); if (flowId === "whatsapp-bot") return renderWhatsAppBotFlow();
if (flowId === "arca-onboarding") return renderArcaOnboardingFlow(); if (flowId === "arca-onboarding") {
if (flowId === "arca-test-connection") return renderArcaTestConnectionFlow(); if (arcaIsBlocked) {
return (
<QuestionCard
isActive
title="Módulo ARCA no disponible"
description="Este flujo requiere un plan con el módulo de pagos habilitado para emitir Factura C."
onNext={() => router.push("/landing/pricing")}
nextLabel="Ver planes"
>
<div style={{ color: "rgba(255,255,255,0.75)", textAlign: "center", lineHeight: 1.6 }}>
La organización seleccionada necesita un plan con el módulo de pagos activo para usar ARCA. Actualizá tu plan o volvé al asistente y elegí otra opción.
</div>
</QuestionCard>
);
}
return renderArcaOnboardingFlow();
}
if (flowId === "arca-test-connection") {
if (arcaIsBlocked) {
return (
<QuestionCard
isActive
title="Test de conexión ARCA no disponible"
description="Este flujo requiere un plan con el módulo de pagos habilitado para probar la conexión con ARCA."
onNext={() => router.push("/landing/pricing")}
nextLabel="Ver planes"
>
<div style={{ color: "rgba(255,255,255,0.75)", textAlign: "center", lineHeight: 1.6 }}>
La organización seleccionada necesita un plan con el módulo de pagos activo para usar ARCA. Actualizá tu plan o volvé al asistente y elegí otra opción.
</div>
</QuestionCard>
);
}
return renderArcaTestConnectionFlow();
}
if (flowId === "disable-schedule") return renderDisableScheduleFlow(); if (flowId === "disable-schedule") return renderDisableScheduleFlow();
if (flowId === "open-reservation-periods") return renderReservationPeriodFlow(); if (flowId === "open-reservation-periods") return renderReservationPeriodFlow();
if (flowId === "override-schedule") return renderOverrideScheduleFlow(); if (flowId === "override-schedule") return renderOverrideScheduleFlow();