feat: agregar validaciones de acceso a pagos y mejorar la estructura del componente de perfil fiscal

This commit is contained in:
2026-09-15 11:07:12 -03:00
parent 0e7e690ecc
commit 4460d66f39
2 changed files with 156 additions and 111 deletions
@@ -1,4 +1,7 @@
import { validatePermissionsByCompany, validateSessionUser } from "../../helpers/check";
import CompaniesList from "../Companies/Companies";
import PlanSubscriptionsList from "../PlanSubscriptions/PlanSubscriptons";
import { PlanFeatures } from "../Plans/Plans.interface";
import { findArcaActivityByCode } from "../ArcaActivities/ArcaActivities.Catalog";
import { OrganizationFiscalProfilesAdapterMongoose } from "./OrganizationFiscalProfiles.Adapter.Mongoose";
import {
@@ -70,6 +73,21 @@ class OrganizationFiscalProfilesManager implements IOrganizationFiscalProfilesMa
await validatePermissionsByCompany({ companyId: data.companyId, sessionUser: data.sessionUser });
const company = await CompaniesList.companies.findOne({ _id: data.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");
}
this.validateProfileFields(data);
const existingProfile = await this.organizationFiscalProfiles.findOne({
@@ -140,6 +158,18 @@ class OrganizationFiscalProfilesManager implements IOrganizationFiscalProfilesMa
companyId: data.companyId,
sessionUser,
});
const company = await CompaniesList.companies.findOne({ _id: data.companyId });
if (company) {
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");
}
}
} else if (data.id) {
const profile = await this.organizationFiscalProfiles.findOne(data);
@@ -152,6 +182,18 @@ class OrganizationFiscalProfilesManager implements IOrganizationFiscalProfilesMa
sessionUser,
});
const company = await CompaniesList.companies.findOne({ _id: String(profile.companyId) });
if (company) {
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");
}
}
return [profile];
} else {
throw new Error("Se requiere una organización para buscar perfiles fiscales");
@@ -17,6 +17,7 @@ import ReceiptLongOutlinedIcon from "@mui/icons-material/ReceiptLongOutlined";
import { Grid2 } from "@mui/material";
import { ApiError } from "@models/Server.Error.model";
import style from "./page.module.css";
import SecureByPlan, { SECURE_CONTENTS } from "@core/app/components/SecureByPlan/SecureByPlan";
import {
findArcaActivities,
findOrganizationFiscalProfiles,
@@ -188,123 +189,125 @@ export default function OrganizationFiscalProfileEdit() {
return (
<>
<Location />
<div className={style.pageContainer}>
<section className={style.heroCard}>
<div className={style.heroIcon}>
<ReceiptLongOutlinedIcon />
</div>
<div>
<h2>Configuración fiscal</h2>
<p>
Estos datos se usan para emitir Factura C desde los movimientos de caja y para precargar el asistente ARCA.
</p>
</div>
</section>
<SecureByPlan content={SECURE_CONTENTS.PAYMENTS} companyId={id}>
<div className={style.pageContainer}>
<section className={style.heroCard}>
<div className={style.heroIcon}>
<ReceiptLongOutlinedIcon />
</div>
<div>
<h2>Configuración fiscal</h2>
<p>
Estos datos se usan para emitir Factura C desde los movimientos de caja y para precargar el asistente ARCA.
</p>
</div>
</section>
<section className={style.formSection}>
<h3>Datos del emisor</h3>
<Grid2 container spacing={2}>
<Grid2 size={{ xs: 12, sm: 6 }}>
<Textbox
name="cuit"
placeholder="CUIT"
type="text"
value={cuit}
width="100%"
onChange={(event) => {
setCuit(event.target.value.replace(/\D/g, ""));
markNeedsSave();
}}
/>
</Grid2>
<Grid2 size={{ xs: 12, sm: 6 }}>
<Textbox
name="legalName"
placeholder="Razón social"
type="text"
value={legalName}
width="100%"
onChange={(event) => {
setLegalName(event.target.value);
markNeedsSave();
}}
/>
</Grid2>
</Grid2>
</section>
<section className={style.formSection}>
<h3>Datos del emisor</h3>
<Grid2 container spacing={2}>
<Grid2 size={{ xs: 12, sm: 6 }}>
<Textbox
name="cuit"
placeholder="CUIT"
type="text"
value={cuit}
width="100%"
onChange={(event) => {
setCuit(event.target.value.replace(/\D/g, ""));
markNeedsSave();
}}
/>
</Grid2>
<Grid2 size={{ xs: 12, sm: 6 }}>
<Textbox
name="legalName"
placeholder="Razón social"
type="text"
value={legalName}
width="100%"
onChange={(event) => {
setLegalName(event.target.value);
markNeedsSave();
}}
/>
</Grid2>
</Grid2>
</section>
<section className={style.formSection}>
<div className={style.sectionHeader}>
<h3>Condición fiscal</h3>
<p>Seleccioná la condición declarada para esta organización.</p>
</div>
<div className={style.optionGroup} role="radiogroup" aria-label="Condición fiscal">
{fiscalConditionOptions.map((option) => {
const selected = taxCondition === option.id;
<section className={style.formSection}>
<div className={style.sectionHeader}>
<h3>Condición fiscal</h3>
<p>Seleccioná la condición declarada para esta organización.</p>
</div>
<div className={style.optionGroup} role="radiogroup" aria-label="Condición fiscal">
{fiscalConditionOptions.map((option) => {
const selected = taxCondition === option.id;
return (
<button
key={option.id}
type="button"
className={`${style.optionCard} ${selected ? style.optionCardSelected : ""}`}
onClick={() => {
setTaxCondition(option.id);
markNeedsSave();
}}
aria-pressed={selected}
>
<span className={style.optionTitle}>{option.title}</span>
<span className={style.optionDescription}>{option.description}</span>
</button>
);
})}
</div>
</section>
<section className={style.formSection}>
<h3>Facturación ARCA</h3>
<Grid2 container spacing={2}>
<Grid2 size={{ xs: 12, sm: 6 }}>
<Textbox
name="pointOfSale"
placeholder="Punto de venta"
type="text"
value={pointOfSale}
width="100%"
onChange={(event) => {
setPointOfSale(event.target.value.replace(/\D/g, ""));
markNeedsSave();
}}
/>
</Grid2>
</Grid2>
{fiscalDataLoaded && (
<TextObjectFilter
key={`${activityCode}-${arcaActivities.length}`}
placeholder="Actividad ARCA/CLAE"
content={activityFilterContent}
data={arcaActivities}
selectedId={activityCode}
onChange={(selectedActivityCode) => {
const selectedActivity = arcaActivities.find(
(activity) => activity.id === selectedActivityCode
return (
<button
key={option.id}
type="button"
className={`${style.optionCard} ${selected ? style.optionCardSelected : ""}`}
onClick={() => {
setTaxCondition(option.id);
markNeedsSave();
}}
aria-pressed={selected}
>
<span className={style.optionTitle}>{option.title}</span>
<span className={style.optionDescription}>{option.description}</span>
</button>
);
})}
</div>
</section>
setActivityCode(selectedActivity?.id || "");
setActivityDescription(selectedActivity?.name || "");
markNeedsSave();
}}
/>
)}
<section className={style.formSection}>
<h3>Facturación ARCA</h3>
<Grid2 container spacing={2}>
<Grid2 size={{ xs: 12, sm: 6 }}>
<Textbox
name="pointOfSale"
placeholder="Punto de venta"
type="text"
value={pointOfSale}
width="100%"
onChange={(event) => {
setPointOfSale(event.target.value.replace(/\D/g, ""));
markNeedsSave();
}}
/>
</Grid2>
</Grid2>
<p className={style.helpText}>
{activityCode && activityDescription
? `${activityCode} - ${activityDescription}`
: "Seleccioná la actividad principal declarada en ARCA."}
</p>
</section>
</div>
{fiscalDataLoaded && (
<TextObjectFilter
key={`${activityCode}-${arcaActivities.length}`}
placeholder="Actividad ARCA/CLAE"
content={activityFilterContent}
data={arcaActivities}
selectedId={activityCode}
onChange={(selectedActivityCode) => {
const selectedActivity = arcaActivities.find(
(activity) => activity.id === selectedActivityCode
);
setActivityCode(selectedActivity?.id || "");
setActivityDescription(selectedActivity?.name || "");
markNeedsSave();
}}
/>
)}
<p className={style.helpText}>
{activityCode && activityDescription
? `${activityCode} - ${activityDescription}`
: "Seleccioná la actividad principal declarada en ARCA."}
</p>
</section>
</div>
</SecureByPlan>
</>
);
}