diff --git a/apps/server/src/register-modules.ts b/apps/server/src/register-modules.ts index 7c18e5cb..6c7418fd 100644 --- a/apps/server/src/register-modules.ts +++ b/apps/server/src/register-modules.ts @@ -1,5 +1,6 @@ import customerInvoicesAPIModule from "@erp/customer-invoices/api"; import customersAPIModule from "@erp/customers/api"; +import verifactuAPIModule from "@erp/verifactu/api"; import { registerModule } from "./lib"; @@ -7,4 +8,5 @@ export const registerModules = () => { //registerModule(authAPIModule); registerModule(customersAPIModule); registerModule(customerInvoicesAPIModule); + registerModule(verifactuAPIModule); }; diff --git a/modules/core/src/web/lib/data-source/axios/create-axios-data-source.ts b/modules/core/src/web/lib/data-source/axios/create-axios-data-source.ts index d03fdb45..793f2902 100644 --- a/modules/core/src/web/lib/data-source/axios/create-axios-data-source.ts +++ b/modules/core/src/web/lib/data-source/axios/create-axios-data-source.ts @@ -52,8 +52,8 @@ export const createAxiosDataSource = (client: AxiosInstance): IDataSource => { return res.data; }, - getOne: async (resource: string, id: string | number) => { - const res = await (client as AxiosInstance).get(`${resource}/${id}`); + getOne: async (resource: string, id: string | number, params?: Record) => { + const res = await (client as AxiosInstance).get(`${resource}/${id}`, params); return res.data; }, @@ -62,18 +62,31 @@ export const createAxiosDataSource = (client: AxiosInstance): IDataSource => { return res.data; }, - createOne: async (resource: string, data: Partial) => { - const res = await (client as AxiosInstance).post(resource, data); + createOne: async ( + resource: string, + data: Partial, + params?: Record + ): Promise => { + const res = await (client as AxiosInstance).post(resource, data, params); return res.data; }, - updateOne: async (resource: string, id: string | number, data: Partial) => { - const res = await (client as AxiosInstance).put(`${resource}/${id}`, data); + updateOne: async ( + resource: string, + id: string | number, + data: Partial, + params?: Record + ) => { + const res = await (client as AxiosInstance).put(`${resource}/${id}`, data, params); return res.data; }, - deleteOne: async (resource: string, id: string | number) => { - await (client as AxiosInstance).delete(`${resource}/${id}`); + deleteOne: async ( + resource: string, + id: string | number, + params?: Record + ) => { + await (client as AxiosInstance).delete(`${resource}/${id}`, params); }, custom: async (customParams: ICustomParams) => { diff --git a/modules/core/src/web/lib/data-source/datasource.interface.ts b/modules/core/src/web/lib/data-source/datasource.interface.ts index b4119a1d..9f2c5fa6 100644 --- a/modules/core/src/web/lib/data-source/datasource.interface.ts +++ b/modules/core/src/web/lib/data-source/datasource.interface.ts @@ -15,11 +15,20 @@ export interface ICustomParams { export interface IDataSource { getBaseUrl(): string; getList(resource: string, params?: Record): Promise; - getOne(resource: string, id: string | number): Promise; + getOne(resource: string, id: string | number, params?: Record): Promise; getMany(resource: string, ids: Array): Promise; - createOne(resource: string, data: Partial): Promise; - updateOne(resource: string, id: string | number, data: Partial): Promise; - deleteOne(resource: string, id: string | number): Promise; + createOne(resource: string, data: Partial, params?: Record): Promise; + updateOne( + resource: string, + id: string | number, + data: Partial, + params?: Record + ): Promise; + deleteOne( + resource: string, + id: string | number, + params?: Record + ): Promise; custom: (customParams: ICustomParams) => Promise; } diff --git a/modules/core/src/web/lib/helpers/form-utils.ts b/modules/core/src/web/lib/helpers/form-utils.ts new file mode 100644 index 00000000..559e797a --- /dev/null +++ b/modules/core/src/web/lib/helpers/form-utils.ts @@ -0,0 +1,44 @@ +/** + * Extrae solo los valores marcados como "dirty" por react-hook-form, + * respetando la estructura anidada de dirtyFields. + */ +export function pickFormDirtyValues>( + values: T, + dirtyFields: Partial> +): Partial { + const result: Partial = {}; + + for (const key in dirtyFields) { + if (!Object.prototype.hasOwnProperty.call(dirtyFields, key)) continue; + + const isDirty = dirtyFields[key]; + const value = values[key]; + + if (isDirty === true) { + // 🔹 Campo "leaf": se ha tocado → copiar valor + result[key] = value; + } else if (typeof isDirty === "object" && isDirty !== null) { + // 🔹 Campo anidado: recursión + const nested = pickFormDirtyValues(value, isDirty); + if (Object.keys(nested).length > 0) { + result[key] = nested as any; + } + } + } + + return result; +} + +/** + * Devuelve true si hay al menos un campo dirty en cualquier nivel. + */ +export function formHasAnyDirty(dirtyFields: Partial> | boolean): boolean { + if (dirtyFields === true) return true; + if (dirtyFields === false || dirtyFields == null) return false; + + if (typeof dirtyFields === "object") { + return Object.values(dirtyFields).some((v) => formHasAnyDirty(v)); + } + + return false; +} diff --git a/modules/core/src/web/lib/helpers/index.ts b/modules/core/src/web/lib/helpers/index.ts index 7e676d89..b6e20656 100644 --- a/modules/core/src/web/lib/helpers/index.ts +++ b/modules/core/src/web/lib/helpers/index.ts @@ -1,2 +1,3 @@ export * from "./date-func"; +export * from "./form-utils"; export * from "./money-funcs"; diff --git a/modules/customers/src/web/components/editor/customer-additional-config-fields.tsx b/modules/customers/src/web/components/editor/customer-additional-config-fields.tsx index a39c4b65..6e3e142b 100644 --- a/modules/customers/src/web/components/editor/customer-additional-config-fields.tsx +++ b/modules/customers/src/web/components/editor/customer-additional-config-fields.tsx @@ -22,7 +22,7 @@ export const CustomerAdditionalConfigFields = () => { {t("form_groups.preferences.description")} -
+
{ const { t } = useTranslation(); const { control } = useFormContext(); + return ( +
+ {t("form_groups.address.title")} + {t("form_groups.address.description")} + + + + + + + + + + +
+ ); + return ( @@ -22,7 +88,7 @@ export const CustomerAddressFields = () => { {t("form_groups.address.description")} -
+
{ description={t("form_fields.postal_code.description")} />
-
+
{ }); return ( - - - Identificación - - -
-
- -
+
+ Identificación + descripción + + + + + + ( + + {t("form_fields.customer_type.label")} + + { + field.onChange(value === "false" ? "false" : "true"); + }} + defaultValue={field.value ? "true" : "false"} + className='flex items-center gap-8' + > + + + + + + {t("form_fields.customer_type.company")} + + + + + + + + {t("form_fields.customer_type.individual")} + + + + + + + )} + /> + -
- ( - - {t("form_fields.customer_type.label")} - - { - field.onChange(value === "false" ? "false" : "true"); - }} - defaultValue={field.value ? "true" : "false"} - className='flex items-center gap-6' - > - - - - - - {t("form_fields.customer_type.company")} - - - - - - - - {t("form_fields.customer_type.individual")} - - - - - - - )} - /> -
- - {isCompany === "false" ? ( -
- -
- ) : ( - <> - )} - -
- -
- -
- -
- - -
-
-
+ ) : ( + <> + )} + + + + + + + ); }; diff --git a/modules/customers/src/web/components/editor/customer-contact-fields.tsx b/modules/customers/src/web/components/editor/customer-contact-fields.tsx index d0270622..248d656f 100644 --- a/modules/customers/src/web/components/editor/customer-contact-fields.tsx +++ b/modules/customers/src/web/components/editor/customer-contact-fields.tsx @@ -1,15 +1,6 @@ -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@repo/shadcn-ui/components"; +import { Description, FieldGroup, Fieldset, Legend, TextField } from "@repo/rdx-ui/components"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@repo/shadcn-ui/components"; -import { TextField } from "@repo/rdx-ui/components"; import { ChevronDown, MailIcon, PhoneIcon, SmartphoneIcon } from "lucide-react"; import { useState } from "react"; import { useFormContext } from "react-hook-form"; @@ -21,91 +12,83 @@ export const CustomerContactFields = () => { const { control } = useFormContext(); return ( - - - {t("form_groups.contact_info.title")} - {t("form_groups.contact_info.description")} - - -
- - } - typePreset='email' - required - /> - - } - /> +
+ {t("form_groups.contact_info.title")} + {t("form_groups.contact_info.description")} + + } + typePreset='email' + required + /> + + } + /> - - } - /> -
-
- - } - /> - - } - /> - - } - /> -
+ + } + /> + + } + /> + + + } + /> + + } + /> @@ -113,30 +96,27 @@ export const CustomerContactFields = () => { -
-
- -
-
- -
-
+ + + +
-
-
+ + ); }; diff --git a/modules/customers/src/web/hooks/use-create-customer-mutation.ts b/modules/customers/src/web/hooks/use-create-customer-mutation.ts index 1ed4d00a..5dd66658 100644 --- a/modules/customers/src/web/hooks/use-create-customer-mutation.ts +++ b/modules/customers/src/web/hooks/use-create-customer-mutation.ts @@ -1,8 +1,8 @@ import { useDataSource } from "@erp/core/hooks"; import { UniqueID, ValidationErrorCollection } from "@repo/rdx-ddd"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { CreateCustomerRequestSchema, CustomerCreationResponseDTO } from "../../common"; -import { CustomerFormData } from "../schemas"; +import { DefaultError, useMutation, useQueryClient } from "@tanstack/react-query"; +import { CreateCustomerRequestSchema } from "../../common"; +import { CustomerData, CustomerFormData } from "../schemas"; import { CUSTOMERS_LIST_KEY } from "./use-update-customer-mutation"; type CreateCustomerPayload = { @@ -14,7 +14,7 @@ export function useCreateCustomerMutation() { const dataSource = useDataSource(); const schema = CreateCustomerRequestSchema; - return useMutation({ + return useMutation({ mutationKey: ["customer:create"], mutationFn: async (payload) => { @@ -38,7 +38,7 @@ export function useCreateCustomerMutation() { } const created = await dataSource.createOne("customers", newCustomerData); - return created as CustomerCreationResponseDTO; + return created as CustomerData; }, onSuccess: () => { diff --git a/modules/customers/src/web/hooks/use-customer-form.ts b/modules/customers/src/web/hooks/use-customer-form.ts index dfc0f6dc..de77aa8e 100644 --- a/modules/customers/src/web/hooks/use-customer-form.ts +++ b/modules/customers/src/web/hooks/use-customer-form.ts @@ -10,7 +10,7 @@ type UseCustomerFormProps = { }; export function useCustomerForm({ initialValues, disabled, onDirtyChange }: UseCustomerFormProps) { - const form = useForm({ + const form = useForm({ resolver: zodResolver(CustomerFormSchema), defaultValues: initialValues, disabled, diff --git a/modules/customers/src/web/hooks/use-customer-query.ts b/modules/customers/src/web/hooks/use-customer-query.ts index 8b13575a..316ae853 100644 --- a/modules/customers/src/web/hooks/use-customer-query.ts +++ b/modules/customers/src/web/hooks/use-customer-query.ts @@ -1,40 +1,51 @@ import { useDataSource } from "@erp/core/hooks"; -import { GetCustomerByIdResponseDTO } from "@erp/customer-invoices/common"; -import { type QueryKey, type UseQueryOptions, useQuery } from "@tanstack/react-query"; +import { DefaultError, type QueryKey, useQuery } from "@tanstack/react-query"; +import { CustomerData } from "../schemas"; export const CUSTOMER_QUERY_KEY = (id: string): QueryKey => ["customer", id] as const; -type Options = Omit< - UseQueryOptions< - GetCustomerByIdResponseDTO, - Error, - GetCustomerByIdResponseDTO, - ReturnType - >, - "queryKey" | "queryFn" | "enabled" -> & { +type CustomerQueryOptions = { enabled?: boolean; }; -export function useCustomerQuery(customerId?: string, options?: Options) { +export function useCustomerQuery(customerId?: string, options?: CustomerQueryOptions) { const dataSource = useDataSource(); const enabled = (options?.enabled ?? true) && !!customerId; - return useQuery< - GetCustomerByIdResponseDTO, - Error, - GetCustomerByIdResponseDTO, - ReturnType - >({ + const queryResult = useQuery({ queryKey: CUSTOMER_QUERY_KEY(customerId ?? "unknown"), - enabled, queryFn: async (context) => { - if (!customerId) throw new Error("customerId is required"); - const { signal } = context; - const customer = await dataSource.getOne("customers", customerId); - return customer as GetCustomerByIdResponseDTO; + if (!customerId) { + if (!customerId) throw new Error("customerId is required"); + } + const customer = await dataSource.getOne("customers", customerId, { + signal, + }); + return customer; }, - ...options, + enabled, }); + + return queryResult; } + +/* + export function useQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey + > + + TQueryFnData: the type returned from the queryFn. + TError: the type of Errors to expect from the queryFn. + TData: the type our data property will eventually have. + Only relevant if you use the select option, + because then the data property can be different + from what the queryFn returns. + Otherwise, it will default to whatever the queryFn returns. + TQueryKey: the type of our queryKey, only relevant + if you use the queryKey that is passed to your queryFn. + +*/ diff --git a/modules/customers/src/web/hooks/use-update-customer-mutation.ts b/modules/customers/src/web/hooks/use-update-customer-mutation.ts index 15a82de5..f302a1d4 100644 --- a/modules/customers/src/web/hooks/use-update-customer-mutation.ts +++ b/modules/customers/src/web/hooks/use-update-customer-mutation.ts @@ -7,9 +7,11 @@ import { CUSTOMER_QUERY_KEY } from "./use-customer-query"; export const CUSTOMERS_LIST_KEY = ["customers"] as const; +type UpdateCustomerContext = {}; + type UpdateCustomerPayload = { id: string; - data: CustomerFormData; + data: Partial; }; export function useUpdateCustomerMutation() { @@ -17,7 +19,7 @@ export function useUpdateCustomerMutation() { const dataSource = useDataSource(); const schema = UpdateCustomerByIdRequestSchema; - return useMutation({ + return useMutation({ mutationKey: ["customer:update"], //, customerId], mutationFn: async (payload) => { @@ -38,9 +40,9 @@ export function useUpdateCustomerMutation() { } const updated = await dataSource.updateOne("customers", customerId, data); - return updated as UpdateCustomerByIdRequestDTO; + return updated as CustomerFormData; }, - onSuccess: (updated, variables) => { + onSuccess: (updated: CustomerFormData, variables) => { const { id: customerId } = variables; // Refresca inmediatamente el detalle diff --git a/modules/customers/src/web/pages/create/customer-create.tsx b/modules/customers/src/web/pages/create/customer-create.tsx index 16ac25f2..a6773a04 100644 --- a/modules/customers/src/web/pages/create/customer-create.tsx +++ b/modules/customers/src/web/pages/create/customer-create.tsx @@ -2,7 +2,7 @@ import { AppBreadcrumb, AppContent } from "@repo/rdx-ui/components"; import { useNavigate } from "react-router-dom"; import { FormCommitButtonGroup, UnsavedChangesProvider } from "@erp/core/hooks"; -import { showErrorToast, showSuccessToast } from "@repo/shadcn-ui/lib/utils"; +import { showErrorToast, showSuccessToast } from "@repo/rdx-ui/helpers"; import { FieldErrors, FormProvider } from "react-hook-form"; import { CustomerEditForm, ErrorAlert } from "../../components"; import { useCreateCustomerMutation, useCustomerForm } from "../../hooks"; @@ -34,7 +34,7 @@ export const CustomerCreate = () => { onSuccess(data) { showSuccessToast(t("pages.create.successTitle"), t("pages.create.successMsg")); - // 🔹 reset limpia el form e isDirty pasa a false + // 🔹 limpiar el form e isDirty pasa a false form.reset(defaultCustomerFormData); navigate("/customers/list", { diff --git a/modules/customers/src/web/pages/update/customer-update.tsx b/modules/customers/src/web/pages/update/customer-update.tsx index 8ecd4ec8..749eb2c4 100644 --- a/modules/customers/src/web/pages/update/customer-update.tsx +++ b/modules/customers/src/web/pages/update/customer-update.tsx @@ -1,9 +1,10 @@ import { AppBreadcrumb, AppContent, BackHistoryButton } from "@repo/rdx-ui/components"; import { useNavigate } from "react-router-dom"; +import { formHasAnyDirty, pickFormDirtyValues } from "@erp/core/client"; import { FormCommitButtonGroup, UnsavedChangesProvider, useUrlParamId } from "@erp/core/hooks"; -import { showErrorToast, showSuccessToast } from "@repo/shadcn-ui/lib/utils"; -import { FieldErrors } from "react-hook-form"; +import { showErrorToast, showSuccessToast, showWarningToast } from "@repo/rdx-ui/helpers"; +import { FieldErrors, FormProvider } from "react-hook-form"; import { CustomerEditForm, CustomerEditorSkeleton, @@ -12,7 +13,7 @@ import { } from "../../components"; import { useCustomerForm, useCustomerQuery, useUpdateCustomerMutation } from "../../hooks"; import { useTranslation } from "../../i18n"; -import { CustomerFormData } from "../../schemas"; +import { CustomerFormData, defaultCustomerFormData } from "../../schemas"; export const CustomerUpdate = () => { const customerId = useUrlParamId(); @@ -37,26 +38,27 @@ export const CustomerUpdate = () => { // 3) Form hook const form = useCustomerForm({ - initialValues: customerData, + initialValues: customerData ?? defaultCustomerFormData, }); - // 3) Submit con navegación condicionada por éxito + // 4) Submit con navegación condicionada por éxito const handleSubmit = (formData: CustomerFormData) => { + const { dirtyFields } = form.formState; + + if (!formHasAnyDirty(dirtyFields)) { + showWarningToast("No hay cambios para guardar"); + return; + } + + const patchData = pickFormDirtyValues(formData, dirtyFields); mutate( - { id: customerId!, data: formData }, + { id: customerId!, data: patchData }, { onSuccess(data) { - setIsDirty(false); showSuccessToast(t("pages.update.successTitle"), t("pages.update.successMsg")); - // El timeout es para que a React le dé tiempo a procesar - // el cambio de estado de isDirty / setIsDirty. - setTimeout(() => { - navigate("/customers/list", { - state: { customerId: data.id, isNew: true }, - replace: true, - }); - }, 0); + // 🔹 limpiar el form e isDirty pasa a false + form.reset(data); }, onError(error) { showErrorToast(t("pages.update.errorTitle"), error.message); @@ -112,7 +114,7 @@ export const CustomerUpdate = () => { <> - +

@@ -127,7 +129,7 @@ export const CustomerUpdate = () => { to: "/customers/list", }} submit={{ - formId: "customer-create-form", + formId: "customer-update-form", disabled: isUpdating, isLoading: isUpdating, }} @@ -145,13 +147,13 @@ export const CustomerUpdate = () => { )}
- + + +
diff --git a/modules/customers/src/web/schemas/customer.api.schema.ts b/modules/customers/src/web/schemas/customer.api.schema.ts index e3fa5371..ebb86040 100644 --- a/modules/customers/src/web/schemas/customer.api.schema.ts +++ b/modules/customers/src/web/schemas/customer.api.schema.ts @@ -1,4 +1,15 @@ -import { CreateCustomerRequestSchema, UpdateCustomerByIdRequestSchema } from "@erp/customers"; +import * as z from "zod/v4"; + +import { + CreateCustomerRequestSchema, + GetCustomerByIdResponseSchema, + UpdateCustomerByIdRequestSchema, +} from "@erp/customers"; export const CustomerCreateSchema = CreateCustomerRequestSchema; export const CustomerUpdateSchema = UpdateCustomerByIdRequestSchema; +export const CustomerSchema = GetCustomerByIdResponseSchema.omit({ + metadata: true, +}); + +export type CustomerData = z.infer; diff --git a/modules/customers/src/web/schemas/customer.form.schema.ts b/modules/customers/src/web/schemas/customer.form.schema.ts index 6e92e8a2..5f1f4b0a 100644 --- a/modules/customers/src/web/schemas/customer.form.schema.ts +++ b/modules/customers/src/web/schemas/customer.form.schema.ts @@ -3,7 +3,7 @@ import * as z from "zod/v4"; export const CustomerFormSchema = z.object({ reference: z.string().optional(), - is_company: z.enum(["true", "false"]), + is_company: z.string().default("true"), name: z .string({ error: "El nombre es obligatorio", diff --git a/modules/verifactu/package.json b/modules/verifactu/package.json index 92d6d13b..214c54e3 100644 --- a/modules/verifactu/package.json +++ b/modules/verifactu/package.json @@ -11,6 +11,7 @@ "dependencies": { "@erp/core": "workspace:*", "@repo/rdx-ddd": "workspace:*", - "@repo/rdx-utils": "workspace:*" + "@repo/rdx-utils": "workspace:*", + "@repo/rdx-logger": "workspace:*" } } diff --git a/modules/verifactu/src/api/application/index.ts b/modules/verifactu/src/api/application/index.ts new file mode 100644 index 00000000..9fbda4ad --- /dev/null +++ b/modules/verifactu/src/api/application/index.ts @@ -0,0 +1,2 @@ +//export * from "./presenters"; +export * from "./use-cases"; diff --git a/modules/verifactu/src/api/application/use-cases/send/index.ts b/modules/verifactu/src/api/application/use-cases/send/index.ts index b6dd097f..bad15979 100644 --- a/modules/verifactu/src/api/application/use-cases/send/index.ts +++ b/modules/verifactu/src/api/application/use-cases/send/index.ts @@ -1 +1 @@ -export * from "./send-invoice-verifactu.use-case"; +export * from "./send-invoice.use-case"; diff --git a/modules/verifactu/src/api/application/use-cases/send/send-invoice-verifactu.use-case.ts b/modules/verifactu/src/api/application/use-cases/send/send-invoice.use-case.ts similarity index 88% rename from modules/verifactu/src/api/application/use-cases/send/send-invoice-verifactu.use-case.ts rename to modules/verifactu/src/api/application/use-cases/send/send-invoice.use-case.ts index dcb67f71..6395de35 100644 --- a/modules/verifactu/src/api/application/use-cases/send/send-invoice-verifactu.use-case.ts +++ b/modules/verifactu/src/api/application/use-cases/send/send-invoice.use-case.ts @@ -4,18 +4,18 @@ import { Result } from "@repo/rdx-utils"; import { Transaction } from "sequelize"; import { VerifactuRecordService } from "../../../domain"; -type SendInvoiceVerifactuUseCaseInput = { +type SendInvoiceUseCaseInput = { invoice_id: string; }; -export class SendInvoiceVerifactuUseCase { +export class SendInvoiceUseCase { constructor( private readonly service: VerifactuRecordService, private readonly transactionManager: ITransactionManager, private readonly presenterRegistry: IPresenterRegistry ) {} - public async execute(params: SendInvoiceVerifactuUseCaseInput) { + public async execute(params: SendInvoiceUseCaseInput) { const { invoice_id } = params; const idOrError = UniqueID.create(invoice_id); diff --git a/modules/verifactu/src/api/domain/aggregates/value-objects/index.ts b/modules/verifactu/src/api/domain/aggregates/value-objects/index.ts new file mode 100644 index 00000000..15dc6f18 --- /dev/null +++ b/modules/verifactu/src/api/domain/aggregates/value-objects/index.ts @@ -0,0 +1,2 @@ +export * from "./verifactu-record-estado"; +export * from "./verifactu-record-url"; diff --git a/modules/verifactu/src/api/domain/aggregates/value-objects/verifactu-record-estado.ts b/modules/verifactu/src/api/domain/aggregates/value-objects/verifactu-record-estado.ts new file mode 100644 index 00000000..97dad944 --- /dev/null +++ b/modules/verifactu/src/api/domain/aggregates/value-objects/verifactu-record-estado.ts @@ -0,0 +1,68 @@ +import { DomainValidationError } from "@erp/core/api"; +import { ValueObject } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +interface IVerifactuRecordEstadoProps { + value: string; +} + +export enum VERIFACTU_RECORD_STATUS { + PENDIENTE = "Pendiente", // <- Registro encolado y no procesado aún + CORRECTO = "Correcto", // <- Registro procesado correctamente por la AEAT + ACEPTADO_CON_ERROR = "Aceptado con errores", // <- Registro aceptado con errores por la AEAT. Se requiere enviar un registro de subsanación o emitir una rectificativa + INCORRECTO = "Incorrecto", // <- Registro considerado incorrecto por la AEAT. Se requiere enviar un registro de subsanación con rechazo_previo=S o rechazo_previo=X o emitir una rectificativa + DUPLICADO = "Duplicado", // <- Registro no aceptado por la AEAT por existir un registro con el mismo (serie, numero, fecha_expedicion) + ANULADO = "Anulado", // <- Registro de anulación procesado correctamente por la AEAT + FACTURA_INEXISTENTE = "Factura inexistente", // <- Registro de anulación no aceptado por la AEAT por no existir la factura. + RECHAZADO = "No registrado", // <- Registro rechazado por la AEAT + ERROR = "Error servidor AEAT", // <- Error en el servidor de la AEAT. Se intentará reenviar el registro de facturación de nuevo +} +export class VerifactuRecordEstado extends ValueObject { + private static readonly ALLOWED_STATUSES = [ + "Pendiente", + "Correcto", + "Aceptado con errores", + "Incorrecto", + "Duplicado", + "Anulado", + "Factura inexistente", + "No registrado", + "Error servidor AEAT", + ]; + private static readonly FIELD = "estado"; + private static readonly ERROR_CODE = "INVALID_RECORD_STATUS"; + /* + private static readonly TRANSITIONS: Record = { + draft: [INVOICE_STATUS.SENT], + sent: [INVOICE_STATUS.APPROVED, INVOICE_STATUS.REJECTED], + approved: [INVOICE_STATUS.EMITTED], + rejected: [INVOICE_STATUS.DRAFT], + }; +*/ + static create(value: string): Result { + if (!VerifactuRecordEstado.ALLOWED_STATUSES.includes(value)) { + const detail = `Estado de la factura no válido: ${value}`; + return Result.fail( + new DomainValidationError( + VerifactuRecordEstado.ERROR_CODE, + VerifactuRecordEstado.FIELD, + detail + ) + ); + } + + return Result.ok(new VerifactuRecordEstado({ value })); + } + + getProps(): string { + return this.props.value; + } + + toPrimitive() { + return this.getProps(); + } + + toString() { + return String(this.props.value); + } +} diff --git a/modules/verifactu/src/api/domain/aggregates/value-objects/verifactu-record-url.ts b/modules/verifactu/src/api/domain/aggregates/value-objects/verifactu-record-url.ts new file mode 100644 index 00000000..806636a3 --- /dev/null +++ b/modules/verifactu/src/api/domain/aggregates/value-objects/verifactu-record-url.ts @@ -0,0 +1,56 @@ +import { DomainValidationError } from "@erp/core/api"; +import { ValueObject } from "@repo/rdx-ddd"; +import { Maybe, Result } from "@repo/rdx-utils"; +import * as z from "zod/v4"; + +interface VerifactuRecordUrlProps { + value: string; +} + +export class VerifactuRecordUrl extends ValueObject { + private static readonly MAX_LENGTH = 255; + private static readonly FIELD = "verifactuRecordUrl"; + private static readonly ERROR_CODE = "INVALID_URL"; + + protected static validate(value: string) { + const schema = z + .string() + .trim() + .max(VerifactuRecordUrl.MAX_LENGTH, { + message: `Description must be at most ${VerifactuRecordUrl.MAX_LENGTH} characters long`, + }); + return schema.safeParse(value); + } + + static create(value: string) { + const valueIsValid = VerifactuRecordUrl.validate(value); + + if (!valueIsValid.success) { + const detail = valueIsValid.error.message; + return Result.fail( + new DomainValidationError(VerifactuRecordUrl.ERROR_CODE, VerifactuRecordUrl.FIELD, detail) + ); + } + return Result.ok(new VerifactuRecordUrl({ value })); + } + + static createNullable(value?: string): Result, Error> { + if (!value || value.trim() === "") { + return Result.ok(Maybe.none()); + } + + return VerifactuRecordUrl.create(value).map((value) => Maybe.some(value)); + } + + getProps(): string { + return this.props.value; + } + + toString() { + return String(this.props.value); + } + + toPrimitive() { + return this.getProps(); + } +} diff --git a/modules/verifactu/src/api/domain/index.ts b/modules/verifactu/src/api/domain/index.ts index ef023faa..832e942b 100644 --- a/modules/verifactu/src/api/domain/index.ts +++ b/modules/verifactu/src/api/domain/index.ts @@ -1,3 +1,7 @@ export * from "./aggregates"; export * from "./repositories"; export * from "./services"; + +//export * from "./entities"; +//export * from "./errors"; +//export * from "./value-objects"; diff --git a/modules/verifactu/src/api/helpers/index.ts b/modules/verifactu/src/api/helpers/index.ts new file mode 100644 index 00000000..41c7bf27 --- /dev/null +++ b/modules/verifactu/src/api/helpers/index.ts @@ -0,0 +1 @@ +export * from "./logger"; diff --git a/modules/verifactu/src/api/helpers/logger.ts b/modules/verifactu/src/api/helpers/logger.ts new file mode 100644 index 00000000..dff536f0 --- /dev/null +++ b/modules/verifactu/src/api/helpers/logger.ts @@ -0,0 +1,3 @@ +import { loggerSingleton } from "@repo/rdx-logger"; + +export const logger = loggerSingleton(); diff --git a/modules/verifactu/src/api/index.ts b/modules/verifactu/src/api/index.ts new file mode 100644 index 00000000..1ffcc85d --- /dev/null +++ b/modules/verifactu/src/api/index.ts @@ -0,0 +1,30 @@ +import { IModuleServer, ModuleParams } from "@erp/core/api"; +import { models, verifactuRouter } from "./infrastructure"; + +export const verifactuAPIModule: IModuleServer = { + name: "verifactu", + version: "1.0.0", + dependencies: ["customers-invoices"], + + async init(params: ModuleParams) { + // const contacts = getService("contacts"); + const { logger } = params; + verifactuRouter(params); + logger.info("🚀 Verifactu module initialized", { label: this.name }); + }, + async registerDependencies(params) { + const { database, logger } = params; + logger.info("🚀 Verifactu module dependencies registered", { + label: this.name, + }); + return { + models, + services: { + sendInvoiceToVerifactu: () => {}, + /*...*/ + }, + }; + }, +}; + +export default verifactuAPIModule; diff --git a/modules/verifactu/src/api/infrastructure/dependencies.ts b/modules/verifactu/src/api/infrastructure/dependencies.ts index 0be3461e..6845a811 100644 --- a/modules/verifactu/src/api/infrastructure/dependencies.ts +++ b/modules/verifactu/src/api/infrastructure/dependencies.ts @@ -2,27 +2,13 @@ import type { IMapperRegistry, IPresenterRegistry, ModuleParams } from "@erp/core/api"; +import { JsonTaxCatalogProvider } from "@erp/core"; import { InMemoryMapperRegistry, InMemoryPresenterRegistry, SequelizeTransactionManager, } from "@erp/core/api"; - -import { - CreateCustomerInvoiceUseCase, - CustomerInvoiceFullPresenter, - CustomerInvoiceItemsFullPresenter, - CustomerInvoiceReportHTMLPresenter, - CustomerInvoiceReportPDFPresenter, - CustomerInvoiceReportPresenter, - GetCustomerInvoiceUseCase, - ListCustomerInvoicesPresenter, - ListCustomerInvoicesUseCase, - RecipientInvoiceFullPresenter, - ReportCustomerInvoiceUseCase, -} from "../application"; - -import { JsonTaxCatalogProvider, spainTaxCatalogProvider } from "@erp/core"; +import { SendCustomerInvoiceUseCase } from "../application"; import { CustomerInvoiceItemsReportPersenter } from "../application/presenters/queries/customer-invoice-items.report.presenter"; import { CustomerInvoiceService } from "../domain"; import { CustomerInvoiceDomainMapper, CustomerInvoiceListMapper } from "./mappers"; @@ -38,19 +24,13 @@ export type CustomerInvoiceDeps = { taxes: JsonTaxCatalogProvider; }; build: { - list: () => ListCustomerInvoicesUseCase; - get: () => GetCustomerInvoiceUseCase; - create: () => CreateCustomerInvoiceUseCase; - //update: () => UpdateCustomerInvoiceUseCase; - //delete: () => DeleteCustomerInvoiceUseCase; - report: () => ReportCustomerInvoiceUseCase; + send: () => SendCustomerInvoiceUseCase; }; }; -export function buildCustomerInvoiceDependencies(params: ModuleParams): CustomerInvoiceDeps { +export function buildVerifactuDependencies(params: ModuleParams): CustomerInvoiceDeps { const { database } = params; const transactionManager = new SequelizeTransactionManager(database); - const catalogs = { taxes: spainTaxCatalogProvider }; // Mapper Registry const mapperRegistry = new InMemoryMapperRegistry(); diff --git a/modules/verifactu/src/api/infrastructure/express/controllers/send-invoice-verifactu.controller.ts b/modules/verifactu/src/api/infrastructure/express/controllers/send-invoice-verifactu.controller.ts index 2975414a..20cebfd1 100644 --- a/modules/verifactu/src/api/infrastructure/express/controllers/send-invoice-verifactu.controller.ts +++ b/modules/verifactu/src/api/infrastructure/express/controllers/send-invoice-verifactu.controller.ts @@ -1,8 +1,8 @@ import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; -import { SendInvoiceVerifactuUseCase } from "../../../application/use-cases/send"; +import { SendInvoiceUseCase } from "@erp/customer-invoices/api/application"; export class SendInvoiceVerifactuController extends ExpressController { - public constructor(private readonly useCase: SendInvoiceVerifactuUseCase) { + public constructor(private readonly useCase: SendInvoiceUseCase) { super(); // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId")); @@ -15,10 +15,12 @@ export class SendInvoiceVerifactuController extends ExpressController { } const { invoice_id } = this.req.params; - const result = await this.useCase.execute({ invoice_id, companyId }); + console.log("CONTROLLER -----ESTO ES UNA PRUEBA>>>>>>"); + + const result = await this.useCase.execute({ invoice_id }); return result.match( - ({ data, filename }) => this.downloadPDF(data, filename), + (data) => this.ok(data, {}), (err) => this.handleError(err) ); } diff --git a/modules/verifactu/src/api/infrastructure/express/index.ts b/modules/verifactu/src/api/infrastructure/express/index.ts new file mode 100644 index 00000000..c4492b7a --- /dev/null +++ b/modules/verifactu/src/api/infrastructure/express/index.ts @@ -0,0 +1 @@ +export * from "./verifactu.routes"; diff --git a/modules/verifactu/src/api/infrastructure/express/verifactu.routes.ts b/modules/verifactu/src/api/infrastructure/express/verifactu.routes.ts index 6b4ead59..1f9064d3 100644 --- a/modules/verifactu/src/api/infrastructure/express/verifactu.routes.ts +++ b/modules/verifactu/src/api/infrastructure/express/verifactu.routes.ts @@ -2,7 +2,8 @@ import { RequestWithAuth, enforceTenant, enforceUser, mockUser } from "@erp/auth import { ILogger, ModuleParams, validateRequest } from "@erp/core/api"; import { Application, NextFunction, Request, Response, Router } from "express"; import { Sequelize } from "sequelize"; -import { ReportCustomerInvoiceByIdRequestSchema } from "../../../common/dto"; +import { SendCustomerInvoiceByIdRequestSchema } from "../../../common/dto"; +import { buildVerifactuDependencies } from "../dependencies"; import { SendInvoiceVerifactuController } from "./controllers"; export const verifactuRouter = (params: ModuleParams) => { @@ -13,7 +14,7 @@ export const verifactuRouter = (params: ModuleParams) => { logger: ILogger; }; - //const deps = buildCustomerInvoiceDependencies(params); + const deps = buildVerifactuDependencies(params); const router: Router = Router({ mergeParams: true }); @@ -38,7 +39,7 @@ export const verifactuRouter = (params: ModuleParams) => { router.get( "/:invoice_id/sendVerifactu", //checkTabContext, - validateRequest(ReportCustomerInvoiceByIdRequestSchema, "params"), + validateRequest(SendCustomerInvoiceByIdRequestSchema, "params"), (req: Request, res: Response, next: NextFunction) => { const useCase = deps.build.report(); const controller = new SendInvoiceVerifactuController(useCase); diff --git a/modules/verifactu/src/api/infrastructure/index.ts b/modules/verifactu/src/api/infrastructure/index.ts index cfb15542..f25a4198 100644 --- a/modules/verifactu/src/api/infrastructure/index.ts +++ b/modules/verifactu/src/api/infrastructure/index.ts @@ -1,3 +1,3 @@ -export * from "./mappers"; -export * from "./sequelize"; +//export * from "./mappers"; +//export * from "./sequelize"; export * from "./express"; diff --git a/modules/verifactu/src/api/infrastructure/mappers/domain/verifactu-record.mapper.ts b/modules/verifactu/src/api/infrastructure/mappers/domain/verifactu-record.mapper.ts new file mode 100644 index 00000000..e8d553fd --- /dev/null +++ b/modules/verifactu/src/api/infrastructure/mappers/domain/verifactu-record.mapper.ts @@ -0,0 +1,120 @@ +import { ISequelizeDomainMapper, MapperParamsType, SequelizeDomainMapper } from "@erp/core/api"; +import { VerifactuRecordEstado } from "@erp/customer-invoices/api/domain/aggregates/value-objects"; +import { + UniqueID, + ValidationErrorCollection, + ValidationErrorDetail, + extractOrPushError, + maybeFromNullableVO, +} from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; +import { InferCreationAttributes } from "sequelize"; +import { VerifactuRecord, VerifactuRecordProps } from "../../../domain"; +import { VerifactuRecordCreationAttributes, VerifactuRecordModel } from "../../sequelize"; + +export interface IVerifactuRecordDomainMapper + extends ISequelizeDomainMapper< + VerifactuRecordModel, + VerifactuRecordCreationAttributes, + VerifactuRecord + > {} + +export class VerifactuRecordDomainMapper + extends SequelizeDomainMapper< + VerifactuRecordModel, + VerifactuRecordCreationAttributes, + VerifactuRecord + > + implements IVerifactuRecordDomainMapper +{ + constructor(params: MapperParamsType) { + super(); + } + + private mapAttributesToDomain(source: VerifactuRecordModel, params?: MapperParamsType) { + const { errors, index, attributes } = params as { + index: number; + errors: ValidationErrorDetail[]; + attributes: Partial; + }; + + const Id = extractOrPushError(UniqueID.create(source.id), `items[${index}].id`, errors); + + const estado = extractOrPushError( + maybeFromNullableVO(source.estado, (value) => VerifactuRecordEstado.create(value)), + `items[${index}].estado`, + errors + ); + /* + const quantity = extractOrPushError( + maybeFromNullableVO(source.quantity_value, (value) => ItemQuantity.create({ value })), + `items[${index}].discount_percentage`, + errors + ); + + const unitAmount = extractOrPushError( + maybeFromNullableVO(source.unit_amount_value, (value) => + ItemAmount.create({ value, currency_code: attributes.currencyCode!.code }) + ), + `items[${index}].unit_amount`, + errors + ); +*/ + return { + Id, + estado, + }; + } + + public mapToDomain( + source: VerifactuRecordModel, + params?: MapperParamsType + ): Result { + const { errors, index, requireIncludes } = params as { + index: number; + requireIncludes: boolean; + errors: ValidationErrorDetail[]; + attributes: Partial; + }; + + // 1) Valores escalares (atributos generales) + const attributes = this.mapAttributesToDomain(source, params); + + // 2) Comprobar relaciones + /* if (requireIncludes) { + if (!source.taxes) { + errors.push({ + path: `items[${index}].taxes`, + message: "Taxes not included in query (requireIncludes=true)", + }); + } + } +*/ + // 5) Construcción del elemento de dominio + + const createResult = VerifactuRecord.create({ + id: attributes.Id, + //invoiceId: attributes.invoiveID + estado: attributes.estado!, + url: attributes.url!, + qr1: attributes.qr1, + }); + + if (createResult.isFailure) { + return Result.fail( + new ValidationErrorCollection("Verifactu record entity creation failed", [ + { path: `items[${index}]`, message: createResult.error.message }, + ]) + ); + } + + return createResult; + } + + public mapToPersistence( + source: VerifactuRecord, + params?: MapperParamsType + ): Result, Error> { + throw new Error("not implemented"); + } +} diff --git a/modules/verifactu/src/api/infrastructure/sequelize/index.ts b/modules/verifactu/src/api/infrastructure/sequelize/index.ts new file mode 100644 index 00000000..45bed18c --- /dev/null +++ b/modules/verifactu/src/api/infrastructure/sequelize/index.ts @@ -0,0 +1,7 @@ +import verifactuRecordModelInit from "./models/verifactu-record.model"; + +export * from "./models"; +export * from "./verifactu-record.repository"; + +// Array de inicializadores para que registerModels() lo use +export const models = [verifactuRecordModelInit]; diff --git a/modules/verifactu/src/api/infrastructure/sequelize/models/index.ts b/modules/verifactu/src/api/infrastructure/sequelize/models/index.ts new file mode 100644 index 00000000..bac0c055 --- /dev/null +++ b/modules/verifactu/src/api/infrastructure/sequelize/models/index.ts @@ -0,0 +1 @@ +export * from "./verifactu-record.model"; diff --git a/modules/verifactu/src/api/infrastructure/sequelize/models/verifactu-record.model.ts b/modules/verifactu/src/api/infrastructure/sequelize/models/verifactu-record.model.ts new file mode 100644 index 00000000..282d491c --- /dev/null +++ b/modules/verifactu/src/api/infrastructure/sequelize/models/verifactu-record.model.ts @@ -0,0 +1,91 @@ +import { DataTypes, InferAttributes, InferCreationAttributes, Model, Sequelize } from "sequelize"; +/*import { + CustomerInvoiceItemTaxCreationAttributes, + CustomerInvoiceItemTaxModel, +} from "./customer-invoice-item-tax.model"; + */ + +export type VerifactuRecordCreationAttributes = InferCreationAttributes; + +export class VerifactuRecordModel extends Model< + InferAttributes, + InferCreationAttributes +> { + declare id: string; + declare invoice_id: string; + + declare estado: string; + declare url: string; + declare qr1: JSON; + declare qr2: Blob; + + static associate(database: Sequelize) { + const { VerifactuRecordModel } = database.models; + + VerifactuRecordModel.belongsTo(VerifactuRecordModel, { + as: "verifactu-record", + targetKey: "id", + foreignKey: "invoice_id", + onDelete: "CASCADE", + onUpdate: "CASCADE", + }); + } +} + +export default (database: Sequelize) => { + VerifactuRecordModel.init( + { + id: { + type: DataTypes.UUID, + primaryKey: true, + }, + + invoice_id: { + type: DataTypes.UUID, + allowNull: false, + }, + + estado: { + type: new DataTypes.TEXT(), + allowNull: true, + defaultValue: null, + }, + + url: { + type: new DataTypes.TEXT(), + allowNull: false, + defaultValue: null, + }, + + qr1: { + type: new DataTypes.JSON(), + allowNull: false, + defaultValue: null, + }, + + qr2: { + type: new DataTypes.BLOB(), + allowNull: false, + defaultValue: null, + }, + }, + { + sequelize: database, + tableName: "verifactu_records", + + underscored: true, + + indexes: [], + + whereMergeStrategy: "and", // <- cómo tratar el merge de un scope + + defaultScope: { + order: [["position", "ASC"]], + }, + + scopes: {}, + } + ); + + return VerifactuRecordModel; +}; diff --git a/modules/verifactu/src/api/infrastructure/sequelize/verifactu-record.repository.ts b/modules/verifactu/src/api/infrastructure/sequelize/verifactu-record.repository.ts new file mode 100644 index 00000000..4cc9ff5b --- /dev/null +++ b/modules/verifactu/src/api/infrastructure/sequelize/verifactu-record.repository.ts @@ -0,0 +1,251 @@ +import { EntityNotFoundError, SequelizeRepository, translateSequelizeError } from "@erp/core/api"; +import { Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server"; +import { UniqueID } from "@repo/rdx-ddd"; +import { Collection, Result } from "@repo/rdx-utils"; +import { Transaction } from "sequelize"; +import { IVerifactuRecordRepository, VerifactuRecord } from "../../domain"; +import { + // VerifactuRecordListDTO, + IVerifactuRecordDomainMapper, +} from "../mappers"; +import { VerifactuRecordModel } from "./models/verifactu-record.model"; + +export class VerifactuRecordRepository + extends SequelizeRepository + implements IVerifactuRecordRepository +{ + getById(id: UniqueID, transaction?: any): Promise> { + throw new Error("Method not implemented."); + } + // Listado por tenant con criteria saneada + /* async searchInCompany(criteria: any, companyId: string): Promise<{ + rows: InvoiceListRow[]; + total: number; + limit: number; + offset: number; + }> { + const { where, order, limit, offset, attributes } = sanitizeListCriteria(criteria); + + // WHERE con scope de company + const scopedWhere = { ...where, company_id: companyId }; + + const options: FindAndCountOptions = { + where: scopedWhere, + order, + limit, + offset, + attributes, + raw: true, // devolvemos objetos planos -> más rápido + nest: false, + distinct: true // por si en el futuro añadimos includes no duplicar count + }; + + const { rows, count } = await VerifactuRecordModel.findAndCountAll(options); + + return { + rows: rows as unknown as InvoiceListRow[], + total: typeof count === "number" ? count : (count as any[]).length, + limit, + offset, + }; + } */ + + /** + * + * Persiste una nueva factura o actualiza una existente. + * + * @param invoice - El agregado a guardar. + * @param transaction - Transacción activa para la operación. + * @returns Result + */ + async save( + invoice: VerifactuRecord, + transaction: Transaction + ): Promise> { + try { + const mapper: IVerifactuRecordDomainMapper = this._registry.getDomainMapper({ + resource: "customer-invoice", + }); + const mapperData = mapper.mapToPersistence(invoice); + + if (mapperData.isFailure) { + return Result.fail(mapperData.error); + } + + const { data } = mapperData; + + const [instance] = await VerifactuRecordModel.upsert(data, { transaction, returning: true }); + const savedInvoice = mapper.mapToDomain(instance); + return savedInvoice; + } catch (err: unknown) { + return Result.fail(translateSequelizeError(err)); + } + } + + /** + * Comprueba si existe una factura con un `id` dentro de una `company`. + * + * @param companyId - Identificador UUID de la empresa a la que pertenece la factura. + * @param id - Identificador UUID de la factura. + * @param transaction - Transacción activa para la operación. + * @returns Result + */ + async existsByIdInCompany( + companyId: UniqueID, + id: UniqueID, + transaction?: Transaction + ): Promise> { + try { + const count = await VerifactuRecordModel.count({ + where: { id: id.toString(), company_id: companyId.toString() }, + transaction, + }); + return Result.ok(Boolean(count > 0)); + } catch (error: unknown) { + return Result.fail(translateSequelizeError(error)); + } + } + + /** + * + * Busca una factura por su identificador único. + * + * @param companyId - Identificador UUID de la empresa a la que pertenece la factura. + * @param id - UUID de la factura. + * @param transaction - Transacción activa para la operación. + * @returns Result + */ + async getByIdInCompany( + companyId: UniqueID, + id: UniqueID, + transaction: Transaction + ): Promise> { + try { + const mapper: IVerifactuRecordDomainMapper = this._registry.getDomainMapper({ + resource: "customer-invoice", + }); + + const { CustomerModel } = this._database.models; + + const row = await VerifactuRecordModel.findOne({ + where: { id: id.toString(), company_id: companyId.toString() }, + order: [[{ model: VerifactuRecordItemModel, as: "items" }, "position", "ASC"]], + include: [ + { + model: CustomerModel, + as: "current_customer", + required: false, // false => LEFT JOIN + }, + { + model: VerifactuRecordItemModel, + as: "items", + required: false, + include: [ + { + model: VerifactuRecordItemTaxModel, + as: "taxes", + required: false, + }, + ], + }, + { + model: VerifactuRecordTaxModel, + as: "taxes", + required: false, + }, + ], + transaction, + }); + + if (!row) { + return Result.fail(new EntityNotFoundError("VerifactuRecord", "id", id.toString())); + } + + const customer = mapper.mapToDomain(row); + return customer; + } catch (err: unknown) { + return Result.fail(translateSequelizeError(err)); + } + } + + /** + * + * Consulta facturas usando un objeto Criteria (filtros, orden, paginación). + * + * @param companyId - Identificador UUID de la empresa a la que pertenece el cliente. + * @param criteria - Criterios de búsqueda. + * @param transaction - Transacción activa para la operación. + * @returns Result + * + * @see Criteria + */ + public async findByCriteriaInCompany( + companyId: UniqueID, + criteria: Criteria, + transaction: Transaction + ): Promise, Error>> { + try { + const mapper: IVerifactuRecordListMapper = this._registry.getQueryMapper({ + resource: "customer-invoice", + query: "LIST", + }); + const { CustomerModel } = this._database.models; + const converter = new CriteriaToSequelizeConverter(); + const query = converter.convert(criteria); + + query.where = { + ...query.where, + company_id: companyId.toString(), + }; + + query.include = [ + { + model: CustomerModel, + as: "current_customer", + required: false, // false => LEFT JOIN + }, + + { + model: VerifactuRecordTaxModel, + as: "taxes", + required: false, + }, + ]; + + const { rows, count } = await VerifactuRecordModel.findAndCountAll({ + ...query, + transaction, + }); + + return mapper.mapToDTOCollection(rows, count); + } catch (err: unknown) { + return Result.fail(translateSequelizeError(err)); + } + } + + /** + * + * Elimina o marca como eliminada una factura. + * + * @param companyId - Identificador UUID de la empresa a la que pertenece el cliente. + * @param id - UUID de la factura a eliminar. + * @param transaction - Transacción activa para la operación. + * @returns Result + */ + async deleteByIdInCompany( + companyId: UniqueID, + id: UniqueID, + transaction: any + ): Promise> { + try { + const deleted = await VerifactuRecordModel.destroy({ + where: { id: id.toString(), company_id: companyId.toString() }, + transaction, + }); + + return Result.ok(); + } catch (err: unknown) { + return Result.fail(translateSequelizeError(err)); + } + } +} diff --git a/modules/verifactu/src/common/dto/index.ts b/modules/verifactu/src/common/dto/index.ts new file mode 100644 index 00000000..204ff2a0 --- /dev/null +++ b/modules/verifactu/src/common/dto/index.ts @@ -0,0 +1,2 @@ +export * from "./request"; +//export * from "./response"; diff --git a/modules/verifactu/src/common/dto/request/index.ts b/modules/verifactu/src/common/dto/request/index.ts new file mode 100644 index 00000000..b8c00e70 --- /dev/null +++ b/modules/verifactu/src/common/dto/request/index.ts @@ -0,0 +1 @@ +export * from "./send-customer-invoice-by-id.request.dto"; diff --git a/modules/verifactu/src/common/dto/request/send-customer-invoice-by-id.request.dto.ts b/modules/verifactu/src/common/dto/request/send-customer-invoice-by-id.request.dto.ts new file mode 100644 index 00000000..5e402160 --- /dev/null +++ b/modules/verifactu/src/common/dto/request/send-customer-invoice-by-id.request.dto.ts @@ -0,0 +1,9 @@ +import * as z from "zod/v4"; + +export const SendCustomerInvoiceByIdRequestSchema = z.object({ + invoice_id: z.string(), +}); + +export type SendCustomerInvoiceByIdRequestDTO = z.infer< + typeof SendCustomerInvoiceByIdRequestSchema +>; diff --git a/modules/verifactu/src/common/index.ts b/modules/verifactu/src/common/index.ts new file mode 100644 index 00000000..0392b1b4 --- /dev/null +++ b/modules/verifactu/src/common/index.ts @@ -0,0 +1 @@ +export * from "./dto"; diff --git a/packages/rdx-ui/package.json b/packages/rdx-ui/package.json index 194e485e..f3de3530 100644 --- a/packages/rdx-ui/package.json +++ b/packages/rdx-ui/package.json @@ -7,15 +7,13 @@ "ui:lint": "biome lint --fix" }, "exports": { + "./helpers": "./src/helpers/index.ts", "./globals.css": "./src/styles/globals.css", "./postcss.config": "./postcss.config.mjs", "./components": "./src/components/index.tsx", "./components/*": "./src/components/*.tsx", "./locales/*": "./src/locales/*", - "./hooks/*": [ - "./src/hooks/*.tsx", - "./src/hooks/*.ts" - ] + "./hooks/*": ["./src/hooks/*.tsx", "./src/hooks/*.ts"] }, "peerDependencies": { "date-fns": "^4.1.0", diff --git a/packages/rdx-ui/src/components/form/fieldset.tsx b/packages/rdx-ui/src/components/form/fieldset.tsx new file mode 100644 index 00000000..42378a80 --- /dev/null +++ b/packages/rdx-ui/src/components/form/fieldset.tsx @@ -0,0 +1,46 @@ +import { cn } from "@repo/shadcn-ui/lib/utils"; +import * as React from "react"; + +export const Fieldset = ({ className, children, ...props }: React.ComponentProps<"fieldset">) => ( +
*+[data-slot=control]]:mt-6", className)} + {...props} + > + {children} +
+); + +export const FieldGroup = ({ className, children, ...props }: React.ComponentProps<"div">) => ( +
+ {children} +
+); + +export const Field = ({ className, children, ...props }: React.ComponentProps<"div">) => ( +
[data-slot=label]+[data-slot=control]]:mt-3 [&>[data-slot=label]+[data-slot=description]]:mt-1 [&>[data-slot=description]+[data-slot=control]]:mt-3 [&>[data-slot=control]+[data-slot=description]]:mt-3 [&>[data-slot=control]+[data-slot=error]]:mt-3 *:data-[slot=label]:font-medium", + className + )} + {...props} + > + {children} +
+); + +export const Legend = ({ className, children, ...props }: React.ComponentProps<"div">) => ( +
+ {children} +
+); + +export const Description = ({ className, children, ...props }: React.ComponentProps<"p">) => ( +

+ {children} +

+); diff --git a/packages/rdx-ui/src/components/form/index.tsx b/packages/rdx-ui/src/components/form/index.tsx index 265784d5..2a0ce589 100644 --- a/packages/rdx-ui/src/components/form/index.tsx +++ b/packages/rdx-ui/src/components/form/index.tsx @@ -1,5 +1,6 @@ export * from "./DatePickerField.tsx"; export * from "./DatePickerInputField.tsx"; +export * from "./fieldset.tsx"; export * from "./form-content.tsx"; export * from "./multi-select-field.tsx"; export * from "./SelectField.tsx"; diff --git a/packages/rdx-ui/src/helpers/index.ts b/packages/rdx-ui/src/helpers/index.ts new file mode 100644 index 00000000..a1b7b909 --- /dev/null +++ b/packages/rdx-ui/src/helpers/index.ts @@ -0,0 +1 @@ +export * from "./toast-utils.ts"; diff --git a/packages/rdx-ui/src/helpers/toast-utils.ts b/packages/rdx-ui/src/helpers/toast-utils.ts new file mode 100644 index 00000000..dc573f04 --- /dev/null +++ b/packages/rdx-ui/src/helpers/toast-utils.ts @@ -0,0 +1,37 @@ +import { toast } from "@repo/shadcn-ui/components"; + +/** + * Muestra un toast de aviso + */ +export function showInfoToast(title: string, description?: string) { + toast.info(title, { + description, + }); +} + +/** + * Muestra un toast de aviso + */ +export function showWarningToast(title: string, description?: string) { + toast.warning(title, { + description, + }); +} + +/** + * Muestra un toast de éxito + */ +export function showSuccessToast(title: string, description?: string) { + toast.success(title, { + description, + }); +} + +/** + * Muestra un toast de error + */ +export function showErrorToast(title: string, description?: string) { + toast.error(title, { + description, + }); +} diff --git a/packages/rdx-ui/src/index.ts b/packages/rdx-ui/src/index.ts index 5d69cd32..3adb68f3 100644 --- a/packages/rdx-ui/src/index.ts +++ b/packages/rdx-ui/src/index.ts @@ -1,4 +1,4 @@ -"use client"; - export const PACKAGE_NAME = "rdx-ui"; + export * from "./components/index.tsx"; +export * from "./helpers/index.ts"; diff --git a/packages/shadcn-ui/src/lib/utils.ts b/packages/shadcn-ui/src/lib/utils.ts index c5f1413e..365058ce 100644 --- a/packages/shadcn-ui/src/lib/utils.ts +++ b/packages/shadcn-ui/src/lib/utils.ts @@ -1,25 +1,6 @@ import { type ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; -import { toast } from "../components/sonner.tsx"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } - -/** - * Muestra un toast de éxito - */ -export function showSuccessToast(title: string, description?: string) { - toast.success(title, { - description, - }); -} - -/** - * Muestra un toast de error - */ -export function showErrorToast(title: string, description?: string) { - toast.error(title, { - description, - }); -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aeeea8ad..e59f0b8e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -665,6 +665,9 @@ importers: '@repo/rdx-ddd': specifier: workspace:* version: link:../../packages/rdx-ddd + '@repo/rdx-logger': + specifier: workspace:* + version: link:../../packages/rdx-logger '@repo/rdx-utils': specifier: workspace:* version: link:../../packages/rdx-utils