refactor: standardize import statements and formatting across components

- Updated import statements in `textarea.tsx`, `toggle.tsx`, and `tooltip.tsx` for consistency.
- Added missing semicolons and adjusted formatting in `use-mobile.ts` and `use-mobile.tsx`.
- Cleaned up `package.json` and `root.json` files by removing unnecessary line breaks.
- Simplified `turbo.json` by consolidating array formatting for better readability.
This commit is contained in:
David Arranz 2026-09-10 13:26:33 +02:00
parent 7a278cb247
commit d57b71c200
816 changed files with 7998 additions and 5405 deletions

View File

@ -83,4 +83,4 @@
"engines": {
"node": ">=22"
}
}
}

View File

@ -1,10 +1,9 @@
import { type IOperationContextStore, createOperationContextMiddleware } from "@erp/core/api";
import cors, { type CorsOptions } from "cors";
import express, { type Application } from "express";
import helmet from "helmet";
import responseTime from "response-time";
import { createOperationContextMiddleware, type IOperationContextStore } from "@erp/core/api";
import type { ConfigType } from "./config";
// ❗️ No cargamos dotenv aquí. Debe hacerse en el entrypoint o en ./config.

View File

@ -1,12 +1,12 @@
{
"acme": {
"certificateId": "acme-prod-cert-v1",
"certificateSecretName": "acme_cert",
"certificatePasswordSecretName": "acme_cert_pwd"
},
"rodax": {
"certificateId": "no se que poner aqui",
"certificateSecretName": "certificate_secret_name",
"certificatePasswordSecretName": "certificate_password_secret_name"
}
}
"acme": {
"certificateId": "acme-prod-cert-v1",
"certificateSecretName": "acme_cert",
"certificatePasswordSecretName": "acme_cert_pwd"
},
"rodax": {
"certificateId": "no se que poner aqui",
"certificateSecretName": "certificate_secret_name",
"certificatePasswordSecretName": "certificate_password_secret_name"
}
}

View File

@ -3,7 +3,9 @@ import type { ILogger } from "@repo/rdx-logger";
import type { ConfigType } from "./config";
const redact = (value: string): string => {
if (value.length <= 4) return "***";
if (value.length <= 4) {
return "***";
}
return `${value.slice(0, 2)}***${value.slice(-2)}`;
};

View File

@ -62,7 +62,9 @@ const serverStop = (server: http.Server) => {
const forceTimeout = 30000;
// Devolvemos siempre la misma promesa para evitar dobles cierres
if (shutdownPromise) return shutdownPromise;
if (shutdownPromise) {
return shutdownPromise;
}
shutdownPromise = new Promise<void>((resolve, reject) => {
logger.warn("⚡️ Shutting down server");
@ -165,7 +167,9 @@ const handleSignal = (signal: NodeJS.Signals) => async () => {
logger.warn(`Received ${signal}. Starting graceful shutdown...`);
try {
await serverStop(server);
if (database) await closeDatabase(database);
if (database) {
await closeDatabase(database);
}
process.exit(0);
} catch (err) {
logger.error("Error during shutdown, forcing exit(1)", { err, label: "handleSignal" });
@ -202,7 +206,9 @@ process.on("unhandledRejection", async (reason: any, promise: Promise<any>) => {
isReady = false;
try {
await serverStop(server);
if (database) await closeDatabase(database);
if (database) {
await closeDatabase(database);
}
} finally {
process.exit(1);
}
@ -219,7 +225,9 @@ process.on("uncaughtException", async (error: Error) => {
isReady = false;
try {
await serverStop(server);
if (database) await closeDatabase(database);
if (database) {
await closeDatabase(database);
}
} finally {
process.exit(1);
}

View File

@ -183,12 +183,16 @@ export class SequelizeAuditReader implements IAuditReader {
};
}
private accessWhere(access: AuditReadAccess): Record<PropertyKey, unknown> {
if (access.mode === "unrestricted") return {};
if (access.mode === "unrestricted") {
return {};
}
const alternatives: Record<string, string | null>[] = access.scopes.map((scope) => ({
scope_type: scope.type,
scope_id: scope.id,
}));
if (access.includeUnscoped) alternatives.push({ scope_type: null, scope_id: null });
if (access.includeUnscoped) {
alternatives.push({ scope_type: null, scope_id: null });
}
return alternatives.length ? { [Op.or]: alternatives } : { id: { [Op.eq]: null } };
}
private quickSearch(value: string): Record<PropertyKey, unknown>[] {

View File

@ -118,13 +118,10 @@ test("module-loader falla cuando una dependencia usada no está declarada", asyn
},
});
await assert.rejects(
async () => {
await initModules(buildModuleParams());
await startModules(buildModuleParams());
},
/without declaring dependency on "catalogs"/i
);
await assert.rejects(async () => {
await initModules(buildModuleParams());
await startModules(buildModuleParams());
}, /without declaring dependency on "catalogs"/i);
});
test("module-loader sigue detectando ciclos reales entre módulos", async (t) => {
@ -165,11 +162,8 @@ test("module-loader rechaza la autoreferencia explícita por nombre de módulo",
},
});
await assert.rejects(
async () => {
await initModules(buildModuleParams());
await startModules(buildModuleParams());
},
/without declaring dependency on "companies"/i
);
await assert.rejects(async () => {
await initModules(buildModuleParams());
await startModules(buildModuleParams());
}, /without declaring dependency on "companies"/i);
});

View File

@ -137,6 +137,7 @@ test("Audit HTTP aplica tenant A/B y valida el DTO estricto", async (t) => {
"order=invalid",
"from=invalid",
"to=invalid",
])
]) {
assert.equal((await runtime.request(`/api/v1/audit?${query}`, { headers })).status, 422);
}
});

View File

@ -109,7 +109,9 @@ const auditProbeModule: IModuleServer = {
before: { name: "Before" },
after: { name: "After" },
});
if (result.isFailure) return result;
if (result.isFailure) {
return result;
}
return Result.fail(new Error("test rollback"));
});
} catch {

View File

@ -678,15 +678,17 @@ for (const scenario of [
await assert.rejects(
() =>
transactionManager.complete(async (transaction) => {
if (scenario.run === "statusChanger")
if (scenario.run === "statusChanger") {
return statusChanger.changeStatus({
companyId,
id: proformaId,
newStatus: "sent",
transaction,
});
if (scenario.run === "deleter")
}
if (scenario.run === "deleter") {
return deleter.delete({ companyId, id: proformaId, transaction });
}
return scenario.name === "unarchive"
? archiver.unarchive({ companyId, id: proformaId, transaction })
: archiver.archive({ companyId, id: proformaId, transaction });
@ -696,9 +698,15 @@ for (const scenario of [
assert.equal(audit.recordMutationCalls, 1);
const persisted = await ProformaModel.findByPk(id);
assert.ok(persisted);
if (scenario.name === "status") assert.equal(persisted.status, "draft");
if (scenario.name === "archive") assert.equal(persisted.archived_at, null);
if (scenario.name === "unarchive") assert.notEqual(persisted.archived_at, null);
if (scenario.name === "status") {
assert.equal(persisted.status, "draft");
}
if (scenario.name === "archive") {
assert.equal(persisted.archived_at, null);
}
if (scenario.name === "unarchive") {
assert.notEqual(persisted.archived_at, null);
}
assert.equal(
await AuditEventModel.count({ where: { action: scenario.action, target_id: id } }),
eventsBefore

View File

@ -1,5 +1,6 @@
import fs from "node:fs";
import path from "node:path";
import { defineConfig } from "tsup";
/**
@ -55,14 +56,20 @@ export default defineConfig({
if (fs.existsSync(abs) && fs.statSync(abs).isDirectory()) {
const indexTs = path.join(abs, "index.ts");
const indexJs = path.join(abs, "index.js");
if (fs.existsSync(indexTs)) return { path: indexTs, external: false };
if (fs.existsSync(indexJs)) return { path: indexJs, external: false };
if (fs.existsSync(indexTs)) {
return { path: indexTs, external: false };
}
if (fs.existsSync(indexJs)) {
return { path: indexJs, external: false };
}
}
// Si es un fichero con extensión válida
const withExts = [".ts", ".js", ".mjs"];
for (const ext of withExts) {
if (fs.existsSync(abs + ext)) return { path: abs + ext, external: false };
if (fs.existsSync(abs + ext)) {
return { path: abs + ext, external: false };
}
}
return { path: abs, external: false };

View File

@ -1,20 +1,18 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex, nofollow">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<head>
<meta charset="UTF-8" />
<meta name="robots" content="noindex, nofollow" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>FactuGES 2026</title>
<link rel="icon" type="image/png" href="/favicon.png">
</head>
<title>FactuGES 2026</title>
<link rel="icon" type="image/png" href="/favicon.png" />
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="factuges" class="app"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="factuges" class="app"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -63,4 +63,4 @@
"tw-animate-css": "^1.4.0",
"vite-plugin-html": "^3.2.2"
}
}
}

View File

@ -2,61 +2,61 @@ import { Avatar, AvatarFallback, AvatarImage } from "@repo/shadcn-ui/components"
export function AvatarDemo() {
return (
<div className='flex flex-row flex-wrap items-center gap-4'>
<div className="flex flex-row flex-wrap items-center gap-4">
<Avatar>
<AvatarImage src='https://github.com/shadcn.png' alt='@shadcn' />
<AvatarImage alt="@shadcn" src="https://github.com/shadcn.png" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar>
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar className='size-12'>
<AvatarImage src='https://github.com/shadcn.png' alt='@shadcn' />
<Avatar className="size-12">
<AvatarImage alt="@shadcn" src="https://github.com/shadcn.png" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar className='rounded-lg'>
<AvatarImage src='https://github.com/evilrabbit.png' alt='@evilrabbit' />
<Avatar className="rounded-lg">
<AvatarImage alt="@evilrabbit" src="https://github.com/evilrabbit.png" />
<AvatarFallback>ER</AvatarFallback>
</Avatar>
<div className='*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale'>
<div className="*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale">
<Avatar>
<AvatarImage src='https://github.com/shadcn.png' alt='@shadcn' />
<AvatarImage alt="@shadcn" src="https://github.com/shadcn.png" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage src='https://github.com/leerob.png' alt='@leerob' />
<AvatarImage alt="@leerob" src="https://github.com/leerob.png" />
<AvatarFallback>LR</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage src='https://github.com/evilrabbit.png' alt='@evilrabbit' />
<AvatarImage alt="@evilrabbit" src="https://github.com/evilrabbit.png" />
<AvatarFallback>ER</AvatarFallback>
</Avatar>
</div>
<div className='*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:size-12 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale'>
<div className="*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:size-12 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale">
<Avatar>
<AvatarImage src='https://github.com/shadcn.png' alt='@shadcn' />
<AvatarImage alt="@shadcn" src="https://github.com/shadcn.png" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage src='https://github.com/leerob.png' alt='@leerob' />
<AvatarImage alt="@leerob" src="https://github.com/leerob.png" />
<AvatarFallback>LR</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage src='https://github.com/evilrabbit.png' alt='@evilrabbit' />
<AvatarImage alt="@evilrabbit" src="https://github.com/evilrabbit.png" />
<AvatarFallback>ER</AvatarFallback>
</Avatar>
</div>
<div className='*:data-[slot=avatar]:ring-background flex -space-x-2 hover:space-x-1 *:data-[slot=avatar]:size-12 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale *:data-[slot=avatar]:transition-all *:data-[slot=avatar]:duration-300 *:data-[slot=avatar]:ease-in-out'>
<div className="*:data-[slot=avatar]:ring-background flex -space-x-2 hover:space-x-1 *:data-[slot=avatar]:size-12 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale *:data-[slot=avatar]:transition-all *:data-[slot=avatar]:duration-300 *:data-[slot=avatar]:ease-in-out">
<Avatar>
<AvatarImage src='https://github.com/shadcn.png' alt='@shadcn' />
<AvatarImage alt="@shadcn" src="https://github.com/shadcn.png" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage src='https://github.com/leerob.png' alt='@leerob' />
<AvatarImage alt="@leerob" src="https://github.com/leerob.png" />
<AvatarFallback>LR</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage src='https://github.com/evilrabbit.png' alt='@evilrabbit' />
<AvatarImage alt="@evilrabbit" src="https://github.com/evilrabbit.png" />
<AvatarFallback>ER</AvatarFallback>
</Avatar>
</div>

View File

@ -6,8 +6,6 @@ import {
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@repo/shadcn-ui/components";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
@ -19,16 +17,16 @@ export function BreadcrumbDemo() {
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href='/'>Home</BreadcrumbLink>
<BreadcrumbLink href="/">Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<DropdownMenu>
<DropdownMenuTrigger className='flex items-center gap-1'>
<BreadcrumbEllipsis className='h-4 w-4' />
<span className='sr-only'>Toggle menu</span>
<DropdownMenuTrigger className="flex items-center gap-1">
<BreadcrumbEllipsis className="h-4 w-4" />
<span className="sr-only">Toggle menu</span>
</DropdownMenuTrigger>
<DropdownMenuContent align='start'>
<DropdownMenuContent align="start">
<DropdownMenuItem>Documentation</DropdownMenuItem>
<DropdownMenuItem>Themes</DropdownMenuItem>
<DropdownMenuItem>GitHub</DropdownMenuItem>
@ -37,7 +35,7 @@ export function BreadcrumbDemo() {
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href='/docs/components'>Components</BreadcrumbLink>
<BreadcrumbLink href="/docs/components">Components</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>

View File

@ -165,8 +165,7 @@ const normalizeRoute = ({
const routeLayout = resolveRouteLayout(route, module, inheritedLayout);
const routeProtected = resolveRouteProtected(route, module, inheritedProtected);
const routeRequireCompany =
routeProtected &&
(route.handle?.requireCompany ?? inheritedRequireCompany ?? true);
routeProtected && (route.handle?.requireCompany ?? inheritedRequireCompany ?? true);
const normalizedChildren = route.children
?.map((child) =>

View File

@ -6,10 +6,10 @@ type SliderProps = React.ComponentProps<typeof Slider>;
export function SliderDemo({ className, ...props }: SliderProps) {
return (
<Slider
className={cn("w-[60%]", className)}
defaultValue={[50]}
max={100}
step={1}
className={cn("w-[60%]", className)}
{...props}
/>
);

View File

@ -33,7 +33,7 @@ export const HomeAttentionPanel = ({ items, hasError }: HomeAttentionPanelProps)
<p className="text-sm text-muted-foreground">{t("web.home.attention.empty")}</p>
) : null}
{!hasError ? (
{hasError ? null : (
<ul className="divide-y divide-border/70">
{items.map((item) => (
<li key={item.id}>
@ -53,7 +53,7 @@ export const HomeAttentionPanel = ({ items, hasError }: HomeAttentionPanelProps)
</li>
))}
</ul>
) : null}
)}
</div>
</section>
);

View File

@ -9,10 +9,7 @@ interface HomeMonthlyBillingSummaryProps {
hasError: boolean;
}
export const HomeMonthlyBillingSummary = ({
data,
hasError,
}: HomeMonthlyBillingSummaryProps) => {
export const HomeMonthlyBillingSummary = ({ data, hasError }: HomeMonthlyBillingSummaryProps) => {
const { t } = useAppTranslation();
return (
@ -22,7 +19,9 @@ export const HomeMonthlyBillingSummary = ({
<h2 className="text-lg font-semibold text-foreground">
{t("web.home.monthlyBilling.title")}
</h2>
<p className="text-sm text-muted-foreground">{t("web.home.monthlyBilling.description")}</p>
<p className="text-sm text-muted-foreground">
{t("web.home.monthlyBilling.description")}
</p>
</div>
{hasError ? (
@ -32,7 +31,7 @@ export const HomeMonthlyBillingSummary = ({
/>
) : null}
{!hasError ? (
{hasError ? null : (
<div className="space-y-2">
<p className="text-3xl font-semibold tracking-tight tabular-nums text-foreground">
{data?.totalAmountLabel ??
@ -56,7 +55,7 @@ export const HomeMonthlyBillingSummary = ({
</p>
) : null}
</div>
) : null}
)}
</div>
</section>
);

View File

@ -10,10 +10,7 @@ interface HomeProformaStatusSummaryProps {
hasError: boolean;
}
export const HomeProformaStatusSummary = ({
items,
hasError,
}: HomeProformaStatusSummaryProps) => {
export const HomeProformaStatusSummary = ({ items, hasError }: HomeProformaStatusSummaryProps) => {
const { t } = useAppTranslation();
return (

View File

@ -28,7 +28,9 @@ export const HomeRecentActivity = ({ items, hasPartialError }: HomeRecentActivit
<h2 className="text-lg font-semibold text-foreground">
{t("web.home.recentActivity.title")}
</h2>
<p className="text-sm text-muted-foreground">{t("web.home.recentActivity.description")}</p>
<p className="text-sm text-muted-foreground">
{t("web.home.recentActivity.description")}
</p>
</div>
{hasPartialError ? (

View File

@ -175,7 +175,10 @@ export const AppSidebarNav = ({ sections }: AppSidebarNavProps) => {
render={
<CollapsibleTrigger
render={
<button className="flex w-full items-center gap-2" type="button">
<button
className="flex w-full items-center gap-2"
type="button"
>
<Icon
aria-hidden="true"
className={cn(

View File

@ -16,10 +16,7 @@ function resolveCompanySecondaryLabel(legalName: string, tradeName: string | nul
}
function resolveCompanyInitials(name: string) {
const words = name
.trim()
.split(/\s+/)
.filter(Boolean);
const words = name.trim().split(/\s+/).filter(Boolean);
if (words.length === 0) {
return "--";

View File

@ -5,7 +5,9 @@ import { ThemeProviderContext } from "./theme-provider";
export const useTheme = () => {
const context = useContext(ThemeProviderContext);
if (context === undefined) throw new Error("useTheme must be used within a ThemeProvider");
if (context === undefined) {
throw new Error("useTheme must be used within a ThemeProvider");
}
return context;
};

View File

@ -1,5 +1,5 @@
import { useEffect } from "react";
import { i18n, registerTranslations } from "@repo/i18next";
import { useEffect } from "react";
import { useTranslation as useI18NextTranslation } from "react-i18next";
import enResources from "./en.json";

View File

@ -10,7 +10,9 @@ import {
const ds = {
custom: async (p: any) => {
if (p.method === "get") return { status: "pending", company_id: null };
if (p.method === "get") {
return { status: "pending", company_id: null };
}
if (p.path.endsWith("/complete")) {
assert.equal(p.data.address.street_2, "2B");
return { company_id: "c1" };

View File

@ -98,13 +98,29 @@ export const completeInitialOnboarding = (
export const validateInitialCompanyInput = (value: InitialCompanyInput): string[] => {
const errors: string[] = [];
if (!value.legalName.trim()) errors.push("legalName");
if (!value.tin.trim()) errors.push("tin");
if (!value.address?.street.trim()) errors.push("address.street");
if (!value.address?.city.trim()) errors.push("address.city");
if (!value.address?.province.trim()) errors.push("address.province");
if (!/^\d{5}$/.test(value.address?.postalCode ?? "")) errors.push("address.postalCode");
if (!/^[A-Z]{2}$/.test(value.address?.countryCode ?? "")) errors.push("address.countryCode");
if (value.email && !/^\S+@\S+\.\S+$/.test(value.email)) errors.push("email");
if (!value.legalName.trim()) {
errors.push("legalName");
}
if (!value.tin.trim()) {
errors.push("tin");
}
if (!value.address?.street.trim()) {
errors.push("address.street");
}
if (!value.address?.city.trim()) {
errors.push("address.city");
}
if (!value.address?.province.trim()) {
errors.push("address.province");
}
if (!/^\d{5}$/.test(value.address?.postalCode ?? "")) {
errors.push("address.postalCode");
}
if (!/^[A-Z]{2}$/.test(value.address?.countryCode ?? "")) {
errors.push("address.countryCode");
}
if (value.email && !/^\S+@\S+\.\S+$/.test(value.email)) {
errors.push("email");
}
return errors;
};

View File

@ -30,9 +30,13 @@ for (const [name, mutate] of Object.entries({
postalCode: () => ({ ...valid, address: { ...valid.address, postalCode: "bad" } }),
countryCode: () => ({ ...valid, address: { ...valid.address, countryCode: "ESP" } }),
email: () => ({ ...valid, email: "bad" }),
}))
})) {
test(name, () => {
const errors = validateInitialCompanyInput(mutate());
if (name === "valid") assert.deepEqual(errors, []);
else assert.ok(errors.length > 0);
if (name === "valid") {
assert.deepEqual(errors, []);
} else {
assert.ok(errors.length > 0);
}
});
}

View File

@ -15,13 +15,19 @@ export async function completeInitialOnboardingAndRestoreSession(
): Promise<CompletionTransition> {
try {
const restored = await actions.restoreSession();
if (restored === null) return { kind: "recoverable", companyId, reason: "restore_failed" };
if (restored === null) {
return { kind: "recoverable", companyId, reason: "restore_failed" };
}
} catch {
return { kind: "recoverable", companyId, reason: "restore_failed" };
}
const companies = actions.getAvailableCompanies();
const created = companies.find((company) => company.id === companyId);
if (!created) return { kind: "recoverable", companyId, reason: "company_missing" };
if (companies.length > 1) await actions.changeActiveCompany(companyId);
if (!created) {
return { kind: "recoverable", companyId, reason: "company_missing" };
}
if (companies.length > 1) {
await actions.changeActiveCompany(companyId);
}
return { kind: "application", companyId };
}

View File

@ -22,18 +22,25 @@ export const InitialOnboardingPage = () => {
const m = useCompleteInitialOnboarding();
const { restoreSession, availableCompanies, changeActiveCompany } = useIdentityAuthSession();
useEffect(() => {
if (!m.data) return;
const found = availableCompanies.find((c) => c.id === m.data);
if (!found) {
if (availableCompanies.length > 0) setRecoverableError("company_missing");
if (!m.data) {
return;
}
if (availableCompanies.length > 1)
const found = availableCompanies.find((c) => c.id === m.data);
if (!found) {
if (availableCompanies.length > 0) {
setRecoverableError("company_missing");
}
return;
}
if (availableCompanies.length > 1) {
changeActiveCompany(m.data).catch(() => setRecoverableError("company_selection_failed"));
}
}, [m.data, availableCompanies, changeActiveCompany]);
const set = (k: string, x: string) => setV((s) => ({ ...s, [k]: x }));
const setA = (k: string, x: string) => setV((s) => ({ ...s, address: { ...s.address, [k]: x } }));
if (m.isSuccess) return <LoadingOverlay />;
if (m.isSuccess) {
return <LoadingOverlay />;
}
return (
<main className="mx-auto flex min-h-screen max-w-3xl flex-col justify-center gap-6 p-6">
<div>

View File

@ -1,5 +1,6 @@
import assert from "node:assert/strict";
import test from "node:test";
import { renderToStaticMarkup } from "react-dom/server";
import { RenderErrorFallback } from "./render-error-fallback";

View File

@ -7,10 +7,7 @@ export interface RenderErrorFallbackProps extends Pick<FallbackProps, "resetErro
onGoHome: () => void;
}
export const RenderErrorFallback = ({
onGoHome,
resetErrorBoundary,
}: RenderErrorFallbackProps) => {
export const RenderErrorFallback = ({ onGoHome, resetErrorBoundary }: RenderErrorFallbackProps) => {
const { t } = useAppTranslation();
return (

View File

@ -3,6 +3,8 @@
import { Navigate, createBrowserRouter } from "react-router-dom";
import { buildModuleRoutes } from "@/components/module-routes";
import { UiKitPage } from "@/development/ui-kit";
import { HomePage } from "@/home";
import {
AppFullscreenLayout,
AppFullscreenLayoutFrame,
@ -11,11 +13,9 @@ import {
AuthLayout,
AuthLayoutFrame,
} from "@/layout";
import { UiKitPage } from "@/development/ui-kit";
import { HomePage } from "@/home";
import ShadcnShowcasePage from "@/pages/shadcn-ui-page";
import { buildSettingsRoute } from "@/settings";
import TailwindV4ShowcasePage from "@/pages/tailwindcss-page";
import { buildSettingsRoute } from "@/settings";
import { RouteErrorPage, StaticRouteErrorPage } from "../pages";
import { modules } from "../register-modules";

View File

@ -26,8 +26,9 @@ export const RequireCompanyRouteGuard = ({ children }: RequireCompanyRouteGuardP
!provision.isPending &&
!provision.isSuccess &&
!provision.isError
)
) {
provision.mutate();
}
}, [
availableCompanies.length,
statusQuery.data?.status,
@ -39,8 +40,9 @@ export const RequireCompanyRouteGuard = ({ children }: RequireCompanyRouteGuardP
if (
isLoading ||
(availableCompanies.length === 0 && (statusQuery.isLoading || provision.isPending))
)
) {
return <LoadingOverlay />;
}
const navigation = statusQuery.data
? resolveCompanyGuardDecision({
isAuthenticated: true,
@ -48,17 +50,22 @@ export const RequireCompanyRouteGuard = ({ children }: RequireCompanyRouteGuardP
onboardingStatus: statusQuery.data.status,
})
: null;
if (activeCompany || navigation === "application") return <>{children}</>;
if (statusQuery.isError || provision.isError)
if (activeCompany || navigation === "application") {
return <>{children}</>;
}
if (statusQuery.isError || provision.isError) {
return <div className="p-8">No se pudo cargar el estado de onboarding.</div>;
if (statusQuery.data?.status === "pending" || provision.isSuccess)
}
if (statusQuery.data?.status === "pending" || provision.isSuccess) {
return <InitialOnboardingPage />;
if (statusQuery.data?.status === "completed")
}
if (statusQuery.data?.status === "completed") {
return (
<div className="p-8">
La cuenta tiene un onboarding completado, pero no hay una empresa disponible.
</div>
);
}
const returnTo = `${location.pathname}${location.search}`;
return <Navigate replace to={`/company-selection?returnTo=${encodeURIComponent(returnTo)}`} />;
};

View File

@ -15,9 +15,17 @@ export type InitialOnboardingNavigationInput = {
export function resolveInitialOnboardingNavigation(
input: InitialOnboardingNavigationInput
): InitialOnboardingNavigation {
if (!input.isAuthenticated) return "login";
if (input.availableCompaniesCount > 0) return "application";
if (input.onboardingStatus === "pending") return "onboarding";
if (input.onboardingStatus === "completed") return "onboarding_inconsistent";
if (!input.isAuthenticated) {
return "login";
}
if (input.availableCompaniesCount > 0) {
return "application";
}
if (input.onboardingStatus === "pending") {
return "onboarding";
}
if (input.onboardingStatus === "completed") {
return "onboarding_inconsistent";
}
return "provision_onboarding";
}

View File

@ -8,11 +8,9 @@ const translate = (key: string) =>
"routeErrors.notFound.title": "Página no encontrada",
"routeErrors.notFound.description": "La dirección no existe o ha cambiado.",
"routeErrors.forbidden.title": "No tienes acceso a esta página",
"routeErrors.forbidden.description":
"No dispones de permisos para acceder a esta sección.",
"routeErrors.forbidden.description": "No dispones de permisos para acceder a esta sección.",
"routeErrors.server.title": "No se pudo mostrar esta página",
"routeErrors.server.description":
"Se produjo un error interno mientras se cargaba la ruta.",
"routeErrors.server.description": "Se produjo un error interno mientras se cargaba la ruta.",
"routeErrors.unavailable.title": "El servicio no está disponible",
"routeErrors.unavailable.description":
"La sección que intentas abrir no está disponible en este momento.",

View File

@ -1,6 +1,5 @@
import { isRouteErrorResponse } from "react-router-dom";
import type { PageErrorStateVariant } from "@erp/core/components";
import { isRouteErrorResponse } from "react-router-dom";
export type RouteErrorPageVariant = "notFound" | "forbidden" | "server" | "unavailable" | "unknown";
@ -11,9 +10,7 @@ export interface RouteErrorPageViewModel {
canRetry: boolean;
}
interface Translate {
(key: string): string;
}
type Translate = (key: string) => string;
const normalizeVariantFromStatus = (status: number): RouteErrorPageVariant => {
if (status === 403 || status === 401) {

View File

@ -62,10 +62,7 @@ export const buildSettingsRoute = ({
{
index: true,
element: (
<SettingsIndexRoute
hasEntries={Boolean(firstEntryPath)}
redirectPath={firstEntryPath}
/>
<SettingsIndexRoute hasEntries={Boolean(firstEntryPath)} redirectPath={firstEntryPath} />
),
},
...settingsChildRoutes,

View File

@ -38,4 +38,4 @@
"engines": {
"node": ">=22"
}
}
}

View File

@ -4,7 +4,19 @@ import dotenv from "dotenv";
import { WorkerEnvSchema } from "./worker-env-schema";
export type WorkerConfig = { readonly env: "development" | "test" | "production"; readonly timezone: string; readonly database: DatabaseConfig; readonly jobs: { readonly recurringProformas: { readonly enabled: boolean; readonly intervalMs: number; readonly batchSize: number; readonly maxOccurrencesPerRecurringPerRun: number } } };
export type WorkerConfig = {
readonly env: "development" | "test" | "production";
readonly timezone: string;
readonly database: DatabaseConfig;
readonly jobs: {
readonly recurringProformas: {
readonly enabled: boolean;
readonly intervalMs: number;
readonly batchSize: number;
readonly maxOccurrencesPerRecurringPerRun: number;
};
};
};
export function loadWorkerConfig(logger: ILogger): WorkerConfig {
dotenv.config();
@ -14,5 +26,27 @@ export function loadWorkerConfig(logger: ILogger): WorkerConfig {
throw new Error("Invalid worker environment configuration");
}
const env = parsed.data;
return { env: env.NODE_ENV, timezone: env.TIMEZONE, database: { dialect: env.DB_DIALECT, host: env.DB_HOST, port: env.DB_PORT, name: env.DB_NAME, user: env.DB_USER, password: env.DB_PASS, logging: env.DB_LOGGING, ssl: env.DB_SSL, timezone: env.TIMEZONE }, jobs: { recurringProformas: { enabled: env.RECURRING_PROFORMAS_JOB_ENABLED, intervalMs: env.RECURRING_PROFORMAS_JOB_INTERVAL_MS, batchSize: env.RECURRING_PROFORMAS_BATCH_SIZE, maxOccurrencesPerRecurringPerRun: env.RECURRING_PROFORMAS_MAX_OCCURRENCES_PER_RUN } } };
return {
env: env.NODE_ENV,
timezone: env.TIMEZONE,
database: {
dialect: env.DB_DIALECT,
host: env.DB_HOST,
port: env.DB_PORT,
name: env.DB_NAME,
user: env.DB_USER,
password: env.DB_PASS,
logging: env.DB_LOGGING,
ssl: env.DB_SSL,
timezone: env.TIMEZONE,
},
jobs: {
recurringProformas: {
enabled: env.RECURRING_PROFORMAS_JOB_ENABLED,
intervalMs: env.RECURRING_PROFORMAS_JOB_INTERVAL_MS,
batchSize: env.RECURRING_PROFORMAS_BATCH_SIZE,
maxOccurrencesPerRecurringPerRun: env.RECURRING_PROFORMAS_MAX_OCCURRENCES_PER_RUN,
},
},
};
}

View File

@ -16,8 +16,8 @@ import { configureLogger, setLoggerSingleton } from "@repo/rdx-logger";
import { loadWorkerConfig } from "./config/worker-config";
import { registerWorkerModules } from "./modules/register-modules";
import { Scheduler } from "./scheduler/scheduler";
import { registerJobs } from "./scheduler/register-jobs";
import { Scheduler } from "./scheduler/scheduler";
const logger = configureLogger("console", { service: "worker" });
setLoggerSingleton(logger);
@ -29,16 +29,57 @@ let stopping: Promise<void> | null = null;
async function bootstrap(): Promise<void> {
logger.info("Worker starting");
const database = await createSequelize(config.database, logger);
const audit = new Audit({ source: { type: "application", id: "factuges-worker" }, contextProvider: new OperationAuditContextProvider(operationContext), store: new SequelizeAuditStore(AuditEventModel, AuditChangeModel, transactionContext) });
const audit = new Audit({
source: { type: "application", id: "factuges-worker" },
contextProvider: new OperationAuditContextProvider(operationContext),
store: new SequelizeAuditStore(AuditEventModel, AuditChangeModel, transactionContext),
});
registerWorkerModules();
registerModels(auditModelInitializers, "worker-infrastructure");
const modulesRuntime = await initModules({ config, database, logger, operationContext, transactionContext, audit, warmup: { timeoutMs: 10_000, strict: false }, databaseSyncMode: "none", getService: () => { throw new Error("Services are available only in module scope."); } });
const scheduler = new Scheduler(registerJobs({ config, logger, runtime: modulesRuntime }), logger);
const modulesRuntime = await initModules({
config,
database,
logger,
operationContext,
transactionContext,
audit,
warmup: { timeoutMs: 10_000, strict: false },
databaseSyncMode: "none",
getService: () => {
throw new Error("Services are available only in module scope.");
},
});
const scheduler = new Scheduler(
registerJobs({ config, logger, runtime: modulesRuntime }),
logger
);
scheduler.start();
logger.info("Worker started");
const shutdown = async () => { if (stopping) return stopping; stopping = (async () => { logger.info("Worker stopping"); await scheduler.stop(); await closeSequelize(database, logger); logger.info("Worker stopped"); })(); return stopping; };
process.on("SIGINT", () => { shutdown().then(() => process.exit(0)).catch(() => process.exit(1)); });
process.on("SIGTERM", () => { shutdown().then(() => process.exit(0)).catch(() => process.exit(1)); });
const shutdown = async () => {
if (stopping) {
return stopping;
}
stopping = (async () => {
logger.info("Worker stopping");
await scheduler.stop();
await closeSequelize(database, logger);
logger.info("Worker stopped");
})();
return stopping;
};
process.on("SIGINT", () => {
shutdown()
.then(() => process.exit(0))
.catch(() => process.exit(1));
});
process.on("SIGTERM", () => {
shutdown()
.then(() => process.exit(0))
.catch(() => process.exit(1));
});
}
bootstrap().catch((error) => { logger.error("Worker bootstrap failed", { error }); process.exit(1); });
bootstrap().catch((error) => {
logger.error("Worker bootstrap failed", { error });
process.exit(1);
});

View File

@ -10,35 +10,62 @@ import type { ScheduledJob } from "../scheduler/scheduled-job";
export class RecurringProformaScheduledJob implements ScheduledJob {
public readonly name = "recurring-proformas";
public constructor(private readonly deps: {
intervalMs: number;
batchSize: number;
maxOccurrencesPerRecurringPerRun: number;
repository: RecurringProformaDiscoveryCapability;
processor: RecurringProformaOccurrenceProcessorCapability;
logger: ILogger;
dueThrough: () => Date;
}) {}
public constructor(
private readonly deps: {
intervalMs: number;
batchSize: number;
maxOccurrencesPerRecurringPerRun: number;
repository: RecurringProformaDiscoveryCapability;
processor: RecurringProformaOccurrenceProcessorCapability;
logger: ILogger;
dueThrough: () => Date;
}
) {}
public get intervalMs(): number { return this.deps.intervalMs; }
public get intervalMs(): number {
return this.deps.intervalMs;
}
public async execute(): Promise<void> {
const dueThrough = this.deps.dueThrough();
const discovered = await this.deps.repository.findDue(dueThrough, this.deps.batchSize);
if (discovered.isFailure) throw discovered.error;
this.deps.logger.info("Recurring proformas job started", { dueThrough: dueThrough.toISOString(), discovered: discovered.data.length });
if (discovered.isFailure) {
throw discovered.error;
}
this.deps.logger.info("Recurring proformas job started", {
dueThrough: dueThrough.toISOString(),
discovered: discovered.data.length,
});
for (const due of discovered.data) {
try {
for (let count = 0; count < this.deps.maxOccurrencesPerRecurringPerRun; count += 1) {
const processed = await this.deps.processor.process({ companyId: due.companyId, recurringProformaId: due.recurringProformaId });
if (!processed.isSuccess) break;
const processed = await this.deps.processor.process({
companyId: due.companyId,
recurringProformaId: due.recurringProformaId,
});
if (!processed.isSuccess) {
break;
}
const state = processed.data;
if (state?.recurringProformaStatus === "completed" || state?.nextOccurrenceDate === null || state?.nextOccurrenceDate === undefined || state.nextOccurrenceDate.getTime() > dueThrough.getTime()) break;
if (
state?.recurringProformaStatus === "completed" ||
state?.nextOccurrenceDate === null ||
state?.nextOccurrenceDate === undefined ||
state.nextOccurrenceDate.getTime() > dueThrough.getTime()
) {
break;
}
}
} catch (error) {
this.deps.logger.error("Recurring proforma job candidate failed", { recurringProformaId: due.recurringProformaId.toPrimitive(), companyId: due.companyId.toPrimitive(), error });
this.deps.logger.error("Recurring proforma job candidate failed", {
recurringProformaId: due.recurringProformaId.toPrimitive(),
companyId: due.companyId.toPrimitive(),
error,
});
}
}
this.deps.logger.info("Recurring proformas job completed", { dueThrough: dueThrough.toISOString() });
this.deps.logger.info("Recurring proformas job completed", {
dueThrough: dueThrough.toISOString(),
});
}
}

View File

@ -1,6 +1,6 @@
import { registerModule } from "@erp/core/api";
import { catalogsModule } from "@erp/catalogs/module";
import { companiesModule } from "@erp/companies/module";
import { registerModule } from "@erp/core/api";
import { customerInvoicesModule } from "@erp/customer-invoices/api/module";
import { customersModule } from "@erp/customers/module";
import { documentSeriesModule } from "@erp/document-series/module";

View File

@ -3,6 +3,7 @@ import type { ILogger } from "@repo/rdx-logger";
import type { WorkerConfig } from "../config/worker-config";
import { RecurringProformaScheduledJob } from "../jobs/recurring-proforma-scheduled-job";
import type { ScheduledJob } from "./scheduled-job";
/** SSOT explícita de jobs activos. */
@ -11,18 +12,23 @@ export function registerJobs(params: {
logger: ILogger;
runtime: ModulesRuntime;
}): ScheduledJob[] {
if (!params.config.jobs.recurringProformas.enabled) return [];
if (!params.config.jobs.recurringProformas.enabled) {
return [];
}
const recurring = params.runtime.customerInvoices.recurringProformas;
return [new RecurringProformaScheduledJob({
intervalMs: params.config.jobs.recurringProformas.intervalMs,
batchSize: params.config.jobs.recurringProformas.batchSize,
maxOccurrencesPerRecurringPerRun: params.config.jobs.recurringProformas.maxOccurrencesPerRecurringPerRun,
repository: recurring.repository,
processor: recurring.occurrenceProcessor,
logger: params.logger,
dueThrough: () => {
const now = new Date();
return new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()));
},
})];
return [
new RecurringProformaScheduledJob({
intervalMs: params.config.jobs.recurringProformas.intervalMs,
batchSize: params.config.jobs.recurringProformas.batchSize,
maxOccurrencesPerRecurringPerRun:
params.config.jobs.recurringProformas.maxOccurrencesPerRecurringPerRun,
repository: recurring.repository,
processor: recurring.occurrenceProcessor,
logger: params.logger,
dueThrough: () => {
const now = new Date();
return new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()));
},
}),
];
}

View File

@ -1 +1,5 @@
export interface ScheduledJob { readonly name: string; readonly intervalMs: number; execute(): Promise<void>; }
export interface ScheduledJob {
readonly name: string;
readonly intervalMs: number;
execute(): Promise<void>;
}

View File

@ -7,7 +7,14 @@ import { Scheduler } from "./scheduler";
test("Scheduler rechaza nombres duplicados", () => {
assert.throws(
() => new Scheduler([{ name: "same", intervalMs: 1, execute: async () => undefined }, { name: "same", intervalMs: 1, execute: async () => undefined }], configureLogger()),
() =>
new Scheduler(
[
{ name: "same", intervalMs: 1, execute: async () => undefined },
{ name: "same", intervalMs: 1, execute: async () => undefined },
],
configureLogger()
),
/Duplicate scheduled job name/
);
});
@ -15,7 +22,21 @@ test("Scheduler rechaza nombres duplicados", () => {
test("Scheduler ejecuta inmediatamente y espera la ejecución activa al parar", async () => {
let started = false;
let release: (() => void) | undefined;
const scheduler = new Scheduler([{ name: "probe", intervalMs: 1, execute: async () => { started = true; await new Promise<void>((resolve) => { release = resolve; }); } }], configureLogger());
const scheduler = new Scheduler(
[
{
name: "probe",
intervalMs: 1,
execute: async () => {
started = true;
await new Promise<void>((resolve) => {
release = resolve;
});
},
},
],
configureLogger()
);
scheduler.start();
await new Promise((resolve) => setImmediate(resolve));
assert.equal(started, true);

View File

@ -1,4 +1,5 @@
import type { ILogger } from "@repo/rdx-logger";
import type { ScheduledJob } from "./scheduled-job";
export class Scheduler {
@ -7,37 +8,66 @@ export class Scheduler {
private readonly delays = new Set<() => void>();
private idleHandle: NodeJS.Timeout | null = null;
constructor(private readonly jobs: readonly ScheduledJob[], private readonly logger: ILogger) {
constructor(
private readonly jobs: readonly ScheduledJob[],
private readonly logger: ILogger
) {
const names = new Set<string>();
for (const job of jobs) { if (names.has(job.name)) throw new Error(`Duplicate scheduled job name: ${job.name}`); names.add(job.name); }
for (const job of jobs) {
if (names.has(job.name)) {
throw new Error(`Duplicate scheduled job name: ${job.name}`);
}
names.add(job.name);
}
}
start(): void {
for (const job of this.jobs) { this.logger.info("Job registered", { job: job.name }); this.loops.push(this.run(job)); }
if (this.jobs.length === 0) this.idleHandle = setInterval(() => undefined, 2_147_483_647);
for (const job of this.jobs) {
this.logger.info("Job registered", { job: job.name });
this.loops.push(this.run(job));
}
if (this.jobs.length === 0) {
this.idleHandle = setInterval(() => undefined, 2_147_483_647);
}
}
async stop(): Promise<void> {
if (this.stopping) return;
if (this.stopping) {
return;
}
this.stopping = true;
if (this.idleHandle) clearInterval(this.idleHandle);
for (const resolve of this.delays) resolve();
if (this.idleHandle) {
clearInterval(this.idleHandle);
}
for (const resolve of this.delays) {
resolve();
}
await Promise.all(this.loops);
}
private async run(job: ScheduledJob): Promise<void> {
while (!this.stopping) {
this.logger.debug("Job started", { job: job.name });
try { await job.execute(); this.logger.debug("Job completed", { job: job.name }); }
catch (error) { this.logger.error("Job failed", { job: job.name, error }); }
if (!this.stopping) await this.delay(job.intervalMs);
try {
await job.execute();
this.logger.debug("Job completed", { job: job.name });
} catch (error) {
this.logger.error("Job failed", { job: job.name, error });
}
if (!this.stopping) {
await this.delay(job.intervalMs);
}
}
}
private async delay(intervalMs: number): Promise<void> {
await new Promise<void>((resolve) => {
let timeout: NodeJS.Timeout;
const cancel = () => { clearTimeout(timeout); this.delays.delete(cancel); resolve(); };
const cancel = () => {
clearTimeout(timeout);
this.delays.delete(cancel);
resolve();
};
timeout = setTimeout(cancel, intervalMs);
this.delays.add(cancel);
});

View File

@ -1,6 +1,13 @@
{
"extends": "@repo/typescript-config/express.json",
"compilerOptions": { "baseUrl": ".", "noEmit": true, "allowImportingTsExtensions": true, "rootDir": "./src", "outDir": "./dist", "types": ["node"] },
"compilerOptions": {
"baseUrl": ".",
"noEmit": true,
"allowImportingTsExtensions": true,
"rootDir": "./src",
"outDir": "./dist",
"types": ["node"]
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

View File

@ -1,3 +1,12 @@
import { defineConfig } from "tsup";
export default defineConfig({ entry: ["src/index.ts"], format: ["esm"], target: "node22", bundle: true, clean: true, outDir: "dist", platform: "node", noExternal: ["@repo", "@erp"] });
export default defineConfig({
entry: ["src/index.ts"],
format: ["esm"],
target: "node22",
bundle: true,
clean: true,
outDir: "dist",
platform: "node",
noExternal: ["@repo", "@erp"],
});

View File

@ -61,9 +61,7 @@
"options": {
"strictCase": false,
"requireAscii": true,
"filenameCases": [
"kebab-case"
]
"filenameCases": ["kebab-case"]
}
},
"useForOf": "error",
@ -78,28 +76,19 @@
"selector": {
"kind": "function"
},
"formats": [
"camelCase",
"PascalCase"
]
"formats": ["camelCase", "PascalCase"]
},
{
"selector": {
"kind": "variable"
},
"formats": [
"camelCase",
"PascalCase",
"CONSTANT_CASE"
]
"formats": ["camelCase", "PascalCase", "CONSTANT_CASE"]
},
{
"selector": {
"kind": "typeLike"
},
"formats": [
"PascalCase"
]
"formats": ["PascalCase"]
}
]
}
@ -242,46 +231,21 @@
"groups": [
":URL:",
":BLANK_LINE:",
[
":BUN:",
":NODE:"
],
[":BUN:", ":NODE:"],
":BLANK_LINE:",
":PACKAGE_WITH_PROTOCOL:",
":BLANK_LINE:",
[
":PACKAGE:"
],
[":PACKAGE:"],
":BLANK_LINE:",
[
"!@/**",
"!#*",
"!~*",
"!$*",
"!%*"
],
["!@/**", "!#*", "!~*", "!$*", "!%*"],
":BLANK_LINE:",
[
"@/**",
"#*",
"~*",
"$*",
"%*"
],
["@/**", "#*", "~*", "$*", "%*"],
":BLANK_LINE:",
[
":PATH:",
"!./**",
"!../**"
],
[":PATH:", "!./**", "!../**"],
":BLANK_LINE:",
[
"../**"
],
["../**"],
":BLANK_LINE:",
[
"./**"
]
["./**"]
],
"identifierOrder": "lexicographic"
}
@ -301,12 +265,7 @@
"semicolons": "always",
"trailingCommas": "es5"
},
"globals": [
"console",
"process",
"__dirname",
"__filename"
]
"globals": ["console", "process", "__dirname", "__filename"]
},
"json": {
"formatter": {
@ -340,11 +299,7 @@
},
"overrides": [
{
"includes": [
"**/*.test.{js,ts,tsx}",
"**/*.spec.{js,ts,tsx}",
"**/__tests__/**"
],
"includes": ["**/*.test.{js,ts,tsx}", "**/*.spec.{js,ts,tsx}", "**/__tests__/**"],
"linter": {
"rules": {
"suspicious": {
@ -358,11 +313,7 @@
}
},
{
"includes": [
"**/next.config.{js,ts}",
"**/tailwind.config.{js,ts}",
"**/*.config.{js,ts}"
],
"includes": ["**/next.config.{js,ts}", "**/tailwind.config.{js,ts}", "**/*.config.{js,ts}"],
"linter": {
"rules": {
"style": {
@ -375,11 +326,7 @@
}
},
{
"includes": [
"**/pages/**",
"**/app/**/page.{tsx,jsx}",
"**/app/**/layout.{tsx,jsx}"
],
"includes": ["**/pages/**", "**/app/**/page.{tsx,jsx}", "**/app/**/layout.{tsx,jsx}"],
"linter": {
"rules": {
"style": {
@ -389,10 +336,7 @@
}
},
{
"includes": [
"**/*.d.ts",
"**/lib/env/*.ts"
],
"includes": ["**/*.d.ts", "**/lib/env/*.ts"],
"linter": {
"rules": {
"style": {
@ -406,10 +350,7 @@
"selector": {
"kind": "objectLiteralProperty"
},
"formats": [
"CONSTANT_CASE",
"camelCase"
]
"formats": ["CONSTANT_CASE", "camelCase"]
}
]
}
@ -430,4 +371,4 @@
}
}
]
}
}

View File

@ -5,14 +5,14 @@ export default [
{
files: ["**/*.ts", "**/*.tsx"],
ignores: [
"**/docs/**",
"**/docs/**",
"**/dist/**",
"**/out/**",
"**/.turbo/**",
"**/.vscode/**",
"**/node_modules/**",
"**/scripts/**",
"**/tools/**"
"**/tools/**",
],
languageOptions: {
parser,
@ -61,4 +61,4 @@ export default [
],
},
},
];
];

View File

@ -40,4 +40,4 @@
"react-router-dom": "^7.14.0",
"zod": "^4.3.6"
}
}
}

View File

@ -37,10 +37,11 @@ export const useAuditPageController = () => {
setSearchInput(state.filters.q ?? "");
}, [state.filters.q]);
useEffect(() => {
if (debouncedSearch.trim() !== (state.filters.q ?? ""))
if (debouncedSearch.trim() !== (state.filters.q ?? "")) {
setSearchParams(
serializeAuditPageState(updateAuditFilters(state, { q: debouncedSearch.trim() || null }))
);
}
}, [debouncedSearch, setSearchParams, state]);
const params = useMemo(() => toAuditSearchParams(state, new Date()), [state]);
const liveState = resolveAuditLiveState({

View File

@ -117,22 +117,29 @@ export const AuditFilterChips = ({
}) => {
const { t } = useTranslation();
const chips: [string, string, Partial<AuditFilters>][] = [];
if (filters.period !== "last7Days")
if (filters.period !== "last7Days") {
chips.push([
"period",
t(`audit.period.${filters.period}`),
{ period: "last7Days", from: null, to: null },
]);
if (filters.kind) chips.push(["kind", t(`audit.kind.${filters.kind}`), { kind: null }]);
if (filters.targetType)
}
if (filters.kind) {
chips.push(["kind", t(`audit.kind.${filters.kind}`), { kind: null }]);
}
if (filters.targetType) {
chips.push([
"target",
t(`audit.targets.${filters.targetType}`, filters.targetType),
{ targetType: null },
]);
if (filters.action)
}
if (filters.action) {
chips.push(["action", t(`audit.actions.${filters.action}`, filters.action), { action: null }]);
if (!(chips.length || filters.q)) return null;
}
if (!(chips.length || filters.q)) {
return null;
}
return (
<div className="flex flex-wrap gap-2">
{chips.map(([id, label, value]) => (

View File

@ -45,16 +45,25 @@ export const presentAuditPath = (path: string, translate: Translate) => {
};
export const presentAuditValue = (value: AuditValue, translate: Translate): string => {
if (value === null) return translate("audit.values.null");
if (value === true) return translate("audit.values.yes");
if (value === false) return translate("audit.values.no");
if (typeof value === "string" || typeof value === "number") return String(value);
if (value === null) {
return translate("audit.values.null");
}
if (value === true) {
return translate("audit.values.yes");
}
if (value === false) {
return translate("audit.values.no");
}
if (typeof value === "string" || typeof value === "number") {
return String(value);
}
return JSON.stringify(value, null, 2);
};
export const getAuditChangeSections = (change: AuditChange, translate: Translate) => {
if (change.kind === "set")
if (change.kind === "set") {
return [{ label: translate("audit.changes.newValue"), value: change.after }];
}
if (change.kind === "unset") {
return [{ label: translate("audit.changes.removedValue"), value: change.before }];
}

View File

@ -63,16 +63,32 @@ export const parseAuditPageState = (params: URLSearchParams): AuditPageState =>
export const serializeAuditPageState = (state: AuditPageState) => {
const p = new URLSearchParams();
const f = state.filters;
if (f.q) p.set("q", f.q);
if (f.period !== "last7Days") p.set("period", f.period);
if (f.period === "custom") {
if (f.from) p.set("from", f.from);
if (f.to) p.set("to", f.to);
if (f.q) {
p.set("q", f.q);
}
if (f.period !== "last7Days") {
p.set("period", f.period);
}
if (f.period === "custom") {
if (f.from) {
p.set("from", f.from);
}
if (f.to) {
p.set("to", f.to);
}
}
if (f.kind) {
p.set("kind", f.kind);
}
if (f.targetType) {
p.set("targetType", f.targetType);
}
if (f.action) {
p.set("action", f.action);
}
if (state.entry) {
p.set("entry", state.entry);
}
if (f.kind) p.set("kind", f.kind);
if (f.targetType) p.set("targetType", f.targetType);
if (f.action) p.set("action", f.action);
if (state.entry) p.set("entry", state.entry);
p.set("pageNumber", String(state.pageNumber + 1));
p.set("pageSize", String(state.pageSize));
p.set("orderBy", state.orderBy);
@ -81,7 +97,9 @@ export const serializeAuditPageState = (state: AuditPageState) => {
};
export const toAuditSearchParams = (state: AuditPageState, now: Date): AuditSearchParams | null => {
const range = resolveAuditPeriodRange(state.filters.period, now, state.filters);
if (!range) return null;
if (!range) {
return null;
}
const f = state.filters;
return {
pageNumber: state.pageNumber,

View File

@ -5,8 +5,12 @@ export const isAuditPeriodLiveCompatible = (
now: Date,
custom?: { from: string | null; to: string | null }
) => {
if (period !== "custom") return true;
if (!(custom?.from && custom.to)) return false;
if (period !== "custom") {
return true;
}
if (!(custom?.from && custom.to)) {
return false;
}
const from = new Date(custom.from).getTime();
const to = new Date(custom.to).getTime();
const current = now.getTime();
@ -17,10 +21,11 @@ export const resolveAuditPeriodRange = (
now: Date,
custom?: { from: string | null; to: string | null }
) => {
if (period === "custom")
if (period === "custom") {
return custom?.from && custom.to && custom.from < custom.to
? { from: custom.from, to: custom.to }
: null;
}
if (period === "today") {
const from = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const to = new Date(from);
@ -35,8 +40,12 @@ export const localDateTimeToIso = (value: string) => {
return Number.isNaN(date.getTime()) ? null : date.toISOString();
};
export const isoToLocalDateTime = (value: string | null) => {
if (!value) return "";
if (!value) {
return "";
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "";
if (Number.isNaN(date.getTime())) {
return "";
}
return new Date(date.getTime() - date.getTimezoneOffset() * 60_000).toISOString().slice(0, 16);
};

View File

@ -13,7 +13,9 @@ export const findAuditEntryById = async (
dataSource: IDataSource,
{ id, signal }: FindAuditEntryByIdParams
): Promise<AuditEntryDetail> => {
if (!id) throw new Error("auditEntryId is required");
if (!id) {
throw new Error("auditEntryId is required");
}
const response = await dataSource.getOne<unknown>("audit", id, { signal });
return mapAuditEntryDetail(AuditEntryDetailDTOSchema.parse(response));

View File

@ -15,7 +15,9 @@ export const useAuditEntry = (id: string | null) => {
return useQuery<AuditEntryDetail, DefaultError>({
queryKey: auditQueryKeys.detail(companyId, id ?? "none"),
queryFn: ({ signal }) => {
if (!id) throw new Error("auditEntryId is required");
if (!id) {
throw new Error("auditEntryId is required");
}
return findAuditEntryById(dataSource, { id, signal });
},
enabled: activeCompany !== null && id !== null,

View File

@ -10,7 +10,9 @@ const mapAuditValue = (value: AuditValueDto): AuditValue => {
) {
return value;
}
if (Array.isArray(value)) return value.map(mapUnknownAuditValue);
if (Array.isArray(value)) {
return value.map(mapUnknownAuditValue);
}
return Object.fromEntries(
Object.entries(value).map(([key, item]) => [key, mapUnknownAuditValue(item)])
);
@ -25,7 +27,9 @@ const mapUnknownAuditValue = (value: unknown): AuditValue => {
) {
return value;
}
if (Array.isArray(value)) return value.map(mapUnknownAuditValue);
if (Array.isArray(value)) {
return value.map(mapUnknownAuditValue);
}
if (typeof value === "object") {
return Object.fromEntries(
Object.entries(value).map(([key, item]) => [key, mapUnknownAuditValue(item)])

View File

@ -37,4 +37,4 @@
"react-router-dom": "^7.14.0",
"react-secure-storage": "^1.3.2"
}
}
}

View File

@ -12,4 +12,5 @@ const authService = new AuthService(authenticatedUserRepository, tabContextRepos
const tabContextService = new TabContextService(tabContextRepository);
const authProvider = new PassportAuthProvider(authService, tabContextService, transactionManager);
export { authProvider };

View File

@ -59,7 +59,7 @@ export class PassportAuthProvider {
constructor(
private readonly authService: IAuthService,
private readonly tabContextService: ITabContextService,
private readonly transactionManager: ITransactionManager
readonly _transactionManager: ITransactionManager
) {}
/**

View File

@ -1,2 +1,2 @@
export * from "./login.dto";
export * from "./get-profile.dto";
export * from "./login.dto";

View File

@ -3,7 +3,7 @@ import type { RouteObject } from "react-router-dom";
import { LoginPage } from "./pages";
export const AuthRoutes = (params: ModuleClientParams): RouteObject[] => {
export const AuthRoutes = (_params: ModuleClientParams): RouteObject[] => {
return [
{
path: "login",

View File

@ -1,5 +1,6 @@
import { JSX } from "react";
import type { JSX } from "react";
import { Navigate } from "react-router-dom";
import { useIsAuthenticated } from "../hooks";
/**
@ -9,7 +10,7 @@ export const AuthGuard = ({ children }: { children: JSX.Element }) => {
const isAuthenticated = useIsAuthenticated();
if (!isAuthenticated) {
return <Navigate to='/login' replace />;
return <Navigate replace to="/login" />;
}
return children;

View File

@ -1,4 +1,4 @@
import { PropsWithChildren } from "react";
import type { PropsWithChildren } from "react";
export const AuthLayout = ({ children }: PropsWithChildren) => {
return <>{children}</>;

View File

@ -14,7 +14,7 @@ import {
Input,
} from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils";
import { SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form";
import { type SubmitErrorHandler, type SubmitHandler, useForm } from "react-hook-form";
type LoginFormValues = {
email: string;
@ -39,20 +39,20 @@ export function LoginForm({ onSubmit, onInvalid, className, ...props }: LoginFor
<div className={cn("flex flex-col gap-6", className)} {...props}>
<Card>
<CardHeader>
<CardTitle className='text-2xl'>Login</CardTitle>
<CardTitle className="text-2xl">Login</CardTitle>
<CardDescription>Enter your email below to login to your account</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit, onInvalid)} className='flex flex-col gap-6'>
<form className="flex flex-col gap-6" onSubmit={form.handleSubmit(onSubmit, onInvalid)}>
<FormField
control={form.control}
name='email'
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type='email' autoComplete='username' {...field} />
<Input autoComplete="username" type="email" {...field} />
</FormControl>
<FormMessage />
</FormItem>
@ -61,19 +61,19 @@ export function LoginForm({ onSubmit, onInvalid, className, ...props }: LoginFor
<FormField
control={form.control}
name='password'
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type='password' autoComplete='current-password' {...field} />
<Input autoComplete="current-password" type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type='submit' className='w-full'>
<Button className="w-full" type="submit">
Login
</Button>
</form>

View File

@ -1,5 +1,6 @@
import { PropsWithChildren, createContext, useEffect, useState } from "react";
import { IAuthService } from "../services";
import { type PropsWithChildren, createContext, useEffect, useState } from "react";
import type { IAuthService } from "../services";
export type AuthContextType = {
token: string | null;
@ -30,7 +31,7 @@ export const AuthProvider = ({
useEffect(() => {
setToken(getAccessToken());
}, []);
}, [getAccessToken]);
const login = async (email: string, password: string) => {
const { token } = await authService.login({ email, password });

View File

@ -1,5 +1,6 @@
import { useContext } from "react";
import { AuthContext, AuthContextType } from "../context";
import { AuthContext, type AuthContextType } from "../context";
/**
* Hook para acceder al contexto de autenticación.

View File

@ -1,5 +1,6 @@
import { useDataSource } from "@erp/core/hooks";
import { ILoginRequestDTO, ILoginResponseDTO } from "../../common";
import type { ILoginRequestDTO, ILoginResponseDTO } from "../../common";
export interface IAuthService {
login: (credentials: ILoginRequestDTO) => Promise<ILoginResponseDTO>;

View File

@ -1,7 +1,9 @@
import { useDataSource } from "@erp/core/hooks";
import { useQuery } from "@tanstack/react-query";
import { useAuth } from "../hooks";
import { User } from "./types";
import type { User } from "./types";
export const useCurrentUser = () => {
const { token } = useAuth();

View File

@ -1,6 +1,7 @@
import { useDataSource } from "@erp/core/hooks";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { User } from "./types";
import type { User } from "./types";
export const useUsers = () => {
const client = useDataSource();

View File

@ -54,4 +54,4 @@
"use-debounce": "^10.1.1",
"zod": "^4.3.6"
}
}
}

View File

@ -51,7 +51,9 @@ export class PaymentMethodCreator implements IPaymentMethodCreator {
before: {},
after: toPaymentMethodAuditState(paymentMethod),
});
if (auditResult.isFailure) return Result.fail(auditResult.error);
if (auditResult.isFailure) {
return Result.fail(auditResult.error);
}
return Result.ok(paymentMethod);
}

View File

@ -76,7 +76,9 @@ export class PaymentMethodDeleter implements IPaymentMethodDeleter {
before,
after: {},
});
if (auditResult.isFailure) return Result.fail(auditResult.error);
if (auditResult.isFailure) {
return Result.fail(auditResult.error);
}
return Result.ok(paymentMethod);
}

View File

@ -58,7 +58,9 @@ export class PaymentMethodStatusChanger implements IPaymentMethodStatusChanger {
before,
after: toPaymentMethodAuditState(paymentMethod),
});
if (auditResult.isFailure) return Result.fail(auditResult.error);
if (auditResult.isFailure) {
return Result.fail(auditResult.error);
}
return Result.ok(paymentMethod);
}

View File

@ -70,7 +70,9 @@ export class PaymentMethodUpdater implements IPaymentMethodUpdater {
before,
after: toPaymentMethodAuditState(paymentMethod),
});
if (auditResult.isFailure) return Result.fail(auditResult.error);
if (auditResult.isFailure) {
return Result.fail(auditResult.error);
}
return Result.ok(paymentMethod);
}

View File

@ -82,7 +82,9 @@ export class SetDefaultPaymentMethodService implements ISetDefaultPaymentMethodS
before: previousDefaultState,
after: toPaymentMethodAuditState(currentDefault),
});
if (auditResult.isFailure) return Result.fail(auditResult.error);
if (auditResult.isFailure) {
return Result.fail(auditResult.error);
}
}
}
@ -103,7 +105,9 @@ export class SetDefaultPaymentMethodService implements ISetDefaultPaymentMethodS
before: previousPaymentMethodState,
after: toPaymentMethodAuditState(paymentMethod),
});
if (auditResult.isFailure) return Result.fail(auditResult.error);
if (auditResult.isFailure) {
return Result.fail(auditResult.error);
}
}
return Result.ok(paymentMethod);

View File

@ -24,7 +24,9 @@ export class DeletePaymentMethodByIdUseCase {
const { payment_method_id, companyId } = params;
const idOrError = UniqueID.create(payment_method_id);
if (idOrError.isFailure) return Result.fail(idOrError.error);
if (idOrError.isFailure) {
return Result.fail(idOrError.error);
}
const paymentMethodId = idOrError.data;

View File

@ -24,7 +24,9 @@ export class DisablePaymentMethodByIdUseCase {
const { payment_method_id, companyId } = params;
const idOrError = UniqueID.create(payment_method_id);
if (idOrError.isFailure) return Result.fail(idOrError.error);
if (idOrError.isFailure) {
return Result.fail(idOrError.error);
}
const paymentMethodId = idOrError.data;

View File

@ -24,7 +24,9 @@ export class EnablePaymentMethodByIdUseCase {
const { payment_method_id, companyId } = params;
const idOrError = UniqueID.create(payment_method_id);
if (idOrError.isFailure) return Result.fail(idOrError.error);
if (idOrError.isFailure) {
return Result.fail(idOrError.error);
}
const paymentMethodId = idOrError.data;

View File

@ -24,7 +24,9 @@ export class DeletePaymentTermByIdUseCase {
const { payment_term_id, companyId } = params;
const idOrError = UniqueID.create(payment_term_id);
if (idOrError.isFailure) return Result.fail(idOrError.error);
if (idOrError.isFailure) {
return Result.fail(idOrError.error);
}
const paymentTermId = idOrError.data;

View File

@ -24,7 +24,9 @@ export class DisablePaymentTermByIdUseCase {
const { payment_term_id, companyId } = params;
const idOrError = UniqueID.create(payment_term_id);
if (idOrError.isFailure) return Result.fail(idOrError.error);
if (idOrError.isFailure) {
return Result.fail(idOrError.error);
}
const paymentTermId = idOrError.data;

View File

@ -24,7 +24,9 @@ export class EnablePaymentTermByIdUseCase {
const { payment_term_id, companyId } = params;
const idOrError = UniqueID.create(payment_term_id);
if (idOrError.isFailure) return Result.fail(idOrError.error);
if (idOrError.isFailure) {
return Result.fail(idOrError.error);
}
const paymentTermId = idOrError.data;

View File

@ -9,7 +9,7 @@ import {
} from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { ITaxDefinitionCreateProps, TaxDefinitionCode } from "../../../domain";
import type { ITaxDefinitionCreateProps } from "../../../domain";
export interface ICreateTaxDefinitionInputMapper {
map(

View File

@ -1 +1 @@
export * from './tax-definition-repository.interface';
export * from "./tax-definition-repository.interface";

View File

@ -17,15 +17,21 @@ export class TaxDefinitionStatusChanger implements ITaxDefinitionStatusChanger {
public async enable(companyId: UniqueID, id: UniqueID, transaction?: unknown) {
const existing = await this.repository.getByIdInCompany(companyId, id, transaction);
if (existing.isFailure) return Result.fail(existing.error);
if (existing.isFailure) {
return Result.fail(existing.error);
}
const taxDefinition = existing.data;
const changeResult = taxDefinition.enable();
if (changeResult.isFailure) return Result.fail(changeResult.error);
if (changeResult.isFailure) {
return Result.fail(changeResult.error);
}
if (changeResult.data) {
const saveResult = await this.repository.update(taxDefinition, transaction);
if (saveResult.isFailure) return Result.fail(saveResult.error);
if (saveResult.isFailure) {
return Result.fail(saveResult.error);
}
}
return Result.ok(changeResult.data);
@ -33,15 +39,21 @@ export class TaxDefinitionStatusChanger implements ITaxDefinitionStatusChanger {
public async disable(companyId: UniqueID, id: UniqueID, transaction?: unknown) {
const existing = await this.repository.getByIdInCompany(companyId, id, transaction);
if (existing.isFailure) return Result.fail(existing.error);
if (existing.isFailure) {
return Result.fail(existing.error);
}
const taxDefinition = existing.data;
const changeResult = taxDefinition.disable();
if (changeResult.isFailure) return Result.fail(changeResult.error);
if (changeResult.isFailure) {
return Result.fail(changeResult.error);
}
if (changeResult.data) {
const saveResult = await this.repository.update(taxDefinition, transaction);
if (saveResult.isFailure) return Result.fail(saveResult.error);
if (saveResult.isFailure) {
return Result.fail(saveResult.error);
}
}
return Result.ok(changeResult.data);

View File

@ -22,16 +22,22 @@ export class TaxDefinitionUpdater implements ITaxDefinitionUpdater {
const { companyId, id, patch, transaction } = params;
const existingResult = await this.repository.getByIdInCompany(companyId, id, transaction);
if (existingResult.isFailure) return Result.fail(existingResult.error);
if (existingResult.isFailure) {
return Result.fail(existingResult.error);
}
const taxDefinition = existingResult.data;
const updateResult = taxDefinition.update(patch);
if (updateResult.isFailure) return Result.fail(updateResult.error);
if (updateResult.isFailure) {
return Result.fail(updateResult.error);
}
if (updateResult.data) {
const saveResult = await this.repository.update(taxDefinition, transaction);
if (saveResult.isFailure) return Result.fail(saveResult.error);
if (saveResult.isFailure) {
return Result.fail(saveResult.error);
}
}
return Result.ok(taxDefinition);

View File

@ -12,7 +12,9 @@ export class DeleteTaxDefinitionByIdUseCase {
public async execute(companyId: UniqueID, id: UniqueID) {
const result = await this.deps.deleter.deleteByIdInCompany(companyId, id);
if (result.isFailure) return result;
if (result.isFailure) {
return result;
}
return Result.ok(result.data);
}

View File

@ -12,7 +12,9 @@ export class DisableTaxDefinitionByIdUseCase {
public async execute(companyId: UniqueID, id: UniqueID) {
const result = await this.deps.statusChanger.disable(companyId, id);
if (result.isFailure) return result;
if (result.isFailure) {
return result;
}
return Result.ok(result.data);
}

View File

@ -12,7 +12,9 @@ export class EnableTaxDefinitionByIdUseCase {
public async execute(companyId: UniqueID, id: UniqueID) {
const result = await this.deps.statusChanger.enable(companyId, id);
if (result.isFailure) return result;
if (result.isFailure) {
return result;
}
return Result.ok(result.data);
}

View File

@ -14,7 +14,9 @@ export class GetTaxDefinitionByIdUseCase {
public async execute(companyId: UniqueID, id: UniqueID) {
const found = await this.deps.finder.getByIdInCompany(companyId, id);
if (found.isFailure) return found;
if (found.isFailure) {
return found;
}
const snapshot = this.deps.fullSnapshotBuilder.toOutput(found.data as any);
return Result.ok(snapshot);

View File

@ -15,7 +15,9 @@ export class ListTaxDefinitionsUseCase {
public async execute(companyId: UniqueID, criteria: Criteria) {
const found = await this.deps.repository.findByCriteriaInCompany(companyId, criteria);
if (found.isFailure) return found;
if (found.isFailure) {
return found;
}
const collection = found.data.map((item) => this.deps.summarySnapshotBuilder.toOutput(item));
return Result.ok(collection);

View File

@ -21,7 +21,9 @@ export class UpdateTaxDefinitionByIdUseCase {
public async execute(companyId: UniqueID, params: UpdateTaxDefinitionByIdUseCaseInput) {
const mapped = this.deps.dtoMapper.map(params.dto as any);
if (mapped.isFailure) return mapped;
if (mapped.isFailure) {
return mapped;
}
const result = await this.deps.updater.update({
companyId,
@ -29,7 +31,9 @@ export class UpdateTaxDefinitionByIdUseCase {
patch: mapped.data,
transaction: undefined,
});
if (result.isFailure) return result;
if (result.isFailure) {
return result;
}
const snapshot = this.deps.fullSnapshotBuilder.toOutput(result.data as any);
return Result.ok(snapshot);

View File

@ -24,7 +24,9 @@ export class DeleteTaxRegimeByIdUseCase {
const { tax_regime_id, companyId } = params;
const idOrError = UniqueID.create(tax_regime_id);
if (idOrError.isFailure) return Result.fail(idOrError.error);
if (idOrError.isFailure) {
return Result.fail(idOrError.error);
}
const taxRegimeId = idOrError.data;

View File

@ -24,7 +24,9 @@ export class DisableTaxRegimeByIdUseCase {
const { tax_regime_id, companyId } = params;
const idOrError = UniqueID.create(tax_regime_id);
if (idOrError.isFailure) return Result.fail(idOrError.error);
if (idOrError.isFailure) {
return Result.fail(idOrError.error);
}
const taxRegimeId = idOrError.data;

View File

@ -24,7 +24,9 @@ export class EnableTaxRegimeByIdUseCase {
const { tax_regime_id, companyId } = params;
const idOrError = UniqueID.create(tax_regime_id);
if (idOrError.isFailure) return Result.fail(idOrError.error);
if (idOrError.isFailure) {
return Result.fail(idOrError.error);
}
const taxRegimeId = idOrError.data;

View File

@ -1,5 +1,5 @@
import { describe, test } from "node:test";
import assert from "node:assert/strict";
import { describe, test } from "node:test";
import { Name, UniqueID } from "@repo/rdx-ddd";
import { Maybe } from "@repo/rdx-utils";

View File

@ -3,8 +3,8 @@ import { type Maybe, Result } from "@repo/rdx-utils";
import {
PaymentMethodCannotBeDefaultError,
PaymentMethodCannotBeDisabledError,
PaymentMethodCannotBeDeletedError,
PaymentMethodCannotBeDisabledError,
PaymentMethodCannotBeUpdatedError,
} from "./errors";

Some files were not shown because too many files have changed in this diff Show More