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
@@ -13,6 +13,19 @@ import {
import { ApiError } from "@core/Models/Server.Error.model";
import * as Yup from "yup";
export type ArcaWsfeLastVoucherParams = {
companyId: string;
sessionUser: string;
};
export type ArcaWsfeLastVoucherResult = {
environment: "homologation";
service: "wsfe";
pointOfSale: number;
voucherType: 11;
lastVoucherNumber: number;
};
const schemaCreateCashFlowMovement = Yup.object().shape({
paymentMethod: Yup.string().required("Debe introducir el tipo de movimiento."),
companyId: Yup.string().required("La organización no es valida."),
@@ -72,3 +85,9 @@ export const cashFlowPaginate = async (
export const cashFlowPaginateBalance = async (data: FindCashFlowBalanceParams): Promise<number> => {
return ApiRequest.post<number>("cashflow/paginate-balance", data);
};
export const getArcaWsfeLastVoucher = async (
data: ArcaWsfeLastVoucherParams
): Promise<ArcaWsfeLastVoucherResult> => {
return ApiRequest.post<ArcaWsfeLastVoucherResult>("arca-credentials/wsfe-last-voucher", data);
};
@@ -13,7 +13,7 @@ import ButtonMaterial from "@mui/material/Button";
import { useNavigation } from "@core/app/hooks/goto";
import { PaginateCashFlowParams, PaginateCashFlowResults } from "@core/Models/CashFlow.model";
import { useSessionStore } from "@core/Store/Sesion.Store";
import { cashFlowPaginate } from "./Cash.Service";
import { ArcaWsfeLastVoucherResult, cashFlowPaginate, getArcaWsfeLastVoucher } from "./Cash.Service";
import { useAlert } from "@core/Store/Alert.Store";
import { IconButton } from "@mui/material";
import RetirarIcon from "@mui/icons-material/CallMadeOutlined";
@@ -42,6 +42,10 @@ export default function OrganizationCashFlowDetail() {
const [dateFrom, setDateFrom] = useState<Dayjs>(dayjs().startOf("day"));
const [dateTo, setDateTo] = useState<Dayjs>(dayjs().endOf("day"));
const [view, setView] = useState<PaginateCashFlowResults | null>(null);
const [arcaLastVoucher, setArcaLastVoucher] = useState<ArcaWsfeLastVoucherResult | null>(null);
const [arcaLastVoucherLoading, setArcaLastVoucherLoading] = useState(false);
const [arcaLastVoucherError, setArcaLastVoucherError] = useState(false);
const [arcaLastVoucherErrorMessage, setArcaLastVoucherErrorMessage] = useState("");
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
@@ -73,6 +77,10 @@ export default function OrganizationCashFlowDetail() {
cargarDatos();
}, [SessionInfo.userId, id, dateFrom, dateTo]);
useEffect(() => {
loadArcaLastVoucher();
}, [SessionInfo.userId, id]);
const cargarDatos = async () => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
@@ -110,6 +118,34 @@ export default function OrganizationCashFlowDetail() {
});
};
const loadArcaLastVoucher = async () => {
if (!SessionInfo.userId || !id) {
return;
}
setArcaLastVoucherLoading(true);
setArcaLastVoucherError(false);
setArcaLastVoucherErrorMessage("");
getArcaWsfeLastVoucher({
companyId: id,
sessionUser: SessionInfo.userId,
})
.then((data) => {
setArcaLastVoucher(data);
})
.catch((error) => {
setArcaLastVoucher(null);
setArcaLastVoucherError(true);
setArcaLastVoucherErrorMessage(
error?.desc || error?.message || "No pudimos consultar ARCA"
);
})
.finally(() => {
setArcaLastVoucherLoading(false);
});
};
const add = (type: CLIENT_ACCOUNT_MOVEMENT_TYPES) => {
goTo("/admin/org/profile/" + id + "/cash/add/" + type);
};
@@ -175,6 +211,40 @@ export default function OrganizationCashFlowDetail() {
>
{formatPrice(view.balance)}
</div>
<div
style={{
marginBottom: "10px",
padding: "8px 12px",
borderRadius: "8px",
backgroundColor: "var(--white)",
color: arcaLastVoucherError
? "var(--gray-dark)"
: "var(--wine-dark)",
fontSize: "13px",
textAlign: "center",
}}
>
{arcaLastVoucherLoading ? (
"Consultando último comprobante ARCA..."
) : arcaLastVoucher ? (
<>
Última Factura C autorizada ARCA: #{" "}
<strong>{arcaLastVoucher.lastVoucherNumber}</strong>
{" · "}PV {arcaLastVoucher.pointOfSale}
</>
) : arcaLastVoucherError ? (
<>
No pudimos consultar ARCA
{arcaLastVoucherErrorMessage && (
<div style={{ marginTop: "4px" }}>
{arcaLastVoucherErrorMessage}
</div>
)}
</>
) : (
"ARCA: esperando consulta del último comprobante"
)}
</div>
<div
style={{
display: "flex",
@@ -19,6 +19,7 @@ import ReviewsIcon from '@mui/icons-material/Reviews';
import NotificationsIcon from '@mui/icons-material/Notifications';
import WhatsAppIcon from '@mui/icons-material/WhatsApp';
import QrCode2Icon from '@mui/icons-material/QrCode2';
import ReceiptLongIcon from '@mui/icons-material/ReceiptLong';
import API from "@services/Api.Service";
import { ReservationPeriodsSummary } from "@models/SchedulesEnabled.type";
@@ -88,6 +89,24 @@ const AVAILABLE_FLOWS: FlowDefinition[] = [
keywords: ["link", "enlace", "url", "slug", "qr", "codigo qr", "publico", "reservas"],
icon: QrCode2Icon
},
{
id: "arca-onboarding",
flowId: "arca-onboarding",
title: "Conectar ARCA",
description: "Configurá los datos fiscales y el certificado para emitir Factura C.",
category: "Integraciones",
keywords: ["arca", "afip", "factura", "factura c", "fiscal", "certificado", "csr", "monotributo"],
icon: ReceiptLongIcon
},
{
id: "arca-test-connection",
flowId: "arca-test-connection",
title: "Probar conexión ARCA",
description: "Validá el certificado y la conexión con el servicio WSFE de ARCA.",
category: "Integraciones",
keywords: ["arca", "afip", "wsaa", "wsfe", "conexion", "certificado", "factura"],
icon: ReceiptLongIcon
},
{
id: "notifications",
flowId: "notifications",
@@ -27,6 +27,8 @@ import NotificationsFlow from "./flows/NotificationsFlow";
import NewCollaboratorFlow from "./flows/NewCollaboratorFlow";
import SetupCollaboratorFlow from "./flows/SetupCollaboratorFlow";
import ReservationPeriodFlow from "./flows/ReservationPeriodFlow";
import ArcaOnboardingFlow from "./flows/ArcaOnboardingFlow";
import ArcaTestConnectionFlow, { ArcaWsaaTestResult, ArcaWsfeDiagnosticsResult } from "./flows/ArcaTestConnectionFlow";
import SuccessScreen from "./components/SuccessScreen";
import OrganizationPicker from "./components/OrganizationPicker";
import { reservationPeriodOptions } from "./constants/reservationPeriods";
@@ -178,6 +180,20 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
const [wapQr, setWapQr] = useState<string | null>(null);
const [wapError, setWapError] = useState<string | null>(null);
// State for ARCA onboarding flow
const [arcaCuit, setArcaCuit] = useState("");
const [arcaLegalName, setArcaLegalName] = useState("");
const [arcaTaxCondition, setArcaTaxCondition] = useState<"MONOTRIBUTO" | "EXENTO">("MONOTRIBUTO");
const [arcaPointOfSale, setArcaPointOfSale] = useState("1");
const [arcaCsrPem, setArcaCsrPem] = useState("");
const [arcaCertificatePem, setArcaCertificatePem] = useState("");
const [arcaStatusLabel, setArcaStatusLabel] = useState("");
const [arcaLoading, setArcaLoading] = useState(false);
const [arcaDiagnosticsLoading, setArcaDiagnosticsLoading] = useState(false);
const [arcaError, setArcaError] = useState<string | null>(null);
const [arcaTestResult, setArcaTestResult] = useState<ArcaWsaaTestResult | null>(null);
const [arcaDiagnosticsResult, setArcaDiagnosticsResult] = useState<ArcaWsfeDiagnosticsResult | null>(null);
// State for Setup Collaborator Flow
const [collaboratorToSetup, setCollaboratorToSetup] = useState<any | null>(null);
const [setupMissingServices, setSetupMissingServices] = useState(false);
@@ -392,11 +408,43 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
setWapView(null);
setWapQr(null);
setWapError(null);
} else if (flowId === "arca-onboarding") {
setCurrentStepIndex(0);
setArcaError(null);
setArcaLoading(false);
setArcaTestResult(null);
setArcaCertificatePem("");
API.post<any[]>("organization-fiscal-profiles/find", {
companyId: org.id || org._id,
sessionUser: SessionInfo.userId
}).then(res => {
const profile = res?.[0];
if (!profile) return;
setArcaCuit(profile.cuit || "");
setArcaLegalName(profile.legalName || "");
setArcaTaxCondition(profile.taxCondition || "MONOTRIBUTO");
setArcaPointOfSale(profile.pointOfSale ? String(profile.pointOfSale) : "1");
}).catch(console.error);
API.post<any[]>("arca-credentials/find", {
companyId: org.id || org._id,
sessionUser: SessionInfo.userId
}).then(res => {
const credential = res?.[0];
if (!credential) return;
setArcaCsrPem(credential.csrPem || "");
setArcaStatusLabel(getArcaStatusLabel(credential.status));
}).catch(console.error);
} else if (flowId === "arca-test-connection") {
setCurrentStepIndex(0);
setArcaError(null);
setArcaLoading(false);
setArcaTestResult(null);
setArcaDiagnosticsResult(null);
}
};
React.useEffect(() => {
if (flowId === "new-collaborator" || flowId === "setup-collaborator" || flowId === "public-visibility" || flowId === "public-link" || flowId === "notifications" || flowId === "whatsapp-bot" || flowId.startsWith("onboarding") || flowId === "update-schedule" || flowId === "override-schedule" || flowId === "disable-schedule" || flowId === "open-reservation-periods") {
if (flowId === "new-collaborator" || flowId === "setup-collaborator" || flowId === "public-visibility" || flowId === "public-link" || flowId === "notifications" || flowId === "whatsapp-bot" || flowId === "arca-onboarding" || flowId === "arca-test-connection" || flowId.startsWith("onboarding") || flowId === "update-schedule" || flowId === "override-schedule" || flowId === "disable-schedule" || flowId === "open-reservation-periods") {
setIsResuming(true);
API.post<any[]>("companies/get-by-user", { sessionUser: SessionInfo.userId })
.then(response => {
@@ -417,7 +465,7 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
.finally(() => setIsResuming(false));
}
if (flowId === "onboarding-service" || flowId === "onboarding-schedule" || flowId === "setup-collaborator" || flowId === "public-visibility" || flowId === "public-link" || flowId === "notifications" || flowId === "whatsapp-bot" || flowId === "open-reservation-periods" || (flowId === "onboarding-org" && action === "new-org")) {
if (flowId === "onboarding-service" || flowId === "onboarding-schedule" || flowId === "setup-collaborator" || flowId === "public-visibility" || flowId === "public-link" || flowId === "notifications" || flowId === "whatsapp-bot" || flowId === "arca-onboarding" || flowId === "arca-test-connection" || flowId === "open-reservation-periods" || (flowId === "onboarding-org" && action === "new-org")) {
setCurrentStepIndex(0);
}
}, [flowId, SessionInfo.metrics.organizationsCount, SessionInfo.userId, router]);
@@ -758,6 +806,185 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
/>
);
const getArcaStatusLabel = (status?: string) => {
if (status === "READY") return "Certificado validado. La conexión con ARCA quedó lista.";
if (status === "CERTIFICATE_UPLOADED") return "Certificado cargado. Estamos listos para validar la conexión.";
if (status === "CSR_GENERATED") return "CSR generado. Descargalo y subilo en ARCA para obtener el certificado.";
if (status === "INVALID") return "El certificado cargado no pudo validarse. Revisá que corresponda al CSR generado.";
return "";
};
const handleSaveArcaFiscalProfile = async () => {
if (!createdCompanyId || arcaLoading) return;
setArcaLoading(true);
setArcaError(null);
try {
await API.post<any>("organization-fiscal-profiles/upsert", {
companyId: createdCompanyId,
cuit: arcaCuit,
legalName: arcaLegalName,
taxCondition: arcaTaxCondition,
pointOfSale: Number(arcaPointOfSale),
sessionUser: SessionInfo.userId
});
handleNext();
} catch (error: any) {
console.error("Error guardando datos fiscales ARCA:", error);
setArcaError(error?.message || "No pudimos guardar los datos fiscales. Revisá la información y volvé a intentar.");
} finally {
setArcaLoading(false);
}
};
const handleGenerateArcaCsr = async () => {
if (!createdCompanyId || arcaLoading) return;
setArcaLoading(true);
setArcaError(null);
try {
const credential = await API.post<any>("arca-credentials/generate-csr", {
companyId: createdCompanyId,
sessionUser: SessionInfo.userId
});
const generatedCsr = credential?.csrPem || "";
setArcaCsrPem(generatedCsr);
setArcaStatusLabel(getArcaStatusLabel(credential?.status));
if (generatedCsr) {
downloadArcaCsr(generatedCsr);
}
} catch (error: any) {
console.error("Error generando CSR ARCA:", error);
setArcaError(error?.message || "No pudimos generar el CSR. Intentá nuevamente.");
} finally {
setArcaLoading(false);
}
};
const downloadArcaCsr = (csrPem: string) => {
const blob = new Blob([csrPem], { type: "application/pkcs10" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `turnosxpress-arca-${arcaCuit || createdCompanyId}.csr`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
const handleDownloadArcaCsr = () => {
if (!arcaCsrPem) return;
downloadArcaCsr(arcaCsrPem);
};
const handleArcaCertificateFile = (file: File | null) => {
if (!file) return;
const reader = new FileReader();
reader.onload = () => setArcaCertificatePem(String(reader.result || ""));
reader.onerror = () => setArcaError("No pudimos leer el archivo. Probá pegando el contenido del certificado.");
reader.readAsText(file);
};
const handleUploadArcaCertificate = async () => {
if (!createdCompanyId || arcaLoading || !arcaCertificatePem.trim()) return;
setArcaLoading(true);
setArcaError(null);
try {
const credential = await API.post<any>("arca-credentials/upload-certificate", {
companyId: createdCompanyId,
certificatePem: arcaCertificatePem,
sessionUser: SessionInfo.userId
});
setArcaStatusLabel(getArcaStatusLabel(credential?.status));
setCurrentStepIndex(5);
} catch (error: any) {
console.error("Error cargando certificado ARCA:", error);
setArcaError(error?.message || "No pudimos guardar el certificado. Revisá que sea el archivo emitido por ARCA.");
} finally {
setArcaLoading(false);
}
};
const handleTestArcaConnection = async () => {
if (!createdCompanyId || arcaLoading) return;
setArcaLoading(true);
setArcaError(null);
setArcaTestResult(null);
setArcaDiagnosticsResult(null);
try {
const result = await API.post<ArcaWsaaTestResult>("arca-credentials/test-wsaa-login", {
companyId: createdCompanyId,
sessionUser: SessionInfo.userId
});
setArcaTestResult(result);
setCurrentStepIndex(1);
setArcaDiagnosticsLoading(true);
try {
const diagnostics = await API.post<ArcaWsfeDiagnosticsResult>("arca-credentials/wsfe-diagnostics", {
companyId: createdCompanyId,
sessionUser: SessionInfo.userId
});
setArcaDiagnosticsResult(diagnostics);
} catch (diagnosticsError: any) {
console.error("Error consultando diagnóstico WSFE ARCA:", diagnosticsError);
setArcaDiagnosticsResult({
dummy: { ok: false, error: diagnosticsError?.message || "No pudimos consultar WSFE." },
voucherTypes: { ok: false, error: diagnosticsError?.message || "No pudimos consultar tipos de comprobante." },
pointsOfSale: { ok: false, error: diagnosticsError?.message || "No pudimos consultar puntos de venta." },
lastVoucher: { ok: false, error: diagnosticsError?.message || "No pudimos consultar el último comprobante." }
});
} finally {
setArcaDiagnosticsLoading(false);
}
} catch (error: any) {
console.error("Error probando conexión ARCA:", error);
setArcaError(error?.message || "No pudimos validar la conexión con ARCA. Revisá el certificado y volvé a intentar.");
setCurrentStepIndex(1);
} finally {
setArcaLoading(false);
}
};
const renderArcaTestConnectionFlow = () => (
<ArcaTestConnectionFlow
currentStepIndex={currentStepIndex}
loading={arcaLoading}
diagnosticsLoading={arcaDiagnosticsLoading}
error={arcaError}
result={arcaTestResult}
diagnostics={arcaDiagnosticsResult}
handleTestConnection={handleTestArcaConnection}
handleFinish={handleActionSuccess}
/>
);
const renderArcaOnboardingFlow = () => (
<ArcaOnboardingFlow
currentStepIndex={currentStepIndex}
cuit={arcaCuit}
setCuit={setArcaCuit}
legalName={arcaLegalName}
setLegalName={setArcaLegalName}
taxCondition={arcaTaxCondition}
setTaxCondition={setArcaTaxCondition}
pointOfSale={arcaPointOfSale}
setPointOfSale={setArcaPointOfSale}
csrPem={arcaCsrPem}
certificatePem={arcaCertificatePem}
setCertificatePem={setArcaCertificatePem}
statusLabel={arcaStatusLabel}
arcaLoading={arcaLoading}
arcaError={arcaError}
handleNext={handleNext}
handleSaveFiscalProfile={handleSaveArcaFiscalProfile}
handleGenerateCsr={handleGenerateArcaCsr}
handleDownloadCsr={handleDownloadArcaCsr}
handleCertificateFile={handleArcaCertificateFile}
handleUploadCertificate={handleUploadArcaCertificate}
handleEditFiscalProfile={() => setCurrentStepIndex(1)}
handleFinish={handleActionSuccess}
/>
);
const renderOnboardingOrgFlow = () => (
<OnboardingOrgFlow
currentStepIndex={currentStepIndex}
@@ -1678,6 +1905,8 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
if (flowId === "public-link") return renderPublicLinkFlow();
if (flowId === "notifications") return renderNotificationsFlow();
if (flowId === "whatsapp-bot") return renderWhatsAppBotFlow();
if (flowId === "arca-onboarding") return renderArcaOnboardingFlow();
if (flowId === "arca-test-connection") return renderArcaTestConnectionFlow();
if (flowId === "disable-schedule") return renderDisableScheduleFlow();
if (flowId === "open-reservation-periods") return renderReservationPeriodFlow();
if (flowId === "override-schedule") return renderOverrideScheduleFlow();
@@ -0,0 +1,132 @@
import React, { useState } from "react";
import ReceiptLongIcon from "@mui/icons-material/ReceiptLong";
import QuestionCard from "../../QuestionCard/QuestionCard";
import QuestionInput from "../../QuestionCard/QuestionInput";
type ArcaTaxCondition = "MONOTRIBUTO" | "EXENTO";
type Props = {
currentStepIndex: number;
cuit: string;
setCuit: (value: string) => void;
legalName: string;
setLegalName: (value: string) => void;
taxCondition: ArcaTaxCondition;
setTaxCondition: (value: ArcaTaxCondition) => void;
pointOfSale: string;
setPointOfSale: (value: string) => void;
csrPem: string;
certificatePem: string;
setCertificatePem: (value: string) => void;
statusLabel: string;
arcaLoading: boolean;
arcaError: string | null;
handleNext: () => void;
handleSaveFiscalProfile: () => void;
handleGenerateCsr: () => void;
handleDownloadCsr: () => void;
handleCertificateFile: (file: File | null) => void;
handleUploadCertificate: () => void;
handleEditFiscalProfile: () => void;
handleFinish: () => void;
};
export default function ArcaOnboardingFlow(props: Props) {
const { currentStepIndex, cuit, setCuit, legalName, setLegalName, taxCondition, setTaxCondition, pointOfSale, setPointOfSale, csrPem, certificatePem, setCertificatePem, statusLabel, arcaLoading, arcaError, handleNext, handleSaveFiscalProfile, handleGenerateCsr, handleDownloadCsr, handleCertificateFile, handleUploadCertificate, handleEditFiscalProfile, handleFinish } = props;
const canSaveFiscalProfile = cuit.trim().length >= 11 && legalName.trim().length >= 3 && Number(pointOfSale) > 0;
const [csrCopyStatus, setCsrCopyStatus] = useState<string | null>(null);
const handleCopyCsr = async () => {
if (!csrPem) return;
try {
await navigator.clipboard.writeText(csrPem);
setCsrCopyStatus("CSR copiado. Pegalo en ARCA cuando te lo pida.");
} catch (error) {
setCsrCopyStatus("No pudimos copiarlo automáticamente. Seleccioná el texto de abajo y copialo manualmente.");
}
};
return (
<>
<QuestionCard isActive={currentStepIndex === 0} title="Conectar ARCA" description="Te voy a guiar para dejar lista la emisión de Factura C. Primero guardamos tus datos fiscales, después generamos el pedido de certificado y al final cargás el certificado que te entrega ARCA." onNext={handleNext} nextLabel="Empezar" topAccessory={<ReceiptLongIcon style={{ fontSize: 52, color: "var(--wine-red)" }} />}>
<InfoPanel>Vas a necesitar tener acceso a ARCA con clave fiscal y permisos para administrar certificados del servicio de facturación electrónica.</InfoPanel>
</QuestionCard>
<QuestionCard isActive={currentStepIndex === 1} title="Datos fiscales" description="Completá los datos que van a identificar a tu organización cuando emitas Factura C." onNext={handleSaveFiscalProfile} disableNext={!canSaveFiscalProfile || arcaLoading} nextLabel={arcaLoading ? "Guardando..." : "Guardar datos fiscales"}>
<div style={{ display: "flex", flexDirection: "column", gap: "14px", width: "100%" }}>
<QuestionInput label="CUIT" value={cuit} onChange={(event) => setCuit(event.target.value.replace(/\D/g, ""))} placeholder="Sin guiones, por ejemplo 20123456789" inputMode="numeric" />
<QuestionInput label="Razón social" value={legalName} onChange={(event) => setLegalName(event.target.value)} placeholder="Nombre fiscal o razón social" />
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(160px, 1fr))", gap: "10px" }}>
<OptionButton active={taxCondition === "MONOTRIBUTO"} onClick={() => setTaxCondition("MONOTRIBUTO")}>Monotributo</OptionButton>
<OptionButton active={taxCondition === "EXENTO"} onClick={() => setTaxCondition("EXENTO")}>Exento</OptionButton>
</div>
<QuestionInput label="Punto de venta" value={pointOfSale} onChange={(event) => setPointOfSale(event.target.value.replace(/\D/g, ""))} placeholder="Ejemplo: 1" inputMode="numeric" />
<StatusMessage error={arcaError} />
</div>
</QuestionCard>
<QuestionCard isActive={currentStepIndex === 2} title="Pedido de certificado" description="Ahora generamos el archivo CSR. Ese archivo se descarga desde TurnosXpress y luego se sube en ARCA para que te devuelvan el certificado." onNext={csrPem ? handleNext : handleGenerateCsr} disableNext={arcaLoading} nextLabel={csrPem ? "Continuar con ARCA" : arcaLoading ? "Generando..." : "Generar y descargar CSR"}>
<div style={{ display: "flex", flexDirection: "column", gap: "12px", width: "100%" }}>
<InfoPanel>{statusLabel || "Todavía no generamos el CSR para esta organización."}</InfoPanel>
{csrPem && <InfoPanel>Ya generamos el CSR. ARCA puede pedirte subir el archivo o pegar el contenido completo, incluyendo BEGIN y END CERTIFICATE REQUEST.</InfoPanel>}
{csrPem && (
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
<textarea value={csrPem} readOnly rows={9} aria-label="Contenido CSR para copiar en ARCA" onFocus={(event) => event.target.select()} style={{ width: "100%", boxSizing: "border-box", borderRadius: "12px", border: "1px solid rgba(255,255,255,0.16)", background: "rgba(255,255,255,0.05)", color: "white", padding: "14px", fontFamily: "monospace", fontSize: "12px", lineHeight: 1.4, resize: "vertical" }} />
{csrCopyStatus && <InfoPanel>{csrCopyStatus}</InfoPanel>}
<SecondaryButton onClick={handleCopyCsr}>Copiar CSR</SecondaryButton>
</div>
)}
{csrPem && <SecondaryButton onClick={handleDownloadCsr}>Descargar archivo .csr</SecondaryButton>}
{csrPem && <SecondaryButton onClick={handleGenerateCsr}>Regenerar CSR</SecondaryButton>}
<StatusMessage error={arcaError} />
</div>
</QuestionCard>
<QuestionCard isActive={currentStepIndex === 3} title="Subir el CSR en ARCA" description="Con el archivo descargado, entrá a ARCA y generá el certificado para facturación electrónica." onNext={handleNext} nextLabel="Ya tengo el certificado">
<div style={{ display: "flex", flexDirection: "column", gap: "12px", width: "100%" }}>
<StepItem number="1">Ingresá a ARCA con clave fiscal y abrí el administrador de certificados digitales.</StepItem>
<StepItem number="2">Creá un nuevo certificado y subí el archivo .csr que descargaste desde TurnosXpress.</StepItem>
<StepItem number="3">Asociá el certificado al servicio de facturación electrónica WSFE.</StepItem>
<StepItem number="4">Descargá el certificado emitido por ARCA en formato .crt o .pem.</StepItem>
</div>
</QuestionCard>
<QuestionCard isActive={currentStepIndex === 4} title="Cargar certificado" description="Pegá el contenido del certificado o seleccioná el archivo que descargaste desde ARCA." onNext={handleUploadCertificate} disableNext={!certificatePem.trim() || arcaLoading} nextLabel={arcaLoading ? "Validando..." : "Guardar certificado"}>
<div style={{ display: "flex", flexDirection: "column", gap: "12px", width: "100%" }}>
<input type="file" accept=".crt,.pem,text/plain" onChange={(event) => handleCertificateFile(event.target.files?.[0] || null)} style={{ color: "rgba(255,255,255,0.75)" }} />
<textarea value={certificatePem} onChange={(event) => setCertificatePem(event.target.value)} placeholder="-----BEGIN CERTIFICATE-----" rows={8} style={{ width: "100%", boxSizing: "border-box", borderRadius: "12px", border: "1px solid rgba(255,255,255,0.16)", background: "rgba(255,255,255,0.05)", color: "white", padding: "14px", fontFamily: "monospace", resize: "vertical" }} />
<StatusMessage error={arcaError} />
</div>
</QuestionCard>
<QuestionCard isActive={currentStepIndex === 5} title="ARCA conectado" description="Tu organización ya tiene los datos fiscales y el certificado guardados para avanzar con Factura C." onNext={handleFinish} nextLabel="Finalizar" topAccessory={<div style={{ fontSize: 48 }}></div>}>
<InfoPanel>{statusLabel || "Configuración lista."}</InfoPanel>
<div style={{ marginTop: "12px" }}>
<SecondaryButton onClick={handleEditFiscalProfile}>Editar datos fiscales</SecondaryButton>
</div>
</QuestionCard>
</>
);
}
function InfoPanel({ children }: { children: React.ReactNode }) {
return <div style={{ padding: "14px", borderRadius: "12px", background: "rgba(255,255,255,0.05)", border: "1px solid rgba(255,255,255,0.12)", color: "rgba(255,255,255,0.75)", fontSize: "14px", lineHeight: 1.5 }}>{children}</div>;
}
function OptionButton({ active, onClick, children }: { active: boolean; onClick: () => void; children: React.ReactNode }) {
return <button type="button" onClick={onClick} style={{ padding: "12px 16px", borderRadius: "10px", border: active ? "1px solid var(--wine-red)" : "1px solid rgba(255,255,255,0.16)", background: active ? "rgba(255,42,127,0.16)" : "rgba(255,255,255,0.05)", color: "white", cursor: "pointer", fontWeight: 700, fontFamily: "inherit" }}>{children}</button>;
}
function SecondaryButton({ onClick, children }: { onClick: () => void; children: React.ReactNode }) {
return <button type="button" onClick={onClick} style={{ padding: "12px 16px", borderRadius: "10px", background: "rgba(255,255,255,0.06)", border: "1px solid rgba(255,255,255,0.16)", color: "white", cursor: "pointer", fontWeight: 700, fontFamily: "inherit" }}>{children}</button>;
}
function StepItem({ number, children }: { number: string; children: React.ReactNode }) {
return <div style={{ display: "flex", gap: "12px", alignItems: "flex-start", padding: "14px", borderRadius: "12px", background: "rgba(255,255,255,0.05)", border: "1px solid rgba(255,255,255,0.12)", color: "rgba(255,255,255,0.78)", lineHeight: 1.5 }}><strong style={{ color: "white" }}>{number}.</strong><span>{children}</span></div>;
}
function StatusMessage({ error }: { error: string | null }) {
if (!error) return null;
return <div style={{ padding: "12px 14px", borderRadius: "10px", background: "rgba(255,60,60,0.1)", border: "1px solid rgba(255,60,60,0.3)", color: "#ff9999", fontSize: "13px", lineHeight: 1.4 }}>{error}</div>;
}
@@ -0,0 +1,141 @@
import React from "react";
import ReceiptLongIcon from "@mui/icons-material/ReceiptLong";
import QuestionCard from "../../QuestionCard/QuestionCard";
export type ArcaWsaaTestResult = {
status?: string;
environment?: string;
service?: string;
expirationTime?: string;
cached?: boolean;
};
export type ArcaWsfeDiagnosticsResult = {
environment?: string;
service?: string;
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;
};
};
type Props = {
currentStepIndex: number;
loading: boolean;
diagnosticsLoading: boolean;
error: string | null;
result: ArcaWsaaTestResult | null;
diagnostics: ArcaWsfeDiagnosticsResult | null;
handleTestConnection: () => void;
handleFinish: () => void;
};
export default function ArcaTestConnectionFlow({ currentStepIndex, loading, diagnosticsLoading, error, result, diagnostics, handleTestConnection, handleFinish }: Props) {
return (
<>
<QuestionCard
isActive={currentStepIndex === 0}
title="Probar conexión ARCA"
description="Vamos a validar que el certificado de esta organización pueda iniciar sesión en WSAA para usar el servicio WSFE."
onNext={handleTestConnection}
disableNext={loading}
nextLabel={loading ? "Probando..." : "Probar conexión"}
topAccessory={<ReceiptLongIcon style={{ fontSize: 52, color: "var(--wine-red)" }} />}
>
<div style={{ display: "flex", flexDirection: "column", gap: "12px", width: "100%" }}>
<InfoPanel>Esta prueba no emite facturas. Solo confirma que ARCA acepta el certificado y devuelve un token válido para WSFE.</InfoPanel>
<StatusMessage error={error} />
</div>
</QuestionCard>
<QuestionCard
isActive={currentStepIndex === 1 && Boolean(result)}
title="Conexión ARCA validada"
description="ARCA respondió correctamente el login WSAA. También consultamos WSFE para aislar problemas del servicio de facturación electrónica."
onNext={handleFinish}
disableNext={diagnosticsLoading}
nextLabel={diagnosticsLoading ? "Consultando WSFE..." : "Finalizar"}
topAccessory={<div style={{ fontSize: 48 }}></div>}
>
<div style={{ display: "flex", flexDirection: "column", gap: "10px", width: "100%" }}>
<InfoPanel>WSAA está OK. El diagnóstico WSFE no emite comprobantes: solo consulta salud, puntos de venta, tipos de comprobante y último número autorizado.</InfoPanel>
<InfoRow label="Ambiente" value={result?.environment || "homologation"} />
<InfoRow label="Servicio" value={result?.service || "wsfe"} />
<InfoRow label="Vencimiento" value={formatExpiration(result?.expirationTime)} />
<InfoRow label="Token" value={result?.cached ? "Reutilizado desde caché" : "Creado en esta prueba"} />
{diagnosticsLoading && <InfoPanel>Consultando FEDummy, puntos de venta, tipos de comprobante y último comprobante autorizado...</InfoPanel>}
{diagnostics && <WsfeDiagnosticsPanel diagnostics={diagnostics} />}
</div>
</QuestionCard>
<QuestionCard
isActive={currentStepIndex === 1 && Boolean(error) && !result}
title="No pudimos conectar con ARCA"
description="Revisá que el certificado esté cargado, corresponda al CSR generado y tenga el servicio WSFE asociado en ARCA."
onNext={handleTestConnection}
disableNext={loading}
nextLabel={loading ? "Probando..." : "Reintentar"}
topAccessory={<div style={{ fontSize: 48 }}></div>}
>
<StatusMessage error={error} />
</QuestionCard>
</>
);
}
function formatExpiration(value?: string) {
if (!value) return "No informado";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return date.toLocaleString("es-AR", { dateStyle: "short", timeStyle: "short" });
}
function InfoPanel({ children }: { children: React.ReactNode }) {
return <div style={{ padding: "14px", borderRadius: "12px", background: "rgba(255,255,255,0.05)", border: "1px solid rgba(255,255,255,0.12)", color: "rgba(255,255,255,0.75)", fontSize: "14px", lineHeight: 1.5 }}>{children}</div>;
}
function InfoRow({ label, value }: { label: string; value: string }) {
return <div style={{ display: "flex", justifyContent: "space-between", gap: "12px", padding: "12px 14px", borderRadius: "10px", background: "rgba(255,255,255,0.05)", border: "1px solid rgba(255,255,255,0.12)", color: "rgba(255,255,255,0.78)", fontSize: "14px" }}><strong style={{ color: "white" }}>{label}</strong><span>{value}</span></div>;
}
function WsfeDiagnosticsPanel({ diagnostics }: { diagnostics: ArcaWsfeDiagnosticsResult }) {
return (
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
<InfoRow label="WSFE FEDummy" value={diagnostics.dummy?.ok ? `OK (${[diagnostics.dummy.appServer, diagnostics.dummy.dbServer, diagnostics.dummy.authServer].filter(Boolean).join(" / ") || "sin detalle"})` : `Error: ${diagnostics.dummy?.error || "sin detalle"}`} />
<InfoRow label="Punto de venta configurado" value={diagnostics.pointsOfSale?.includesConfiguredPointOfSale ? `OK (${diagnostics.pointsOfSale.configuredPointOfSale})` : `No aparece (${diagnostics.pointsOfSale?.configuredPointOfSale || "sin configurar"})`} />
{diagnostics.pointsOfSale?.points?.length ? <InfoRow label="Puntos de venta ARCA" value={diagnostics.pointsOfSale.points.join(", ")} /> : null}
{diagnostics.pointsOfSale?.error ? <StatusMessage error={diagnostics.pointsOfSale.error} /> : null}
<InfoRow label="Factura C tipo 11" value={diagnostics.voucherTypes?.includesFacturaC ? `OK (${diagnostics.voucherTypes.count || 0} tipos)` : `No aparece (${diagnostics.voucherTypes?.count || 0} tipos)`} />
{diagnostics.voucherTypes?.error ? <StatusMessage error={diagnostics.voucherTypes.error} /> : null}
<InfoRow label="Último comprobante Factura C" value={diagnostics.lastVoucher?.ok ? String(diagnostics.lastVoucher.lastVoucherNumber ?? 0) : `Error: ${diagnostics.lastVoucher?.error || "sin detalle"}`} />
</div>
);
}
function StatusMessage({ error }: { error: string | null }) {
if (!error) return null;
return <div style={{ padding: "12px 14px", borderRadius: "10px", background: "rgba(255,60,60,0.1)", border: "1px solid rgba(255,60,60,0.3)", color: "#ff9999", fontSize: "13px", lineHeight: 1.4 }}>{error}</div>;
}