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