From 0fc0717822058b3756397d33347681cb9473d43c Mon Sep 17 00:00:00 2001 From: david Date: Tue, 5 May 2026 20:37:29 +0200 Subject: [PATCH] . Co-authored-by: Copilot --- .vscode/settings.json | 7 + biome.json | 5 + .../components/form/simple-search-input.tsx | 2 +- modules/core/src/web/hooks/index.ts | 1 + modules/core/src/web/hooks/use-debounce.ts | 15 + .../proforma-repository.interface.ts | 6 - .../src/api/domain/services/index.ts | 2 - .../issue-customer-invoice-domain-service.ts | 83 --- ...roforma-customer-invoice-domain-service.ts | 66 -- .../models/customer-invoice.model.ts | 11 - .../repositories/proforma.repository.ts | 87 --- .../list/hooks/use-issued-invoices-list.ts | 5 +- .../use-list-proformas.controller.ts | 2 +- .../use-select-customer-dialog-controller.ts | 2 +- .../use-list-customers.controller.ts | 2 +- .../api/application/di/factuges-finder.di.ts | 9 + .../api/application/di/factuges-linker.di.ts | 12 + .../application/di/factuges-use-cases.di.ts | 15 +- modules/factuges/src/api/application/index.ts | 1 + ...uges-proforma-link-repository.interface.ts | 20 + .../src/api/application/repositories/index.ts | 1 + .../services/factuges-proforma-finder.ts | 55 ++ .../services/factuges-proforma-linker.ts | 81 +++ .../src/api/application/services/index.ts | 2 + .../create-proforma-from-factuges.use-case.ts | 36 +- .../factuges-proforma-link.aggregate.ts | 61 ++ .../src/api/domain/aggregates/index.ts | 1 + modules/factuges/src/api/domain/index.ts | 1 + .../di/factuges-persistence-mappers.di.ts | 14 + .../di/factuges-repositories.di.ts | 14 + .../src/api/infraestructure/di/factuges.di.ts | 20 +- .../factuges/src/api/infraestructure/index.ts | 1 + .../api/infraestructure/persistence/index.ts | 1 + .../persistence/sequelize/index.ts | 8 + .../sequelize/mappers/domain/index.ts | 1 + ...ze-factuges-proforma-link-domain.mapper.ts | 76 +++ .../persistence/sequelize/mappers/index.ts | 1 + .../models/factuges-customer-invoice.model.ts | 66 ++ .../persistence/sequelize/models/index.ts | 1 + .../factuges-proforma-link.repository.ts | 100 +++ .../sequelize/repositories/index.ts | 1 + packages/rdx-criteria/package.json | 2 - .../src/events/domain-event.interface.ts | 2 +- packages/rdx-ddd/src/events/domain-event.ts | 19 +- .../src/strategies/sentry-logger.ts | 2 +- .../src/strategies/winston-logger.ts | 3 +- .../src/components/form/NumberField.tsx | 65 -- packages/rdx-ui/src/components/index.ts | 1 - .../src/components/multiple-selector.tsx | 616 ------------------ packages/rdx-utils/src/helpers/date-helper.ts | 2 +- .../rdx-utils/src/helpers/rule-validator.ts | 1 - packages/shadcn-ui/package.json | 2 - 52 files changed, 627 insertions(+), 983 deletions(-) create mode 100644 modules/core/src/web/hooks/use-debounce.ts delete mode 100644 modules/customer-invoices/src/api/domain/services/index.ts delete mode 100644 modules/customer-invoices/src/api/domain/services/issue-customer-invoice-domain-service.ts delete mode 100644 modules/customer-invoices/src/api/domain/services/proforma-customer-invoice-domain-service.ts create mode 100644 modules/factuges/src/api/application/di/factuges-finder.di.ts create mode 100644 modules/factuges/src/api/application/di/factuges-linker.di.ts create mode 100644 modules/factuges/src/api/application/repositories/factuges-proforma-link-repository.interface.ts create mode 100644 modules/factuges/src/api/application/repositories/index.ts create mode 100644 modules/factuges/src/api/application/services/factuges-proforma-finder.ts create mode 100644 modules/factuges/src/api/application/services/factuges-proforma-linker.ts create mode 100644 modules/factuges/src/api/application/services/index.ts create mode 100644 modules/factuges/src/api/domain/aggregates/factuges-proforma-link.aggregate.ts create mode 100644 modules/factuges/src/api/domain/aggregates/index.ts create mode 100644 modules/factuges/src/api/domain/index.ts create mode 100644 modules/factuges/src/api/infraestructure/di/factuges-persistence-mappers.di.ts create mode 100644 modules/factuges/src/api/infraestructure/di/factuges-repositories.di.ts create mode 100644 modules/factuges/src/api/infraestructure/persistence/index.ts create mode 100644 modules/factuges/src/api/infraestructure/persistence/sequelize/index.ts create mode 100644 modules/factuges/src/api/infraestructure/persistence/sequelize/mappers/domain/index.ts create mode 100644 modules/factuges/src/api/infraestructure/persistence/sequelize/mappers/domain/sequelize-factuges-proforma-link-domain.mapper.ts create mode 100644 modules/factuges/src/api/infraestructure/persistence/sequelize/mappers/index.ts create mode 100644 modules/factuges/src/api/infraestructure/persistence/sequelize/models/factuges-customer-invoice.model.ts create mode 100644 modules/factuges/src/api/infraestructure/persistence/sequelize/models/index.ts create mode 100644 modules/factuges/src/api/infraestructure/persistence/sequelize/repositories/factuges-proforma-link.repository.ts create mode 100644 modules/factuges/src/api/infraestructure/persistence/sequelize/repositories/index.ts delete mode 100644 packages/rdx-ui/src/components/form/NumberField.tsx delete mode 100644 packages/rdx-ui/src/components/multiple-selector.tsx diff --git a/.vscode/settings.json b/.vscode/settings.json index 7d656bb9..7b4957cc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -38,6 +38,13 @@ "editor.defaultFormatter": "biomejs.biome" }, + // Codex OpenAI + "codex.enableAutoSuggest": true, + "codex.contextAwareness": true, + "codex.cloudTasks": true, + "codex.panelPosition": "right", + "codex.maxContextLines": 1000, + // Biome "biome.enabled": true, "editor.defaultFormatter": "biomejs.biome", diff --git a/biome.json b/biome.json index a2478208..603080f8 100644 --- a/biome.json +++ b/biome.json @@ -10,6 +10,11 @@ "ignoreUnknown": true, "includes": [ "**", + "!!**/supplier-invoices", + "!!**/suppliers", + "!!**/auth", + "!!**/rdx-criteria", + "!!**/shadcn-ui", "!!**/node_modules", "!!**/.next", "!!**/dist", diff --git a/modules/core/src/web/components/form/simple-search-input.tsx b/modules/core/src/web/components/form/simple-search-input.tsx index f13fcb3b..74dba947 100644 --- a/modules/core/src/web/components/form/simple-search-input.tsx +++ b/modules/core/src/web/components/form/simple-search-input.tsx @@ -1,4 +1,3 @@ -import { useDebounce } from "@repo/rdx-ui/components"; import { Button, InputGroup, @@ -9,6 +8,7 @@ import { import { SearchIcon, XIcon } from "lucide-react"; import { useEffect, useRef, useState } from "react"; +import { useDebounce } from "../../hooks"; import { useTranslation } from "../../i18n"; type SimpleSearchInputProps = { diff --git a/modules/core/src/web/hooks/index.ts b/modules/core/src/web/hooks/index.ts index 23e4fe07..167ce6ac 100644 --- a/modules/core/src/web/hooks/index.ts +++ b/modules/core/src/web/hooks/index.ts @@ -1,4 +1,5 @@ export * from "./use-datasource"; +export * from "./use-debounce"; export * from "./use-hook-form"; export * from "./use-rhf-error-focus"; export * from "./use-unsaved-changes-notifier"; diff --git a/modules/core/src/web/hooks/use-debounce.ts b/modules/core/src/web/hooks/use-debounce.ts new file mode 100644 index 00000000..669cf586 --- /dev/null +++ b/modules/core/src/web/hooks/use-debounce.ts @@ -0,0 +1,15 @@ +import * as React from "react"; + +export function useDebounce(value: T, delay?: number): T { + const [debouncedValue, setDebouncedValue] = React.useState(value); + + React.useEffect(() => { + const timer = setTimeout(() => setDebouncedValue(value), delay || 500); + + return () => { + clearTimeout(timer); + }; + }, [value, delay]); + + return debouncedValue; +} diff --git a/modules/customer-invoices/src/api/application/proformas/repositories/proforma-repository.interface.ts b/modules/customer-invoices/src/api/application/proformas/repositories/proforma-repository.interface.ts index 2de26706..56cef017 100644 --- a/modules/customer-invoices/src/api/application/proformas/repositories/proforma-repository.interface.ts +++ b/modules/customer-invoices/src/api/application/proformas/repositories/proforma-repository.interface.ts @@ -22,12 +22,6 @@ export interface IProformaRepository { transaction: unknown ): Promise>; - getByFactuGESIdInCompany( - companyId: UniqueID, - factugesId: string, - transaction: unknown - ): Promise>; - findByCriteriaInCompany( companyId: UniqueID, criteria: Criteria, diff --git a/modules/customer-invoices/src/api/domain/services/index.ts b/modules/customer-invoices/src/api/domain/services/index.ts deleted file mode 100644 index aa1f213d..00000000 --- a/modules/customer-invoices/src/api/domain/services/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./issue-customer-invoice-domain-service"; -export * from "./proforma-customer-invoice-domain-service"; diff --git a/modules/customer-invoices/src/api/domain/services/issue-customer-invoice-domain-service.ts b/modules/customer-invoices/src/api/domain/services/issue-customer-invoice-domain-service.ts deleted file mode 100644 index ce9a97b2..00000000 --- a/modules/customer-invoices/src/api/domain/services/issue-customer-invoice-domain-service.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { UniqueID, type UtcDate } from "@repo/rdx-ddd"; -import { Maybe, Result } from "@repo/rdx-utils"; - -import { CustomerInvoice } from "../aggregates"; -import { VerifactuRecord } from "../common/entities"; -import { type InvoiceNumber, InvoiceStatus, VerifactuRecordEstado } from "../common/value-objects"; -import { EntityIsNotProformaError, ProformaCannotBeConvertedToInvoiceError } from "../errors"; -import { - CustomerInvoiceIsProformaSpecification, - ProformaCanTranstionToIssuedSpecification, -} from "../specs"; - -/** - * Servicio de dominio que encapsula la lógica de emisión de factura definitiva desde una proforma. - */ -export class IssueCustomerInvoiceDomainService { - private readonly isProformaSpec = new CustomerInvoiceIsProformaSpecification(); - private readonly isApprovedSpec = new ProformaCanTranstionToIssuedSpecification(); - - /** - * Convierte una proforma en factura definitiva. - * - * @param proforma - Entidad CustomerInvoice en estado proforma aprobada. - * @param params.issueNumber - Número de la nueva factura. - * @param params.issueDate - Fecha de emisión. - * @returns Result - Nueva factura emitida o error de dominio. - */ - public async issueFromProforma( - proforma: CustomerInvoice, - params: { - issueNumber: InvoiceNumber; - issueDate: UtcDate; - } - ): Promise> { - const { issueDate, issueNumber } = params; - - /** 1. Validar que la entidad origen es una proforma */ - if (!(await this.isProformaSpec.isSatisfiedBy(proforma))) { - return Result.fail(new EntityIsNotProformaError(proforma.id.toString())); - } - - /** 2. Validar que la proforma puede emitirse */ - if (!(await this.isApprovedSpec.isSatisfiedBy(proforma))) { - return Result.fail(new ProformaCannotBeConvertedToInvoiceError(proforma.id.toString())); - } - - const verifactuRecordOrError = VerifactuRecord.create( - { - estado: VerifactuRecordEstado.createPendiente(), - qrCode: Maybe.none(), - url: Maybe.none(), - uuid: Maybe.none(), - operacion: Maybe.none(), - }, - UniqueID.generateNewID() - ); - - if (verifactuRecordOrError.isFailure) { - return Result.fail(new ProformaCannotBeConvertedToInvoiceError(proforma.id.toString())); - } - - const verifactuRecord = verifactuRecordOrError.data; - - /** 3. Generar la nueva factura definitiva (inmutable) */ - const proformaProps = proforma.getProps(); - const newInvoiceOrError = CustomerInvoice.create({ - ...proformaProps, - isProforma: false, - proformaId: Maybe.some(proforma.id), - status: InvoiceStatus.issued(), - invoiceNumber: issueNumber, - invoiceDate: issueDate, - description: proformaProps.description.isNone() ? Maybe.some(".") : proformaProps.description, - verifactu: Maybe.some(verifactuRecord), - }); - - if (newInvoiceOrError.isFailure) { - return Result.fail(newInvoiceOrError.error); - } - - return Result.ok(newInvoiceOrError.data); - } -} diff --git a/modules/customer-invoices/src/api/domain/services/proforma-customer-invoice-domain-service.ts b/modules/customer-invoices/src/api/domain/services/proforma-customer-invoice-domain-service.ts deleted file mode 100644 index 09bbfa4a..00000000 --- a/modules/customer-invoices/src/api/domain/services/proforma-customer-invoice-domain-service.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { Result } from "@repo/rdx-utils"; - -import { CustomerInvoice } from "../aggregates"; -import { INVOICE_STATUS, InvoiceStatus } from "../common/value-objects"; -import { EntityIsNotProformaError, InvalidProformaTransitionError } from "../errors"; -import { CustomerInvoiceIsProformaSpecification } from "../specs"; - -/** - * Servicio de dominio que encapsula la lógica de emisión de factura definitiva desde una proforma. - */ -export class ProformaCustomerInvoiceDomainService { - /** Aplica la transición si está permitida según INVOICE_TRANSITIONS. */ - async transition( - proforma: CustomerInvoice, - nextStatus: string - ): Promise> { - // Validar que la entidad es una proforma - const isProformaSpec = new CustomerInvoiceIsProformaSpecification(); - if (!(await isProformaSpec.isSatisfiedBy(proforma))) { - return Result.fail(new EntityIsNotProformaError(proforma.id.toString())); - } - - const current = proforma.status.toString(); - const allowed = proforma.canTransitionTo(nextStatus); - - if (!allowed) { - return Result.fail( - new InvalidProformaTransitionError(current, nextStatus, proforma.id.toString()) - ); - } - - // Validaciones adicionales de dominio, si las hubiera - // (por ejemplo, no aprobar si no hay líneas) - // new ProformaHasLinesSpecification().isSatisfiedBy(proforma) - - return CustomerInvoice.create({ - ...proforma.getProps(), - status: InvoiceStatus.create(nextStatus).data, - }); - } - - /** Envía la proforma (draft → sent) */ - async send(proforma: CustomerInvoice): Promise> { - return this.transition(proforma, INVOICE_STATUS.SENT); - } - - /** Aprueba la proforma (sent → approved) */ - async approve(proforma: CustomerInvoice): Promise> { - return this.transition(proforma, INVOICE_STATUS.APPROVED); - } - - /** Rechaza la proforma (sent → rejected) */ - async reject(proforma: CustomerInvoice): Promise> { - return this.transition(proforma, INVOICE_STATUS.REJECTED); - } - - /** Reabre una proforma rechazada (rejected → draft) */ - async reopen(proforma: CustomerInvoice): Promise> { - return this.transition(proforma, INVOICE_STATUS.DRAFT); - } - - /** Marca la proforma como emitida (approved → issued) */ - async markAsIssued(proforma: CustomerInvoice): Promise> { - return this.transition(proforma, INVOICE_STATUS.ISSUED); - } -} diff --git a/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/customer-invoice.model.ts b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/customer-invoice.model.ts index 61cbbba6..225424c8 100644 --- a/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/customer-invoice.model.ts +++ b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/customer-invoice.model.ts @@ -119,9 +119,6 @@ export class CustomerInvoiceModel extends Model< declare customer_postal_code: CreationOptional; declare customer_country: CreationOptional; - // FactuGES - declare factuges_id: CreationOptional; - // Relaciones declare items: NonAttribute; declare taxes: NonAttribute; @@ -493,12 +490,6 @@ export default (database: Sequelize) => { allowNull: true, defaultValue: null, }, - - factuges_id: { - type: DataTypes.STRING, - allowNull: true, - defaultValue: null, - }, }, { sequelize: database, @@ -529,8 +520,6 @@ export default (database: Sequelize) => { { name: "idx_invoice_company_id", fields: ["id", "company_id"], unique: true }, // <- para consulta get - { name: "idx_invoice_factuges", fields: ["factuges_id"], unique: false }, // <- para el proceso python - { name: "uq_invoice_proforma_id", fields: ["proforma_id"], unique: true }, // <- para asegurar que una proforma solo tenga una factura vinculada // Para búsquedas simples => se hace con el "CriteriaToSequelizeConverter" diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/proforma.repository.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/proforma.repository.ts index 5fec2766..6b1932e9 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/proforma.repository.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/proforma.repository.ts @@ -326,93 +326,6 @@ export class ProformaRepository } } - /** - * - * Busca una factura por su identificador único de FactuGES. - * - * @param companyId - Identificador UUID de la empresa a la que pertenece la factura. - * @param factugesId - ID de la factura en FactuGES. - * @param transaction - Transacción activa para la operación. - * @param options - Opciones adicionales para la consulta (Sequelize FindOptions) - * @returns Result - */ - async getByFactuGESIdInCompany( - companyId: UniqueID, - factugesId: string, - transaction: Transaction, - options: FindOptions> = {} - ): Promise> { - const { CustomerModel } = this.database.models; - - try { - // Normalización defensiva de order/include - const normalizedOrder = Array.isArray(options.order) - ? options.order - : options.order - ? [options.order] - : []; - - const normalizedInclude = Array.isArray(options.include) - ? options.include - : options.include - ? [options.include] - : []; - - const mergedOptions: FindOptions> = { - ...options, - where: { - ...(options.where ?? {}), - factuges_id: factugesId, - is_proforma: true, - company_id: companyId.toString(), - }, - order: [ - ...normalizedOrder, - [{ model: CustomerInvoiceItemModel, as: "items" }, "position", "ASC"], - ], - include: [ - ...normalizedInclude, - - { - model: CustomerModel, - as: "current_customer", - required: false, - }, - { - model: CustomerInvoiceItemModel, - as: "items", - required: false, - }, - { - model: CustomerInvoiceTaxModel, - as: "taxes", - required: false, - }, - { - model: CustomerInvoiceModel, - as: "linked_invoice", - required: false, - attributes: ["id"], - }, - ], - transaction, - }; - - const row = await CustomerInvoiceModel.findOne(mergedOptions); - - if (!row) { - return Result.fail( - new EntityNotFoundError("CustomerInvoice", "factuges_id", factugesId.toString()) - ); - } - - const invoice = this.domainMapper.mapToDomain(row); - return invoice; - } catch (err: unknown) { - return Result.fail(translateSequelizeError(err)); - } - } - /** * * Consulta facturas usando un objeto Criteria (filtros, orden, paginación). diff --git a/modules/customer-invoices/src/web/issued-invoices/pages/list/hooks/use-issued-invoices-list.ts b/modules/customer-invoices/src/web/issued-invoices/pages/list/hooks/use-issued-invoices-list.ts index 40d3056b..4a7b5665 100644 --- a/modules/customer-invoices/src/web/issued-invoices/pages/list/hooks/use-issued-invoices-list.ts +++ b/modules/customer-invoices/src/web/issued-invoices/pages/list/hooks/use-issued-invoices-list.ts @@ -1,10 +1,9 @@ // src/modules/issued-invoices/hooks/use-proformas-list.ts import type { CriteriaDTO } from "@erp/core"; -import { useDebounce } from "@repo/rdx-ui/components"; +import { useDebounce } from "@erp/core/hooks"; import { useMemo, useState } from "react"; -import { IssuedInvoiceSummaryDtoAdapter } from "../../../adapters/issued-invoice-summary-dto.adapter"; import { useIssuedInvoicesQuery } from "../../../hooks"; export const useIssuedInvoicesList = () => { @@ -17,7 +16,7 @@ export const useIssuedInvoicesList = () => { const criteria = useMemo(() => { const baseFilters = - status !== "all" ? [{ field: "status", operator: "CONTAINS", value: status }] : []; + status === "all" ? [] : [{ field: "status", operator: "CONTAINS", value: status }]; return { q: debouncedQ || "", diff --git a/modules/customer-invoices/src/web/proformas/list/controllers/use-list-proformas.controller.ts b/modules/customer-invoices/src/web/proformas/list/controllers/use-list-proformas.controller.ts index cf1d065a..38b05bd4 100644 --- a/modules/customer-invoices/src/web/proformas/list/controllers/use-list-proformas.controller.ts +++ b/modules/customer-invoices/src/web/proformas/list/controllers/use-list-proformas.controller.ts @@ -1,4 +1,4 @@ -import { useDebounce } from "@repo/rdx-ui/components"; +import { useDebounce } from "@erp/core/hooks"; import { useMemo, useState } from "react"; import { diff --git a/modules/customers/src/common/features/customer-selection/controllers/use-select-customer-dialog-controller.ts b/modules/customers/src/common/features/customer-selection/controllers/use-select-customer-dialog-controller.ts index 02765103..a9dfdaf4 100644 --- a/modules/customers/src/common/features/customer-selection/controllers/use-select-customer-dialog-controller.ts +++ b/modules/customers/src/common/features/customer-selection/controllers/use-select-customer-dialog-controller.ts @@ -1,4 +1,4 @@ -import { useDebounce } from "@repo/rdx-ui/components"; +import { useDebounce } from "@erp/core/hooks"; import { useMemo, useState } from "react"; import { type ListCustomersByCriteriaParams, useCustomersListQuery } from "../../../../web/shared"; diff --git a/modules/customers/src/web/list/controllers/use-list-customers.controller.ts b/modules/customers/src/web/list/controllers/use-list-customers.controller.ts index 3c853b8a..069c2603 100644 --- a/modules/customers/src/web/list/controllers/use-list-customers.controller.ts +++ b/modules/customers/src/web/list/controllers/use-list-customers.controller.ts @@ -1,4 +1,4 @@ -import { useDebounce } from "@repo/rdx-ui/components"; +import { useDebounce } from "@erp/core/hooks"; import { useMemo, useState } from "react"; import { type ListCustomersByCriteriaParams, useCustomersListQuery } from "../../shared"; diff --git a/modules/factuges/src/api/application/di/factuges-finder.di.ts b/modules/factuges/src/api/application/di/factuges-finder.di.ts new file mode 100644 index 00000000..de4f8d90 --- /dev/null +++ b/modules/factuges/src/api/application/di/factuges-finder.di.ts @@ -0,0 +1,9 @@ +import type { IFactuGESProformaLinkRepository } from "../repositories"; +import { FactuGESProformaFinder, type IFactuGESProformaFinder } from "../services"; + +export function buildFactuGESFinder(params: { + repository: IFactuGESProformaLinkRepository; +}): IFactuGESProformaFinder { + const { repository } = params; + return new FactuGESProformaFinder(repository); +} diff --git a/modules/factuges/src/api/application/di/factuges-linker.di.ts b/modules/factuges/src/api/application/di/factuges-linker.di.ts new file mode 100644 index 00000000..ab879b67 --- /dev/null +++ b/modules/factuges/src/api/application/di/factuges-linker.di.ts @@ -0,0 +1,12 @@ +import type { IFactuGESProformaLinkRepository } from "../repositories"; +import { FactuGESProformaLinker, type IFactuGESProformaLinker } from "../services"; + +export const buildFactuGESLinker = (params: { + repository: IFactuGESProformaLinkRepository; +}): IFactuGESProformaLinker => { + const { repository } = params; + + return new FactuGESProformaLinker({ + repository, + }); +}; diff --git a/modules/factuges/src/api/application/di/factuges-use-cases.di.ts b/modules/factuges/src/api/application/di/factuges-use-cases.di.ts index ec0abe6a..b308c82c 100644 --- a/modules/factuges/src/api/application/di/factuges-use-cases.di.ts +++ b/modules/factuges/src/api/application/di/factuges-use-cases.di.ts @@ -1,29 +1,32 @@ import type { ICatalogs, ITransactionManager } from "@erp/core/api"; -import type { ProformaPublicServices } from "@erp/customer-invoices/api"; -import type { CustomerPublicServices } from "@erp/customers/api"; +import type { ICustomerPublicServices } from "@erp/customers/api"; import type { ICreateProformaFromFactugesInputMapper } from "../mappers"; +import type { IFactuGESProformaFinder, IFactuGESProformaLinker } from "../services"; import { CreateProformaFromFactugesUseCase } from "../use-cases"; export function buildCreateProformaFromFactugesUseCase(deps: { + linker: IFactuGESProformaLinker; + finder: IFactuGESProformaFinder; publicServices: { - customerServices: CustomerPublicServices; - proformaServices: ProformaPublicServices; + customerServices: ICustomerPublicServices; }; dtoMapper: ICreateProformaFromFactugesInputMapper; catalogs: ICatalogs; transactionManager: ITransactionManager; }) { const { + linker, dtoMapper, transactionManager, - publicServices: { customerServices, proformaServices }, + publicServices: { customerServices }, } = deps; const { taxCatalog } = deps.catalogs; return new CreateProformaFromFactugesUseCase({ + linker, + finder, customerServices, - proformaServices, dtoMapper, taxCatalog, transactionManager, diff --git a/modules/factuges/src/api/application/index.ts b/modules/factuges/src/api/application/index.ts index 005229d8..9c08d30c 100644 --- a/modules/factuges/src/api/application/index.ts +++ b/modules/factuges/src/api/application/index.ts @@ -1,2 +1,3 @@ export * from "./mappers"; +export * from "./repositories"; export * from "./use-cases"; diff --git a/modules/factuges/src/api/application/repositories/factuges-proforma-link-repository.interface.ts b/modules/factuges/src/api/application/repositories/factuges-proforma-link-repository.interface.ts new file mode 100644 index 00000000..4b214dce --- /dev/null +++ b/modules/factuges/src/api/application/repositories/factuges-proforma-link-repository.interface.ts @@ -0,0 +1,20 @@ +import type { UniqueID } from "@repo/rdx-ddd"; +import type { Maybe, Result } from "@repo/rdx-utils"; + +import type { FactuGESProformaLink } from "../../domain"; + +export interface IFactuGESProformaLinkRepository { + save(link: FactuGESProformaLink, transaction: unknown): Promise>; + + findByFactuGESIdInCompany( + companyId: UniqueID, + factuGESId: string, + transaction?: unknown + ): Promise, Error>>; + + existsByFactuGESIdInCompany( + companyId: UniqueID, + factuGESId: string, + transaction?: unknown + ): Promise>; +} diff --git a/modules/factuges/src/api/application/repositories/index.ts b/modules/factuges/src/api/application/repositories/index.ts new file mode 100644 index 00000000..ac2a1799 --- /dev/null +++ b/modules/factuges/src/api/application/repositories/index.ts @@ -0,0 +1 @@ +export * from "./factuges-proforma-link-repository.interface"; diff --git a/modules/factuges/src/api/application/services/factuges-proforma-finder.ts b/modules/factuges/src/api/application/services/factuges-proforma-finder.ts new file mode 100644 index 00000000..340df269 --- /dev/null +++ b/modules/factuges/src/api/application/services/factuges-proforma-finder.ts @@ -0,0 +1,55 @@ +import type { UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import type { IFactuGESProformaLinkRepository } from "../repositories"; + +export interface IFactuGESProformaFinder { + existsProformaByFactuGESId( + companyId: UniqueID, + factuGESId: string, + transaction?: unknown + ): Promise>; + + findProformaIdByFactuGESId( + companyId: UniqueID, + factuGESId: string, + transaction?: unknown + ): Promise>; +} + +export class FactuGESProformaFinder implements IFactuGESProformaFinder { + constructor(private readonly repository: IFactuGESProformaLinkRepository) {} + + async existsProformaByFactuGESId( + companyId: UniqueID, + factuGESId: string, + transaction?: unknown + ): Promise> { + return this.repository.existsByFactuGESIdInCompany(companyId, factuGESId, transaction); + } + + async findProformaIdByFactuGESId( + companyId: UniqueID, + factuGESId: string, + transaction?: unknown + ): Promise> { + const result = await this.repository.findByFactuGESIdInCompany( + companyId, + factuGESId, + transaction + ); + + if (result.isFailure) { + return Result.fail(result.error); + } + + const linkOrNone = result.data; + + if (linkOrNone.isNone()) { + return Result.fail(new Error("Proforma not found")); + } + + const link = linkOrNone.unwrap(); + return Result.ok(link.proformaId); + } +} diff --git a/modules/factuges/src/api/application/services/factuges-proforma-linker.ts b/modules/factuges/src/api/application/services/factuges-proforma-linker.ts new file mode 100644 index 00000000..2b4def40 --- /dev/null +++ b/modules/factuges/src/api/application/services/factuges-proforma-linker.ts @@ -0,0 +1,81 @@ +// modules/factuges/src/api/application/services/factuges-proforma-link.creator.ts +import type { UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import { + FactuGESProformaLink, + type IFactuGESProformaLinkCreateProps, +} from "../../domain/aggregates/factuges-proforma-link.aggregate"; +import type { IFactuGESProformaLinkRepository } from "../repositories"; + +export interface IFactuGESProformaLinkerParams { + companyId: UniqueID; + proformaId: UniqueID; + factuGESId: string; + transaction: unknown; +} + +export interface IFactuGESProformaLinker { + create(params: IFactuGESProformaLinkerParams): Promise>; +} + +export class FactuGESProformaLinker implements IFactuGESProformaLinker { + constructor( + private readonly deps: { + repository: IFactuGESProformaLinkRepository; + } + ) {} + + /** + * Crea la relación entre una factura de FactuGES y una proforma nueva. + * + * La operación es idempotente por `(companyId, factuGESId)`: si el link ya existe + * con la misma `proformaId`, devuelve el existente. Si existe apuntando a otra + * proforma, falla para evitar sobrescrituras silenciosas. + */ + public async create( + params: IFactuGESProformaLinkerParams + ): Promise> { + const { companyId, factuGESId, transaction } = params; + + const existingLinkResult = await this.deps.repository.findByFactuGESIdInCompany( + companyId, + factuGESId, + transaction + ); + + if (existingLinkResult.isFailure) { + console.error("Error fetching link by FactuGES ID:", existingLinkResult.error); + return Result.fail(existingLinkResult.error); + } + + const linkOrNone = existingLinkResult.data; + + if (linkOrNone.isSome()) { + const actualLink = linkOrNone.unwrap(); + if (actualLink.proformaId.toString() !== params.proformaId.toString()) { + return Result.fail( + new Error( + `FactuGES proforma link already exists for company "${params.companyId.toString()}" and FactuGES id "${params.factuGESId}".` + ) + ); + } + } + + const createProps: IFactuGESProformaLinkCreateProps = { + companyId: params.companyId, + proformaId: params.proformaId, + factuGESId: params.factuGESId, + }; + + const linkResult = FactuGESProformaLink.create(createProps); + + if (linkResult.isFailure) { + return Result.fail(linkResult.error); + } + + await this.deps.repository.save(linkResult.data, transaction); + + return Result.ok(linkResult.data); + } +} diff --git a/modules/factuges/src/api/application/services/index.ts b/modules/factuges/src/api/application/services/index.ts new file mode 100644 index 00000000..57dd9530 --- /dev/null +++ b/modules/factuges/src/api/application/services/index.ts @@ -0,0 +1,2 @@ +export * from "./factuges-proforma-finder"; +export * from "./factuges-proforma-linker"; diff --git a/modules/factuges/src/api/application/use-cases/create-proforma-from-factuges.use-case.ts b/modules/factuges/src/api/application/use-cases/create-proforma-from-factuges.use-case.ts index 1948b4ee..cd6f68d7 100644 --- a/modules/factuges/src/api/application/use-cases/create-proforma-from-factuges.use-case.ts +++ b/modules/factuges/src/api/application/use-cases/create-proforma-from-factuges.use-case.ts @@ -28,6 +28,7 @@ import type { Transaction } from "sequelize"; import type { CreateProformaFromFactugesRequestDTO } from "../../../common"; import type { FactugesProformaPayload, ICreateProformaFromFactugesInputMapper } from "../mappers"; +import type { IFactuGESProformaFinder, IFactuGESProformaLinker } from "../services"; import paymentsCatalog from "./payments.json"; @@ -43,6 +44,8 @@ type CreateProformaFromFactugesUseCaseInput = { }; type CreateProformaFromFactugesUseCaseDeps = { + linker: IFactuGESProformaLinker; + finder: IFactuGESProformaFinder; customerServices: ICustomerPublicServices; proformaServices: IProformaPublicServices; dtoMapper: ICreateProformaFromFactugesInputMapper; @@ -54,12 +57,16 @@ type CreateProformaProps = Parameters export class CreateProformaFromFactugesUseCase { private readonly dtoMapper: ICreateProformaFromFactugesInputMapper; + private readonly linker: IFactuGESProformaLinker; + private readonly finder: IFactuGESProformaFinder; private readonly customerServices: ICustomerPublicServices; private readonly proformaServices: IProformaPublicServices; private readonly taxCatalog: JsonTaxCatalogProvider; private readonly transactionManager: ITransactionManager; constructor(deps: CreateProformaFromFactugesUseCaseDeps) { + this.linker = deps.linker; + this.finder = deps.finder; this.customerServices = deps.customerServices; this.proformaServices = deps.proformaServices; this.dtoMapper = deps.dtoMapper; @@ -80,17 +87,16 @@ export class CreateProformaFromFactugesUseCase { mappedPropsResult.data; // 2) Comprobar si la proforma ya existe (idempotencia) - const existingProformaResult = await this.proformaServices.getProformaByFactuGESId( - proformaDraft.factugesID, - { companyId, transaction: null } + const proformaIdResult = await this.finder.findProformaIdByFactuGESId( + companyId, + proformaDraft.factugesID ); - if (existingProformaResult.isSuccess) { - const existingProforma = existingProformaResult.data; + if (proformaIdResult.isSuccess) { + const existingProforma = proformaIdResult.data; return Result.ok({ - customer_id: existingProforma.customerId.toString(), - proforma_id: existingProforma.id.toString(), + proforma_id: existingProforma.toString(), }); } @@ -147,9 +153,18 @@ export class CreateProformaFromFactugesUseCase { return Result.fail(createResult.error); } - // Valida que los datos de entrada coincidan con el snapshot - const proforma = createResult.data; - const validationResult = this.validateDraftAgainstProforma(proformaDraft, proforma); + // Guardar la relación entre la proforma generada y la factura de FactuGES + await this.linker.create({ + companyId, + factuGESId: proformaDraft.factugesID, + proformaId: createResult.data.id, + transaction, + }); + + // Validación extra: los datos de entrada deben coincidir con el snapshot + const newProforma = createResult.data; + const validationResult = this.validateDraftAgainstProforma(proformaDraft, newProforma); + if (validationResult.isFailure) { return Result.fail(validationResult.error); } @@ -169,7 +184,6 @@ export class CreateProformaFromFactugesUseCase { const snapshot = readResult.data; const result = { - customer_id: customer.id.toString(), proforma_id: snapshot.id.toString(), }; diff --git a/modules/factuges/src/api/domain/aggregates/factuges-proforma-link.aggregate.ts b/modules/factuges/src/api/domain/aggregates/factuges-proforma-link.aggregate.ts new file mode 100644 index 00000000..89c0b111 --- /dev/null +++ b/modules/factuges/src/api/domain/aggregates/factuges-proforma-link.aggregate.ts @@ -0,0 +1,61 @@ +import { AggregateRoot, type UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +export interface IFactuGESProformaLinkCreateProps { + companyId: UniqueID; + proformaId: UniqueID; + factuGESId: string; +} + +export interface IFactuGESProformaLink { + companyId: UniqueID; + proformaId: UniqueID; + factuGESId: string; +} + +export type FactuGESProformaLinkInternalProps = IFactuGESProformaLinkCreateProps; + +export class FactuGESProformaLink + extends AggregateRoot + implements IFactuGESProformaLink +{ + // Creación funcional + static create( + props: IFactuGESProformaLinkCreateProps, + id?: UniqueID + ): Result { + const validationResult = FactuGESProformaLink.validateCreateProps(props); + + if (validationResult.isFailure) { + return Result.fail(validationResult.error); + } + + const link = new FactuGESProformaLink(props, id); + + return Result.ok(link); + } + + private static validateCreateProps( + props: IFactuGESProformaLinkCreateProps | FactuGESProformaLinkInternalProps + ): Result { + return Result.ok(); + } + + // Rehidratación desde persistencia + static rehydrate(props: FactuGESProformaLinkInternalProps, id: UniqueID): FactuGESProformaLink { + return new FactuGESProformaLink(props, id); + } + + // Getters + public get companyId(): UniqueID { + return this.props.companyId; + } + + public get proformaId(): UniqueID { + return this.props.proformaId; + } + + public get factuGESId(): string { + return this.props.factuGESId; + } +} diff --git a/modules/factuges/src/api/domain/aggregates/index.ts b/modules/factuges/src/api/domain/aggregates/index.ts new file mode 100644 index 00000000..a3679d35 --- /dev/null +++ b/modules/factuges/src/api/domain/aggregates/index.ts @@ -0,0 +1 @@ +export * from "./factuges-proforma-link.aggregate"; diff --git a/modules/factuges/src/api/domain/index.ts b/modules/factuges/src/api/domain/index.ts new file mode 100644 index 00000000..aa87856e --- /dev/null +++ b/modules/factuges/src/api/domain/index.ts @@ -0,0 +1 @@ +export * from "./aggregates"; diff --git a/modules/factuges/src/api/infraestructure/di/factuges-persistence-mappers.di.ts b/modules/factuges/src/api/infraestructure/di/factuges-persistence-mappers.di.ts new file mode 100644 index 00000000..84f4993d --- /dev/null +++ b/modules/factuges/src/api/infraestructure/di/factuges-persistence-mappers.di.ts @@ -0,0 +1,14 @@ +import { SequelizeFactuGESProformaLinkDomainMapper } from "../persistence"; + +export interface IFactuGESPersistenceMappers { + domainMapper: SequelizeFactuGESProformaLinkDomainMapper; +} + +export const buildFactuGESPersistenceMappers = (): IFactuGESPersistenceMappers => { + // Mappers para el repositorio + const domainMapper = new SequelizeFactuGESProformaLinkDomainMapper(); + + return { + domainMapper, + }; +}; diff --git a/modules/factuges/src/api/infraestructure/di/factuges-repositories.di.ts b/modules/factuges/src/api/infraestructure/di/factuges-repositories.di.ts new file mode 100644 index 00000000..8c6022a0 --- /dev/null +++ b/modules/factuges/src/api/infraestructure/di/factuges-repositories.di.ts @@ -0,0 +1,14 @@ +import type { Sequelize } from "sequelize"; + +import { FactuGESProformaLinkRepository } from "../persistence"; + +import type { IFactuGESPersistenceMappers } from "./factuges-persistence-mappers.di"; + +export const buildFactugesRepository = (params: { + database: Sequelize; + mappers: IFactuGESPersistenceMappers; +}) => { + const { database, mappers } = params; + + return new FactuGESProformaLinkRepository(mappers.domainMapper, database); +}; diff --git a/modules/factuges/src/api/infraestructure/di/factuges.di.ts b/modules/factuges/src/api/infraestructure/di/factuges.di.ts index 26ad7c60..0fbdf708 100644 --- a/modules/factuges/src/api/infraestructure/di/factuges.di.ts +++ b/modules/factuges/src/api/infraestructure/di/factuges.di.ts @@ -6,8 +6,13 @@ import { buildCreateProformaFromFactugesUseCase, buildFactugesInputMappers, } from "../../application/di"; +import { buildFactuGESFinder } from "../../application/di/factuges-finder.di"; +import { buildFactuGESLinker } from "../../application/di/factuges-linker.di"; import type { CreateProformaFromFactugesUseCase } from "../../application/use-cases"; +import { buildFactuGESPersistenceMappers } from "./factuges-persistence-mappers.di"; +import { buildFactugesRepository } from "./factuges-repositories.di"; + export type FactugesInternalDeps = { useCases: { createProforma: (publicServices: { @@ -22,19 +27,24 @@ export function buildFactugesDependencies(params: SetupParams): FactugesInternal // Infrastructure const transactionManager = buildTransactionManager(database); + const catalogs = buildCatalogs(); + const inputMappers = buildFactugesInputMappers(catalogs); + const persistenceMappers = buildFactuGESPersistenceMappers(); + + const repository = buildFactugesRepository({ database, mappers: persistenceMappers }); // Application helpers - const inputMappers = buildFactugesInputMappers(catalogs); + const finder = buildFactuGESFinder({ repository }); + const linker = buildFactuGESLinker({ repository }); // Internal use cases (factories) return { useCases: { - createProforma: (publicServices: { - customerServices: ICustomerPublicServices; - proformaServices: IProformaPublicServices; - }) => + createProforma: (publicServices: { customerServices: ICustomerPublicServices }) => buildCreateProformaFromFactugesUseCase({ + linker, + finder, dtoMapper: inputMappers.createInputMapper, publicServices, catalogs, diff --git a/modules/factuges/src/api/infraestructure/index.ts b/modules/factuges/src/api/infraestructure/index.ts index 6b5f6511..4263e0ee 100644 --- a/modules/factuges/src/api/infraestructure/index.ts +++ b/modules/factuges/src/api/infraestructure/index.ts @@ -1 +1,2 @@ export * from "./express"; +export * from "./persistence"; diff --git a/modules/factuges/src/api/infraestructure/persistence/index.ts b/modules/factuges/src/api/infraestructure/persistence/index.ts new file mode 100644 index 00000000..62f8ac11 --- /dev/null +++ b/modules/factuges/src/api/infraestructure/persistence/index.ts @@ -0,0 +1 @@ +export * from "./sequelize"; diff --git a/modules/factuges/src/api/infraestructure/persistence/sequelize/index.ts b/modules/factuges/src/api/infraestructure/persistence/sequelize/index.ts new file mode 100644 index 00000000..34249064 --- /dev/null +++ b/modules/factuges/src/api/infraestructure/persistence/sequelize/index.ts @@ -0,0 +1,8 @@ +import factugesCustomerInvoiceModelInit from "./models/factuges-customer-invoice.model"; + +export * from "./mappers"; +export * from "./models"; +export * from "./repositories"; + +// Array de inicializadores para que registerModels() lo use +export const models = [factugesCustomerInvoiceModelInit]; diff --git a/modules/factuges/src/api/infraestructure/persistence/sequelize/mappers/domain/index.ts b/modules/factuges/src/api/infraestructure/persistence/sequelize/mappers/domain/index.ts new file mode 100644 index 00000000..1da97226 --- /dev/null +++ b/modules/factuges/src/api/infraestructure/persistence/sequelize/mappers/domain/index.ts @@ -0,0 +1 @@ +export * from "./sequelize-factuges-proforma-link-domain.mapper"; diff --git a/modules/factuges/src/api/infraestructure/persistence/sequelize/mappers/domain/sequelize-factuges-proforma-link-domain.mapper.ts b/modules/factuges/src/api/infraestructure/persistence/sequelize/mappers/domain/sequelize-factuges-proforma-link-domain.mapper.ts new file mode 100644 index 00000000..14c0fc71 --- /dev/null +++ b/modules/factuges/src/api/infraestructure/persistence/sequelize/mappers/domain/sequelize-factuges-proforma-link-domain.mapper.ts @@ -0,0 +1,76 @@ +// modules/factuges/src/api/infrastructure/persistence/sequelize/mappers/domain/sequelize-factuges-proforma-link-domain.mapper.ts +import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api"; +import { + UniqueID, + ValidationErrorCollection, + type ValidationErrorDetail, + extractOrPushError, +} from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import { + FactuGESProformaLink, + type FactuGESProformaLinkInternalProps, +} from "../../../../../domain"; +import type { + FactuGESCustomerInvoiceLinkCreationAttributes, + FactuGESCustomerInvoiceLinkModel, +} from "../../models/factuges-customer-invoice.model"; + +export class SequelizeFactuGESProformaLinkDomainMapper extends SequelizeDomainMapper< + FactuGESCustomerInvoiceLinkModel, + FactuGESCustomerInvoiceLinkCreationAttributes, + FactuGESProformaLink +> { + public mapToDomain( + source: FactuGESCustomerInvoiceLinkModel, + _params?: MapperParamsType + ): Result { + try { + const errors: ValidationErrorDetail[] = []; + + const companyId = extractOrPushError( + UniqueID.create(source.company_id), + "company_id", + errors + ); + + const proformaId = extractOrPushError( + UniqueID.create(source.invoice_id), + "invoice_id", + errors + ); + + if (errors.length > 0) { + return Result.fail( + new ValidationErrorCollection("FactuGES proforma link props mapping failed", errors) + ); + } + + const props: FactuGESProformaLinkInternalProps = { + companyId: companyId!, + proformaId: proformaId!, + factuGESId: source.factuges_id, + }; + + const link = FactuGESProformaLink.rehydrate(props, proformaId!); + + return Result.ok(link); + } catch (err: unknown) { + return Result.fail(err as Error); + } + } + + public mapToPersistence( + source: FactuGESProformaLink, + _params?: MapperParamsType + ): Result { + const values: Partial = { + company_id: source.companyId.toPrimitive(), + invoice_id: source.proformaId.toPrimitive(), + factuges_id: source.factuGESId, + }; + + return Result.ok(values as FactuGESCustomerInvoiceLinkCreationAttributes); + } +} diff --git a/modules/factuges/src/api/infraestructure/persistence/sequelize/mappers/index.ts b/modules/factuges/src/api/infraestructure/persistence/sequelize/mappers/index.ts new file mode 100644 index 00000000..7886141d --- /dev/null +++ b/modules/factuges/src/api/infraestructure/persistence/sequelize/mappers/index.ts @@ -0,0 +1 @@ +export * from "./domain"; diff --git a/modules/factuges/src/api/infraestructure/persistence/sequelize/models/factuges-customer-invoice.model.ts b/modules/factuges/src/api/infraestructure/persistence/sequelize/models/factuges-customer-invoice.model.ts new file mode 100644 index 00000000..a8198854 --- /dev/null +++ b/modules/factuges/src/api/infraestructure/persistence/sequelize/models/factuges-customer-invoice.model.ts @@ -0,0 +1,66 @@ +import { + DataTypes, + type InferAttributes, + type InferCreationAttributes, + Model, + type Sequelize, +} from "sequelize"; + +export type FactuGESCustomerInvoiceLinkCreationAttributes = InferCreationAttributes< + FactuGESCustomerInvoiceLinkModel, + {} +> & {}; + +export class FactuGESCustomerInvoiceLinkModel extends Model< + InferAttributes, + InferCreationAttributes +> { + declare company_id: string; + declare invoice_id: string; + declare factuges_id: string; + + static associate(_database: Sequelize) {} + static hooks(_database: Sequelize) {} +} + +export default (database: Sequelize) => { + FactuGESCustomerInvoiceLinkModel.init( + { + company_id: { + type: DataTypes.UUID, + allowNull: false, + }, + invoice_id: { + type: DataTypes.UUID, + primaryKey: true, + }, + factuges_id: { + type: DataTypes.STRING, + allowNull: false, + }, + }, + { + sequelize: database, + modelName: "FactuGESCustomerInvoiceLinkModel", + tableName: "factuges_customer_invoice_links", + + underscored: true, + paranoid: false, // no softs deletes + timestamps: true, + + createdAt: "created_at", + updatedAt: "updated_at", + deletedAt: "deleted_at", + + indexes: [], + + whereMergeStrategy: "and", // <- cómo tratar el merge de un scope + + defaultScope: {}, + + scopes: {}, + } + ); + + return FactuGESCustomerInvoiceLinkModel; +}; diff --git a/modules/factuges/src/api/infraestructure/persistence/sequelize/models/index.ts b/modules/factuges/src/api/infraestructure/persistence/sequelize/models/index.ts new file mode 100644 index 00000000..7a73f36a --- /dev/null +++ b/modules/factuges/src/api/infraestructure/persistence/sequelize/models/index.ts @@ -0,0 +1 @@ +export * from "./factuges-customer-invoice.model"; diff --git a/modules/factuges/src/api/infraestructure/persistence/sequelize/repositories/factuges-proforma-link.repository.ts b/modules/factuges/src/api/infraestructure/persistence/sequelize/repositories/factuges-proforma-link.repository.ts new file mode 100644 index 00000000..6c93d1aa --- /dev/null +++ b/modules/factuges/src/api/infraestructure/persistence/sequelize/repositories/factuges-proforma-link.repository.ts @@ -0,0 +1,100 @@ +import { SequelizeRepository, translateSequelizeError } from "@erp/core/api"; +import type { UniqueID } from "@repo/rdx-ddd"; +import { Maybe, Result } from "@repo/rdx-utils"; +import type { Sequelize, Transaction } from "sequelize"; + +import type { IFactuGESProformaLinkRepository } from "../../../../application"; +import type { FactuGESProformaLink } from "../../../../domain"; +import type { SequelizeFactuGESProformaLinkDomainMapper } from "../mappers"; +import { FactuGESCustomerInvoiceLinkModel } from "../models"; + +export class FactuGESProformaLinkRepository + extends SequelizeRepository + implements IFactuGESProformaLinkRepository +{ + constructor( + private readonly domainMapper: SequelizeFactuGESProformaLinkDomainMapper, + database: Sequelize + ) { + super({ database }); + } + + /** + * Persiste una relación entre factura legacy FactuGES y proforma. + * + * La unicidad debe garantizarse también en base de datos mediante índice único + * sobre `(company_id, factuges_id)`. + */ + public async save( + link: FactuGESProformaLink, + transaction?: Transaction + ): Promise> { + try { + const dtoResult = this.domainMapper.mapToPersistence(link); + + if (dtoResult.isFailure) { + return Result.fail(dtoResult.error); + } + + const dto = dtoResult.data; + + await FactuGESCustomerInvoiceLinkModel.create(dto, { transaction }); + return Result.ok(); + } catch (error: unknown) { + return Result.fail(translateSequelizeError(error)); + } + } + + /** + * Busca el link asociado a una factura de FactuGES dentro de una company. + */ + public async findByFactuGESIdInCompany( + companyId: UniqueID, + factuGESId: string, + transaction?: Transaction + ): Promise, Error>> { + try { + const row = await FactuGESCustomerInvoiceLinkModel.findOne({ + where: { + company_id: companyId.toString(), + factuges_id: factuGESId, + }, + transaction, + }); + + if (!row) { + return Result.ok(Maybe.none()); + } + + const linkResult = this.domainMapper.mapToDomain(row); + return linkResult.isFailure + ? Result.fail(linkResult.error) + : Result.ok(Maybe.some(linkResult.data)); + } catch (error: unknown) { + throw translateSequelizeError(error); + } + } + + /** + * Comprueba existencia del link por factura legacy dentro de una company. + */ + public async existsByFactuGESIdInCompany( + companyId: UniqueID, + factuGESId: string, + transaction?: Transaction + ): Promise> { + try { + const count = await FactuGESCustomerInvoiceLinkModel.count({ + where: { + company_id: companyId.toString(), + factuges_id: factuGESId, + }, + transaction, + }); + + return Result.ok(Boolean(count > 0)); + } catch (error: unknown) { + return Result.fail(translateSequelizeError(error)); + } + } +} diff --git a/modules/factuges/src/api/infraestructure/persistence/sequelize/repositories/index.ts b/modules/factuges/src/api/infraestructure/persistence/sequelize/repositories/index.ts new file mode 100644 index 00000000..c73bf0c3 --- /dev/null +++ b/modules/factuges/src/api/infraestructure/persistence/sequelize/repositories/index.ts @@ -0,0 +1 @@ +export * from "./factuges-proforma-link.repository"; diff --git a/packages/rdx-criteria/package.json b/packages/rdx-criteria/package.json index 8e173d94..20efc887 100644 --- a/packages/rdx-criteria/package.json +++ b/packages/rdx-criteria/package.json @@ -6,8 +6,6 @@ "sideEffects": false, "scripts": { "typecheck": "tsc -p tsconfig.json --noEmit", - "check": "biome check .", - "lint": "biome lint .", "clean": "rimraf .turbo node_modules dist" }, "exports": { diff --git a/packages/rdx-ddd/src/events/domain-event.interface.ts b/packages/rdx-ddd/src/events/domain-event.interface.ts index 8a2730d9..ac0eb6a4 100644 --- a/packages/rdx-ddd/src/events/domain-event.interface.ts +++ b/packages/rdx-ddd/src/events/domain-event.interface.ts @@ -1,4 +1,4 @@ -import { UniqueID } from "../value-objects/unique-id"; +import type { UniqueID } from "../value-objects/unique-id"; export interface IDomainEvent { eventName: string; // Nombre del evento diff --git a/packages/rdx-ddd/src/events/domain-event.ts b/packages/rdx-ddd/src/events/domain-event.ts index f0c388e6..da960780 100644 --- a/packages/rdx-ddd/src/events/domain-event.ts +++ b/packages/rdx-ddd/src/events/domain-event.ts @@ -1,10 +1,11 @@ // https://khalilstemmler.com/articles/typescript-domain-driven-design/chain-business-logic-domain-events/ -import { AggregateRoot } from "../aggregate-root"; -import { UniqueID } from "../value-objects"; -import { IDomainEvent } from "./domain-event.interface"; +import type { AggregateRoot } from "../aggregate-root"; +import type { UniqueID } from "../value-objects"; -// biome-ignore lint/complexity/noStaticOnlyClass: +import type { IDomainEvent } from "./domain-event.interface"; + +// biome-ignore lint/complexity/noStaticOnlyClass: export class DomainEvents { private static handlersMap: { [key: string]: Array<(event: IDomainEvent) => void> } = {}; private static markedAggregates: AggregateRoot[] = []; @@ -33,7 +34,9 @@ export class DomainEvents { */ private static dispatchAggregateEvents(aggregate: AggregateRoot): void { - aggregate.domainEvents.forEach((event: IDomainEvent) => DomainEvents.dispatch(event)); + for (const event of aggregate.domainEvents) { + DomainEvents.dispatch(event); + } } /** @@ -91,10 +94,10 @@ export class DomainEvents { */ public static register(callback: (event: IDomainEvent) => void, eventClassName: string): void { - if (!Object.prototype.hasOwnProperty.call(DomainEvents.handlersMap, eventClassName)) { + if (!Object.hasOwn(DomainEvents.handlersMap, eventClassName)) { DomainEvents.handlersMap[eventClassName] = []; } - DomainEvents.handlersMap[eventClassName].push(callback); + DomainEvents.handlersMap[eventClassName]!.push(callback); } /** @@ -127,7 +130,7 @@ export class DomainEvents { const eventClassName: string = event.constructor.name; if (Object.hasOwn(DomainEvents.handlersMap, eventClassName)) { - const handlers: any[] = DomainEvents.handlersMap[eventClassName]; + const handlers = DomainEvents.handlersMap[eventClassName]!; for (const handler of handlers) { handler(event); } diff --git a/packages/rdx-logger/src/strategies/sentry-logger.ts b/packages/rdx-logger/src/strategies/sentry-logger.ts index 138c83b7..8e127e53 100644 --- a/packages/rdx-logger/src/strategies/sentry-logger.ts +++ b/packages/rdx-logger/src/strategies/sentry-logger.ts @@ -1,4 +1,4 @@ -import { ILogger } from "../types"; +import type { ILogger } from "../types"; export class SentryLogger implements ILogger { // biome-ignore lint/complexity/noUselessConstructor: diff --git a/packages/rdx-logger/src/strategies/winston-logger.ts b/packages/rdx-logger/src/strategies/winston-logger.ts index 7522aa8f..e0bc6bf0 100644 --- a/packages/rdx-logger/src/strategies/winston-logger.ts +++ b/packages/rdx-logger/src/strategies/winston-logger.ts @@ -1,6 +1,7 @@ import rTracer from "cls-rtracer"; import { createLogger, format, transports } from "winston"; -import { ILogger } from "../types"; + +import type { ILogger } from "../types"; const winston = createLogger({ level: "info", diff --git a/packages/rdx-ui/src/components/form/NumberField.tsx b/packages/rdx-ui/src/components/form/NumberField.tsx deleted file mode 100644 index 8ff592a3..00000000 --- a/packages/rdx-ui/src/components/form/NumberField.tsx +++ /dev/null @@ -1,65 +0,0 @@ -// DatePickerField.tsx - -import { - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, - Input, -} from "@repo/shadcn-ui/components"; - -import { cn } from "@repo/shadcn-ui/lib/utils"; -import type { Control, FieldPath, FieldValues } from "react-hook-form"; -import { useTranslation } from "../../locales/i18n.ts"; - -type NumberFieldProps = { - control: Control; - name: FieldPath; - label: string; - placeholder?: string; - description?: string; - disabled?: boolean; - required?: boolean; - readOnly?: boolean; - className?: string; -}; - -export function NumberField({ - control, - name, - label, - placeholder, - description, - disabled = false, - required = false, - readOnly = false, - className, -}: NumberFieldProps) { - const { t } = useTranslation(); - const isDisabled = disabled || readOnly; - - return ( - ( - -
- {label} - {required && {t("common.required")}} -
- - - - - - {description || "\u00A0"} - - -
- )} - /> - ); -} diff --git a/packages/rdx-ui/src/components/index.ts b/packages/rdx-ui/src/components/index.ts index 8be5ddf3..60f81e0d 100644 --- a/packages/rdx-ui/src/components/index.ts +++ b/packages/rdx-ui/src/components/index.ts @@ -13,7 +13,6 @@ export * from "./loading-overlay/index.ts"; export * from "./logo-verifactu.tsx"; export * from "./lookup-dialog/index.ts"; export * from "./multi-select.tsx"; -export * from "./multiple-selector.tsx"; export * from "./right-panel/index.ts"; export * from "./scroll-to-top.tsx"; export * from "./tailwind-indicator.tsx"; diff --git a/packages/rdx-ui/src/components/multiple-selector.tsx b/packages/rdx-ui/src/components/multiple-selector.tsx deleted file mode 100644 index 4e3f0862..00000000 --- a/packages/rdx-ui/src/components/multiple-selector.tsx +++ /dev/null @@ -1,616 +0,0 @@ -// https://shadcnui-expansions.typeart.cc/docs/multiple-selector - -import { Badge, Command, CommandGroup, CommandItem, CommandList } from "@repo/shadcn-ui/components"; -import { cn } from "@repo/shadcn-ui/lib/utils"; -import { Command as CommandPrimitive, useCommandState } from "cmdk"; -import { ChevronDownIcon, X } from "lucide-react"; -import * as React from "react"; -import { forwardRef, useEffect } from "react"; - -export interface MultipleSelectorOption { - value: string; - label: string; - disable?: boolean; - /** fixed option that can't be removed. */ - fixed?: boolean; - /** Group the options by providing key. */ - [key: string]: string | boolean | undefined; -} -interface GroupOption { - [key: string]: MultipleSelectorOption[]; -} - -interface MultipleSelectorProps { - value?: MultipleSelectorOption[]; - defaultOptions?: MultipleSelectorOption[]; - /** manually controlled options */ - options?: MultipleSelectorOption[]; - placeholder?: string; - /** Loading component. */ - loadingIndicator?: React.ReactNode; - /** Empty component. */ - emptyIndicator?: React.ReactNode; - /** Debounce time for async search. Only work with `onSearch`. */ - delay?: number; - /** - * Only work with `onSearch` prop. Trigger search when `onFocus`. - * For example, when user click on the input, it will trigger the search to get initial options. - **/ - triggerSearchOnFocus?: boolean; - /** async search */ - onSearch?: (value: string) => Promise; - /** - * sync search. This search will not showing loadingIndicator. - * The rest props are the same as async search. - * i.e.: creatable, groupBy, delay. - **/ - onSearchSync?: (value: string) => MultipleSelectorOption[]; - onChange?: (options: MultipleSelectorOption[]) => void; - /** Limit the maximum number of selected options. */ - maxSelected?: number; - /** When the number of selected options exceeds the limit, the onMaxSelected will be called. */ - onMaxSelected?: (maxLimit: number) => void; - /** Hide the placeholder when there are options selected. */ - hidePlaceholderWhenSelected?: boolean; - disabled?: boolean; - /** Group the options base on provided key. */ - groupBy?: string; - className?: string; - badgeClassName?: string; - /** - * First item selected is a default behavior by cmdk. That is why the default is true. - * This is a workaround solution by add a dummy item. - * - * @reference: https://github.com/pacocoursey/cmdk/issues/171 - */ - selectFirstItem?: boolean; - /** Allow user to create option when there is no option matched. */ - creatable?: boolean; - /** Props of `Command` */ - commandProps?: React.ComponentPropsWithoutRef; - /** Props of `CommandInput` */ - inputProps?: Omit< - React.ComponentPropsWithoutRef, - "value" | "placeholder" | "disabled" - >; - /** hide the clear all button. */ - hideClearAllButton?: boolean; -} - -export interface MultipleSelectorRef { - selectedValue: MultipleSelectorOption[]; - input: HTMLInputElement; - focus: () => void; - reset: () => void; -} - -export function useDebounce(value: T, delay?: number): T { - const [debouncedValue, setDebouncedValue] = React.useState(value); - - useEffect(() => { - const timer = setTimeout(() => setDebouncedValue(value), delay || 500); - - return () => { - clearTimeout(timer); - }; - }, [value, delay]); - - return debouncedValue; -} - -function transToGroupOption(options: MultipleSelectorOption[], groupBy?: string) { - if (options.length === 0) { - return {}; - } - if (!groupBy) { - return { - "": options, - }; - } - - const groupOption: GroupOption = {}; - options.forEach((option) => { - const key = (option[groupBy] as string) || ""; - if (!groupOption[key]) { - groupOption[key] = []; - } - groupOption[key].push(option); - }); - return groupOption; -} - -function removePickedOption(groupOption: GroupOption, picked: MultipleSelectorOption[]) { - const cloneOption = JSON.parse(JSON.stringify(groupOption)) as GroupOption; - - for (const [key, value] of Object.entries(cloneOption)) { - cloneOption[key] = value.filter((val) => !picked.find((p) => p.value === val.value)); - } - return cloneOption; -} - -function isOptionsExist(groupOption: GroupOption, targetOption: MultipleSelectorOption[]) { - for (const [, value] of Object.entries(groupOption)) { - if (value.some((option) => targetOption.find((p) => p.value === option.value))) { - return true; - } - } - return false; -} - -/** - * The `CommandEmpty` of shadcn/ui will cause the cmdk empty not rendering correctly. - * So we create one and copy the `Empty` implementation from `cmdk`. - * - * @reference: https://github.com/hsuanyi-chou/shadcn-ui-expansions/issues/34#issuecomment-1949561607 - **/ -const CommandEmpty = forwardRef< - HTMLDivElement, - React.ComponentProps ->(({ className, ...props }, forwardedRef) => { - const render = useCommandState((state) => state.filtered.count === 0); - - if (!render) return null; - - return ( -
- ); -}); - -CommandEmpty.displayName = "CommandEmpty"; - -export const MultipleSelector = React.forwardRef( - ( - { - value, - onChange, - placeholder, - defaultOptions: arrayDefaultOptions = [], - options: arrayOptions, - delay, - onSearch, - onSearchSync, - loadingIndicator, - emptyIndicator, - maxSelected = Number.MAX_SAFE_INTEGER, - onMaxSelected, - hidePlaceholderWhenSelected, - disabled, - groupBy, - className, - badgeClassName, - selectFirstItem = true, - creatable = false, - triggerSearchOnFocus = false, - commandProps, - inputProps, - hideClearAllButton = false, - }: MultipleSelectorProps, - ref: React.Ref - ) => { - const inputRef = React.useRef(null); - const [open, setOpen] = React.useState(false); - const [onScrollbar, setOnScrollbar] = React.useState(false); - const [isLoading, setIsLoading] = React.useState(false); - const dropdownRef = React.useRef(null); // Added this - - const [selected, setSelected] = React.useState(value || []); - const [options, setOptions] = React.useState( - transToGroupOption(arrayDefaultOptions, groupBy) - ); - const [inputValue, setInputValue] = React.useState(""); - const debouncedSearchTerm = useDebounce(inputValue, delay || 500); - - React.useImperativeHandle( - ref, - () => ({ - selectedValue: [...selected], - input: inputRef.current as HTMLInputElement, - focus: () => inputRef?.current?.focus(), - reset: () => setSelected([]), - }), - [selected] - ); - - const handleClickOutside = (event: MouseEvent | TouchEvent) => { - if ( - dropdownRef.current && - !dropdownRef.current.contains(event.target as Node) && - inputRef.current && - !inputRef.current.contains(event.target as Node) - ) { - setOpen(false); - inputRef.current.blur(); - } - }; - - const handleUnselect = React.useCallback( - (option: MultipleSelectorOption) => { - const newOptions = selected.filter((s) => s.value !== option.value); - setSelected(newOptions); - onChange?.(newOptions); - }, - [onChange, selected] - ); - - const handleKeyDown = React.useCallback( - (e: React.KeyboardEvent) => { - const input = inputRef.current; - if (input) { - if (e.key === "Delete" || e.key === "Backspace") { - if (input.value === "" && selected.length > 0) { - const lastSelectOption = selected[selected.length - 1]; - // If there is a last item and it is not fixed, we can remove it. - if (lastSelectOption && !lastSelectOption.fixed) { - handleUnselect(lastSelectOption); - } - } - } - // This is not a default behavior of the field - if (e.key === "Escape") { - input.blur(); - } - } - }, - [handleUnselect, selected] - ); - - useEffect(() => { - if (open) { - document.addEventListener("mousedown", handleClickOutside); - document.addEventListener("touchend", handleClickOutside); - } else { - document.removeEventListener("mousedown", handleClickOutside); - document.removeEventListener("touchend", handleClickOutside); - } - - return () => { - document.removeEventListener("mousedown", handleClickOutside); - document.removeEventListener("touchend", handleClickOutside); - }; - }, [open]); - - useEffect(() => { - if (value) { - setSelected(value); - } - }, [value]); - - useEffect(() => { - /** If `onSearch` is provided, do not trigger options updated. */ - if (!arrayOptions || onSearch) { - return; - } - const newOption = transToGroupOption(arrayOptions || [], groupBy); - if (JSON.stringify(newOption) !== JSON.stringify(options)) { - setOptions(newOption); - } - }, [arrayDefaultOptions, arrayOptions, groupBy, onSearch, options]); - - useEffect(() => { - /** sync search */ - - const doSearchSync = () => { - const res = onSearchSync?.(debouncedSearchTerm); - setOptions(transToGroupOption(res || [], groupBy)); - }; - - const exec = async () => { - if (!(onSearchSync && open)) return; - - if (triggerSearchOnFocus) { - doSearchSync(); - } - - if (debouncedSearchTerm) { - doSearchSync(); - } - }; - - void exec(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [debouncedSearchTerm, groupBy, open, triggerSearchOnFocus]); - - useEffect(() => { - /** async search */ - - const doSearch = async () => { - setIsLoading(true); - const res = await onSearch?.(debouncedSearchTerm); - setOptions(transToGroupOption(res || [], groupBy)); - setIsLoading(false); - }; - - const exec = async () => { - if (!(onSearch && open)) return; - - if (triggerSearchOnFocus) { - await doSearch(); - } - - if (debouncedSearchTerm) { - await doSearch(); - } - }; - - void exec(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [debouncedSearchTerm, groupBy, open, triggerSearchOnFocus]); - - const CreatableItem = () => { - if (!creatable) return undefined; - if ( - isOptionsExist(options, [{ value: inputValue, label: inputValue }]) || - selected.find((s) => s.value === inputValue) - ) { - return undefined; - } - - const Item = ( - { - e.preventDefault(); - e.stopPropagation(); - }} - onSelect={(value: string) => { - if (selected.length >= maxSelected) { - onMaxSelected?.(selected.length); - return; - } - setInputValue(""); - const newOptions = [...selected, { value, label: value }]; - setSelected(newOptions); - onChange?.(newOptions); - }} - > - {`Create "${inputValue}"`} - - ); - - // For normal creatable - if (!onSearch && inputValue.length > 0) { - return Item; - } - - // For async search creatable. avoid showing creatable item before loading at first. - if (onSearch && debouncedSearchTerm.length > 0 && !isLoading) { - return Item; - } - - return undefined; - }; - - const EmptyItem = React.useCallback(() => { - if (!emptyIndicator) return undefined; - - // For async search that showing emptyIndicator - if (onSearch && !creatable && Object.keys(options).length === 0) { - return ( - - {emptyIndicator} - - ); - } - - return {emptyIndicator}; - }, [creatable, emptyIndicator, onSearch, options]); - - const selectables = React.useMemo( - () => removePickedOption(options, selected), - [options, selected] - ); - - /** Avoid Creatable Selector freezing or lagging when paste a long string. */ - const commandFilter = React.useCallback(() => { - if (commandProps?.filter) { - return commandProps.filter; - } - - if (creatable) { - return (value: string, search: string) => { - return value.toLowerCase().includes(search.toLowerCase()) ? 1 : -1; - }; - } - // Using default filter in `cmdk`. We don't have to provide it. - return undefined; - }, [creatable, commandProps?.filter]); - - return ( - { - handleKeyDown(e); - commandProps?.onKeyDown?.(e); - }} - className={cn("h-auto overflow-visible bg-transparent", commandProps?.className)} - shouldFilter={ - commandProps?.shouldFilter === undefined ? !onSearch : commandProps.shouldFilter - } // When onSearch is provided, we don't want to filter the options. You can still override it. - filter={commandFilter()} - > -
{ - if (disabled) return; - inputRef?.current?.focus(); - }} - onKeyDown={(e) => { - if ((e.key === "Enter" || e.key === " ") && !disabled) { - inputRef?.current?.focus(); - } - }} - > -
- {selected.map((option) => { - return ( - - {option.label} - - - ); - })} - {/* Avoid having the "Search" Icon */} - { - setInputValue(value); - inputProps?.onValueChange?.(value); - }} - onBlur={(event) => { - if (!onScrollbar) { - setOpen(false); - } - inputProps?.onBlur?.(event); - }} - onFocus={(event) => { - setOpen(true); - inputProps?.onFocus?.(event); - }} - placeholder={hidePlaceholderWhenSelected && selected.length !== 0 ? "" : placeholder} - className={cn( - "flex-1 self-baseline bg-transparent outline-none placeholder:text-muted-foreground", - { - "w-full": hidePlaceholderWhenSelected, - "ml-1": selected.length !== 0, - }, - inputProps?.className - )} - /> -
- - = 1 || - selected.filter((s) => s.fixed).length !== selected.length) && - "hidden" - )} - /> -
-
- {open && ( - { - setOnScrollbar(false); - }} - onMouseEnter={() => { - setOnScrollbar(true); - }} - onMouseUp={() => { - inputRef?.current?.focus(); - }} - > - {isLoading ? ( - <>{loadingIndicator} - ) : ( - <> - {EmptyItem()} - {CreatableItem()} - {!selectFirstItem && } - {Object.entries(selectables).map(([key, dropdowns]) => ( - - {dropdowns.map((option) => { - return ( - { - e.preventDefault(); - e.stopPropagation(); - }} - onSelect={() => { - if (selected.length >= maxSelected) { - onMaxSelected?.(selected.length); - return; - } - setInputValue(""); - const newOptions = [...selected, option]; - setSelected(newOptions); - onChange?.(newOptions); - }} - className={cn( - "cursor-pointer", - option.disable && "cursor-default text-muted-foreground" - )} - > - {option.label} - - ); - })} - - ))} - - )} - - )} -
-
- ); - } -); - -MultipleSelector.displayName = "MultipleSelector"; diff --git a/packages/rdx-utils/src/helpers/date-helper.ts b/packages/rdx-utils/src/helpers/date-helper.ts index cb3b51b7..cb2a9ca9 100644 --- a/packages/rdx-utils/src/helpers/date-helper.ts +++ b/packages/rdx-utils/src/helpers/date-helper.ts @@ -14,7 +14,7 @@ function formatDate( options?: Intl.DateTimeFormatOptions ): string { const date = normalizeToDate(input); - if (!date || isNaN(date.getTime())) return ""; + if (!date || Number.isNaN(date.getTime())) return ""; // Por defecto, formato corto y consistente. const fmt = new Intl.DateTimeFormat(locale, { diff --git a/packages/rdx-utils/src/helpers/rule-validator.ts b/packages/rdx-utils/src/helpers/rule-validator.ts index dc004e3e..919b2aa3 100644 --- a/packages/rdx-utils/src/helpers/rule-validator.ts +++ b/packages/rdx-utils/src/helpers/rule-validator.ts @@ -4,7 +4,6 @@ import { Result } from "./result"; export type TRuleValidatorResult = Result; -// biome-ignore lint/complexity/noStaticOnlyClass: export class RuleValidator { public static readonly RULE_NOT_NULL_OR_UNDEFINED = Joi.any() .required() // <- undefined diff --git a/packages/shadcn-ui/package.json b/packages/shadcn-ui/package.json index 12c86838..2778baf0 100644 --- a/packages/shadcn-ui/package.json +++ b/packages/shadcn-ui/package.json @@ -16,8 +16,6 @@ ] }, "scripts": { - "check": "biome check .", - "lint": "biome lint .", "ui:add": "pnpm dlx shadcn@latest add" }, "peerDependencies": {