Importación desde FactuGES
This commit is contained in:
parent
5e4ad04314
commit
9a45e7ee9a
@ -35,8 +35,9 @@
|
||||
"dependencies": {
|
||||
"@erp/auth": "workspace:*",
|
||||
"@erp/core": "workspace:*",
|
||||
"@erp/customer-invoices": "workspace:*",
|
||||
"@erp/customers": "workspace:*",
|
||||
"@erp/customer-invoices": "workspace:*",
|
||||
"@erp/factuges": "workspace:*",
|
||||
"@repo/rdx-logger": "workspace:*",
|
||||
"@repo/rdx-utils": "workspace:*",
|
||||
"bcrypt": "^5.1.1",
|
||||
|
||||
@ -132,8 +132,11 @@ async function setupModule(name: string, params: ModuleParams, stack: string[])
|
||||
// 5) services (namespaced)
|
||||
if (pkgApi?.services) {
|
||||
await withPhase(name, "registerServices", async () => {
|
||||
validateModuleServices(name, pkgApi.services);
|
||||
|
||||
for (const [serviceKey, serviceApi] of Object.entries(pkgApi.services!)) {
|
||||
registerService(`${name}:${serviceKey}`, serviceApi);
|
||||
const fullName = buildServiceName(name, serviceKey);
|
||||
registerService(fullName, serviceApi);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -187,6 +190,24 @@ function trackDependencyUse(requester: string, dep: string) {
|
||||
set.add(dep);
|
||||
}
|
||||
|
||||
function buildServiceName(moduleName: string, serviceKey: string): string {
|
||||
return `${moduleName}:${serviceKey}`;
|
||||
}
|
||||
|
||||
function validateModuleServices(moduleName: string, services: Record<string, unknown>) {
|
||||
for (const [serviceKey, serviceApi] of Object.entries(services)) {
|
||||
if (!serviceKey || typeof serviceKey !== "string") {
|
||||
throw new Error(`Invalid service key from module "${moduleName}"`);
|
||||
}
|
||||
|
||||
const fullName = `${moduleName}:${serviceKey}`;
|
||||
|
||||
if (serviceApi === undefined) {
|
||||
throw new Error(`Service "${fullName}" is undefined`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateModuleDependencies() {
|
||||
for (const [moduleName, pkg] of registeredModules.entries()) {
|
||||
const declared = new Set(pkg.dependencies ?? []);
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import customerInvoicesAPIModule from "@erp/customer-invoices/api";
|
||||
|
||||
//import verifactuAPIModule from "@erp/verifactu/api";
|
||||
|
||||
import customersAPIModule from "@erp/customers/api";
|
||||
import factuGESAPIModule from "@erp/factuges/api";
|
||||
|
||||
import { registerModule } from "./lib";
|
||||
|
||||
@ -10,5 +8,5 @@ export const registerModules = () => {
|
||||
//registerModule(authAPIModule);
|
||||
registerModule(customersAPIModule);
|
||||
registerModule(customerInvoicesAPIModule);
|
||||
//registerModule(verifactuAPIModule);
|
||||
registerModule(factuGESAPIModule);
|
||||
};
|
||||
|
||||
@ -140,7 +140,7 @@
|
||||
"noControlCharactersInRegex": "error",
|
||||
"noDoubleEquals": "error",
|
||||
"noDuplicateCase": "error",
|
||||
"noEmptyBlockStatements": "error",
|
||||
"noEmptyBlockStatements": "off",
|
||||
"noFallthroughSwitchClause": "error",
|
||||
"noFunctionAssign": "error",
|
||||
"noGlobalAssign": "error",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -114,7 +114,7 @@ export class ApiErrorMapper {
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────────
|
||||
// Reglas por defecto (prioridad alta a más específicas)
|
||||
// Reglas por defecto: a prioridad más alta en valor, error más específico.
|
||||
// ────────────────────────────────────────────────────────────────────────────────
|
||||
const defaultRules: ReadonlyArray<ErrorToApiRule> = [
|
||||
// 1) Validación múltiple (colección)
|
||||
@ -165,7 +165,7 @@ const defaultRules: ReadonlyArray<ErrorToApiRule> = [
|
||||
|
||||
// 5.5) Errores de FastReport inesperados
|
||||
{
|
||||
priority: 55,
|
||||
priority: 56,
|
||||
matches: (e) => isDocumentGenerationError(e),
|
||||
build: (e) => {
|
||||
const error = e as DocumentGenerationError;
|
||||
@ -178,6 +178,7 @@ const defaultRules: ReadonlyArray<ErrorToApiRule> = [
|
||||
return new InternalApiError(cause.message, title);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
priority: 55,
|
||||
matches: (e) => isFastReportError(e),
|
||||
@ -198,10 +199,11 @@ const defaultRules: ReadonlyArray<ErrorToApiRule> = [
|
||||
|
||||
// 7) Autenticación/autorización por nombre (si no tienes clases dedicadas)
|
||||
{
|
||||
priority: 40,
|
||||
priority: 45,
|
||||
matches: (e): e is Error => e instanceof Error && e.name === "UnauthorizedError",
|
||||
build: (e) => new UnauthorizedApiError((e as Error).message || "Unauthorized"),
|
||||
},
|
||||
|
||||
{
|
||||
priority: 40,
|
||||
matches: (e): e is Error => e instanceof Error && e.name === "ForbiddenError",
|
||||
@ -223,10 +225,22 @@ function defaultFallback(e: unknown): ApiError {
|
||||
return e; // ya es un ApiError
|
||||
}
|
||||
|
||||
const message = typeof (e as any)?.message === "string" ? (e as any).message : "";
|
||||
const detail = typeof (e as any)?.detail === "string" ? (e as any).detail : "";
|
||||
const message =
|
||||
typeof (e as Partial<{ message: string }>).message === "string"
|
||||
? String((e as Partial<{ message: string }>).message)
|
||||
: "";
|
||||
|
||||
return new InternalApiError(`${message} ${detail}`);
|
||||
const detail =
|
||||
typeof (e as Partial<{ detail: string }>).detail === "string"
|
||||
? String((e as Partial<{ detail: string }>).detail)
|
||||
: "";
|
||||
|
||||
const cause =
|
||||
typeof (e as Partial<{ cause: string }>).cause === "string"
|
||||
? String((e as Partial<{ cause: string }>).cause)
|
||||
: "";
|
||||
|
||||
return new InternalApiError(`${message} ${detail} ${cause}`);
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@ -22,9 +22,7 @@ export type ModuleSetupResult = {
|
||||
internal?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type SetupParams = ModuleParams & {
|
||||
registerService: (name: string, api: unknown) => void;
|
||||
};
|
||||
export type SetupParams = ModuleParams;
|
||||
|
||||
export type StartParams = ModuleParams & {
|
||||
/**
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
export * from "./error-alert";
|
||||
export * from "./form";
|
||||
export * from "./not-found-card";
|
||||
export * from "./page-header";
|
||||
|
||||
@ -5,6 +5,7 @@ export * from "./use-pagination";
|
||||
export * from "./use-percentage";
|
||||
export * from "./use-quantity";
|
||||
export * from "./use-query-key";
|
||||
export * from "./use-rhf-error-focus";
|
||||
export * from "./use-toggle";
|
||||
export * from "./use-unsaved-changes-notifier";
|
||||
export * from "./use-url-param-id";
|
||||
|
||||
11
modules/core/src/web/hooks/use-rhf-error-focus.ts
Normal file
11
modules/core/src/web/hooks/use-rhf-error-focus.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import type { FieldErrors, FieldValues } from "react-hook-form";
|
||||
|
||||
export function useRHFErrorFocus<T extends FieldValues>() {
|
||||
return (errors: FieldErrors<T>) => {
|
||||
const firstKey = Object.keys(errors)[0] as keyof T | undefined;
|
||||
|
||||
if (firstKey) {
|
||||
document.querySelector<HTMLElement>(`[name="${String(firstKey)}"]`)?.focus();
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -12,6 +12,7 @@
|
||||
".": "./src/common/index.ts",
|
||||
"./common": "./src/common/index.ts",
|
||||
"./api": "./src/api/index.ts",
|
||||
"./api/domain": "./src/api/domain/index.ts",
|
||||
"./client": "./src/web/manifest.ts",
|
||||
"./globals.css": "./src/web/globals.css"
|
||||
},
|
||||
|
||||
@ -10,7 +10,12 @@ import {
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { CreateCustomerInvoiceRequestDTO } from "../../../common";
|
||||
import { type IProformaProps, InvoiceNumber, InvoiceSerie, InvoiceStatus } from "../../domain";
|
||||
import {
|
||||
type IProformaCreateProps,
|
||||
InvoiceNumber,
|
||||
InvoiceSerie,
|
||||
InvoiceStatus,
|
||||
} from "../../domain";
|
||||
|
||||
import { mapDTOToCustomerInvoiceItemsProps } from "./map-dto-to-customer-invoice-items-props";
|
||||
|
||||
@ -66,12 +71,12 @@ export function mapDTOToCustomerInvoiceProps(dto: CreateCustomerInvoiceRequestDT
|
||||
return Result.fail(new ValidationErrorCollection("Invoice dto mapping failed", errors));
|
||||
}
|
||||
|
||||
const invoiceProps: IProformaProps = {
|
||||
const invoiceProps: IProformaCreateProps = {
|
||||
invoiceNumber: invoiceNumber!,
|
||||
series: invoiceSeries!,
|
||||
invoiceDate: invoiceDate!,
|
||||
operationDate: operationDate!,
|
||||
status: InvoiceStatus.createDraft(),
|
||||
status: InvoiceStatus.fromDraft(),
|
||||
currencyCode: currencyCode!,
|
||||
};
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import type { Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { IProformaProps, Proforma } from "../../../domain";
|
||||
import type { IProformaCreateProps, Proforma } from "../../../domain";
|
||||
|
||||
export interface IProformaFactory {
|
||||
/**
|
||||
@ -11,7 +11,7 @@ export interface IProformaFactory {
|
||||
*/
|
||||
createProforma(
|
||||
companyId: UniqueID,
|
||||
props: Omit<IProformaProps, "companyId">,
|
||||
props: Omit<IProformaCreateProps, "companyId">,
|
||||
proformaId?: UniqueID
|
||||
): Result<Proforma, Error>;
|
||||
}
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import type { Result } from "@repo/rdx-utils";
|
||||
|
||||
import { type IProformaProps, Proforma } from "../../../domain";
|
||||
import { type IProformaCreateProps, Proforma } from "../../../domain";
|
||||
|
||||
import type { IProformaFactory } from "./proforma-factory.interface";
|
||||
|
||||
export class ProformaFactory implements IProformaFactory {
|
||||
createProforma(
|
||||
companyId: UniqueID,
|
||||
props: Omit<IProformaProps, "companyId">,
|
||||
props: Omit<IProformaCreateProps, "companyId">,
|
||||
proformaId?: UniqueID
|
||||
): Result<Proforma, Error> {
|
||||
return Proforma.create({ ...props, companyId }, proformaId);
|
||||
|
||||
@ -17,8 +17,8 @@ import { Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { CreateProformaItemRequestDTO, CreateProformaRequestDTO } from "../../../../common";
|
||||
import {
|
||||
type IProformaCreateProps,
|
||||
type IProformaItemProps,
|
||||
type IProformaProps,
|
||||
InvoiceNumber,
|
||||
InvoicePaymentMethod,
|
||||
type InvoiceRecipient,
|
||||
@ -51,7 +51,7 @@ export interface ICreateProformaInputMapper {
|
||||
map(
|
||||
dto: CreateProformaRequestDTO,
|
||||
params: { companyId: UniqueID }
|
||||
): Result<{ id: UniqueID; props: IProformaProps }>;
|
||||
): Result<{ id: UniqueID; props: IProformaCreateProps }>;
|
||||
}
|
||||
|
||||
export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/ {
|
||||
@ -64,12 +64,12 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/
|
||||
public map(
|
||||
dto: CreateProformaRequestDTO,
|
||||
params: { companyId: UniqueID }
|
||||
): Result<{ id: UniqueID; props: IProformaProps }> {
|
||||
): Result<{ id: UniqueID; props: IProformaCreateProps }> {
|
||||
const errors: ValidationErrorDetail[] = [];
|
||||
const { companyId } = params;
|
||||
|
||||
try {
|
||||
const defaultStatus = InvoiceStatus.createDraft();
|
||||
const defaultStatus = InvoiceStatus.fromDraft();
|
||||
|
||||
const proformaId = extractOrPushError(UniqueID.create(dto.id), "id", errors);
|
||||
|
||||
@ -159,13 +159,9 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/
|
||||
errors,
|
||||
});
|
||||
|
||||
if (errors.length > 0) {
|
||||
return Result.fail(
|
||||
new ValidationErrorCollection("Customer invoice props mapping failed", errors)
|
||||
);
|
||||
}
|
||||
this.throwIfValidationErrors(errors);
|
||||
|
||||
const props: IProformaProps = {
|
||||
const props: IProformaCreateProps = {
|
||||
companyId,
|
||||
status: defaultStatus,
|
||||
|
||||
@ -200,6 +196,12 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/
|
||||
}
|
||||
}
|
||||
|
||||
private throwIfValidationErrors(errors: ValidationErrorDetail[]): void {
|
||||
if (errors.length > 0) {
|
||||
throw new ValidationErrorCollection("Customer proforma props mapping failed", errors);
|
||||
}
|
||||
}
|
||||
|
||||
private mapItemsProps(
|
||||
dto: CreateProformaRequestDTO,
|
||||
params: {
|
||||
@ -241,6 +243,8 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/
|
||||
errors: params.errors,
|
||||
});
|
||||
|
||||
this.throwIfValidationErrors(params.errors);
|
||||
|
||||
itemsProps.push({
|
||||
globalDiscountPercentage: params.globalDiscountPercentage,
|
||||
languageCode: params.languageCode,
|
||||
@ -321,6 +325,8 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/
|
||||
}
|
||||
});
|
||||
|
||||
this.throwIfValidationErrors(errors);
|
||||
|
||||
return taxesProps;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,19 +2,21 @@ import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import type { Transaction } from "sequelize";
|
||||
|
||||
import type { IProformaProps, Proforma } from "../../../domain";
|
||||
import type { IProformaCreateProps, Proforma } from "../../../domain";
|
||||
import type { IProformaFactory } from "../factories";
|
||||
import type { IProformaRepository } from "../repositories";
|
||||
|
||||
import type { IProformaNumberGenerator } from "./proforma-number-generator.interface";
|
||||
|
||||
export interface IProformaCreatorParams {
|
||||
companyId: UniqueID;
|
||||
id: UniqueID;
|
||||
props: Omit<IProformaCreateProps, "invoiceNumber">;
|
||||
transaction: Transaction;
|
||||
}
|
||||
|
||||
export interface IProformaCreator {
|
||||
create(params: {
|
||||
companyId: UniqueID;
|
||||
id: UniqueID;
|
||||
props: IProformaProps;
|
||||
transaction: Transaction;
|
||||
}): Promise<Result<Proforma, Error>>;
|
||||
create(params: IProformaCreatorParams): Promise<Result<Proforma, Error>>;
|
||||
}
|
||||
|
||||
type ProformaCreatorDeps = {
|
||||
@ -37,7 +39,7 @@ export class ProformaCreator implements IProformaCreator {
|
||||
async create(params: {
|
||||
companyId: UniqueID;
|
||||
id: UniqueID;
|
||||
props: IProformaProps;
|
||||
props: IProformaCreateProps;
|
||||
transaction: Transaction;
|
||||
}): Promise<Result<Proforma, Error>> {
|
||||
const { companyId, id, props, transaction } = params;
|
||||
|
||||
@ -39,34 +39,34 @@ export class InvoiceStatus extends ValueObject<IInvoiceStatusProps> {
|
||||
|
||||
return Result.ok(
|
||||
value === "rejected"
|
||||
? InvoiceStatus.createRejected()
|
||||
? InvoiceStatus.fromRejected()
|
||||
: value === "sent"
|
||||
? InvoiceStatus.createSent()
|
||||
? InvoiceStatus.fromSent()
|
||||
: value === "issued"
|
||||
? InvoiceStatus.createIssued()
|
||||
? InvoiceStatus.fromIssued()
|
||||
: value === "approved"
|
||||
? InvoiceStatus.createApproved()
|
||||
: InvoiceStatus.createDraft()
|
||||
? InvoiceStatus.fromApproved()
|
||||
: InvoiceStatus.fromDraft()
|
||||
);
|
||||
}
|
||||
|
||||
public static createDraft(): InvoiceStatus {
|
||||
public static fromDraft(): InvoiceStatus {
|
||||
return new InvoiceStatus({ value: INVOICE_STATUS.DRAFT });
|
||||
}
|
||||
|
||||
public static createIssued(): InvoiceStatus {
|
||||
public static fromIssued(): InvoiceStatus {
|
||||
return new InvoiceStatus({ value: INVOICE_STATUS.ISSUED });
|
||||
}
|
||||
|
||||
public static createSent(): InvoiceStatus {
|
||||
public static fromSent(): InvoiceStatus {
|
||||
return new InvoiceStatus({ value: INVOICE_STATUS.SENT });
|
||||
}
|
||||
|
||||
public static createApproved(): InvoiceStatus {
|
||||
public static fromApproved(): InvoiceStatus {
|
||||
return new InvoiceStatus({ value: INVOICE_STATUS.APPROVED });
|
||||
}
|
||||
|
||||
public static createRejected(): InvoiceStatus {
|
||||
public static fromRejected(): InvoiceStatus {
|
||||
return new InvoiceStatus({ value: INVOICE_STATUS.REJECTED });
|
||||
}
|
||||
|
||||
|
||||
@ -30,7 +30,7 @@ import { ProformaItemMismatch } from "../errors";
|
||||
import { type IProformaTaxTotals, ProformaTaxesCalculator } from "../services";
|
||||
import { ProformaItemTaxes } from "../value-objects";
|
||||
|
||||
export interface IProformaProps {
|
||||
export interface IProformaCreateProps {
|
||||
companyId: UniqueID;
|
||||
status: InvoiceStatus;
|
||||
|
||||
@ -100,16 +100,15 @@ export interface IProforma {
|
||||
totals(): IProformaTotals;
|
||||
}
|
||||
|
||||
export type ProformaPatchProps = Partial<Omit<IProformaProps, "companyId" | "items">> & {
|
||||
export type ProformaPatchProps = Partial<Omit<IProformaCreateProps, "companyId" | "items">> & {
|
||||
//items?: ProformaItems;
|
||||
};
|
||||
|
||||
type CreateProformaProps = IProformaProps;
|
||||
type InternalProformaProps = Omit<IProformaProps, "items">;
|
||||
type InternalProformaProps = Omit<IProformaCreateProps, "items">;
|
||||
|
||||
export class Proforma extends AggregateRoot<InternalProformaProps> implements IProforma {
|
||||
// Creación funcional
|
||||
static create(props: CreateProformaProps, id?: UniqueID): Result<Proforma, Error> {
|
||||
static create(props: IProformaCreateProps, id?: UniqueID): Result<Proforma, Error> {
|
||||
const { items, ...internalProps } = props;
|
||||
const proforma = new Proforma(internalProps, id);
|
||||
|
||||
@ -221,12 +220,12 @@ export class Proforma extends AggregateRoot<InternalProformaProps> implements IP
|
||||
|
||||
// Mutabilidad
|
||||
public update(
|
||||
partialProforma: Partial<Omit<IProformaProps, "companyId">>
|
||||
partialProforma: Partial<Omit<IProformaCreateProps, "companyId">>
|
||||
): Result<Proforma, Error> {
|
||||
const updatedProps = {
|
||||
...this.props,
|
||||
...partialProforma,
|
||||
} as IProformaProps;
|
||||
} as IProformaCreateProps;
|
||||
|
||||
return Proforma.create(updatedProps, this.id);
|
||||
}
|
||||
|
||||
@ -67,7 +67,7 @@ export class IssueCustomerInvoiceDomainService {
|
||||
...proformaProps,
|
||||
isProforma: false,
|
||||
proformaId: Maybe.some(proforma.id),
|
||||
status: InvoiceStatus.createIssued(),
|
||||
status: InvoiceStatus.fromIssued(),
|
||||
invoiceNumber: issueNumber,
|
||||
invoiceDate: issueDate,
|
||||
description: proformaProps.description.isNone() ? Maybe.some(".") : proformaProps.description,
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import type { IModuleServer } from "@erp/core/api";
|
||||
|
||||
import {
|
||||
type IssuedInvoicePublicServices,
|
||||
type IssuedInvoicesInternalDeps,
|
||||
type ProformaPublicServices,
|
||||
type ProformasInternalDeps,
|
||||
buildIssuedInvoiceServices,
|
||||
buildIssuedInvoicesDependencies,
|
||||
@ -12,6 +14,8 @@ import {
|
||||
proformasRouter,
|
||||
} from "./infrastructure";
|
||||
|
||||
export type { IssuedInvoicePublicServices, ProformaPublicServices };
|
||||
|
||||
export const customerInvoicesAPIModule: IModuleServer = {
|
||||
name: "customer-invoices",
|
||||
version: "1.0.0",
|
||||
@ -28,12 +32,18 @@ export const customerInvoicesAPIModule: IModuleServer = {
|
||||
const { env: ENV, app, database, baseRoutePath: API_BASE_PATH, logger } = params;
|
||||
|
||||
// 1) Dominio interno
|
||||
const issuedInvoicesInternalDeps = buildIssuedInvoicesDependencies(params);
|
||||
const proformasInternalDeps = buildProformasDependencies(params);
|
||||
const issuedInvoicesInternal = buildIssuedInvoicesDependencies(params);
|
||||
const proformasInternal = buildProformasDependencies(params);
|
||||
|
||||
// 2) Servicios públicos (Application Services)
|
||||
const issuedInvoicesServices = buildIssuedInvoiceServices(issuedInvoicesInternalDeps);
|
||||
const proformasServices = buildProformaServices(proformasInternalDeps);
|
||||
const issuedInvoicesServices: IssuedInvoicePublicServices = buildIssuedInvoiceServices(
|
||||
params,
|
||||
issuedInvoicesInternal
|
||||
);
|
||||
const proformasServices: ProformaPublicServices = buildProformaServices(
|
||||
params,
|
||||
proformasInternal
|
||||
);
|
||||
|
||||
logger.info("🚀 CustomerInvoices module dependencies registered", { label: this.name });
|
||||
|
||||
@ -43,14 +53,14 @@ export const customerInvoicesAPIModule: IModuleServer = {
|
||||
|
||||
// Servicios expuestos a otros módulos
|
||||
services: {
|
||||
issuedInvoices: issuedInvoicesServices,
|
||||
proformas: proformasServices,
|
||||
issuedInvoices: issuedInvoicesServices, // 'customer-invoices:issuedInvoices'
|
||||
proformas: proformasServices, // 'customer-invoices:proformas'
|
||||
},
|
||||
|
||||
// Implementación privada del módulo
|
||||
internal: {
|
||||
issuedInvoices: issuedInvoicesInternalDeps,
|
||||
proformas: proformasInternalDeps,
|
||||
issuedInvoices: issuedInvoicesInternal,
|
||||
proformas: proformasInternal,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
@ -16,7 +16,7 @@ import {
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import {
|
||||
type IProformaProps,
|
||||
type IProformaCreateProps,
|
||||
IssuedInvoiceItem,
|
||||
type IssuedInvoiceItemProps,
|
||||
ItemAmount,
|
||||
@ -68,7 +68,7 @@ export class CustomerInvoiceItemDomainMapper
|
||||
const { errors, index, attributes } = params as {
|
||||
index: number;
|
||||
errors: ValidationErrorDetail[];
|
||||
attributes: Partial<IProformaProps>;
|
||||
attributes: Partial<IProformaCreateProps>;
|
||||
};
|
||||
|
||||
const itemId = extractOrPushError(
|
||||
@ -163,7 +163,7 @@ export class CustomerInvoiceItemDomainMapper
|
||||
const { errors, index } = params as {
|
||||
index: number;
|
||||
errors: ValidationErrorDetail[];
|
||||
attributes: Partial<IProformaProps>;
|
||||
attributes: Partial<IProformaCreateProps>;
|
||||
};
|
||||
|
||||
// 1) Valores escalares (atributos generales)
|
||||
|
||||
@ -20,7 +20,7 @@ import { Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils";
|
||||
|
||||
import {
|
||||
CustomerInvoiceItems,
|
||||
type IProformaProps,
|
||||
type IProformaCreateProps,
|
||||
InvoiceNumber,
|
||||
InvoicePaymentMethod,
|
||||
InvoiceSerie,
|
||||
@ -249,7 +249,7 @@ export class CustomerInvoiceDomainMapper
|
||||
items: itemsResults.data.getAll(),
|
||||
});
|
||||
|
||||
const invoiceProps: IProformaProps = {
|
||||
const invoiceProps: IProformaCreateProps = {
|
||||
companyId: attributes.companyId!,
|
||||
|
||||
isProforma: attributes.isProforma,
|
||||
|
||||
@ -15,7 +15,11 @@ import {
|
||||
} from "@repo/rdx-ddd";
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import { type IProformaProps, InvoiceRecipient, type Proforma } from "../../../../../../domain";
|
||||
import {
|
||||
type IProformaCreateProps,
|
||||
InvoiceRecipient,
|
||||
type Proforma,
|
||||
} from "../../../../../../domain";
|
||||
import type { CustomerInvoiceModel } from "../../../../sequelize";
|
||||
|
||||
export class InvoiceRecipientDomainMapper {
|
||||
@ -30,7 +34,7 @@ export class InvoiceRecipientDomainMapper {
|
||||
|
||||
const { errors, attributes } = params as {
|
||||
errors: ValidationErrorDetail[];
|
||||
attributes: Partial<IProformaProps>;
|
||||
attributes: Partial<IProformaCreateProps>;
|
||||
};
|
||||
|
||||
const { isProforma } = attributes;
|
||||
|
||||
@ -12,7 +12,7 @@ import {
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import {
|
||||
type IProformaProps,
|
||||
type IProformaCreateProps,
|
||||
type Proforma,
|
||||
VerifactuRecord,
|
||||
VerifactuRecordEstado,
|
||||
@ -43,7 +43,7 @@ export class CustomerInvoiceVerifactuDomainMapper
|
||||
): Result<Maybe<VerifactuRecord>, Error> {
|
||||
const { errors, attributes } = params as {
|
||||
errors: ValidationErrorDetail[];
|
||||
attributes: Partial<IProformaProps>;
|
||||
attributes: Partial<IProformaCreateProps>;
|
||||
};
|
||||
|
||||
if (!source) {
|
||||
|
||||
@ -1,66 +0,0 @@
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import { type Maybe, Result } from "@repo/rdx-utils";
|
||||
import { type Transaction, type WhereOptions, literal } from "sequelize";
|
||||
|
||||
import { InvoiceNumber, type InvoiceSerie } from "../../../../domain";
|
||||
|
||||
import { CustomerInvoiceModel } from "./models";
|
||||
|
||||
/**
|
||||
* Generador de números de factura
|
||||
*/
|
||||
export class SequelizeInvoiceNumberGenerator implements ICustomerInvoiceNumberGenerator {
|
||||
public async nextForCompany(
|
||||
companyId: UniqueID,
|
||||
series: Maybe<InvoiceSerie>,
|
||||
transaction: Transaction
|
||||
): Promise<Result<InvoiceNumber, Error>> {
|
||||
const where: WhereOptions = {
|
||||
company_id: companyId.toString(),
|
||||
is_proforma: false,
|
||||
};
|
||||
|
||||
series.match(
|
||||
(serieVO) => {
|
||||
where.series = serieVO.toString();
|
||||
},
|
||||
() => {
|
||||
where.series = null;
|
||||
}
|
||||
);
|
||||
|
||||
try {
|
||||
const lastInvoice = await CustomerInvoiceModel.findOne({
|
||||
attributes: ["invoice_number"],
|
||||
where,
|
||||
// Orden numérico real: CAST(... AS UNSIGNED)
|
||||
order: [literal("CAST(invoice_number AS UNSIGNED) DESC")],
|
||||
transaction,
|
||||
raw: true,
|
||||
// Bloqueo opcional para evitar carreras si estás dentro de una TX
|
||||
lock: transaction.LOCK.UPDATE, // requiere InnoDB y TX abierta
|
||||
});
|
||||
|
||||
let nextValue = "001"; // valor inicial por defecto
|
||||
|
||||
if (lastInvoice) {
|
||||
const current = Number(lastInvoice.invoice_number);
|
||||
const next = Number.isFinite(current) && current > 0 ? current + 1 : 1;
|
||||
nextValue = String(next).padStart(3, "0");
|
||||
}
|
||||
|
||||
const numberResult = InvoiceNumber.create(nextValue);
|
||||
if (numberResult.isFailure) {
|
||||
return Result.fail(numberResult.error);
|
||||
}
|
||||
|
||||
return Result.ok(numberResult.data);
|
||||
} catch (error) {
|
||||
return Result.fail(
|
||||
new Error(
|
||||
`Error generating invoice number for company ${companyId}: ${(error as Error).message}`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,3 @@
|
||||
export * from "./common/persistence";
|
||||
export * from "./issued-invoices";
|
||||
export * from "./proformas";
|
||||
export * from "./renderers";
|
||||
|
||||
@ -1,26 +1,48 @@
|
||||
import type { SetupParams } from "@erp/core/api";
|
||||
import { buildCatalogs, buildTransactionManager } from "@erp/core/api";
|
||||
|
||||
import {
|
||||
buildIssuedInvoiceFinder,
|
||||
buildIssuedInvoiceSnapshotBuilders,
|
||||
} from "../../../application/issued-invoices";
|
||||
|
||||
import { buildIssuedInvoiceDocumentService } from "./issued-invoice-documents.di";
|
||||
import { buildIssuedInvoicePersistenceMappers } from "./issued-invoice-persistence-mappers.di";
|
||||
import { buildIssuedInvoiceRepository } from "./issued-invoice-repositories.di";
|
||||
import type { IssuedInvoicesInternalDeps } from "./issued-invoices.di";
|
||||
|
||||
export type IssuedInvoicesServiceslDeps = {
|
||||
services: {
|
||||
listIssuedInvoices: (filters: unknown, context: unknown) => null;
|
||||
getIssuedInvoiceById: (id: unknown, context: unknown) => null;
|
||||
generateIssuedInvoiceReport: (id: unknown, options: unknown, context: unknown) => null;
|
||||
};
|
||||
export type IssuedInvoicePublicServices = {
|
||||
listIssuedInvoices: (filters: unknown, context: unknown) => null;
|
||||
getIssuedInvoiceById: (id: unknown, context: unknown) => null;
|
||||
generateIssuedInvoiceReport: (id: unknown, options: unknown, context: unknown) => null;
|
||||
};
|
||||
|
||||
export function buildIssuedInvoiceServices(
|
||||
params: SetupParams,
|
||||
deps: IssuedInvoicesInternalDeps
|
||||
): IssuedInvoicesServiceslDeps {
|
||||
): IssuedInvoicePublicServices {
|
||||
const { database } = params;
|
||||
|
||||
// Infrastructure
|
||||
const transactionManager = buildTransactionManager(database);
|
||||
const catalogs = buildCatalogs();
|
||||
const persistenceMappers = buildIssuedInvoicePersistenceMappers(catalogs);
|
||||
|
||||
const repository = buildIssuedInvoiceRepository({ database, mappers: persistenceMappers });
|
||||
|
||||
// Application helpers
|
||||
const finder = buildIssuedInvoiceFinder(repository);
|
||||
const snapshotBuilders = buildIssuedInvoiceSnapshotBuilders();
|
||||
const documentGeneratorPipeline = buildIssuedInvoiceDocumentService(params);
|
||||
|
||||
return {
|
||||
services: {
|
||||
listIssuedInvoices: (filters, context) => null,
|
||||
//internal.useCases.listIssuedInvoices().execute(filters, context),
|
||||
listIssuedInvoices: (filters, context) => null,
|
||||
//internal.useCases.listIssuedInvoices().execute(filters, context),
|
||||
|
||||
getIssuedInvoiceById: (id, context) => null,
|
||||
//internal.useCases.getIssuedInvoiceById().execute(id, context),
|
||||
getIssuedInvoiceById: (id, context) => null,
|
||||
//internal.useCases.getIssuedInvoiceById().execute(id, context),
|
||||
|
||||
generateIssuedInvoiceReport: (id, options, context) => null,
|
||||
//internal.useCases.reportIssuedInvoice().execute(id, options, context),
|
||||
},
|
||||
generateIssuedInvoiceReport: (id, options, context) => null,
|
||||
//internal.useCases.reportIssuedInvoice().execute(id, options, context),
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,24 +1,73 @@
|
||||
import { type SetupParams, buildCatalogs } from "@erp/core/api";
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import type { Transaction } from "sequelize";
|
||||
|
||||
import { type IProformaCreatorParams, buildProformaCreator } from "../../../application";
|
||||
import type { Proforma } from "../../../domain";
|
||||
|
||||
import { buildProformaNumberGenerator } from "./proforma-number-generator.di";
|
||||
import { buildProformaPersistenceMappers } from "./proforma-persistence-mappers.di";
|
||||
import { buildProformaRepository } from "./proforma-repositories.di";
|
||||
import type { ProformasInternalDeps } from "./proformas.di";
|
||||
|
||||
export type ProformasServicesDeps = {
|
||||
services: {
|
||||
listProformas: (filters: unknown, context: unknown) => null;
|
||||
getProformaById: (id: unknown, context: unknown) => null;
|
||||
generateProformaReport: (id: unknown, options: unknown, context: unknown) => null;
|
||||
};
|
||||
type ProformaServicesContext = {
|
||||
transaction: Transaction;
|
||||
companyId: UniqueID;
|
||||
};
|
||||
|
||||
export function buildProformaServices(deps: ProformasInternalDeps): ProformasServicesDeps {
|
||||
export type ProformaPublicServices = {
|
||||
createProforma: (
|
||||
id: UniqueID,
|
||||
props: IProformaCreatorParams["props"],
|
||||
context: ProformaServicesContext
|
||||
) => Promise<Result<Proforma, Error>>;
|
||||
|
||||
listProformas: (filters: unknown, context: unknown) => null;
|
||||
getProformaById: (id: unknown, context: unknown) => null;
|
||||
generateProformaReport: (id: unknown, options: unknown, context: unknown) => null;
|
||||
};
|
||||
|
||||
export function buildProformaServices(
|
||||
params: SetupParams,
|
||||
deps: ProformasInternalDeps
|
||||
): ProformaPublicServices {
|
||||
const { database } = params;
|
||||
|
||||
// Infrastructure
|
||||
const catalogs = buildCatalogs();
|
||||
const persistenceMappers = buildProformaPersistenceMappers(catalogs);
|
||||
|
||||
const repository = buildProformaRepository({ database, mappers: persistenceMappers });
|
||||
const numberService = buildProformaNumberGenerator();
|
||||
|
||||
// Application helpers
|
||||
const creator = buildProformaCreator({ numberService, repository });
|
||||
|
||||
return {
|
||||
services: {
|
||||
listProformas: (filters, context) => null,
|
||||
//internal.useCases.listProformas().execute(filters, context),
|
||||
createProforma: async (
|
||||
id: UniqueID,
|
||||
props: IProformaCreatorParams["props"],
|
||||
context: ProformaServicesContext
|
||||
) => {
|
||||
const { transaction, companyId } = context;
|
||||
|
||||
getProformaById: (id, context) => null,
|
||||
//internal.useCases.getProformaById().execute(id, context),
|
||||
const createResult = await creator.create({ companyId, id, props, transaction });
|
||||
|
||||
generateProformaReport: (id, options, context) => null,
|
||||
//internal.useCases.reportProforma().execute(id, options, context),
|
||||
if (createResult.isFailure) {
|
||||
return Result.fail(createResult.error);
|
||||
}
|
||||
|
||||
return Result.ok(createResult.data);
|
||||
},
|
||||
|
||||
listProformas: (filters, context) => null,
|
||||
//internal.useCases.listProformas().execute(filters, context),
|
||||
|
||||
getProformaById: (id, context) => null,
|
||||
//internal.useCases.getProformaById().execute(id, context),
|
||||
|
||||
generateProformaReport: (id, options, context) => null,
|
||||
//internal.useCases.reportProforma().execute(id, options, context),
|
||||
};
|
||||
}
|
||||
|
||||
@ -16,8 +16,8 @@ import { Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { CreateProformaItemRequestDTO, CreateProformaRequestDTO } from "../../../../../common";
|
||||
import {
|
||||
type IProformaCreateProps,
|
||||
type IProformaItemProps,
|
||||
type IProformaProps,
|
||||
InvoiceNumber,
|
||||
InvoicePaymentMethod,
|
||||
type InvoiceRecipient,
|
||||
@ -41,7 +41,6 @@ import {
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
export class CreateProformaRequestMapper {
|
||||
private readonly taxCatalog: JsonTaxCatalogProvider;
|
||||
private errors: ValidationErrorDetail[] = [];
|
||||
@ -58,7 +57,7 @@ export class CreateProformaRequestMapper {
|
||||
try {
|
||||
this.errors = [];
|
||||
|
||||
const defaultStatus = InvoiceStatus.createDraft();
|
||||
const defaultStatus = InvoiceStatus.fromDraft();
|
||||
|
||||
const proformaId = extractOrPushError(UniqueID.create(dto.id), "id", this.errors);
|
||||
|
||||
@ -149,7 +148,7 @@ export class CreateProformaRequestMapper {
|
||||
);
|
||||
}
|
||||
|
||||
const proformaProps: Omit<IProformaProps, "items"> & { items: IProformaItemProps[] } = {
|
||||
const proformaProps: Omit<IProformaCreateProps, "items"> & { items: IProformaItemProps[] } = {
|
||||
companyId,
|
||||
status: defaultStatus!,
|
||||
|
||||
@ -182,7 +181,7 @@ export class CreateProformaRequestMapper {
|
||||
}
|
||||
}
|
||||
|
||||
private mapItems(items: CreateProformaItemRequestDTO[]): IProformaItemProps[] {
|
||||
private mapItems(items: CreateProformaItemRequestDTO[]): IProformaItemProps[] {
|
||||
const proformaItems = CustomerInvoiceItems.create({
|
||||
currencyCode: this.currencyCode!,
|
||||
languageCode: this.languageCode!,
|
||||
|
||||
@ -14,7 +14,7 @@ import {
|
||||
import { Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils";
|
||||
|
||||
import {
|
||||
type IProformaProps,
|
||||
type IProformaCreateProps,
|
||||
InvoiceNumber,
|
||||
InvoicePaymentMethod,
|
||||
InvoiceSerie,
|
||||
@ -217,7 +217,7 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
|
||||
items: itemsResults.data.getAll(),
|
||||
});
|
||||
|
||||
const invoiceProps: IProformaProps = {
|
||||
const invoiceProps: IProformaCreateProps = {
|
||||
companyId: attributes.companyId!,
|
||||
|
||||
status: attributes.status!,
|
||||
|
||||
@ -16,8 +16,8 @@ import {
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import {
|
||||
type IProformaCreateProps,
|
||||
type IProformaItemProps,
|
||||
type IProformaProps,
|
||||
ItemAmount,
|
||||
ItemDescription,
|
||||
ItemQuantity,
|
||||
@ -58,7 +58,7 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
|
||||
const { errors, index, parent } = params as {
|
||||
index: number;
|
||||
errors: ValidationErrorDetail[];
|
||||
parent: Partial<IProformaProps>;
|
||||
parent: Partial<IProformaCreateProps>;
|
||||
};
|
||||
|
||||
const itemId = extractOrPushError(
|
||||
@ -139,7 +139,7 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
|
||||
const { errors, index } = params as {
|
||||
index: number;
|
||||
errors: ValidationErrorDetail[];
|
||||
parent: Partial<IProformaProps>;
|
||||
parent: Partial<IProformaCreateProps>;
|
||||
};
|
||||
|
||||
// 1) Valores escalares (atributos generales)
|
||||
|
||||
@ -14,7 +14,7 @@ import {
|
||||
} from "@repo/rdx-ddd";
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import { type IProformaProps, InvoiceRecipient } from "../../../../../../domain";
|
||||
import { type IProformaCreateProps, InvoiceRecipient } from "../../../../../../domain";
|
||||
import type { CustomerInvoiceModel } from "../../../../../common";
|
||||
|
||||
export class SequelizeProformaRecipientDomainMapper {
|
||||
@ -28,7 +28,7 @@ export class SequelizeProformaRecipientDomainMapper {
|
||||
|
||||
const { errors, parent } = params as {
|
||||
errors: ValidationErrorDetail[];
|
||||
parent: Partial<IProformaProps>;
|
||||
parent: Partial<IProformaCreateProps>;
|
||||
};
|
||||
|
||||
/* if (!source.current_customer) {
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { PageHeader, SimpleSearchInput } from "@erp/core/components";
|
||||
import { ErrorAlert } from "@erp/customers/components";
|
||||
import { ErrorAlert, PageHeader, SimpleSearchInput } from "@erp/core/components";
|
||||
import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components";
|
||||
import {
|
||||
Button,
|
||||
|
||||
@ -28,11 +28,6 @@
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
"../core/src/api/domain/value-objects/tax-percentage.vo.ts",
|
||||
"../core/src/api/domain/value-objects/discount-percentage.vo.ts",
|
||||
"../core/src/api/infrastructure/di/catalogs.di.ts"
|
||||
],
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
".": "./src/common/index.ts",
|
||||
"./common": "./src/common/index.ts",
|
||||
"./api": "./src/api/index.ts",
|
||||
"./api/domain": "./src/api/domain/index.ts",
|
||||
"./client": "./src/web/manifest.ts",
|
||||
"./globals.css": "./src/web/globals.css",
|
||||
"./components": "./src/web/components/index.ts"
|
||||
|
||||
@ -1,12 +1,17 @@
|
||||
import type { ITransactionManager } from "@erp/core/api";
|
||||
|
||||
import type { ICreateCustomerInputMapper } from "../mappers";
|
||||
import type { ICustomerCreator, ICustomerFinder } from "../services";
|
||||
import type { ICreateCustomerInputMapper, IUpdateCustomerInputMapper } from "../mappers";
|
||||
import type { ICustomerCreator, ICustomerFinder, ICustomerUpdater } from "../services";
|
||||
import type {
|
||||
ICustomerFullSnapshotBuilder,
|
||||
ICustomerSummarySnapshotBuilder,
|
||||
} from "../snapshot-builders";
|
||||
import { CreateCustomerUseCase, GetCustomerByIdUseCase, ListCustomersUseCase } from "../use-cases";
|
||||
import {
|
||||
CreateCustomerUseCase,
|
||||
GetCustomerByIdUseCase,
|
||||
ListCustomersUseCase,
|
||||
UpdateCustomerUseCase,
|
||||
} from "../use-cases";
|
||||
|
||||
export function buildGetCustomerByIdUseCase(deps: {
|
||||
finder: ICustomerFinder;
|
||||
@ -42,6 +47,20 @@ export function buildCreateCustomerUseCase(deps: {
|
||||
});
|
||||
}
|
||||
|
||||
export function buildUpdateCustomerUseCase(deps: {
|
||||
updater: ICustomerUpdater;
|
||||
dtoMapper: IUpdateCustomerInputMapper;
|
||||
fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
|
||||
transactionManager: ITransactionManager;
|
||||
}) {
|
||||
return new UpdateCustomerUseCase({
|
||||
dtoMapper: deps.dtoMapper,
|
||||
updater: deps.updater,
|
||||
fullSnapshotBuilder: deps.fullSnapshotBuilder,
|
||||
transactionManager: deps.transactionManager,
|
||||
});
|
||||
}
|
||||
|
||||
/*export function buildReportCustomerUseCase(deps: {
|
||||
finder: ICustomerFinder;
|
||||
fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
|
||||
@ -58,12 +77,7 @@ export function buildCreateCustomerUseCase(deps: {
|
||||
);
|
||||
}*/
|
||||
|
||||
/*export function buildUpdateCustomerUseCase(deps: {
|
||||
finder: ICustomerFinder;
|
||||
fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
|
||||
}) {
|
||||
return new UpdateCustomerUseCase(deps.finder, deps.fullSnapshotBuilder);
|
||||
}
|
||||
/*
|
||||
|
||||
export function buildDeleteCustomerUseCase(deps: { finder: ICustomerFinder }) {
|
||||
return new DeleteCustomerUseCase(deps.finder);
|
||||
|
||||
@ -22,7 +22,7 @@ import {
|
||||
extractOrPushError,
|
||||
maybeFromNullableResult,
|
||||
} from "@repo/rdx-ddd";
|
||||
import { Collection, Result } from "@repo/rdx-utils";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { CreateCustomerRequestDTO } from "../../../common";
|
||||
import { CustomerStatus, type ICustomerCreateProps } from "../../domain";
|
||||
@ -176,7 +176,7 @@ export class CreateCustomerInputMapper implements ICreateCustomerInputMapper {
|
||||
errors
|
||||
);
|
||||
|
||||
const defaultTaxes = new Collection<TaxCode>();
|
||||
const defaultTaxes: TaxCode[] = [];
|
||||
|
||||
/*if (!isNullishOrEmpty(dto.default_taxes)) {
|
||||
dto.default_taxes!.map((taxCode, index) => {
|
||||
|
||||
@ -1 +1,2 @@
|
||||
export * from "./create-customer-input.mapper";
|
||||
export * from "./update-customer-input.mapper";
|
||||
|
||||
@ -0,0 +1,289 @@
|
||||
import {
|
||||
City,
|
||||
Country,
|
||||
CurrencyCode,
|
||||
DomainError,
|
||||
EmailAddress,
|
||||
LanguageCode,
|
||||
Name,
|
||||
PhoneNumber,
|
||||
type PostalAddressPatchProps,
|
||||
PostalCode,
|
||||
Province,
|
||||
Street,
|
||||
TINNumber,
|
||||
type TaxCode,
|
||||
TextValue,
|
||||
URLAddress,
|
||||
type UniqueID,
|
||||
ValidationErrorCollection,
|
||||
type ValidationErrorDetail,
|
||||
extractOrPushError,
|
||||
maybeFromNullableResult,
|
||||
} from "@repo/rdx-ddd";
|
||||
import { Collection, Result, isNullishOrEmpty, toPatchField } from "@repo/rdx-utils";
|
||||
|
||||
import type { UpdateCustomerByIdRequestDTO } from "../../../common";
|
||||
import type { CustomerPatchProps } from "../../domain";
|
||||
|
||||
/**
|
||||
* UpdateCustomerInputMapper
|
||||
* Convierte el DTO a las props validadas (CustomerProps).
|
||||
* No construye directamente el agregado.
|
||||
* Tri-estado:
|
||||
* - campo omitido → no se cambia
|
||||
* - campo con valor null/"" → se quita el valor -> set(None()),
|
||||
* - campo con valor no-vacío → se pone el nuevo valor -> set(Some(VO)).
|
||||
*
|
||||
* @param dto - DTO con los datos a cambiar en el cliente
|
||||
* @returns Cambios en las propiedades del cliente
|
||||
*
|
||||
*/
|
||||
|
||||
export interface IUpdateCustomerInputMapper {
|
||||
map(
|
||||
dto: UpdateCustomerByIdRequestDTO,
|
||||
params: { companyId: UniqueID }
|
||||
): Result<CustomerPatchProps>;
|
||||
}
|
||||
|
||||
export class UpdateCustomerInputMapper implements IUpdateCustomerInputMapper {
|
||||
public map(dto: UpdateCustomerByIdRequestDTO, params: { companyId: UniqueID }) {
|
||||
try {
|
||||
const errors: ValidationErrorDetail[] = [];
|
||||
const customerPatchProps: CustomerPatchProps = {};
|
||||
|
||||
toPatchField(dto.reference).ifSet((reference) => {
|
||||
customerPatchProps.reference = extractOrPushError(
|
||||
maybeFromNullableResult(reference, (value) => Name.create(value)),
|
||||
"reference",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.is_company).ifSet((is_company) => {
|
||||
if (isNullishOrEmpty(is_company)) {
|
||||
errors.push({ path: "is_company", message: "is_company cannot be empty" });
|
||||
return;
|
||||
}
|
||||
customerPatchProps.isCompany = extractOrPushError(
|
||||
Result.ok(Boolean(is_company!)),
|
||||
"is_company",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.name).ifSet((name) => {
|
||||
if (isNullishOrEmpty(name)) {
|
||||
errors.push({ path: "name", message: "Name cannot be empty" });
|
||||
return;
|
||||
}
|
||||
customerPatchProps.name = extractOrPushError(Name.create(name!), "name", errors);
|
||||
});
|
||||
|
||||
toPatchField(dto.trade_name).ifSet((trade_name) => {
|
||||
customerPatchProps.tradeName = extractOrPushError(
|
||||
maybeFromNullableResult(trade_name, (value) => Name.create(value)),
|
||||
"trade_name",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.tin).ifSet((tin) => {
|
||||
customerPatchProps.tin = extractOrPushError(
|
||||
maybeFromNullableResult(tin, (value) => TINNumber.create(value)),
|
||||
"tin",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.email_primary).ifSet((email_primary) => {
|
||||
customerPatchProps.emailPrimary = extractOrPushError(
|
||||
maybeFromNullableResult(email_primary, (value) => EmailAddress.create(value)),
|
||||
"email_primary",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.email_secondary).ifSet((email_secondary) => {
|
||||
customerPatchProps.emailSecondary = extractOrPushError(
|
||||
maybeFromNullableResult(email_secondary, (value) => EmailAddress.create(value)),
|
||||
"email_secondary",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.mobile_primary).ifSet((mobile_primary) => {
|
||||
customerPatchProps.mobilePrimary = extractOrPushError(
|
||||
maybeFromNullableResult(mobile_primary, (value) => PhoneNumber.create(value)),
|
||||
"mobile_primary",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.mobile_secondary).ifSet((mobile_secondary) => {
|
||||
customerPatchProps.mobilePrimary = extractOrPushError(
|
||||
maybeFromNullableResult(mobile_secondary, (value) => PhoneNumber.create(value)),
|
||||
"mobile_secondary",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.phone_primary).ifSet((phone_primary) => {
|
||||
customerPatchProps.phonePrimary = extractOrPushError(
|
||||
maybeFromNullableResult(phone_primary, (value) => PhoneNumber.create(value)),
|
||||
"phone_primary",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.phone_secondary).ifSet((phone_secondary) => {
|
||||
customerPatchProps.phoneSecondary = extractOrPushError(
|
||||
maybeFromNullableResult(phone_secondary, (value) => PhoneNumber.create(value)),
|
||||
"phone_secondary",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.fax).ifSet((fax) => {
|
||||
customerPatchProps.fax = extractOrPushError(
|
||||
maybeFromNullableResult(fax, (value) => PhoneNumber.create(value)),
|
||||
"fax",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.website).ifSet((website) => {
|
||||
customerPatchProps.website = extractOrPushError(
|
||||
maybeFromNullableResult(website, (value) => URLAddress.create(value)),
|
||||
"website",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.legal_record).ifSet((legalRecord) => {
|
||||
customerPatchProps.legalRecord = extractOrPushError(
|
||||
maybeFromNullableResult(legalRecord, (value) => TextValue.create(value)),
|
||||
"legal_record",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.language_code).ifSet((languageCode) => {
|
||||
if (isNullishOrEmpty(languageCode)) {
|
||||
errors.push({ path: "language_code", message: "Language code cannot be empty" });
|
||||
return;
|
||||
}
|
||||
|
||||
customerPatchProps.languageCode = extractOrPushError(
|
||||
LanguageCode.create(languageCode!),
|
||||
"language_code",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.currency_code).ifSet((currencyCode) => {
|
||||
if (isNullishOrEmpty(currencyCode)) {
|
||||
errors.push({ path: "currency_code", message: "Currency code cannot be empty" });
|
||||
return;
|
||||
}
|
||||
|
||||
customerPatchProps.currencyCode = extractOrPushError(
|
||||
CurrencyCode.create(currencyCode!),
|
||||
"currency_code",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
// Default taxes
|
||||
const defaultTaxesCollection = new Collection<TaxCode>();
|
||||
/*toPatchField(dto.default_taxes).ifSet((defaultTaxes) => {
|
||||
customerPatchProps.defaultTaxes = defaultTaxesCollection;
|
||||
|
||||
if (isNullishOrEmpty(defaultTaxes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
defaultTaxes!.forEach((taxCode, index) => {
|
||||
const tax = extractOrPushError(TaxCode.create(taxCode), `default_taxes.${index}`, errors);
|
||||
if (tax && customerPatchProps.defaultTaxes) {
|
||||
customerPatchProps.defaultTaxes.add(tax);
|
||||
}
|
||||
});
|
||||
});*/
|
||||
|
||||
// PostalAddress
|
||||
const addressPatchProps = this.mapPostalAddress(dto, errors);
|
||||
if (addressPatchProps) {
|
||||
customerPatchProps.address = addressPatchProps;
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
return Result.fail(
|
||||
new ValidationErrorCollection("Customer props mapping failed (update)", errors)
|
||||
);
|
||||
}
|
||||
|
||||
return Result.ok(customerPatchProps);
|
||||
} catch (err: unknown) {
|
||||
return Result.fail(new DomainError("Customer props mapping failed", { cause: err }));
|
||||
}
|
||||
}
|
||||
|
||||
public mapPostalAddress(
|
||||
dto: UpdateCustomerByIdRequestDTO,
|
||||
errors: ValidationErrorDetail[]
|
||||
): PostalAddressPatchProps | undefined {
|
||||
const postalAddressPatchProps: PostalAddressPatchProps = {};
|
||||
|
||||
toPatchField(dto.street).ifSet((street) => {
|
||||
postalAddressPatchProps.street = extractOrPushError(
|
||||
maybeFromNullableResult(street, (value) => Street.create(value)),
|
||||
"street",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.street2).ifSet((street2) => {
|
||||
postalAddressPatchProps.street2 = extractOrPushError(
|
||||
maybeFromNullableResult(street2, (value) => Street.create(value)),
|
||||
"street2",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.city).ifSet((city) => {
|
||||
postalAddressPatchProps.city = extractOrPushError(
|
||||
maybeFromNullableResult(city, (value) => City.create(value)),
|
||||
"city",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.province).ifSet((province) => {
|
||||
postalAddressPatchProps.province = extractOrPushError(
|
||||
maybeFromNullableResult(province, (value) => Province.create(value)),
|
||||
"province",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.postal_code).ifSet((postalCode) => {
|
||||
postalAddressPatchProps.postalCode = extractOrPushError(
|
||||
maybeFromNullableResult(postalCode, (value) => PostalCode.create(value)),
|
||||
"postal_code",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.country).ifSet((country) => {
|
||||
postalAddressPatchProps.country = extractOrPushError(
|
||||
maybeFromNullableResult(country, (value) => Country.create(value)),
|
||||
"country",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
return Object.keys(postalAddressPatchProps).length > 0 ? postalAddressPatchProps : undefined;
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import type { Criteria } from "@repo/rdx-criteria/server";
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import type { TINNumber, UniqueID } from "@repo/rdx-ddd";
|
||||
import type { Collection, Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { Customer } from "../../domain/aggregates";
|
||||
@ -48,6 +48,16 @@ export interface ICustomerRepository {
|
||||
transaction: unknown
|
||||
): Promise<Result<Customer, Error>>;
|
||||
|
||||
/**
|
||||
* Recupera un Customer por su TIN y companyId.
|
||||
* Devuelve un `NotFoundError` si no se encuentra.
|
||||
*/
|
||||
getByTINInCompany(
|
||||
companyId: UniqueID,
|
||||
tin: TINNumber,
|
||||
transaction?: unknown
|
||||
): Promise<Result<Customer, Error>>;
|
||||
|
||||
/**
|
||||
* Recupera múltiples customers dentro de una empresa
|
||||
* según un criterio dinámico (búsqueda, paginación, etc.).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { Criteria } from "@repo/rdx-criteria/server";
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import type { TINNumber, UniqueID } from "@repo/rdx-ddd";
|
||||
import type { Collection, Result } from "@repo/rdx-utils";
|
||||
import type { Transaction } from "sequelize";
|
||||
|
||||
@ -10,7 +10,13 @@ import type { ICustomerRepository } from "../repositories";
|
||||
export interface ICustomerFinder {
|
||||
findCustomerById(
|
||||
companyId: UniqueID,
|
||||
invoiceId: UniqueID,
|
||||
customerId: UniqueID,
|
||||
transaction?: Transaction
|
||||
): Promise<Result<Customer, Error>>;
|
||||
|
||||
findCustomerByTIN(
|
||||
companyId: UniqueID,
|
||||
tin: TINNumber,
|
||||
transaction?: Transaction
|
||||
): Promise<Result<Customer, Error>>;
|
||||
|
||||
@ -38,6 +44,14 @@ export class CustomerFinder implements ICustomerFinder {
|
||||
return this.repository.getByIdInCompany(companyId, customerId, transaction);
|
||||
}
|
||||
|
||||
findCustomerByTIN(
|
||||
companyId: UniqueID,
|
||||
tin: TINNumber,
|
||||
transaction?: Transaction
|
||||
): Promise<Result<Customer, Error>> {
|
||||
return this.repository.getByTINInCompany(companyId, tin, transaction);
|
||||
}
|
||||
|
||||
async customerExists(
|
||||
companyId: UniqueID,
|
||||
customerId: UniqueID,
|
||||
|
||||
@ -2,14 +2,14 @@ import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import type { Transaction } from "sequelize";
|
||||
|
||||
import type { Customer, ICustomerCreateProps } from "../../domain";
|
||||
import type { Customer, CustomerPatchProps } from "../../domain";
|
||||
import type { ICustomerRepository } from "../repositories";
|
||||
|
||||
export interface ICustomerUpdater {
|
||||
update(params: {
|
||||
companyId: UniqueID;
|
||||
id: UniqueID;
|
||||
props: Partial<ICustomerCreateProps>;
|
||||
props: CustomerPatchProps;
|
||||
transaction: Transaction;
|
||||
}): Promise<Result<Customer, Error>>;
|
||||
}
|
||||
@ -28,7 +28,7 @@ export class CustomerUpdater implements ICustomerUpdater {
|
||||
async update(params: {
|
||||
companyId: UniqueID;
|
||||
id: UniqueID;
|
||||
props: Partial<ICustomerCreateProps>;
|
||||
props: CustomerPatchProps;
|
||||
transaction: Transaction;
|
||||
}): Promise<Result<Customer, Error>> {
|
||||
const { companyId, id, props, transaction } = params;
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
export * from "./customer-creator";
|
||||
export * from "./customer-finder";
|
||||
export * from "./customer-updater";
|
||||
|
||||
@ -1,279 +0,0 @@
|
||||
import {
|
||||
City,
|
||||
Country,
|
||||
CurrencyCode,
|
||||
DomainError,
|
||||
EmailAddress,
|
||||
LanguageCode,
|
||||
Name,
|
||||
PhoneNumber,
|
||||
type PostalAddressPatchProps,
|
||||
PostalCode,
|
||||
Province,
|
||||
Street,
|
||||
TINNumber,
|
||||
TaxCode,
|
||||
TextValue,
|
||||
URLAddress,
|
||||
ValidationErrorCollection,
|
||||
type ValidationErrorDetail,
|
||||
extractOrPushError,
|
||||
maybeFromNullableResult,
|
||||
} from "@repo/rdx-ddd";
|
||||
import { Collection, Result, isNullishOrEmpty, toPatchField } from "@repo/rdx-utils";
|
||||
|
||||
import type { UpdateCustomerByIdRequestDTO } from "../../../../common";
|
||||
import type { CustomerPatchProps } from "../../../domain";
|
||||
|
||||
/**
|
||||
* mapDTOToUpdateCustomerPatchProps
|
||||
* Convierte el DTO a las props validadas (CustomerProps).
|
||||
* No construye directamente el agregado.
|
||||
* Tri-estado:
|
||||
* - campo omitido → no se cambia
|
||||
* - campo con valor null/"" → se quita el valor -> set(None()),
|
||||
* - campo con valor no-vacío → se pone el nuevo valor -> set(Some(VO)).
|
||||
*
|
||||
* @param dto - DTO con los datos a cambiar en el cliente
|
||||
* @returns Cambios en las propiedades del cliente
|
||||
*
|
||||
*/
|
||||
|
||||
export function mapDTOToUpdateCustomerPatchProps(dto: UpdateCustomerByIdRequestDTO) {
|
||||
try {
|
||||
const errors: ValidationErrorDetail[] = [];
|
||||
const customerPatchProps: CustomerPatchProps = {};
|
||||
|
||||
toPatchField(dto.reference).ifSet((reference) => {
|
||||
customerPatchProps.reference = extractOrPushError(
|
||||
maybeFromNullableResult(reference, (value) => Name.create(value)),
|
||||
"reference",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.is_company).ifSet((is_company) => {
|
||||
if (isNullishOrEmpty(is_company)) {
|
||||
errors.push({ path: "is_company", message: "is_company cannot be empty" });
|
||||
return;
|
||||
}
|
||||
customerPatchProps.isCompany = extractOrPushError(
|
||||
Result.ok(Boolean(is_company!)),
|
||||
"is_company",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.name).ifSet((name) => {
|
||||
if (isNullishOrEmpty(name)) {
|
||||
errors.push({ path: "name", message: "Name cannot be empty" });
|
||||
return;
|
||||
}
|
||||
customerPatchProps.name = extractOrPushError(Name.create(name!), "name", errors);
|
||||
});
|
||||
|
||||
toPatchField(dto.trade_name).ifSet((trade_name) => {
|
||||
customerPatchProps.tradeName = extractOrPushError(
|
||||
maybeFromNullableResult(trade_name, (value) => Name.create(value)),
|
||||
"trade_name",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.tin).ifSet((tin) => {
|
||||
customerPatchProps.tin = extractOrPushError(
|
||||
maybeFromNullableResult(tin, (value) => TINNumber.create(value)),
|
||||
"tin",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.email_primary).ifSet((email_primary) => {
|
||||
customerPatchProps.emailPrimary = extractOrPushError(
|
||||
maybeFromNullableResult(email_primary, (value) => EmailAddress.create(value)),
|
||||
"email_primary",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.email_secondary).ifSet((email_secondary) => {
|
||||
customerPatchProps.emailSecondary = extractOrPushError(
|
||||
maybeFromNullableResult(email_secondary, (value) => EmailAddress.create(value)),
|
||||
"email_secondary",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.mobile_primary).ifSet((mobile_primary) => {
|
||||
customerPatchProps.mobilePrimary = extractOrPushError(
|
||||
maybeFromNullableResult(mobile_primary, (value) => PhoneNumber.create(value)),
|
||||
"mobile_primary",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.mobile_secondary).ifSet((mobile_secondary) => {
|
||||
customerPatchProps.mobilePrimary = extractOrPushError(
|
||||
maybeFromNullableResult(mobile_secondary, (value) => PhoneNumber.create(value)),
|
||||
"mobile_secondary",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.phone_primary).ifSet((phone_primary) => {
|
||||
customerPatchProps.phonePrimary = extractOrPushError(
|
||||
maybeFromNullableResult(phone_primary, (value) => PhoneNumber.create(value)),
|
||||
"phone_primary",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.phone_secondary).ifSet((phone_secondary) => {
|
||||
customerPatchProps.phoneSecondary = extractOrPushError(
|
||||
maybeFromNullableResult(phone_secondary, (value) => PhoneNumber.create(value)),
|
||||
"phone_secondary",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.fax).ifSet((fax) => {
|
||||
customerPatchProps.fax = extractOrPushError(
|
||||
maybeFromNullableResult(fax, (value) => PhoneNumber.create(value)),
|
||||
"fax",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.website).ifSet((website) => {
|
||||
customerPatchProps.website = extractOrPushError(
|
||||
maybeFromNullableResult(website, (value) => URLAddress.create(value)),
|
||||
"website",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.legal_record).ifSet((legalRecord) => {
|
||||
customerPatchProps.legalRecord = extractOrPushError(
|
||||
maybeFromNullableResult(legalRecord, (value) => TextValue.create(value)),
|
||||
"legal_record",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.language_code).ifSet((languageCode) => {
|
||||
if (isNullishOrEmpty(languageCode)) {
|
||||
errors.push({ path: "language_code", message: "Language code cannot be empty" });
|
||||
return;
|
||||
}
|
||||
|
||||
customerPatchProps.languageCode = extractOrPushError(
|
||||
LanguageCode.create(languageCode!),
|
||||
"language_code",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.currency_code).ifSet((currencyCode) => {
|
||||
if (isNullishOrEmpty(currencyCode)) {
|
||||
errors.push({ path: "currency_code", message: "Currency code cannot be empty" });
|
||||
return;
|
||||
}
|
||||
|
||||
customerPatchProps.currencyCode = extractOrPushError(
|
||||
CurrencyCode.create(currencyCode!),
|
||||
"currency_code",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
// Default taxes
|
||||
const defaultTaxesCollection = new Collection<TaxCode>();
|
||||
toPatchField(dto.default_taxes).ifSet((defaultTaxes) => {
|
||||
customerPatchProps.defaultTaxes = defaultTaxesCollection;
|
||||
|
||||
if (isNullishOrEmpty(defaultTaxes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
defaultTaxes!.forEach((taxCode, index) => {
|
||||
const tax = extractOrPushError(TaxCode.create(taxCode), `default_taxes.${index}`, errors);
|
||||
if (tax && customerPatchProps.defaultTaxes) {
|
||||
customerPatchProps.defaultTaxes.add(tax);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// PostalAddress
|
||||
const addressPatchProps = mapDTOToUpdatePostalAddressPatchProps(dto, errors);
|
||||
if (addressPatchProps) {
|
||||
customerPatchProps.address = addressPatchProps;
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
return Result.fail(
|
||||
new ValidationErrorCollection("Customer props mapping failed (update)", errors)
|
||||
);
|
||||
}
|
||||
|
||||
return Result.ok(customerPatchProps);
|
||||
} catch (err: unknown) {
|
||||
return Result.fail(new DomainError("Customer props mapping failed", { cause: err }));
|
||||
}
|
||||
}
|
||||
|
||||
function mapDTOToUpdatePostalAddressPatchProps(
|
||||
dto: UpdateCustomerByIdRequestDTO,
|
||||
errors: ValidationErrorDetail[]
|
||||
): PostalAddressPatchProps | undefined {
|
||||
const postalAddressPatchProps: PostalAddressPatchProps = {};
|
||||
|
||||
toPatchField(dto.street).ifSet((street) => {
|
||||
postalAddressPatchProps.street = extractOrPushError(
|
||||
maybeFromNullableResult(street, (value) => Street.create(value)),
|
||||
"street",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.street2).ifSet((street2) => {
|
||||
postalAddressPatchProps.street2 = extractOrPushError(
|
||||
maybeFromNullableResult(street2, (value) => Street.create(value)),
|
||||
"street2",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.city).ifSet((city) => {
|
||||
postalAddressPatchProps.city = extractOrPushError(
|
||||
maybeFromNullableResult(city, (value) => City.create(value)),
|
||||
"city",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.province).ifSet((province) => {
|
||||
postalAddressPatchProps.province = extractOrPushError(
|
||||
maybeFromNullableResult(province, (value) => Province.create(value)),
|
||||
"province",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.postal_code).ifSet((postalCode) => {
|
||||
postalAddressPatchProps.postalCode = extractOrPushError(
|
||||
maybeFromNullableResult(postalCode, (value) => PostalCode.create(value)),
|
||||
"postal_code",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.country).ifSet((country) => {
|
||||
postalAddressPatchProps.country = extractOrPushError(
|
||||
maybeFromNullableResult(country, (value) => Country.create(value)),
|
||||
"country",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
return Object.keys(postalAddressPatchProps).length > 0 ? postalAddressPatchProps : undefined;
|
||||
}
|
||||
@ -1,11 +1,13 @@
|
||||
import type { IPresenterRegistry, ITransactionManager } from "@erp/core/api";
|
||||
import type { ITransactionManager } from "@erp/core/api";
|
||||
import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import type { Transaction } from "sequelize";
|
||||
|
||||
import type { UpdateCustomerByIdRequestDTO } from "../../../../common/dto";
|
||||
import type { CustomerPatchProps } from "../../../domain";
|
||||
import type { CustomerApplicationService } from "../../customer-application.service";
|
||||
import type { IUpdateCustomerInputMapper } from "../../mappers";
|
||||
import type { ICustomerUpdater } from "../../services";
|
||||
import type { ICustomerFullSnapshotBuilder } from "../../snapshot-builders";
|
||||
|
||||
type UpdateCustomerUseCaseInput = {
|
||||
companyId: UniqueID;
|
||||
@ -13,12 +15,25 @@ type UpdateCustomerUseCaseInput = {
|
||||
dto: UpdateCustomerByIdRequestDTO;
|
||||
};
|
||||
|
||||
type UpdateCustomerUseCaseDeps = {
|
||||
dtoMapper: IUpdateCustomerInputMapper;
|
||||
updater: ICustomerUpdater;
|
||||
fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
|
||||
transactionManager: ITransactionManager;
|
||||
};
|
||||
|
||||
export class UpdateCustomerUseCase {
|
||||
constructor(
|
||||
private readonly service: CustomerApplicationService,
|
||||
private readonly transactionManager: ITransactionManager,
|
||||
private readonly presenterRegistry: IPresenterRegistry
|
||||
) {}
|
||||
private readonly dtoMapper: IUpdateCustomerInputMapper;
|
||||
private readonly updater: ICustomerUpdater;
|
||||
private readonly fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
|
||||
private readonly transactionManager: ITransactionManager;
|
||||
|
||||
constructor(deps: UpdateCustomerUseCaseDeps) {
|
||||
this.dtoMapper = deps.dtoMapper;
|
||||
this.updater = deps.updater;
|
||||
this.fullSnapshotBuilder = deps.fullSnapshotBuilder;
|
||||
this.transactionManager = deps.transactionManager;
|
||||
}
|
||||
|
||||
public execute(params: UpdateCustomerUseCaseInput) {
|
||||
const { companyId, customer_id, dto } = params;
|
||||
@ -27,6 +42,8 @@ export class UpdateCustomerUseCase {
|
||||
if (idOrError.isFailure) {
|
||||
return Result.fail(idOrError.error);
|
||||
}
|
||||
const id = idOrError.data;
|
||||
|
||||
// Mapear DTO → props de dominio
|
||||
const patchPropsResult = this.dtoMapper.map(dto, { companyId });
|
||||
if (patchPropsResult.isFailure) {
|
||||
@ -37,20 +54,20 @@ export class UpdateCustomerUseCase {
|
||||
|
||||
return this.transactionManager.complete(async (transaction: Transaction) => {
|
||||
try {
|
||||
const updatedCustomer = await this.service.patchCustomerByIdInCompany(
|
||||
const updateResult = await this.updater.update({
|
||||
companyId,
|
||||
customerId,
|
||||
patchProps,
|
||||
transaction
|
||||
);
|
||||
id,
|
||||
props: patchProps,
|
||||
transaction,
|
||||
});
|
||||
|
||||
if (updatedCustomer.isFailure) {
|
||||
return Result.fail(updatedCustomer.error);
|
||||
if (updateResult.isFailure) {
|
||||
return Result.fail(updateResult.error);
|
||||
}
|
||||
|
||||
const customerOrError = await this.service.updateCustomerInCompany(
|
||||
companyId,
|
||||
updatedCustomer.data,
|
||||
updateResult.data,
|
||||
transaction
|
||||
);
|
||||
const customer = customerOrError.data;
|
||||
|
||||
@ -9,14 +9,13 @@ import {
|
||||
type PostalAddressPatchProps,
|
||||
type PostalAddressProps,
|
||||
type TINNumber,
|
||||
type TaxCode,
|
||||
type TextValue,
|
||||
type URLAddress,
|
||||
type UniqueID,
|
||||
} from "@repo/rdx-ddd";
|
||||
import { type Collection, type Maybe, Result } from "@repo/rdx-utils";
|
||||
import { type Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { CustomerStatus } from "../value-objects";
|
||||
import type { CustomerStatus, CustomerTaxesProps } from "../value-objects";
|
||||
|
||||
export interface ICustomerCreateProps {
|
||||
companyId: UniqueID;
|
||||
@ -44,14 +43,14 @@ export interface ICustomerCreateProps {
|
||||
|
||||
legalRecord: Maybe<TextValue>;
|
||||
|
||||
defaultTaxes: TaxCode[];
|
||||
defaultTaxes: CustomerTaxesProps;
|
||||
|
||||
languageCode: LanguageCode;
|
||||
currencyCode: CurrencyCode;
|
||||
}
|
||||
|
||||
export type CustomerPatchProps = Partial<
|
||||
Omit<ICustomerCreateProps, "companyId" | "address" | "isCompany" | "status">
|
||||
Omit<ICustomerCreateProps, "companyId" | "address" | "status">
|
||||
> & {
|
||||
address?: PostalAddressPatchProps;
|
||||
};
|
||||
@ -87,7 +86,7 @@ export interface ICustomer {
|
||||
readonly website: Maybe<URLAddress>;
|
||||
readonly legalRecord: Maybe<TextValue>;
|
||||
|
||||
readonly defaultTaxes: Collection<TaxCode>;
|
||||
readonly defaultTaxes: CustomerTaxesProps;
|
||||
|
||||
readonly languageCode: LanguageCode;
|
||||
readonly currencyCode: CurrencyCode;
|
||||
@ -142,8 +141,6 @@ export class Customer extends AggregateRoot<CustomerInternalProps> implements IC
|
||||
if (addressResult.isFailure) {
|
||||
return Result.fail(addressResult.error);
|
||||
}
|
||||
|
||||
this.props.address = addressResult.data;
|
||||
}
|
||||
|
||||
return Result.ok();
|
||||
@ -223,7 +220,7 @@ export class Customer extends AggregateRoot<CustomerInternalProps> implements IC
|
||||
return this.props.legalRecord;
|
||||
}
|
||||
|
||||
public get defaultTaxes(): Collection<TaxCode> {
|
||||
public get defaultTaxes(): CustomerTaxesProps {
|
||||
return this.props.defaultTaxes;
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,65 @@
|
||||
import type { Tax } from "@erp/core/api";
|
||||
import { ValueObject } from "@repo/rdx-ddd";
|
||||
import { type Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
export type CustomerTaxesProps = {
|
||||
iva: Maybe<Tax>; // si existe
|
||||
rec: Maybe<Tax>; // si existe
|
||||
retention: Maybe<Tax>; // si existe
|
||||
};
|
||||
|
||||
export interface ICustomerItemTaxes {
|
||||
iva: Maybe<Tax>; // si existe
|
||||
rec: Maybe<Tax>; // si existe
|
||||
retention: Maybe<Tax>; // si existe
|
||||
|
||||
toKey(): string; // Clave para representar un trío.
|
||||
}
|
||||
|
||||
export class CustomerTaxes
|
||||
extends ValueObject<CustomerTaxesProps>
|
||||
implements ICustomerItemTaxes
|
||||
{
|
||||
static create(props: CustomerTaxesProps) {
|
||||
return Result.ok(new CustomerTaxes(props));
|
||||
}
|
||||
|
||||
toKey(): string {
|
||||
const ivaCode = this.props.iva.match(
|
||||
(iva) => iva.code,
|
||||
() => "#"
|
||||
);
|
||||
|
||||
const recCode = this.props.rec.match(
|
||||
(rec) => rec.code,
|
||||
() => "#"
|
||||
);
|
||||
|
||||
const retentionCode = this.props.retention.match(
|
||||
(retention) => retention.code,
|
||||
() => "#"
|
||||
);
|
||||
|
||||
return `${ivaCode};${recCode};${retentionCode}`;
|
||||
}
|
||||
|
||||
get iva(): Maybe<Tax> {
|
||||
return this.props.iva;
|
||||
}
|
||||
|
||||
get rec(): Maybe<Tax> {
|
||||
return this.props.rec;
|
||||
}
|
||||
|
||||
get retention(): Maybe<Tax> {
|
||||
return this.props.retention;
|
||||
}
|
||||
|
||||
getProps() {
|
||||
return this.props;
|
||||
}
|
||||
|
||||
toPrimitive() {
|
||||
return this.getProps();
|
||||
}
|
||||
}
|
||||
@ -2,3 +2,4 @@ export * from "./customer-address-type.vo";
|
||||
export * from "./customer-number.vo";
|
||||
export * from "./customer-serie.vo";
|
||||
export * from "./customer-status.vo";
|
||||
export * from "./customer-taxes.vo";
|
||||
@ -1,10 +1,16 @@
|
||||
import type { IModuleServer } from "@erp/core/api";
|
||||
|
||||
import { customersRouter, models } from "./infrastructure";
|
||||
import { buildCustomersDependencies, buildCustomerServices, CustomersInternalDeps } from "./infrastructure/di";
|
||||
import { type CustomerPublicServices, customersRouter, models } from "./infrastructure";
|
||||
import {
|
||||
type CustomersInternalDeps,
|
||||
buildCustomerServices,
|
||||
buildCustomersDependencies,
|
||||
} from "./infrastructure/di";
|
||||
|
||||
export * from "./infrastructure/sequelize";
|
||||
|
||||
export type { CustomerPublicServices };
|
||||
|
||||
export const customersAPIModule: IModuleServer = {
|
||||
name: "customers",
|
||||
version: "1.0.0",
|
||||
@ -21,10 +27,10 @@ export const customersAPIModule: IModuleServer = {
|
||||
const { env: ENV, app, database, baseRoutePath: API_BASE_PATH, logger } = params;
|
||||
|
||||
// 1) Dominio interno
|
||||
const customerInternalDeps = buildCustomersDependencies(params);
|
||||
const internal = buildCustomersDependencies(params);
|
||||
|
||||
// 2) Servicios públicos (Application Services)
|
||||
const customerServices = buildCustomerServices(customerInternalDeps);
|
||||
const customersServices: CustomerPublicServices = buildCustomerServices(params, internal);
|
||||
|
||||
logger.info("🚀 Customers module dependencies registered", {
|
||||
label: this.name,
|
||||
@ -35,10 +41,12 @@ export const customersAPIModule: IModuleServer = {
|
||||
models,
|
||||
|
||||
// Servicios expuestos a otros módulos
|
||||
services: customerServices,
|
||||
services: {
|
||||
general: customersServices, // 'customers:general'
|
||||
},
|
||||
|
||||
// Implementación privada del módulo
|
||||
internal:customerInternalDeps,
|
||||
internal,
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@ -1,24 +1,79 @@
|
||||
import type { SetupParams } from "@erp/core/api";
|
||||
import type { TINNumber, UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import type { Transaction } from "sequelize";
|
||||
|
||||
import { buildCustomerCreator, buildCustomerFinder } from "../../application";
|
||||
import type { Customer, ICustomerCreateProps } from "../../domain";
|
||||
|
||||
import { buildCustomerPersistenceMappers } from "./customer-persistence-mappers.di";
|
||||
import { buildCustomerRepository } from "./customer-repositories.di";
|
||||
import type { CustomersInternalDeps } from "./customers.di";
|
||||
|
||||
export type CustomersServicesDeps = {
|
||||
services: {
|
||||
listCustomers: (filters: unknown, context: unknown) => null;
|
||||
getCustomerById: (id: unknown, context: unknown) => null;
|
||||
//generateCustomerReport: (id: unknown, options: unknown, context: unknown) => null;
|
||||
};
|
||||
type CustomerServicesContext = {
|
||||
transaction: Transaction;
|
||||
companyId: UniqueID;
|
||||
};
|
||||
|
||||
export function buildCustomerServices(deps: CustomersInternalDeps): CustomersServicesDeps {
|
||||
export type CustomerPublicServices = {
|
||||
//listCustomers: (filters: unknown, context: unknown) => null;
|
||||
findCustomerByTIN: (
|
||||
tin: TINNumber,
|
||||
context: CustomerServicesContext
|
||||
) => Promise<Result<Customer, Error>>;
|
||||
createCustomer: (
|
||||
id: UniqueID,
|
||||
props: ICustomerCreateProps,
|
||||
context: CustomerServicesContext
|
||||
) => Promise<Result<Customer, Error>>;
|
||||
//generateCustomerReport: (id: unknown, options: unknown, context: unknown) => null;
|
||||
};
|
||||
|
||||
export function buildCustomerServices(
|
||||
params: SetupParams,
|
||||
deps: CustomersInternalDeps
|
||||
): CustomerPublicServices {
|
||||
const { database } = params;
|
||||
|
||||
// Infrastructure
|
||||
const persistenceMappers = buildCustomerPersistenceMappers();
|
||||
const repository = buildCustomerRepository({ database, mappers: persistenceMappers });
|
||||
|
||||
const finder = buildCustomerFinder({ repository });
|
||||
const creator = buildCustomerCreator({ repository });
|
||||
|
||||
return {
|
||||
services: {
|
||||
listCustomers: (filters, context) => null,
|
||||
//internal.useCases.listCustomers().execute(filters, context),
|
||||
findCustomerByTIN: async (tin: TINNumber, context: CustomerServicesContext) => {
|
||||
const { companyId, transaction } = context;
|
||||
|
||||
getCustomerById: (id, context) => null,
|
||||
//internal.useCases.getCustomerById().execute(id, context),
|
||||
const customerResult = await finder.findCustomerByTIN(companyId, tin, transaction);
|
||||
|
||||
//generateCustomerReport: (id, options, context) => null,
|
||||
//internal.useCases.reportCustomer().execute(id, options, context),
|
||||
if (customerResult.isFailure) {
|
||||
return Result.fail(customerResult.error);
|
||||
}
|
||||
|
||||
return Result.ok(customerResult.data);
|
||||
},
|
||||
|
||||
createCustomer: async (
|
||||
id: UniqueID,
|
||||
props: ICustomerCreateProps,
|
||||
context: CustomerServicesContext
|
||||
) => {
|
||||
const { companyId, transaction } = context;
|
||||
|
||||
const customerResult = await creator.create({
|
||||
companyId,
|
||||
id,
|
||||
props,
|
||||
transaction,
|
||||
});
|
||||
|
||||
if (customerResult.isFailure) {
|
||||
return Result.fail(customerResult.error);
|
||||
}
|
||||
|
||||
return Result.ok(customerResult.data);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -6,6 +6,8 @@ import {
|
||||
CreateCustomerRequestSchema,
|
||||
CustomerListRequestSchema,
|
||||
GetCustomerByIdRequestSchema,
|
||||
UpdateCustomerByIdParamsRequestSchema,
|
||||
UpdateCustomerByIdRequestSchema,
|
||||
} from "../../../common/dto";
|
||||
import type { CustomersInternalDeps } from "../di";
|
||||
|
||||
@ -13,6 +15,7 @@ import {
|
||||
CreateCustomerController,
|
||||
GetCustomerController,
|
||||
ListCustomersController,
|
||||
UpdateCustomerController,
|
||||
} from "./controllers";
|
||||
|
||||
export const customersRouter = (params: ModuleParams, deps: CustomersInternalDeps) => {
|
||||
@ -75,19 +78,19 @@ export const customersRouter = (params: ModuleParams, deps: CustomersInternalDep
|
||||
}
|
||||
);
|
||||
|
||||
/* router.put(
|
||||
router.put(
|
||||
"/:customer_id",
|
||||
//checkTabContext,
|
||||
|
||||
validateRequest(UpdateCustomerByIdParamsRequestSchema, "params"),
|
||||
validateRequest(UpdateCustomerByIdRequestSchema, "body"),
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const useCase = deps.useCases.update();
|
||||
const useCase = deps.useCases.updateCustomer();
|
||||
const controller = new UpdateCustomerController(useCase);
|
||||
return controller.execute(req, res, next);
|
||||
}
|
||||
);
|
||||
*/
|
||||
|
||||
/*router.delete(
|
||||
"/:customer_id",
|
||||
//checkTabContext,
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
export * from "./di";
|
||||
export * from "./express";
|
||||
export * from "./mappers";
|
||||
export * from "./sequelize";
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import {
|
||||
CreationOptional,
|
||||
type CreationOptional,
|
||||
DataTypes,
|
||||
InferAttributes,
|
||||
InferCreationAttributes,
|
||||
type InferAttributes,
|
||||
type InferCreationAttributes,
|
||||
Model,
|
||||
Sequelize,
|
||||
type Sequelize,
|
||||
} from "sequelize";
|
||||
|
||||
export type CustomerCreationAttributes = InferCreationAttributes<CustomerModel, {}> & {};
|
||||
@ -238,6 +238,7 @@ export default (database: Sequelize) => {
|
||||
fields: ["company_id", "deleted_at", "name"],
|
||||
},
|
||||
{ name: "idx_name", fields: ["name"] }, // <- para ordenación
|
||||
{ name: "idx_tin", fields: ["tin"] }, // <- para servicios externos
|
||||
{ name: "idx_company_idx", fields: ["id", "company_id"], unique: true }, // <- para consulta get
|
||||
{ name: "idx_factuges", fields: ["factuges_id"], unique: true }, // <- para el proceso python
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ import {
|
||||
translateSequelizeError,
|
||||
} from "@erp/core/api";
|
||||
import { type Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server";
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import type { TINNumber, UniqueID } from "@repo/rdx-ddd";
|
||||
import { type Collection, Result } from "@repo/rdx-utils";
|
||||
import type { FindOptions, InferAttributes, OrderItem, Sequelize, Transaction } from "sequelize";
|
||||
|
||||
@ -162,6 +162,59 @@ export class CustomerRepository
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recupera un cliente por su ID y companyId.
|
||||
*
|
||||
* @param companyId - Identificador UUID de la empresa a la que pertenece el cliente.
|
||||
* @param tin - TIN del cliente.
|
||||
* @param transaction - Transacción activa para la operación.
|
||||
* @returns Result<Customer, Error>
|
||||
*/
|
||||
async getByTINInCompany(
|
||||
companyId: UniqueID,
|
||||
tin: TINNumber,
|
||||
transaction?: Transaction,
|
||||
options: FindOptions<InferAttributes<CustomerModel>> = {}
|
||||
): Promise<Result<Customer, Error>> {
|
||||
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<InferAttributes<CustomerModel>> = {
|
||||
...options,
|
||||
where: {
|
||||
...(options.where ?? {}),
|
||||
tin: tin.toString(),
|
||||
company_id: companyId.toString(),
|
||||
},
|
||||
order: normalizedOrder,
|
||||
include: normalizedInclude,
|
||||
transaction,
|
||||
};
|
||||
|
||||
const row = await CustomerModel.findOne(mergedOptions);
|
||||
|
||||
if (!row) {
|
||||
return Result.fail(new EntityNotFoundError("Customer", "tin", tin.toString()));
|
||||
}
|
||||
|
||||
const customer = this.domainMapper.mapToDomain(row);
|
||||
return customer;
|
||||
} catch (error: unknown) {
|
||||
return Result.fail(translateSequelizeError(error));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recupera múltiples customers dentro de una empresa según un criterio dinámico (búsqueda, paginación, etc.).
|
||||
*
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import {
|
||||
GetCustomerByIdResponseDTO,
|
||||
type GetCustomerByIdResponseDTO,
|
||||
GetCustomerByIdResponseSchema,
|
||||
} from "./get-customer-by-id.response.dto";
|
||||
|
||||
|
||||
12
modules/customers/src/web/common/api/api-types.ts
Normal file
12
modules/customers/src/web/common/api/api-types.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import type { ArrayElement } from "@repo/rdx-utils";
|
||||
|
||||
import type { GetCustomerByIdResponseDTO, ListCustomersResponseDTO } from "../../../common";
|
||||
|
||||
// Elemento de consulta paginada
|
||||
export type CustomerSummary = Omit<ArrayElement<ListCustomersResponseDTO["items"]>, "metadata">;
|
||||
|
||||
// Consulta paginada con criteria
|
||||
export type CustomerSummaryPage = Omit<ListCustomersResponseDTO, "metadata">;
|
||||
|
||||
// Cliente
|
||||
export type Customer = Omit<GetCustomerByIdResponseDTO, "metadata">;
|
||||
@ -1,2 +1,3 @@
|
||||
export * from "./api-types";
|
||||
export * from "./get-customer-by-ip.api";
|
||||
export * from "./get-customer-list.api";
|
||||
3
modules/customers/src/web/common/hooks/index.ts
Normal file
3
modules/customers/src/web/common/hooks/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./use-customer-get-query";
|
||||
export * from "./use-customer-list-query";
|
||||
export * from "./use-customer-update-mutation";
|
||||
10
modules/customers/src/web/common/hooks/toValidationErrors.ts
Normal file
10
modules/customers/src/web/common/hooks/toValidationErrors.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import type { ZodError } from "zod";
|
||||
|
||||
// Helpers de validación a errores de dominio
|
||||
|
||||
export function toValidationErrors(error: ZodError<unknown>) {
|
||||
return error.issues.map((err) => ({
|
||||
field: err.path.join("."),
|
||||
message: err.message,
|
||||
}));
|
||||
}
|
||||
@ -8,7 +8,7 @@ import {
|
||||
useQuery,
|
||||
} from "@tanstack/react-query";
|
||||
|
||||
import { type CustomerSummaryPage, getCustomerListApi } from "../api";
|
||||
import { type CustomerSummaryPage, getCustomerListApi } from "..";
|
||||
|
||||
export const CUSTOMERS_QUERY_KEY = (criteria?: CriteriaDTO): QueryKey => [
|
||||
"customers",
|
||||
@ -0,0 +1,60 @@
|
||||
import { useDataSource } from "@erp/core/hooks";
|
||||
import { ValidationErrorCollection } from "@repo/rdx-ddd";
|
||||
import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { UpdateCustomerByIdRequestSchema } from "../../../common";
|
||||
import type { Customer } from "../api";
|
||||
|
||||
import { toValidationErrors } from "./toValidationErrors";
|
||||
|
||||
export const CUSTOMER_UPDATE_KEY = ["customers", "update"] as const;
|
||||
|
||||
type UpdateCustomerContext = {};
|
||||
|
||||
type UpdateCustomerPayload = {
|
||||
id: string;
|
||||
data: Partial<CustomerFormData>;
|
||||
};
|
||||
|
||||
export const useCustomerUpdateMutation = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const dataSource = useDataSource();
|
||||
const schema = UpdateCustomerByIdRequestSchema;
|
||||
|
||||
return useMutation<Customer, DefaultError, UpdateCustomerPayload, UpdateCustomerContext>({
|
||||
mutationKey: CUSTOMER_UPDATE_KEY,
|
||||
|
||||
mutationFn: async (payload) => {
|
||||
const { id: customerId, data } = payload;
|
||||
if (!customerId) {
|
||||
throw new Error("customerId is required");
|
||||
}
|
||||
|
||||
const result = schema.safeParse(data);
|
||||
if (!result.success) {
|
||||
throw new ValidationErrorCollection("Validation failed", toValidationErrors(result.error));
|
||||
}
|
||||
|
||||
const updated = await dataSource.updateOne("customers", customerId, data);
|
||||
return updated as Customer;
|
||||
},
|
||||
|
||||
onSuccess: (updated: Customer, variables) => {
|
||||
const { id: customerId } = updated;
|
||||
|
||||
// Invalida el listado para refrescar desde servidor
|
||||
//invalidateCustomerListCache(queryClient);
|
||||
|
||||
// Actualiza detalle
|
||||
//setCustomerDetailCache(queryClient, customerId, updated);
|
||||
|
||||
// Actualiza todas las páginas donde aparezca
|
||||
//upsertCustomerIntoListCaches(queryClient, { ...updated });
|
||||
},
|
||||
|
||||
onSettled: () => {
|
||||
// Refresca todos los listados
|
||||
//invalidateCustomerListCache(queryClient);
|
||||
},
|
||||
});
|
||||
};
|
||||
2
modules/customers/src/web/common/index.ts
Normal file
2
modules/customers/src/web/common/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./api";
|
||||
export * from "./hooks";
|
||||
@ -1,10 +1,10 @@
|
||||
import { FormDebug } from "@erp/core/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
|
||||
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||
import { CustomerAdditionalConfigFields } from "./customer-additional-config-fields";
|
||||
import { CustomerAddressFields } from "./customer-address-fields";
|
||||
import { CustomerBasicInfoFields } from "./customer-basic-info-fields";
|
||||
import { CustomerContactFields } from './customer-contact-fields';
|
||||
import { CustomerContactFields } from "./customer-contact-fields";
|
||||
|
||||
type CustomerFormProps = {
|
||||
formId: string;
|
||||
@ -15,7 +15,7 @@ type CustomerFormProps = {
|
||||
|
||||
export const CustomerEditForm = ({ formId, onSubmit, className, focusRef }: CustomerFormProps) => {
|
||||
return (
|
||||
<form noValidate id={formId} onSubmit={onSubmit}>
|
||||
<form id={formId} noValidate onSubmit={onSubmit}>
|
||||
<FormDebug />
|
||||
<section className={cn("space-y-6 p-6", className)}>
|
||||
<CustomerBasicInfoFields focusRef={focusRef} />
|
||||
|
||||
@ -1,2 +1 @@
|
||||
//export * from "./customer-edit-form";
|
||||
export * from "../../view/ui/components/customer-editor-skeleton";
|
||||
export * from "./customer-edit-form";
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
//export * from "./client-selector-modal";
|
||||
//export * from "./customer-modal-selector";
|
||||
//export * from "./editor";
|
||||
export * from "../../../../core/src/web/components/error-alert";
|
||||
//export * from "./not-found-card";
|
||||
export * from "./editor";
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { PropsWithChildren, createContext } from "react";
|
||||
import { type PropsWithChildren, createContext } from "react";
|
||||
|
||||
/**
|
||||
* ────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@ -8,9 +8,9 @@ const CustomersList = lazy(() => import("./list").then((m) => ({ default: m.Cust
|
||||
const CustomerView = lazy(() => import("./view").then((m) => ({ default: m.CustomerViewPage })));
|
||||
|
||||
//const CustomerAdd = lazy(() => import("./pages").then((m) => ({ default: m.CustomerCreatePage })));
|
||||
/*const CustomerUpdate = lazy(() =>
|
||||
import("./pages").then((m) => ({ default: m.CustomerUpdatePage }))
|
||||
);*/
|
||||
const CustomerUpdate = lazy(() =>
|
||||
import("./update").then((m) => ({ default: m.CustomerUpdatePage }))
|
||||
);
|
||||
|
||||
export const CustomerRoutes = (params: ModuleClientParams): RouteObject[] => {
|
||||
return [
|
||||
@ -26,7 +26,7 @@ export const CustomerRoutes = (params: ModuleClientParams): RouteObject[] => {
|
||||
{ path: "list", element: <CustomersList /> },
|
||||
//{ path: "create", element: <CustomerAdd /> },
|
||||
{ path: ":id", element: <CustomerView /> },
|
||||
//{ path: ":id/edit", element: <CustomerUpdate /> },
|
||||
{ path: ":id/edit", element: <CustomerUpdate /> },
|
||||
|
||||
//
|
||||
/*{ path: "create", element: <CustomersList /> },
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { useDataSource } from "@erp/core/hooks";
|
||||
import { UniqueID, ValidationErrorCollection } from "@repo/rdx-ddd";
|
||||
import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import type { ZodError } from "zod/v4";
|
||||
|
||||
import { CreateCustomerRequestSchema } from "../../common";
|
||||
import { toValidationErrors } from "../common/hooks/toValidationErrors";
|
||||
import type { Customer, CustomerFormData } from "../schemas";
|
||||
|
||||
import { CUSTOMERS_LIST_KEY, invalidateCustomerListCache } from "./use-customer-list-query";
|
||||
@ -15,14 +15,6 @@ type CreateCustomerPayload = {
|
||||
data: CustomerFormData;
|
||||
};
|
||||
|
||||
// Helpers de validación a errores de dominio
|
||||
export function toValidationErrors(error: ZodError<unknown>) {
|
||||
return error.issues.map((err) => ({
|
||||
field: err.path.join("."),
|
||||
message: err.message,
|
||||
}));
|
||||
}
|
||||
|
||||
export function useCreateCustomer() {
|
||||
const queryClient = useQueryClient();
|
||||
const dataSource = useDataSource();
|
||||
|
||||
@ -3,9 +3,9 @@ import { ValidationErrorCollection } from "@repo/rdx-ddd";
|
||||
import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { UpdateCustomerByIdRequestSchema } from "../../common";
|
||||
import { toValidationErrors } from "../common/hooks/toValidationErrors";
|
||||
import type { Customer, CustomerFormData } from "../schemas";
|
||||
|
||||
import { toValidationErrors } from "./use-create-customer-mutation";
|
||||
import {
|
||||
invalidateCustomerListCache,
|
||||
upsertCustomerIntoListCaches,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { CustomerSummaryPage } from "../api";
|
||||
import type { CustomerSummaryPage } from "../../common";
|
||||
import type { CustomerSummaryPageData } from "../types";
|
||||
|
||||
/**
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
import type { ListCustomersResponseDTO } from "@erp/customers/common";
|
||||
import type { ArrayElement } from "@repo/rdx-utils";
|
||||
|
||||
// Resultado de consulta con criteria (paginado, etc.)
|
||||
export type CustomerSummaryPage = Omit<ListCustomersResponseDTO, "metadata">;
|
||||
export type CustomerSummary = Omit<ArrayElement<CustomerSummaryPage>, "metadata">;
|
||||
@ -1,2 +0,0 @@
|
||||
export * from "./api-types";
|
||||
export * from "./get-customer-list.api";
|
||||
@ -2,8 +2,8 @@ import type { CriteriaDTO } from "@erp/core";
|
||||
import { useDebounce } from "@repo/rdx-ui/components";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import { useCustomerListQuery } from "../../common";
|
||||
import { CustomerSummaryDtoAdapter } from "../adapters";
|
||||
import { useCustomerListQuery } from "../hooks";
|
||||
|
||||
export const useCustomerListController = () => {
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export * from "./use-customer-list-query";
|
||||
@ -1,5 +1,4 @@
|
||||
import type { CustomerSummary } from "../../schemas";
|
||||
import type { CustomerSummaryPage } from "../api";
|
||||
import type { CustomerSummary, CustomerSummaryPage } from "../../common";
|
||||
|
||||
export type CustomerSummaryData = CustomerSummary;
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
export * from "./address-cell";
|
||||
export * from "./contact-cell";
|
||||
export * from "./initials";
|
||||
export * from "./kind-badge";
|
||||
export * from "./soft";
|
||||
export * from "./contact-cell";
|
||||
export * from "./address-cell";
|
||||
@ -1,3 +1 @@
|
||||
//export * from "./blocks";
|
||||
//export * from "./components";
|
||||
export * from "./pages";
|
||||
|
||||
@ -1,4 +1,2 @@
|
||||
export * from "./create";
|
||||
export * from "./list";
|
||||
export * from "./update";
|
||||
export * from "./view";
|
||||
|
||||
@ -9,8 +9,7 @@ import {
|
||||
NotFoundCard,
|
||||
} from "../../components";
|
||||
import { useTranslation } from "../../i18n";
|
||||
|
||||
import { useCustomerUpdateController } from "./use-customer-update-controller";
|
||||
import { useCustomerUpdateController } from "../../update/controllers/use-customer-update-page.controller";
|
||||
|
||||
export const CustomerUpdatePage = () => {
|
||||
const customerId = useUrlParamId();
|
||||
|
||||
@ -1,333 +0,0 @@
|
||||
import { PageHeader } from "@erp/core/components";
|
||||
import { useUrlParamId } from "@erp/core/hooks";
|
||||
import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import {
|
||||
Banknote,
|
||||
EditIcon,
|
||||
FileText,
|
||||
Globe,
|
||||
Languages,
|
||||
Mail,
|
||||
MapPin,
|
||||
MoreVertical,
|
||||
Phone,
|
||||
Smartphone,
|
||||
} from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { CustomerEditorSkeleton, ErrorAlert } from "../../components";
|
||||
import { useCustomerQuery } from "../../hooks";
|
||||
import { useTranslation } from "../../i18n";
|
||||
|
||||
export const CustomerViewPage = () => {
|
||||
const customerId = useUrlParamId();
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 1) Estado de carga del cliente (query)
|
||||
const {
|
||||
data: customer,
|
||||
isLoading: isLoadingCustomer,
|
||||
isError: isLoadError,
|
||||
error: loadError,
|
||||
} = useCustomerQuery(customerId, { enabled: !!customerId });
|
||||
|
||||
if (isLoadingCustomer) {
|
||||
return <CustomerEditorSkeleton />;
|
||||
}
|
||||
|
||||
if (isLoadError) {
|
||||
return (
|
||||
<>
|
||||
<AppContent>
|
||||
<ErrorAlert
|
||||
message={
|
||||
(loadError as Error)?.message ??
|
||||
t("pages.update.loadErrorMsg", "Inténtalo de nuevo más tarde.")
|
||||
}
|
||||
title={t("pages.update.loadErrorTitle", "No se pudo cargar el cliente")}
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-end">
|
||||
<BackHistoryButton />
|
||||
</div>
|
||||
</AppContent>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppHeader>
|
||||
<PageHeader
|
||||
backIcon
|
||||
description={
|
||||
<div className="mt-2 flex items-center gap-3">
|
||||
<Badge className="font-mono" variant="secondary">
|
||||
{customer?.tin}
|
||||
</Badge>
|
||||
<Badge variant="outline">{customer?.is_company ? "Empresa" : "Persona"}</Badge>
|
||||
</div>
|
||||
}
|
||||
rightSlot={
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={() => navigate("/customers/list")} size="icon" variant="outline">
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button>
|
||||
<EditIcon className="mr-2 h-4 w-4" />
|
||||
Editar
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
title={
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{customer?.name}{" "}
|
||||
{customer?.trade_name && (
|
||||
<span className="text-muted-foreground">({customer.trade_name})</span>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</AppHeader>
|
||||
<AppContent>
|
||||
{/* Main Content Grid */}
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{/* Información Básica */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg">
|
||||
<FileText className="size-5 text-primary" />
|
||||
Información Básica
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-muted-foreground">Nombre</dt>
|
||||
<dd className="mt-1 text-base text-foreground">{customer?.name}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-muted-foreground">Referencia</dt>
|
||||
<dd className="mt-1 font-mono text-base text-foreground">{customer?.reference}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-muted-foreground">Registro Legal</dt>
|
||||
<dd className="mt-1 text-base text-foreground">{customer?.legal_record}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-muted-foreground">Impuestos por Defecto</dt>
|
||||
<dd className="mt-1">
|
||||
{customer?.default_taxes.map((tax) => (
|
||||
<Badge key={tax} variant={"secondary"}>
|
||||
{tax}
|
||||
</Badge>
|
||||
))}
|
||||
</dd>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Dirección */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg">
|
||||
<MapPin className="size-5 text-primary" />
|
||||
Dirección
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-muted-foreground">Calle</dt>
|
||||
<dd className="mt-1 text-base text-foreground">
|
||||
{customer?.street}
|
||||
{customer?.street2 && (
|
||||
<>
|
||||
<br />
|
||||
{customer?.street2}
|
||||
</>
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-muted-foreground">Ciudad</dt>
|
||||
<dd className="mt-1 text-base text-foreground">{customer?.city}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-muted-foreground">Código Postal</dt>
|
||||
<dd className="mt-1 text-base text-foreground">{customer?.postal_code}</dd>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-muted-foreground">Provincia</dt>
|
||||
<dd className="mt-1 text-base text-foreground">{customer?.province}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-muted-foreground">País</dt>
|
||||
<dd className="mt-1 text-base text-foreground">{customer?.country}</dd>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Información de Contacto */}
|
||||
<Card className="md:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg">
|
||||
<Mail className="size-5 text-primary" />
|
||||
Información de Contacto
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{/* Contacto Principal */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-semibold text-foreground">Contacto Principal</h3>
|
||||
{customer?.email_primary && (
|
||||
<div className="flex items-start gap-3">
|
||||
<Mail className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<dt className="text-sm font-medium text-muted-foreground">Email</dt>
|
||||
<dd className="mt-1 text-base text-foreground">
|
||||
{customer?.email_primary}
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{customer?.mobile_primary && (
|
||||
<div className="flex items-start gap-3">
|
||||
<Smartphone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<dt className="text-sm font-medium text-muted-foreground">Móvil</dt>
|
||||
<dd className="mt-1 text-base text-foreground">
|
||||
{customer?.mobile_primary}
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{customer?.phone_primary && (
|
||||
<div className="flex items-start gap-3">
|
||||
<Phone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<dt className="text-sm font-medium text-muted-foreground">Teléfono</dt>
|
||||
<dd className="mt-1 text-base text-foreground">
|
||||
{customer?.phone_primary}
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Contacto Secundario */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-semibold text-foreground">Contacto Secundario</h3>
|
||||
{customer?.email_secondary && (
|
||||
<div className="flex items-start gap-3">
|
||||
<Mail className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<dt className="text-sm font-medium text-muted-foreground">Email</dt>
|
||||
<dd className="mt-1 text-base text-foreground">
|
||||
{customer?.email_secondary}
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{customer?.mobile_secondary && (
|
||||
<div className="flex items-start gap-3">
|
||||
<Smartphone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<dt className="text-sm font-medium text-muted-foreground">Móvil</dt>
|
||||
<dd className="mt-1 text-base text-foreground">
|
||||
{customer?.mobile_secondary}
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{customer?.phone_secondary && (
|
||||
<div className="flex items-start gap-3">
|
||||
<Phone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<dt className="text-sm font-medium text-muted-foreground">Teléfono</dt>
|
||||
<dd className="mt-1 text-base text-foreground">
|
||||
{customer?.phone_secondary}
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Otros Contactos */}
|
||||
{(customer?.website || customer?.fax) && (
|
||||
<div className="space-y-4 md:col-span-2">
|
||||
<h3 className="font-semibold text-foreground">Otros</h3>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{customer?.website && (
|
||||
<div className="flex items-start gap-3">
|
||||
<Globe className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<dt className="text-sm font-medium text-muted-foreground">Sitio Web</dt>
|
||||
<dd className="mt-1 text-base text-primary hover:underline">
|
||||
<a href={customer?.website} rel="noopener noreferrer" target="_blank">
|
||||
{customer?.website}
|
||||
</a>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{customer?.fax && (
|
||||
<div className="flex items-start gap-3">
|
||||
<Phone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<dt className="text-sm font-medium text-muted-foreground">Fax</dt>
|
||||
<dd className="mt-1 text-base text-foreground">{customer?.fax}</dd>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Preferencias */}
|
||||
<Card className="md:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg">
|
||||
<Languages className="size-5 text-primary" />
|
||||
Preferencias
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="flex items-start gap-3">
|
||||
<Languages className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<dt className="text-sm font-medium text-muted-foreground">Idioma Preferido</dt>
|
||||
<dd className="mt-1 text-base text-foreground">{customer?.language_code}</dd>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<Banknote className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<dt className="text-sm font-medium text-muted-foreground">Moneda Preferida</dt>
|
||||
<dd className="mt-1 text-base text-foreground">{customer?.currency_code}</dd>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</AppContent>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1 +0,0 @@
|
||||
export * from "./customer-view-page";
|
||||
1
modules/customers/src/web/update/controllers/index.ts
Normal file
1
modules/customers/src/web/update/controllers/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./use-customer-update-page.controller";
|
||||
@ -4,7 +4,7 @@ import { showErrorToast, showSuccessToast, showWarningToast } from "@repo/rdx-ui
|
||||
import { useEffect, useId, useMemo } from "react";
|
||||
import { type FieldErrors, FormProvider } from "react-hook-form";
|
||||
|
||||
import { useCustomerQuery, useUpdateCustomer } from "../../hooks";
|
||||
import { useCustomerGetQuery, useCustomerUpdateMutation } from "../../common";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import {
|
||||
type Customer,
|
||||
@ -34,7 +34,7 @@ export const useCustomerUpdateController = (
|
||||
isLoading,
|
||||
isError: isLoadError,
|
||||
error: loadError,
|
||||
} = useCustomerQuery(customerId, { enabled: Boolean(customerId) });
|
||||
} = useCustomerGetQuery(customerId, { enabled: Boolean(customerId) });
|
||||
|
||||
// 2) Estado de creación (mutación)
|
||||
const {
|
||||
@ -42,7 +42,7 @@ export const useCustomerUpdateController = (
|
||||
isPending: isUpdating,
|
||||
isError: isUpdateError,
|
||||
error: updateError,
|
||||
} = useUpdateCustomer();
|
||||
} = useCustomerUpdateMutation();
|
||||
|
||||
const initialValues = useMemo(() => customerData ?? defaultCustomerFormData, [customerData]);
|
||||
|
||||
2
modules/customers/src/web/update/hooks/index.ts
Normal file
2
modules/customers/src/web/update/hooks/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./use-customer-form";
|
||||
export * from "./use-customer-update-mutation";
|
||||
22
modules/customers/src/web/update/hooks/use-customer-form.ts
Normal file
22
modules/customers/src/web/update/hooks/use-customer-form.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { useHookForm } from "@erp/core/hooks";
|
||||
import { useEffect, useMemo } from "react";
|
||||
|
||||
import type { Customer } from "../api";
|
||||
|
||||
function useCustomerForm(customerData: Customer | undefined, isDisabled: boolean) {
|
||||
const initialValues = useMemo(() => customerData ?? defaultCustomerFormData, [customerData]);
|
||||
|
||||
const form = useHookForm()<CustomerFormData>({
|
||||
resolverSchema: CustomerFormSchema,
|
||||
initialValues,
|
||||
disabled: isDisabled,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (customerData) form.reset(customerData);
|
||||
}, [customerData, form]);
|
||||
|
||||
const resetForm = () => form.reset(customerData ?? defaultCustomerFormData);
|
||||
|
||||
return { form, resetForm };
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
import { useDataSource } from "@erp/core/hooks";
|
||||
import {
|
||||
type DefaultError,
|
||||
type QueryKey,
|
||||
type UseQueryResult,
|
||||
useQuery,
|
||||
} from "@tanstack/react-query";
|
||||
|
||||
import { type Customer, getCustomerById } from "../api";
|
||||
|
||||
export const CUSTOMER_QUERY_KEY = (customerId?: string): QueryKey => [
|
||||
"customers:detail",
|
||||
{
|
||||
customerId,
|
||||
},
|
||||
];
|
||||
|
||||
type CustomerQueryOptions = {
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
export const useCustomerGetQuery = (
|
||||
customerId?: string,
|
||||
options?: CustomerQueryOptions
|
||||
): UseQueryResult<Customer, DefaultError> => {
|
||||
const dataSource = useDataSource();
|
||||
const enabled = options?.enabled ?? Boolean(customerId);
|
||||
|
||||
return useQuery<Customer, DefaultError>({
|
||||
queryKey: CUSTOMER_QUERY_KEY(customerId),
|
||||
queryFn: async ({ signal }) => getCustomerById(dataSource, signal, customerId),
|
||||
enabled,
|
||||
placeholderData: (previousData, _previousQuery) => previousData, // Mantener datos previos mientras se carga nueva datos (antiguo `keepPreviousData`)
|
||||
});
|
||||
};
|
||||
@ -0,0 +1,46 @@
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
|
||||
import type { Customer } from "../api";
|
||||
|
||||
function useCustomerUpdateMutation(
|
||||
customerId?: string,
|
||||
options?: UseCustomerUpdateControllerOptions
|
||||
) {
|
||||
const queryClient = useQueryClient();
|
||||
const { mutateAsync, isPending } = useUpdateCustomer();
|
||||
|
||||
const updateCustomer = async (
|
||||
patchData: ReturnType<typeof pickFormDirtyValues>,
|
||||
previousData: Customer | undefined,
|
||||
form: UseFormReturn<CustomerFormData>
|
||||
) => {
|
||||
if (!customerId) return;
|
||||
|
||||
if (options?.undoAllowed) {
|
||||
queryClient.setQueryData(["customers", customerId], (old: Customer) => ({
|
||||
...old,
|
||||
...patchData,
|
||||
}));
|
||||
}
|
||||
|
||||
try {
|
||||
const updated = await mutateAsync({ id: customerId, data: patchData });
|
||||
|
||||
queryClient.setQueryData(["customers", customerId], updated);
|
||||
form.reset(updated);
|
||||
|
||||
options?.onUpdated?.(updated);
|
||||
} catch (error: any) {
|
||||
queryClient.setQueryData(["customers", customerId], previousData);
|
||||
form.reset(previousData);
|
||||
|
||||
options?.onError?.(error, patchData);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
updateCustomer,
|
||||
isUpdating: isPending,
|
||||
};
|
||||
}
|
||||
1
modules/customers/src/web/update/index.ts
Normal file
1
modules/customers/src/web/update/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./ui";
|
||||
1
modules/customers/src/web/update/types/index.ts
Normal file
1
modules/customers/src/web/update/types/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./types";
|
||||
3
modules/customers/src/web/update/types/types.ts
Normal file
3
modules/customers/src/web/update/types/types.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import type { Customer } from "../api";
|
||||
|
||||
export type CustomerData = Customer;
|
||||
1
modules/customers/src/web/update/ui/index.ts
Normal file
1
modules/customers/src/web/update/ui/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./pages";
|
||||
@ -0,0 +1,117 @@
|
||||
import { ErrorAlert, NotFoundCard, PageHeader } from "@erp/core/components";
|
||||
import { UnsavedChangesProvider, UpdateCommitButtonGroup, useUrlParamId } from "@erp/core/hooks";
|
||||
import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { CustomerEditForm } from "../../../components";
|
||||
import { useTranslation } from "../../../i18n";
|
||||
import { useCustomerUpdateController } from "../../controllers";
|
||||
import { CustomerEditorSkeleton } from "../components";
|
||||
|
||||
export const CustomerUpdatePage = () => {
|
||||
const initialCustomerId = useUrlParamId();
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {
|
||||
form,
|
||||
formId,
|
||||
onSubmit,
|
||||
resetForm,
|
||||
|
||||
customerData,
|
||||
isLoading,
|
||||
isLoadError,
|
||||
loadError,
|
||||
|
||||
isUpdating,
|
||||
isUpdateError,
|
||||
updateError,
|
||||
|
||||
FormProvider,
|
||||
} = useCustomerUpdateController(initialCustomerId, {});
|
||||
|
||||
if (isLoading) {
|
||||
return <CustomerEditorSkeleton />;
|
||||
}
|
||||
|
||||
if (isLoadError) {
|
||||
return (
|
||||
<>
|
||||
<AppContent>
|
||||
<ErrorAlert
|
||||
message={
|
||||
(loadError as Error)?.message ??
|
||||
t("pages.update.loadErrorMsg", "Inténtalo de nuevo más tarde.")
|
||||
}
|
||||
title={t("pages.update.loadErrorTitle", "No se pudo cargar el cliente")}
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-end">
|
||||
<BackHistoryButton />
|
||||
</div>
|
||||
</AppContent>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (!customerData)
|
||||
return (
|
||||
<>
|
||||
<AppContent>
|
||||
<NotFoundCard
|
||||
message={t("pages.update.notFoundMsg", "Revisa el identificador o vuelve al listado.")}
|
||||
title={t("pages.update.notFoundTitle", "Cliente no encontrado")}
|
||||
/>
|
||||
</AppContent>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<UnsavedChangesProvider isDirty={form.formState.isDirty}>
|
||||
<AppHeader>
|
||||
<PageHeader
|
||||
backIcon
|
||||
description={t("pages.update.description")}
|
||||
rightSlot={
|
||||
<UpdateCommitButtonGroup
|
||||
cancel={{
|
||||
formId,
|
||||
to: "/customers/list",
|
||||
disabled: isUpdating,
|
||||
}}
|
||||
disabled={isUpdating}
|
||||
isLoading={isUpdating}
|
||||
onReset={resetForm}
|
||||
submit={{
|
||||
formId,
|
||||
disabled: isUpdating,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
title={t("pages.update.title")}
|
||||
/>
|
||||
</AppHeader>
|
||||
<AppContent>
|
||||
{/* Alerta de error de actualización (si ha fallado el último intento) */}
|
||||
{isUpdateError && (
|
||||
<ErrorAlert
|
||||
message={
|
||||
(updateError as Error)?.message ??
|
||||
t("pages.update.errorMsg", "Revisa los datos e inténtalo de nuevo.")
|
||||
}
|
||||
title={t("pages.update.errorTitle", "No se pudo guardar los cambios")}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormProvider {...form}>
|
||||
<CustomerEditForm
|
||||
className="bg-white rounded-xl border shadow-xl max-w-7xl mx-auto mt-6" // para que el botón del header pueda hacer submit
|
||||
formId={formId}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</FormProvider>
|
||||
</AppContent>
|
||||
</UnsavedChangesProvider>
|
||||
);
|
||||
};
|
||||
1
modules/customers/src/web/update/ui/pages/index.ts
Normal file
1
modules/customers/src/web/update/ui/pages/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./customer-update-page";
|
||||
@ -1,4 +1,4 @@
|
||||
import type { Customer } from "../api";
|
||||
import type { Customer } from "../../common";
|
||||
import type { CustomerData } from "../types";
|
||||
|
||||
/**
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user