Importación desde FactuGES
This commit is contained in:
parent
5e4ad04314
commit
9a45e7ee9a
@ -35,8 +35,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@erp/auth": "workspace:*",
|
"@erp/auth": "workspace:*",
|
||||||
"@erp/core": "workspace:*",
|
"@erp/core": "workspace:*",
|
||||||
"@erp/customer-invoices": "workspace:*",
|
|
||||||
"@erp/customers": "workspace:*",
|
"@erp/customers": "workspace:*",
|
||||||
|
"@erp/customer-invoices": "workspace:*",
|
||||||
|
"@erp/factuges": "workspace:*",
|
||||||
"@repo/rdx-logger": "workspace:*",
|
"@repo/rdx-logger": "workspace:*",
|
||||||
"@repo/rdx-utils": "workspace:*",
|
"@repo/rdx-utils": "workspace:*",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
|
|||||||
@ -132,8 +132,11 @@ async function setupModule(name: string, params: ModuleParams, stack: string[])
|
|||||||
// 5) services (namespaced)
|
// 5) services (namespaced)
|
||||||
if (pkgApi?.services) {
|
if (pkgApi?.services) {
|
||||||
await withPhase(name, "registerServices", async () => {
|
await withPhase(name, "registerServices", async () => {
|
||||||
|
validateModuleServices(name, pkgApi.services);
|
||||||
|
|
||||||
for (const [serviceKey, serviceApi] of Object.entries(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);
|
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() {
|
function validateModuleDependencies() {
|
||||||
for (const [moduleName, pkg] of registeredModules.entries()) {
|
for (const [moduleName, pkg] of registeredModules.entries()) {
|
||||||
const declared = new Set(pkg.dependencies ?? []);
|
const declared = new Set(pkg.dependencies ?? []);
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
import customerInvoicesAPIModule from "@erp/customer-invoices/api";
|
import customerInvoicesAPIModule from "@erp/customer-invoices/api";
|
||||||
|
|
||||||
//import verifactuAPIModule from "@erp/verifactu/api";
|
|
||||||
|
|
||||||
import customersAPIModule from "@erp/customers/api";
|
import customersAPIModule from "@erp/customers/api";
|
||||||
|
import factuGESAPIModule from "@erp/factuges/api";
|
||||||
|
|
||||||
import { registerModule } from "./lib";
|
import { registerModule } from "./lib";
|
||||||
|
|
||||||
@ -10,5 +8,5 @@ export const registerModules = () => {
|
|||||||
//registerModule(authAPIModule);
|
//registerModule(authAPIModule);
|
||||||
registerModule(customersAPIModule);
|
registerModule(customersAPIModule);
|
||||||
registerModule(customerInvoicesAPIModule);
|
registerModule(customerInvoicesAPIModule);
|
||||||
//registerModule(verifactuAPIModule);
|
registerModule(factuGESAPIModule);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -140,7 +140,7 @@
|
|||||||
"noControlCharactersInRegex": "error",
|
"noControlCharactersInRegex": "error",
|
||||||
"noDoubleEquals": "error",
|
"noDoubleEquals": "error",
|
||||||
"noDuplicateCase": "error",
|
"noDuplicateCase": "error",
|
||||||
"noEmptyBlockStatements": "error",
|
"noEmptyBlockStatements": "off",
|
||||||
"noFallthroughSwitchClause": "error",
|
"noFallthroughSwitchClause": "error",
|
||||||
"noFunctionAssign": "error",
|
"noFunctionAssign": "error",
|
||||||
"noGlobalAssign": "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> = [
|
const defaultRules: ReadonlyArray<ErrorToApiRule> = [
|
||||||
// 1) Validación múltiple (colección)
|
// 1) Validación múltiple (colección)
|
||||||
@ -165,7 +165,7 @@ const defaultRules: ReadonlyArray<ErrorToApiRule> = [
|
|||||||
|
|
||||||
// 5.5) Errores de FastReport inesperados
|
// 5.5) Errores de FastReport inesperados
|
||||||
{
|
{
|
||||||
priority: 55,
|
priority: 56,
|
||||||
matches: (e) => isDocumentGenerationError(e),
|
matches: (e) => isDocumentGenerationError(e),
|
||||||
build: (e) => {
|
build: (e) => {
|
||||||
const error = e as DocumentGenerationError;
|
const error = e as DocumentGenerationError;
|
||||||
@ -178,6 +178,7 @@ const defaultRules: ReadonlyArray<ErrorToApiRule> = [
|
|||||||
return new InternalApiError(cause.message, title);
|
return new InternalApiError(cause.message, title);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
priority: 55,
|
priority: 55,
|
||||||
matches: (e) => isFastReportError(e),
|
matches: (e) => isFastReportError(e),
|
||||||
@ -198,10 +199,11 @@ const defaultRules: ReadonlyArray<ErrorToApiRule> = [
|
|||||||
|
|
||||||
// 7) Autenticación/autorización por nombre (si no tienes clases dedicadas)
|
// 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",
|
matches: (e): e is Error => e instanceof Error && e.name === "UnauthorizedError",
|
||||||
build: (e) => new UnauthorizedApiError((e as Error).message || "Unauthorized"),
|
build: (e) => new UnauthorizedApiError((e as Error).message || "Unauthorized"),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
priority: 40,
|
priority: 40,
|
||||||
matches: (e): e is Error => e instanceof Error && e.name === "ForbiddenError",
|
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
|
return e; // ya es un ApiError
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = typeof (e as any)?.message === "string" ? (e as any).message : "";
|
const message =
|
||||||
const detail = typeof (e as any)?.detail === "string" ? (e as any).detail : "";
|
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>;
|
internal?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SetupParams = ModuleParams & {
|
export type SetupParams = ModuleParams;
|
||||||
registerService: (name: string, api: unknown) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type StartParams = ModuleParams & {
|
export type StartParams = ModuleParams & {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
export * from "./error-alert";
|
export * from "./error-alert";
|
||||||
export * from "./form";
|
export * from "./form";
|
||||||
|
export * from "./not-found-card";
|
||||||
export * from "./page-header";
|
export * from "./page-header";
|
||||||
|
|||||||
@ -5,6 +5,7 @@ export * from "./use-pagination";
|
|||||||
export * from "./use-percentage";
|
export * from "./use-percentage";
|
||||||
export * from "./use-quantity";
|
export * from "./use-quantity";
|
||||||
export * from "./use-query-key";
|
export * from "./use-query-key";
|
||||||
|
export * from "./use-rhf-error-focus";
|
||||||
export * from "./use-toggle";
|
export * from "./use-toggle";
|
||||||
export * from "./use-unsaved-changes-notifier";
|
export * from "./use-unsaved-changes-notifier";
|
||||||
export * from "./use-url-param-id";
|
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",
|
".": "./src/common/index.ts",
|
||||||
"./common": "./src/common/index.ts",
|
"./common": "./src/common/index.ts",
|
||||||
"./api": "./src/api/index.ts",
|
"./api": "./src/api/index.ts",
|
||||||
|
"./api/domain": "./src/api/domain/index.ts",
|
||||||
"./client": "./src/web/manifest.ts",
|
"./client": "./src/web/manifest.ts",
|
||||||
"./globals.css": "./src/web/globals.css"
|
"./globals.css": "./src/web/globals.css"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -10,7 +10,12 @@ import {
|
|||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import type { CreateCustomerInvoiceRequestDTO } from "../../../common";
|
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";
|
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));
|
return Result.fail(new ValidationErrorCollection("Invoice dto mapping failed", errors));
|
||||||
}
|
}
|
||||||
|
|
||||||
const invoiceProps: IProformaProps = {
|
const invoiceProps: IProformaCreateProps = {
|
||||||
invoiceNumber: invoiceNumber!,
|
invoiceNumber: invoiceNumber!,
|
||||||
series: invoiceSeries!,
|
series: invoiceSeries!,
|
||||||
invoiceDate: invoiceDate!,
|
invoiceDate: invoiceDate!,
|
||||||
operationDate: operationDate!,
|
operationDate: operationDate!,
|
||||||
status: InvoiceStatus.createDraft(),
|
status: InvoiceStatus.fromDraft(),
|
||||||
currencyCode: currencyCode!,
|
currencyCode: currencyCode!,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type { UniqueID } from "@repo/rdx-ddd";
|
import type { UniqueID } from "@repo/rdx-ddd";
|
||||||
import type { Result } from "@repo/rdx-utils";
|
import type { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import type { IProformaProps, Proforma } from "../../../domain";
|
import type { IProformaCreateProps, Proforma } from "../../../domain";
|
||||||
|
|
||||||
export interface IProformaFactory {
|
export interface IProformaFactory {
|
||||||
/**
|
/**
|
||||||
@ -11,7 +11,7 @@ export interface IProformaFactory {
|
|||||||
*/
|
*/
|
||||||
createProforma(
|
createProforma(
|
||||||
companyId: UniqueID,
|
companyId: UniqueID,
|
||||||
props: Omit<IProformaProps, "companyId">,
|
props: Omit<IProformaCreateProps, "companyId">,
|
||||||
proformaId?: UniqueID
|
proformaId?: UniqueID
|
||||||
): Result<Proforma, Error>;
|
): Result<Proforma, Error>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
import type { UniqueID } from "@repo/rdx-ddd";
|
import type { UniqueID } from "@repo/rdx-ddd";
|
||||||
import type { Result } from "@repo/rdx-utils";
|
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";
|
import type { IProformaFactory } from "./proforma-factory.interface";
|
||||||
|
|
||||||
export class ProformaFactory implements IProformaFactory {
|
export class ProformaFactory implements IProformaFactory {
|
||||||
createProforma(
|
createProforma(
|
||||||
companyId: UniqueID,
|
companyId: UniqueID,
|
||||||
props: Omit<IProformaProps, "companyId">,
|
props: Omit<IProformaCreateProps, "companyId">,
|
||||||
proformaId?: UniqueID
|
proformaId?: UniqueID
|
||||||
): Result<Proforma, Error> {
|
): Result<Proforma, Error> {
|
||||||
return Proforma.create({ ...props, companyId }, proformaId);
|
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 { CreateProformaItemRequestDTO, CreateProformaRequestDTO } from "../../../../common";
|
||||||
import {
|
import {
|
||||||
|
type IProformaCreateProps,
|
||||||
type IProformaItemProps,
|
type IProformaItemProps,
|
||||||
type IProformaProps,
|
|
||||||
InvoiceNumber,
|
InvoiceNumber,
|
||||||
InvoicePaymentMethod,
|
InvoicePaymentMethod,
|
||||||
type InvoiceRecipient,
|
type InvoiceRecipient,
|
||||||
@ -51,7 +51,7 @@ export interface ICreateProformaInputMapper {
|
|||||||
map(
|
map(
|
||||||
dto: CreateProformaRequestDTO,
|
dto: CreateProformaRequestDTO,
|
||||||
params: { companyId: UniqueID }
|
params: { companyId: UniqueID }
|
||||||
): Result<{ id: UniqueID; props: IProformaProps }>;
|
): Result<{ id: UniqueID; props: IProformaCreateProps }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/ {
|
export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/ {
|
||||||
@ -64,12 +64,12 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/
|
|||||||
public map(
|
public map(
|
||||||
dto: CreateProformaRequestDTO,
|
dto: CreateProformaRequestDTO,
|
||||||
params: { companyId: UniqueID }
|
params: { companyId: UniqueID }
|
||||||
): Result<{ id: UniqueID; props: IProformaProps }> {
|
): Result<{ id: UniqueID; props: IProformaCreateProps }> {
|
||||||
const errors: ValidationErrorDetail[] = [];
|
const errors: ValidationErrorDetail[] = [];
|
||||||
const { companyId } = params;
|
const { companyId } = params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const defaultStatus = InvoiceStatus.createDraft();
|
const defaultStatus = InvoiceStatus.fromDraft();
|
||||||
|
|
||||||
const proformaId = extractOrPushError(UniqueID.create(dto.id), "id", errors);
|
const proformaId = extractOrPushError(UniqueID.create(dto.id), "id", errors);
|
||||||
|
|
||||||
@ -159,13 +159,9 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/
|
|||||||
errors,
|
errors,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (errors.length > 0) {
|
this.throwIfValidationErrors(errors);
|
||||||
return Result.fail(
|
|
||||||
new ValidationErrorCollection("Customer invoice props mapping failed", errors)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const props: IProformaProps = {
|
const props: IProformaCreateProps = {
|
||||||
companyId,
|
companyId,
|
||||||
status: defaultStatus,
|
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(
|
private mapItemsProps(
|
||||||
dto: CreateProformaRequestDTO,
|
dto: CreateProformaRequestDTO,
|
||||||
params: {
|
params: {
|
||||||
@ -241,6 +243,8 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/
|
|||||||
errors: params.errors,
|
errors: params.errors,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.throwIfValidationErrors(params.errors);
|
||||||
|
|
||||||
itemsProps.push({
|
itemsProps.push({
|
||||||
globalDiscountPercentage: params.globalDiscountPercentage,
|
globalDiscountPercentage: params.globalDiscountPercentage,
|
||||||
languageCode: params.languageCode,
|
languageCode: params.languageCode,
|
||||||
@ -321,6 +325,8 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.throwIfValidationErrors(errors);
|
||||||
|
|
||||||
return taxesProps;
|
return taxesProps;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,19 +2,21 @@ import type { UniqueID } from "@repo/rdx-ddd";
|
|||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
import type { Transaction } from "sequelize";
|
import type { Transaction } from "sequelize";
|
||||||
|
|
||||||
import type { IProformaProps, Proforma } from "../../../domain";
|
import type { IProformaCreateProps, Proforma } from "../../../domain";
|
||||||
import type { IProformaFactory } from "../factories";
|
import type { IProformaFactory } from "../factories";
|
||||||
import type { IProformaRepository } from "../repositories";
|
import type { IProformaRepository } from "../repositories";
|
||||||
|
|
||||||
import type { IProformaNumberGenerator } from "./proforma-number-generator.interface";
|
import type { IProformaNumberGenerator } from "./proforma-number-generator.interface";
|
||||||
|
|
||||||
|
export interface IProformaCreatorParams {
|
||||||
|
companyId: UniqueID;
|
||||||
|
id: UniqueID;
|
||||||
|
props: Omit<IProformaCreateProps, "invoiceNumber">;
|
||||||
|
transaction: Transaction;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IProformaCreator {
|
export interface IProformaCreator {
|
||||||
create(params: {
|
create(params: IProformaCreatorParams): Promise<Result<Proforma, Error>>;
|
||||||
companyId: UniqueID;
|
|
||||||
id: UniqueID;
|
|
||||||
props: IProformaProps;
|
|
||||||
transaction: Transaction;
|
|
||||||
}): Promise<Result<Proforma, Error>>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProformaCreatorDeps = {
|
type ProformaCreatorDeps = {
|
||||||
@ -37,7 +39,7 @@ export class ProformaCreator implements IProformaCreator {
|
|||||||
async create(params: {
|
async create(params: {
|
||||||
companyId: UniqueID;
|
companyId: UniqueID;
|
||||||
id: UniqueID;
|
id: UniqueID;
|
||||||
props: IProformaProps;
|
props: IProformaCreateProps;
|
||||||
transaction: Transaction;
|
transaction: Transaction;
|
||||||
}): Promise<Result<Proforma, Error>> {
|
}): Promise<Result<Proforma, Error>> {
|
||||||
const { companyId, id, props, transaction } = params;
|
const { companyId, id, props, transaction } = params;
|
||||||
|
|||||||
@ -39,34 +39,34 @@ export class InvoiceStatus extends ValueObject<IInvoiceStatusProps> {
|
|||||||
|
|
||||||
return Result.ok(
|
return Result.ok(
|
||||||
value === "rejected"
|
value === "rejected"
|
||||||
? InvoiceStatus.createRejected()
|
? InvoiceStatus.fromRejected()
|
||||||
: value === "sent"
|
: value === "sent"
|
||||||
? InvoiceStatus.createSent()
|
? InvoiceStatus.fromSent()
|
||||||
: value === "issued"
|
: value === "issued"
|
||||||
? InvoiceStatus.createIssued()
|
? InvoiceStatus.fromIssued()
|
||||||
: value === "approved"
|
: value === "approved"
|
||||||
? InvoiceStatus.createApproved()
|
? InvoiceStatus.fromApproved()
|
||||||
: InvoiceStatus.createDraft()
|
: InvoiceStatus.fromDraft()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static createDraft(): InvoiceStatus {
|
public static fromDraft(): InvoiceStatus {
|
||||||
return new InvoiceStatus({ value: INVOICE_STATUS.DRAFT });
|
return new InvoiceStatus({ value: INVOICE_STATUS.DRAFT });
|
||||||
}
|
}
|
||||||
|
|
||||||
public static createIssued(): InvoiceStatus {
|
public static fromIssued(): InvoiceStatus {
|
||||||
return new InvoiceStatus({ value: INVOICE_STATUS.ISSUED });
|
return new InvoiceStatus({ value: INVOICE_STATUS.ISSUED });
|
||||||
}
|
}
|
||||||
|
|
||||||
public static createSent(): InvoiceStatus {
|
public static fromSent(): InvoiceStatus {
|
||||||
return new InvoiceStatus({ value: INVOICE_STATUS.SENT });
|
return new InvoiceStatus({ value: INVOICE_STATUS.SENT });
|
||||||
}
|
}
|
||||||
|
|
||||||
public static createApproved(): InvoiceStatus {
|
public static fromApproved(): InvoiceStatus {
|
||||||
return new InvoiceStatus({ value: INVOICE_STATUS.APPROVED });
|
return new InvoiceStatus({ value: INVOICE_STATUS.APPROVED });
|
||||||
}
|
}
|
||||||
|
|
||||||
public static createRejected(): InvoiceStatus {
|
public static fromRejected(): InvoiceStatus {
|
||||||
return new InvoiceStatus({ value: INVOICE_STATUS.REJECTED });
|
return new InvoiceStatus({ value: INVOICE_STATUS.REJECTED });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -30,7 +30,7 @@ import { ProformaItemMismatch } from "../errors";
|
|||||||
import { type IProformaTaxTotals, ProformaTaxesCalculator } from "../services";
|
import { type IProformaTaxTotals, ProformaTaxesCalculator } from "../services";
|
||||||
import { ProformaItemTaxes } from "../value-objects";
|
import { ProformaItemTaxes } from "../value-objects";
|
||||||
|
|
||||||
export interface IProformaProps {
|
export interface IProformaCreateProps {
|
||||||
companyId: UniqueID;
|
companyId: UniqueID;
|
||||||
status: InvoiceStatus;
|
status: InvoiceStatus;
|
||||||
|
|
||||||
@ -100,16 +100,15 @@ export interface IProforma {
|
|||||||
totals(): IProformaTotals;
|
totals(): IProformaTotals;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProformaPatchProps = Partial<Omit<IProformaProps, "companyId" | "items">> & {
|
export type ProformaPatchProps = Partial<Omit<IProformaCreateProps, "companyId" | "items">> & {
|
||||||
//items?: ProformaItems;
|
//items?: ProformaItems;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CreateProformaProps = IProformaProps;
|
type InternalProformaProps = Omit<IProformaCreateProps, "items">;
|
||||||
type InternalProformaProps = Omit<IProformaProps, "items">;
|
|
||||||
|
|
||||||
export class Proforma extends AggregateRoot<InternalProformaProps> implements IProforma {
|
export class Proforma extends AggregateRoot<InternalProformaProps> implements IProforma {
|
||||||
// Creación funcional
|
// 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 { items, ...internalProps } = props;
|
||||||
const proforma = new Proforma(internalProps, id);
|
const proforma = new Proforma(internalProps, id);
|
||||||
|
|
||||||
@ -221,12 +220,12 @@ export class Proforma extends AggregateRoot<InternalProformaProps> implements IP
|
|||||||
|
|
||||||
// Mutabilidad
|
// Mutabilidad
|
||||||
public update(
|
public update(
|
||||||
partialProforma: Partial<Omit<IProformaProps, "companyId">>
|
partialProforma: Partial<Omit<IProformaCreateProps, "companyId">>
|
||||||
): Result<Proforma, Error> {
|
): Result<Proforma, Error> {
|
||||||
const updatedProps = {
|
const updatedProps = {
|
||||||
...this.props,
|
...this.props,
|
||||||
...partialProforma,
|
...partialProforma,
|
||||||
} as IProformaProps;
|
} as IProformaCreateProps;
|
||||||
|
|
||||||
return Proforma.create(updatedProps, this.id);
|
return Proforma.create(updatedProps, this.id);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -67,7 +67,7 @@ export class IssueCustomerInvoiceDomainService {
|
|||||||
...proformaProps,
|
...proformaProps,
|
||||||
isProforma: false,
|
isProforma: false,
|
||||||
proformaId: Maybe.some(proforma.id),
|
proformaId: Maybe.some(proforma.id),
|
||||||
status: InvoiceStatus.createIssued(),
|
status: InvoiceStatus.fromIssued(),
|
||||||
invoiceNumber: issueNumber,
|
invoiceNumber: issueNumber,
|
||||||
invoiceDate: issueDate,
|
invoiceDate: issueDate,
|
||||||
description: proformaProps.description.isNone() ? Maybe.some(".") : proformaProps.description,
|
description: proformaProps.description.isNone() ? Maybe.some(".") : proformaProps.description,
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import type { IModuleServer } from "@erp/core/api";
|
import type { IModuleServer } from "@erp/core/api";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
type IssuedInvoicePublicServices,
|
||||||
type IssuedInvoicesInternalDeps,
|
type IssuedInvoicesInternalDeps,
|
||||||
|
type ProformaPublicServices,
|
||||||
type ProformasInternalDeps,
|
type ProformasInternalDeps,
|
||||||
buildIssuedInvoiceServices,
|
buildIssuedInvoiceServices,
|
||||||
buildIssuedInvoicesDependencies,
|
buildIssuedInvoicesDependencies,
|
||||||
@ -12,6 +14,8 @@ import {
|
|||||||
proformasRouter,
|
proformasRouter,
|
||||||
} from "./infrastructure";
|
} from "./infrastructure";
|
||||||
|
|
||||||
|
export type { IssuedInvoicePublicServices, ProformaPublicServices };
|
||||||
|
|
||||||
export const customerInvoicesAPIModule: IModuleServer = {
|
export const customerInvoicesAPIModule: IModuleServer = {
|
||||||
name: "customer-invoices",
|
name: "customer-invoices",
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
@ -28,12 +32,18 @@ export const customerInvoicesAPIModule: IModuleServer = {
|
|||||||
const { env: ENV, app, database, baseRoutePath: API_BASE_PATH, logger } = params;
|
const { env: ENV, app, database, baseRoutePath: API_BASE_PATH, logger } = params;
|
||||||
|
|
||||||
// 1) Dominio interno
|
// 1) Dominio interno
|
||||||
const issuedInvoicesInternalDeps = buildIssuedInvoicesDependencies(params);
|
const issuedInvoicesInternal = buildIssuedInvoicesDependencies(params);
|
||||||
const proformasInternalDeps = buildProformasDependencies(params);
|
const proformasInternal = buildProformasDependencies(params);
|
||||||
|
|
||||||
// 2) Servicios públicos (Application Services)
|
// 2) Servicios públicos (Application Services)
|
||||||
const issuedInvoicesServices = buildIssuedInvoiceServices(issuedInvoicesInternalDeps);
|
const issuedInvoicesServices: IssuedInvoicePublicServices = buildIssuedInvoiceServices(
|
||||||
const proformasServices = buildProformaServices(proformasInternalDeps);
|
params,
|
||||||
|
issuedInvoicesInternal
|
||||||
|
);
|
||||||
|
const proformasServices: ProformaPublicServices = buildProformaServices(
|
||||||
|
params,
|
||||||
|
proformasInternal
|
||||||
|
);
|
||||||
|
|
||||||
logger.info("🚀 CustomerInvoices module dependencies registered", { label: this.name });
|
logger.info("🚀 CustomerInvoices module dependencies registered", { label: this.name });
|
||||||
|
|
||||||
@ -43,14 +53,14 @@ export const customerInvoicesAPIModule: IModuleServer = {
|
|||||||
|
|
||||||
// Servicios expuestos a otros módulos
|
// Servicios expuestos a otros módulos
|
||||||
services: {
|
services: {
|
||||||
issuedInvoices: issuedInvoicesServices,
|
issuedInvoices: issuedInvoicesServices, // 'customer-invoices:issuedInvoices'
|
||||||
proformas: proformasServices,
|
proformas: proformasServices, // 'customer-invoices:proformas'
|
||||||
},
|
},
|
||||||
|
|
||||||
// Implementación privada del módulo
|
// Implementación privada del módulo
|
||||||
internal: {
|
internal: {
|
||||||
issuedInvoices: issuedInvoicesInternalDeps,
|
issuedInvoices: issuedInvoicesInternal,
|
||||||
proformas: proformasInternalDeps,
|
proformas: proformasInternal,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import {
|
|||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type IProformaProps,
|
type IProformaCreateProps,
|
||||||
IssuedInvoiceItem,
|
IssuedInvoiceItem,
|
||||||
type IssuedInvoiceItemProps,
|
type IssuedInvoiceItemProps,
|
||||||
ItemAmount,
|
ItemAmount,
|
||||||
@ -68,7 +68,7 @@ export class CustomerInvoiceItemDomainMapper
|
|||||||
const { errors, index, attributes } = params as {
|
const { errors, index, attributes } = params as {
|
||||||
index: number;
|
index: number;
|
||||||
errors: ValidationErrorDetail[];
|
errors: ValidationErrorDetail[];
|
||||||
attributes: Partial<IProformaProps>;
|
attributes: Partial<IProformaCreateProps>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const itemId = extractOrPushError(
|
const itemId = extractOrPushError(
|
||||||
@ -163,7 +163,7 @@ export class CustomerInvoiceItemDomainMapper
|
|||||||
const { errors, index } = params as {
|
const { errors, index } = params as {
|
||||||
index: number;
|
index: number;
|
||||||
errors: ValidationErrorDetail[];
|
errors: ValidationErrorDetail[];
|
||||||
attributes: Partial<IProformaProps>;
|
attributes: Partial<IProformaCreateProps>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 1) Valores escalares (atributos generales)
|
// 1) Valores escalares (atributos generales)
|
||||||
|
|||||||
@ -20,7 +20,7 @@ import { Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
CustomerInvoiceItems,
|
CustomerInvoiceItems,
|
||||||
type IProformaProps,
|
type IProformaCreateProps,
|
||||||
InvoiceNumber,
|
InvoiceNumber,
|
||||||
InvoicePaymentMethod,
|
InvoicePaymentMethod,
|
||||||
InvoiceSerie,
|
InvoiceSerie,
|
||||||
@ -249,7 +249,7 @@ export class CustomerInvoiceDomainMapper
|
|||||||
items: itemsResults.data.getAll(),
|
items: itemsResults.data.getAll(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const invoiceProps: IProformaProps = {
|
const invoiceProps: IProformaCreateProps = {
|
||||||
companyId: attributes.companyId!,
|
companyId: attributes.companyId!,
|
||||||
|
|
||||||
isProforma: attributes.isProforma,
|
isProforma: attributes.isProforma,
|
||||||
|
|||||||
@ -15,7 +15,11 @@ import {
|
|||||||
} from "@repo/rdx-ddd";
|
} from "@repo/rdx-ddd";
|
||||||
import { Maybe, Result } from "@repo/rdx-utils";
|
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";
|
import type { CustomerInvoiceModel } from "../../../../sequelize";
|
||||||
|
|
||||||
export class InvoiceRecipientDomainMapper {
|
export class InvoiceRecipientDomainMapper {
|
||||||
@ -30,7 +34,7 @@ export class InvoiceRecipientDomainMapper {
|
|||||||
|
|
||||||
const { errors, attributes } = params as {
|
const { errors, attributes } = params as {
|
||||||
errors: ValidationErrorDetail[];
|
errors: ValidationErrorDetail[];
|
||||||
attributes: Partial<IProformaProps>;
|
attributes: Partial<IProformaCreateProps>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const { isProforma } = attributes;
|
const { isProforma } = attributes;
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import {
|
|||||||
import { Maybe, Result } from "@repo/rdx-utils";
|
import { Maybe, Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type IProformaProps,
|
type IProformaCreateProps,
|
||||||
type Proforma,
|
type Proforma,
|
||||||
VerifactuRecord,
|
VerifactuRecord,
|
||||||
VerifactuRecordEstado,
|
VerifactuRecordEstado,
|
||||||
@ -43,7 +43,7 @@ export class CustomerInvoiceVerifactuDomainMapper
|
|||||||
): Result<Maybe<VerifactuRecord>, Error> {
|
): Result<Maybe<VerifactuRecord>, Error> {
|
||||||
const { errors, attributes } = params as {
|
const { errors, attributes } = params as {
|
||||||
errors: ValidationErrorDetail[];
|
errors: ValidationErrorDetail[];
|
||||||
attributes: Partial<IProformaProps>;
|
attributes: Partial<IProformaCreateProps>;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!source) {
|
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 "./common/persistence";
|
||||||
export * from "./issued-invoices";
|
export * from "./issued-invoices";
|
||||||
export * from "./proformas";
|
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";
|
import type { IssuedInvoicesInternalDeps } from "./issued-invoices.di";
|
||||||
|
|
||||||
export type IssuedInvoicesServiceslDeps = {
|
export type IssuedInvoicePublicServices = {
|
||||||
services: {
|
listIssuedInvoices: (filters: unknown, context: unknown) => null;
|
||||||
listIssuedInvoices: (filters: unknown, context: unknown) => null;
|
getIssuedInvoiceById: (id: unknown, context: unknown) => null;
|
||||||
getIssuedInvoiceById: (id: unknown, context: unknown) => null;
|
generateIssuedInvoiceReport: (id: unknown, options: unknown, context: unknown) => null;
|
||||||
generateIssuedInvoiceReport: (id: unknown, options: unknown, context: unknown) => null;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function buildIssuedInvoiceServices(
|
export function buildIssuedInvoiceServices(
|
||||||
|
params: SetupParams,
|
||||||
deps: IssuedInvoicesInternalDeps
|
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 {
|
return {
|
||||||
services: {
|
listIssuedInvoices: (filters, context) => null,
|
||||||
listIssuedInvoices: (filters, context) => null,
|
//internal.useCases.listIssuedInvoices().execute(filters, context),
|
||||||
//internal.useCases.listIssuedInvoices().execute(filters, context),
|
|
||||||
|
|
||||||
getIssuedInvoiceById: (id, context) => null,
|
getIssuedInvoiceById: (id, context) => null,
|
||||||
//internal.useCases.getIssuedInvoiceById().execute(id, context),
|
//internal.useCases.getIssuedInvoiceById().execute(id, context),
|
||||||
|
|
||||||
generateIssuedInvoiceReport: (id, options, context) => null,
|
generateIssuedInvoiceReport: (id, options, context) => null,
|
||||||
//internal.useCases.reportIssuedInvoice().execute(id, options, context),
|
//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";
|
import type { ProformasInternalDeps } from "./proformas.di";
|
||||||
|
|
||||||
export type ProformasServicesDeps = {
|
type ProformaServicesContext = {
|
||||||
services: {
|
transaction: Transaction;
|
||||||
listProformas: (filters: unknown, context: unknown) => null;
|
companyId: UniqueID;
|
||||||
getProformaById: (id: unknown, context: unknown) => null;
|
|
||||||
generateProformaReport: (id: unknown, options: unknown, context: unknown) => null;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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 {
|
return {
|
||||||
services: {
|
createProforma: async (
|
||||||
listProformas: (filters, context) => null,
|
id: UniqueID,
|
||||||
//internal.useCases.listProformas().execute(filters, context),
|
props: IProformaCreatorParams["props"],
|
||||||
|
context: ProformaServicesContext
|
||||||
|
) => {
|
||||||
|
const { transaction, companyId } = context;
|
||||||
|
|
||||||
getProformaById: (id, context) => null,
|
const createResult = await creator.create({ companyId, id, props, transaction });
|
||||||
//internal.useCases.getProformaById().execute(id, context),
|
|
||||||
|
|
||||||
generateProformaReport: (id, options, context) => null,
|
if (createResult.isFailure) {
|
||||||
//internal.useCases.reportProforma().execute(id, options, context),
|
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 { CreateProformaItemRequestDTO, CreateProformaRequestDTO } from "../../../../../common";
|
||||||
import {
|
import {
|
||||||
|
type IProformaCreateProps,
|
||||||
type IProformaItemProps,
|
type IProformaItemProps,
|
||||||
type IProformaProps,
|
|
||||||
InvoiceNumber,
|
InvoiceNumber,
|
||||||
InvoicePaymentMethod,
|
InvoicePaymentMethod,
|
||||||
type InvoiceRecipient,
|
type InvoiceRecipient,
|
||||||
@ -41,7 +41,6 @@ import {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
export class CreateProformaRequestMapper {
|
export class CreateProformaRequestMapper {
|
||||||
private readonly taxCatalog: JsonTaxCatalogProvider;
|
private readonly taxCatalog: JsonTaxCatalogProvider;
|
||||||
private errors: ValidationErrorDetail[] = [];
|
private errors: ValidationErrorDetail[] = [];
|
||||||
@ -58,7 +57,7 @@ export class CreateProformaRequestMapper {
|
|||||||
try {
|
try {
|
||||||
this.errors = [];
|
this.errors = [];
|
||||||
|
|
||||||
const defaultStatus = InvoiceStatus.createDraft();
|
const defaultStatus = InvoiceStatus.fromDraft();
|
||||||
|
|
||||||
const proformaId = extractOrPushError(UniqueID.create(dto.id), "id", this.errors);
|
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,
|
companyId,
|
||||||
status: defaultStatus!,
|
status: defaultStatus!,
|
||||||
|
|
||||||
@ -182,7 +181,7 @@ export class CreateProformaRequestMapper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapItems(items: CreateProformaItemRequestDTO[]): IProformaItemProps[] {
|
private mapItems(items: CreateProformaItemRequestDTO[]): IProformaItemProps[] {
|
||||||
const proformaItems = CustomerInvoiceItems.create({
|
const proformaItems = CustomerInvoiceItems.create({
|
||||||
currencyCode: this.currencyCode!,
|
currencyCode: this.currencyCode!,
|
||||||
languageCode: this.languageCode!,
|
languageCode: this.languageCode!,
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import {
|
|||||||
import { Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils";
|
import { Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type IProformaProps,
|
type IProformaCreateProps,
|
||||||
InvoiceNumber,
|
InvoiceNumber,
|
||||||
InvoicePaymentMethod,
|
InvoicePaymentMethod,
|
||||||
InvoiceSerie,
|
InvoiceSerie,
|
||||||
@ -217,7 +217,7 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
|
|||||||
items: itemsResults.data.getAll(),
|
items: itemsResults.data.getAll(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const invoiceProps: IProformaProps = {
|
const invoiceProps: IProformaCreateProps = {
|
||||||
companyId: attributes.companyId!,
|
companyId: attributes.companyId!,
|
||||||
|
|
||||||
status: attributes.status!,
|
status: attributes.status!,
|
||||||
|
|||||||
@ -16,8 +16,8 @@ import {
|
|||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
type IProformaCreateProps,
|
||||||
type IProformaItemProps,
|
type IProformaItemProps,
|
||||||
type IProformaProps,
|
|
||||||
ItemAmount,
|
ItemAmount,
|
||||||
ItemDescription,
|
ItemDescription,
|
||||||
ItemQuantity,
|
ItemQuantity,
|
||||||
@ -58,7 +58,7 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
|
|||||||
const { errors, index, parent } = params as {
|
const { errors, index, parent } = params as {
|
||||||
index: number;
|
index: number;
|
||||||
errors: ValidationErrorDetail[];
|
errors: ValidationErrorDetail[];
|
||||||
parent: Partial<IProformaProps>;
|
parent: Partial<IProformaCreateProps>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const itemId = extractOrPushError(
|
const itemId = extractOrPushError(
|
||||||
@ -139,7 +139,7 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
|
|||||||
const { errors, index } = params as {
|
const { errors, index } = params as {
|
||||||
index: number;
|
index: number;
|
||||||
errors: ValidationErrorDetail[];
|
errors: ValidationErrorDetail[];
|
||||||
parent: Partial<IProformaProps>;
|
parent: Partial<IProformaCreateProps>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 1) Valores escalares (atributos generales)
|
// 1) Valores escalares (atributos generales)
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import {
|
|||||||
} from "@repo/rdx-ddd";
|
} from "@repo/rdx-ddd";
|
||||||
import { Maybe, Result } from "@repo/rdx-utils";
|
import { Maybe, Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import { type IProformaProps, InvoiceRecipient } from "../../../../../../domain";
|
import { type IProformaCreateProps, InvoiceRecipient } from "../../../../../../domain";
|
||||||
import type { CustomerInvoiceModel } from "../../../../../common";
|
import type { CustomerInvoiceModel } from "../../../../../common";
|
||||||
|
|
||||||
export class SequelizeProformaRecipientDomainMapper {
|
export class SequelizeProformaRecipientDomainMapper {
|
||||||
@ -28,7 +28,7 @@ export class SequelizeProformaRecipientDomainMapper {
|
|||||||
|
|
||||||
const { errors, parent } = params as {
|
const { errors, parent } = params as {
|
||||||
errors: ValidationErrorDetail[];
|
errors: ValidationErrorDetail[];
|
||||||
parent: Partial<IProformaProps>;
|
parent: Partial<IProformaCreateProps>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/* if (!source.current_customer) {
|
/* if (!source.current_customer) {
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { PageHeader, SimpleSearchInput } from "@erp/core/components";
|
import { ErrorAlert, PageHeader, SimpleSearchInput } from "@erp/core/components";
|
||||||
import { ErrorAlert } from "@erp/customers/components";
|
|
||||||
import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components";
|
import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
|||||||
@ -28,11 +28,6 @@
|
|||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true
|
"noUncheckedSideEffectImports": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["src"],
|
||||||
"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"
|
|
||||||
],
|
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,7 @@
|
|||||||
".": "./src/common/index.ts",
|
".": "./src/common/index.ts",
|
||||||
"./common": "./src/common/index.ts",
|
"./common": "./src/common/index.ts",
|
||||||
"./api": "./src/api/index.ts",
|
"./api": "./src/api/index.ts",
|
||||||
|
"./api/domain": "./src/api/domain/index.ts",
|
||||||
"./client": "./src/web/manifest.ts",
|
"./client": "./src/web/manifest.ts",
|
||||||
"./globals.css": "./src/web/globals.css",
|
"./globals.css": "./src/web/globals.css",
|
||||||
"./components": "./src/web/components/index.ts"
|
"./components": "./src/web/components/index.ts"
|
||||||
|
|||||||
@ -1,12 +1,17 @@
|
|||||||
import type { ITransactionManager } from "@erp/core/api";
|
import type { ITransactionManager } from "@erp/core/api";
|
||||||
|
|
||||||
import type { ICreateCustomerInputMapper } from "../mappers";
|
import type { ICreateCustomerInputMapper, IUpdateCustomerInputMapper } from "../mappers";
|
||||||
import type { ICustomerCreator, ICustomerFinder } from "../services";
|
import type { ICustomerCreator, ICustomerFinder, ICustomerUpdater } from "../services";
|
||||||
import type {
|
import type {
|
||||||
ICustomerFullSnapshotBuilder,
|
ICustomerFullSnapshotBuilder,
|
||||||
ICustomerSummarySnapshotBuilder,
|
ICustomerSummarySnapshotBuilder,
|
||||||
} from "../snapshot-builders";
|
} from "../snapshot-builders";
|
||||||
import { CreateCustomerUseCase, GetCustomerByIdUseCase, ListCustomersUseCase } from "../use-cases";
|
import {
|
||||||
|
CreateCustomerUseCase,
|
||||||
|
GetCustomerByIdUseCase,
|
||||||
|
ListCustomersUseCase,
|
||||||
|
UpdateCustomerUseCase,
|
||||||
|
} from "../use-cases";
|
||||||
|
|
||||||
export function buildGetCustomerByIdUseCase(deps: {
|
export function buildGetCustomerByIdUseCase(deps: {
|
||||||
finder: ICustomerFinder;
|
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: {
|
/*export function buildReportCustomerUseCase(deps: {
|
||||||
finder: ICustomerFinder;
|
finder: ICustomerFinder;
|
||||||
fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
|
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 }) {
|
export function buildDeleteCustomerUseCase(deps: { finder: ICustomerFinder }) {
|
||||||
return new DeleteCustomerUseCase(deps.finder);
|
return new DeleteCustomerUseCase(deps.finder);
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import {
|
|||||||
extractOrPushError,
|
extractOrPushError,
|
||||||
maybeFromNullableResult,
|
maybeFromNullableResult,
|
||||||
} from "@repo/rdx-ddd";
|
} from "@repo/rdx-ddd";
|
||||||
import { Collection, Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import type { CreateCustomerRequestDTO } from "../../../common";
|
import type { CreateCustomerRequestDTO } from "../../../common";
|
||||||
import { CustomerStatus, type ICustomerCreateProps } from "../../domain";
|
import { CustomerStatus, type ICustomerCreateProps } from "../../domain";
|
||||||
@ -176,7 +176,7 @@ export class CreateCustomerInputMapper implements ICreateCustomerInputMapper {
|
|||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
|
|
||||||
const defaultTaxes = new Collection<TaxCode>();
|
const defaultTaxes: TaxCode[] = [];
|
||||||
|
|
||||||
/*if (!isNullishOrEmpty(dto.default_taxes)) {
|
/*if (!isNullishOrEmpty(dto.default_taxes)) {
|
||||||
dto.default_taxes!.map((taxCode, index) => {
|
dto.default_taxes!.map((taxCode, index) => {
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
export * from "./create-customer-input.mapper";
|
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 { 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 { Collection, Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import type { Customer } from "../../domain/aggregates";
|
import type { Customer } from "../../domain/aggregates";
|
||||||
@ -48,6 +48,16 @@ export interface ICustomerRepository {
|
|||||||
transaction: unknown
|
transaction: unknown
|
||||||
): Promise<Result<Customer, Error>>;
|
): 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
|
* Recupera múltiples customers dentro de una empresa
|
||||||
* según un criterio dinámico (búsqueda, paginación, etc.).
|
* 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 { 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 { Collection, Result } from "@repo/rdx-utils";
|
||||||
import type { Transaction } from "sequelize";
|
import type { Transaction } from "sequelize";
|
||||||
|
|
||||||
@ -10,7 +10,13 @@ import type { ICustomerRepository } from "../repositories";
|
|||||||
export interface ICustomerFinder {
|
export interface ICustomerFinder {
|
||||||
findCustomerById(
|
findCustomerById(
|
||||||
companyId: UniqueID,
|
companyId: UniqueID,
|
||||||
invoiceId: UniqueID,
|
customerId: UniqueID,
|
||||||
|
transaction?: Transaction
|
||||||
|
): Promise<Result<Customer, Error>>;
|
||||||
|
|
||||||
|
findCustomerByTIN(
|
||||||
|
companyId: UniqueID,
|
||||||
|
tin: TINNumber,
|
||||||
transaction?: Transaction
|
transaction?: Transaction
|
||||||
): Promise<Result<Customer, Error>>;
|
): Promise<Result<Customer, Error>>;
|
||||||
|
|
||||||
@ -38,6 +44,14 @@ export class CustomerFinder implements ICustomerFinder {
|
|||||||
return this.repository.getByIdInCompany(companyId, customerId, transaction);
|
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(
|
async customerExists(
|
||||||
companyId: UniqueID,
|
companyId: UniqueID,
|
||||||
customerId: UniqueID,
|
customerId: UniqueID,
|
||||||
|
|||||||
@ -2,14 +2,14 @@ import type { UniqueID } from "@repo/rdx-ddd";
|
|||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
import type { Transaction } from "sequelize";
|
import type { Transaction } from "sequelize";
|
||||||
|
|
||||||
import type { Customer, ICustomerCreateProps } from "../../domain";
|
import type { Customer, CustomerPatchProps } from "../../domain";
|
||||||
import type { ICustomerRepository } from "../repositories";
|
import type { ICustomerRepository } from "../repositories";
|
||||||
|
|
||||||
export interface ICustomerUpdater {
|
export interface ICustomerUpdater {
|
||||||
update(params: {
|
update(params: {
|
||||||
companyId: UniqueID;
|
companyId: UniqueID;
|
||||||
id: UniqueID;
|
id: UniqueID;
|
||||||
props: Partial<ICustomerCreateProps>;
|
props: CustomerPatchProps;
|
||||||
transaction: Transaction;
|
transaction: Transaction;
|
||||||
}): Promise<Result<Customer, Error>>;
|
}): Promise<Result<Customer, Error>>;
|
||||||
}
|
}
|
||||||
@ -28,7 +28,7 @@ export class CustomerUpdater implements ICustomerUpdater {
|
|||||||
async update(params: {
|
async update(params: {
|
||||||
companyId: UniqueID;
|
companyId: UniqueID;
|
||||||
id: UniqueID;
|
id: UniqueID;
|
||||||
props: Partial<ICustomerCreateProps>;
|
props: CustomerPatchProps;
|
||||||
transaction: Transaction;
|
transaction: Transaction;
|
||||||
}): Promise<Result<Customer, Error>> {
|
}): Promise<Result<Customer, Error>> {
|
||||||
const { companyId, id, props, transaction } = params;
|
const { companyId, id, props, transaction } = params;
|
||||||
|
|||||||
@ -1,2 +1,3 @@
|
|||||||
export * from "./customer-creator";
|
export * from "./customer-creator";
|
||||||
export * from "./customer-finder";
|
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 { UniqueID } from "@repo/rdx-ddd";
|
||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
import type { Transaction } from "sequelize";
|
import type { Transaction } from "sequelize";
|
||||||
|
|
||||||
import type { UpdateCustomerByIdRequestDTO } from "../../../../common/dto";
|
import type { UpdateCustomerByIdRequestDTO } from "../../../../common/dto";
|
||||||
import type { CustomerPatchProps } from "../../../domain";
|
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 = {
|
type UpdateCustomerUseCaseInput = {
|
||||||
companyId: UniqueID;
|
companyId: UniqueID;
|
||||||
@ -13,12 +15,25 @@ type UpdateCustomerUseCaseInput = {
|
|||||||
dto: UpdateCustomerByIdRequestDTO;
|
dto: UpdateCustomerByIdRequestDTO;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type UpdateCustomerUseCaseDeps = {
|
||||||
|
dtoMapper: IUpdateCustomerInputMapper;
|
||||||
|
updater: ICustomerUpdater;
|
||||||
|
fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
|
||||||
|
transactionManager: ITransactionManager;
|
||||||
|
};
|
||||||
|
|
||||||
export class UpdateCustomerUseCase {
|
export class UpdateCustomerUseCase {
|
||||||
constructor(
|
private readonly dtoMapper: IUpdateCustomerInputMapper;
|
||||||
private readonly service: CustomerApplicationService,
|
private readonly updater: ICustomerUpdater;
|
||||||
private readonly transactionManager: ITransactionManager,
|
private readonly fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
|
||||||
private readonly presenterRegistry: IPresenterRegistry
|
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) {
|
public execute(params: UpdateCustomerUseCaseInput) {
|
||||||
const { companyId, customer_id, dto } = params;
|
const { companyId, customer_id, dto } = params;
|
||||||
@ -27,6 +42,8 @@ export class UpdateCustomerUseCase {
|
|||||||
if (idOrError.isFailure) {
|
if (idOrError.isFailure) {
|
||||||
return Result.fail(idOrError.error);
|
return Result.fail(idOrError.error);
|
||||||
}
|
}
|
||||||
|
const id = idOrError.data;
|
||||||
|
|
||||||
// Mapear DTO → props de dominio
|
// Mapear DTO → props de dominio
|
||||||
const patchPropsResult = this.dtoMapper.map(dto, { companyId });
|
const patchPropsResult = this.dtoMapper.map(dto, { companyId });
|
||||||
if (patchPropsResult.isFailure) {
|
if (patchPropsResult.isFailure) {
|
||||||
@ -37,20 +54,20 @@ export class UpdateCustomerUseCase {
|
|||||||
|
|
||||||
return this.transactionManager.complete(async (transaction: Transaction) => {
|
return this.transactionManager.complete(async (transaction: Transaction) => {
|
||||||
try {
|
try {
|
||||||
const updatedCustomer = await this.service.patchCustomerByIdInCompany(
|
const updateResult = await this.updater.update({
|
||||||
companyId,
|
companyId,
|
||||||
customerId,
|
id,
|
||||||
patchProps,
|
props: patchProps,
|
||||||
transaction
|
transaction,
|
||||||
);
|
});
|
||||||
|
|
||||||
if (updatedCustomer.isFailure) {
|
if (updateResult.isFailure) {
|
||||||
return Result.fail(updatedCustomer.error);
|
return Result.fail(updateResult.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const customerOrError = await this.service.updateCustomerInCompany(
|
const customerOrError = await this.service.updateCustomerInCompany(
|
||||||
companyId,
|
companyId,
|
||||||
updatedCustomer.data,
|
updateResult.data,
|
||||||
transaction
|
transaction
|
||||||
);
|
);
|
||||||
const customer = customerOrError.data;
|
const customer = customerOrError.data;
|
||||||
|
|||||||
@ -9,14 +9,13 @@ import {
|
|||||||
type PostalAddressPatchProps,
|
type PostalAddressPatchProps,
|
||||||
type PostalAddressProps,
|
type PostalAddressProps,
|
||||||
type TINNumber,
|
type TINNumber,
|
||||||
type TaxCode,
|
|
||||||
type TextValue,
|
type TextValue,
|
||||||
type URLAddress,
|
type URLAddress,
|
||||||
type UniqueID,
|
type UniqueID,
|
||||||
} from "@repo/rdx-ddd";
|
} 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 {
|
export interface ICustomerCreateProps {
|
||||||
companyId: UniqueID;
|
companyId: UniqueID;
|
||||||
@ -44,14 +43,14 @@ export interface ICustomerCreateProps {
|
|||||||
|
|
||||||
legalRecord: Maybe<TextValue>;
|
legalRecord: Maybe<TextValue>;
|
||||||
|
|
||||||
defaultTaxes: TaxCode[];
|
defaultTaxes: CustomerTaxesProps;
|
||||||
|
|
||||||
languageCode: LanguageCode;
|
languageCode: LanguageCode;
|
||||||
currencyCode: CurrencyCode;
|
currencyCode: CurrencyCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CustomerPatchProps = Partial<
|
export type CustomerPatchProps = Partial<
|
||||||
Omit<ICustomerCreateProps, "companyId" | "address" | "isCompany" | "status">
|
Omit<ICustomerCreateProps, "companyId" | "address" | "status">
|
||||||
> & {
|
> & {
|
||||||
address?: PostalAddressPatchProps;
|
address?: PostalAddressPatchProps;
|
||||||
};
|
};
|
||||||
@ -87,7 +86,7 @@ export interface ICustomer {
|
|||||||
readonly website: Maybe<URLAddress>;
|
readonly website: Maybe<URLAddress>;
|
||||||
readonly legalRecord: Maybe<TextValue>;
|
readonly legalRecord: Maybe<TextValue>;
|
||||||
|
|
||||||
readonly defaultTaxes: Collection<TaxCode>;
|
readonly defaultTaxes: CustomerTaxesProps;
|
||||||
|
|
||||||
readonly languageCode: LanguageCode;
|
readonly languageCode: LanguageCode;
|
||||||
readonly currencyCode: CurrencyCode;
|
readonly currencyCode: CurrencyCode;
|
||||||
@ -142,8 +141,6 @@ export class Customer extends AggregateRoot<CustomerInternalProps> implements IC
|
|||||||
if (addressResult.isFailure) {
|
if (addressResult.isFailure) {
|
||||||
return Result.fail(addressResult.error);
|
return Result.fail(addressResult.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.address = addressResult.data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.ok();
|
return Result.ok();
|
||||||
@ -223,7 +220,7 @@ export class Customer extends AggregateRoot<CustomerInternalProps> implements IC
|
|||||||
return this.props.legalRecord;
|
return this.props.legalRecord;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get defaultTaxes(): Collection<TaxCode> {
|
public get defaultTaxes(): CustomerTaxesProps {
|
||||||
return this.props.defaultTaxes;
|
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-number.vo";
|
||||||
export * from "./customer-serie.vo";
|
export * from "./customer-serie.vo";
|
||||||
export * from "./customer-status.vo";
|
export * from "./customer-status.vo";
|
||||||
|
export * from "./customer-taxes.vo";
|
||||||
@ -1,10 +1,16 @@
|
|||||||
import type { IModuleServer } from "@erp/core/api";
|
import type { IModuleServer } from "@erp/core/api";
|
||||||
|
|
||||||
import { customersRouter, models } from "./infrastructure";
|
import { type CustomerPublicServices, customersRouter, models } from "./infrastructure";
|
||||||
import { buildCustomersDependencies, buildCustomerServices, CustomersInternalDeps } from "./infrastructure/di";
|
import {
|
||||||
|
type CustomersInternalDeps,
|
||||||
|
buildCustomerServices,
|
||||||
|
buildCustomersDependencies,
|
||||||
|
} from "./infrastructure/di";
|
||||||
|
|
||||||
export * from "./infrastructure/sequelize";
|
export * from "./infrastructure/sequelize";
|
||||||
|
|
||||||
|
export type { CustomerPublicServices };
|
||||||
|
|
||||||
export const customersAPIModule: IModuleServer = {
|
export const customersAPIModule: IModuleServer = {
|
||||||
name: "customers",
|
name: "customers",
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
@ -21,10 +27,10 @@ export const customersAPIModule: IModuleServer = {
|
|||||||
const { env: ENV, app, database, baseRoutePath: API_BASE_PATH, logger } = params;
|
const { env: ENV, app, database, baseRoutePath: API_BASE_PATH, logger } = params;
|
||||||
|
|
||||||
// 1) Dominio interno
|
// 1) Dominio interno
|
||||||
const customerInternalDeps = buildCustomersDependencies(params);
|
const internal = buildCustomersDependencies(params);
|
||||||
|
|
||||||
// 2) Servicios públicos (Application Services)
|
// 2) Servicios públicos (Application Services)
|
||||||
const customerServices = buildCustomerServices(customerInternalDeps);
|
const customersServices: CustomerPublicServices = buildCustomerServices(params, internal);
|
||||||
|
|
||||||
logger.info("🚀 Customers module dependencies registered", {
|
logger.info("🚀 Customers module dependencies registered", {
|
||||||
label: this.name,
|
label: this.name,
|
||||||
@ -35,10 +41,12 @@ export const customersAPIModule: IModuleServer = {
|
|||||||
models,
|
models,
|
||||||
|
|
||||||
// Servicios expuestos a otros módulos
|
// Servicios expuestos a otros módulos
|
||||||
services: customerServices,
|
services: {
|
||||||
|
general: customersServices, // 'customers:general'
|
||||||
|
},
|
||||||
|
|
||||||
// Implementación privada del módulo
|
// 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";
|
import type { CustomersInternalDeps } from "./customers.di";
|
||||||
|
|
||||||
export type CustomersServicesDeps = {
|
type CustomerServicesContext = {
|
||||||
services: {
|
transaction: Transaction;
|
||||||
listCustomers: (filters: unknown, context: unknown) => null;
|
companyId: UniqueID;
|
||||||
getCustomerById: (id: unknown, context: unknown) => null;
|
|
||||||
//generateCustomerReport: (id: unknown, options: unknown, context: unknown) => null;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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 {
|
return {
|
||||||
services: {
|
findCustomerByTIN: async (tin: TINNumber, context: CustomerServicesContext) => {
|
||||||
listCustomers: (filters, context) => null,
|
const { companyId, transaction } = context;
|
||||||
//internal.useCases.listCustomers().execute(filters, context),
|
|
||||||
|
|
||||||
getCustomerById: (id, context) => null,
|
const customerResult = await finder.findCustomerByTIN(companyId, tin, transaction);
|
||||||
//internal.useCases.getCustomerById().execute(id, context),
|
|
||||||
|
|
||||||
//generateCustomerReport: (id, options, context) => null,
|
if (customerResult.isFailure) {
|
||||||
//internal.useCases.reportCustomer().execute(id, options, context),
|
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,
|
CreateCustomerRequestSchema,
|
||||||
CustomerListRequestSchema,
|
CustomerListRequestSchema,
|
||||||
GetCustomerByIdRequestSchema,
|
GetCustomerByIdRequestSchema,
|
||||||
|
UpdateCustomerByIdParamsRequestSchema,
|
||||||
|
UpdateCustomerByIdRequestSchema,
|
||||||
} from "../../../common/dto";
|
} from "../../../common/dto";
|
||||||
import type { CustomersInternalDeps } from "../di";
|
import type { CustomersInternalDeps } from "../di";
|
||||||
|
|
||||||
@ -13,6 +15,7 @@ import {
|
|||||||
CreateCustomerController,
|
CreateCustomerController,
|
||||||
GetCustomerController,
|
GetCustomerController,
|
||||||
ListCustomersController,
|
ListCustomersController,
|
||||||
|
UpdateCustomerController,
|
||||||
} from "./controllers";
|
} from "./controllers";
|
||||||
|
|
||||||
export const customersRouter = (params: ModuleParams, deps: CustomersInternalDeps) => {
|
export const customersRouter = (params: ModuleParams, deps: CustomersInternalDeps) => {
|
||||||
@ -75,19 +78,19 @@ export const customersRouter = (params: ModuleParams, deps: CustomersInternalDep
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
/* router.put(
|
router.put(
|
||||||
"/:customer_id",
|
"/:customer_id",
|
||||||
//checkTabContext,
|
//checkTabContext,
|
||||||
|
|
||||||
validateRequest(UpdateCustomerByIdParamsRequestSchema, "params"),
|
validateRequest(UpdateCustomerByIdParamsRequestSchema, "params"),
|
||||||
validateRequest(UpdateCustomerByIdRequestSchema, "body"),
|
validateRequest(UpdateCustomerByIdRequestSchema, "body"),
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const useCase = deps.useCases.update();
|
const useCase = deps.useCases.updateCustomer();
|
||||||
const controller = new UpdateCustomerController(useCase);
|
const controller = new UpdateCustomerController(useCase);
|
||||||
return controller.execute(req, res, next);
|
return controller.execute(req, res, next);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
*/
|
|
||||||
/*router.delete(
|
/*router.delete(
|
||||||
"/:customer_id",
|
"/:customer_id",
|
||||||
//checkTabContext,
|
//checkTabContext,
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
export * from "./di";
|
||||||
export * from "./express";
|
export * from "./express";
|
||||||
export * from "./mappers";
|
export * from "./mappers";
|
||||||
export * from "./sequelize";
|
export * from "./sequelize";
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
CreationOptional,
|
type CreationOptional,
|
||||||
DataTypes,
|
DataTypes,
|
||||||
InferAttributes,
|
type InferAttributes,
|
||||||
InferCreationAttributes,
|
type InferCreationAttributes,
|
||||||
Model,
|
Model,
|
||||||
Sequelize,
|
type Sequelize,
|
||||||
} from "sequelize";
|
} from "sequelize";
|
||||||
|
|
||||||
export type CustomerCreationAttributes = InferCreationAttributes<CustomerModel, {}> & {};
|
export type CustomerCreationAttributes = InferCreationAttributes<CustomerModel, {}> & {};
|
||||||
@ -238,6 +238,7 @@ export default (database: Sequelize) => {
|
|||||||
fields: ["company_id", "deleted_at", "name"],
|
fields: ["company_id", "deleted_at", "name"],
|
||||||
},
|
},
|
||||||
{ name: "idx_name", fields: ["name"] }, // <- para ordenación
|
{ 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_company_idx", fields: ["id", "company_id"], unique: true }, // <- para consulta get
|
||||||
{ name: "idx_factuges", fields: ["factuges_id"], unique: true }, // <- para el proceso python
|
{ name: "idx_factuges", fields: ["factuges_id"], unique: true }, // <- para el proceso python
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import {
|
|||||||
translateSequelizeError,
|
translateSequelizeError,
|
||||||
} from "@erp/core/api";
|
} from "@erp/core/api";
|
||||||
import { type Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server";
|
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 Collection, Result } from "@repo/rdx-utils";
|
||||||
import type { FindOptions, InferAttributes, OrderItem, Sequelize, Transaction } from "sequelize";
|
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.).
|
* Recupera múltiples customers dentro de una empresa según un criterio dinámico (búsqueda, paginación, etc.).
|
||||||
*
|
*
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
GetCustomerByIdResponseDTO,
|
type GetCustomerByIdResponseDTO,
|
||||||
GetCustomerByIdResponseSchema,
|
GetCustomerByIdResponseSchema,
|
||||||
} from "./get-customer-by-id.response.dto";
|
} 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 "./api-types";
|
||||||
export * from "./get-customer-by-ip.api";
|
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,
|
useQuery,
|
||||||
} from "@tanstack/react-query";
|
} from "@tanstack/react-query";
|
||||||
|
|
||||||
import { type CustomerSummaryPage, getCustomerListApi } from "../api";
|
import { type CustomerSummaryPage, getCustomerListApi } from "..";
|
||||||
|
|
||||||
export const CUSTOMERS_QUERY_KEY = (criteria?: CriteriaDTO): QueryKey => [
|
export const CUSTOMERS_QUERY_KEY = (criteria?: CriteriaDTO): QueryKey => [
|
||||||
"customers",
|
"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 { 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 { CustomerAdditionalConfigFields } from "./customer-additional-config-fields";
|
||||||
import { CustomerAddressFields } from "./customer-address-fields";
|
import { CustomerAddressFields } from "./customer-address-fields";
|
||||||
import { CustomerBasicInfoFields } from "./customer-basic-info-fields";
|
import { CustomerBasicInfoFields } from "./customer-basic-info-fields";
|
||||||
import { CustomerContactFields } from './customer-contact-fields';
|
import { CustomerContactFields } from "./customer-contact-fields";
|
||||||
|
|
||||||
type CustomerFormProps = {
|
type CustomerFormProps = {
|
||||||
formId: string;
|
formId: string;
|
||||||
@ -15,7 +15,7 @@ type CustomerFormProps = {
|
|||||||
|
|
||||||
export const CustomerEditForm = ({ formId, onSubmit, className, focusRef }: CustomerFormProps) => {
|
export const CustomerEditForm = ({ formId, onSubmit, className, focusRef }: CustomerFormProps) => {
|
||||||
return (
|
return (
|
||||||
<form noValidate id={formId} onSubmit={onSubmit}>
|
<form id={formId} noValidate onSubmit={onSubmit}>
|
||||||
<FormDebug />
|
<FormDebug />
|
||||||
<section className={cn("space-y-6 p-6", className)}>
|
<section className={cn("space-y-6 p-6", className)}>
|
||||||
<CustomerBasicInfoFields focusRef={focusRef} />
|
<CustomerBasicInfoFields focusRef={focusRef} />
|
||||||
|
|||||||
@ -1,2 +1 @@
|
|||||||
//export * from "./customer-edit-form";
|
export * from "./customer-edit-form";
|
||||||
export * from "../../view/ui/components/customer-editor-skeleton";
|
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
//export * from "./client-selector-modal";
|
//export * from "./client-selector-modal";
|
||||||
//export * from "./customer-modal-selector";
|
//export * from "./customer-modal-selector";
|
||||||
//export * from "./editor";
|
export * from "./editor";
|
||||||
export * from "../../../../core/src/web/components/error-alert";
|
|
||||||
//export * from "./not-found-card";
|
|
||||||
|
|||||||
@ -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 CustomerView = lazy(() => import("./view").then((m) => ({ default: m.CustomerViewPage })));
|
||||||
|
|
||||||
//const CustomerAdd = lazy(() => import("./pages").then((m) => ({ default: m.CustomerCreatePage })));
|
//const CustomerAdd = lazy(() => import("./pages").then((m) => ({ default: m.CustomerCreatePage })));
|
||||||
/*const CustomerUpdate = lazy(() =>
|
const CustomerUpdate = lazy(() =>
|
||||||
import("./pages").then((m) => ({ default: m.CustomerUpdatePage }))
|
import("./update").then((m) => ({ default: m.CustomerUpdatePage }))
|
||||||
);*/
|
);
|
||||||
|
|
||||||
export const CustomerRoutes = (params: ModuleClientParams): RouteObject[] => {
|
export const CustomerRoutes = (params: ModuleClientParams): RouteObject[] => {
|
||||||
return [
|
return [
|
||||||
@ -26,7 +26,7 @@ export const CustomerRoutes = (params: ModuleClientParams): RouteObject[] => {
|
|||||||
{ path: "list", element: <CustomersList /> },
|
{ path: "list", element: <CustomersList /> },
|
||||||
//{ path: "create", element: <CustomerAdd /> },
|
//{ path: "create", element: <CustomerAdd /> },
|
||||||
{ path: ":id", element: <CustomerView /> },
|
{ path: ":id", element: <CustomerView /> },
|
||||||
//{ path: ":id/edit", element: <CustomerUpdate /> },
|
{ path: ":id/edit", element: <CustomerUpdate /> },
|
||||||
|
|
||||||
//
|
//
|
||||||
/*{ path: "create", element: <CustomersList /> },
|
/*{ path: "create", element: <CustomersList /> },
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { useDataSource } from "@erp/core/hooks";
|
import { useDataSource } from "@erp/core/hooks";
|
||||||
import { UniqueID, ValidationErrorCollection } from "@repo/rdx-ddd";
|
import { UniqueID, ValidationErrorCollection } from "@repo/rdx-ddd";
|
||||||
import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import type { ZodError } from "zod/v4";
|
|
||||||
|
|
||||||
import { CreateCustomerRequestSchema } from "../../common";
|
import { CreateCustomerRequestSchema } from "../../common";
|
||||||
|
import { toValidationErrors } from "../common/hooks/toValidationErrors";
|
||||||
import type { Customer, CustomerFormData } from "../schemas";
|
import type { Customer, CustomerFormData } from "../schemas";
|
||||||
|
|
||||||
import { CUSTOMERS_LIST_KEY, invalidateCustomerListCache } from "./use-customer-list-query";
|
import { CUSTOMERS_LIST_KEY, invalidateCustomerListCache } from "./use-customer-list-query";
|
||||||
@ -15,14 +15,6 @@ type CreateCustomerPayload = {
|
|||||||
data: CustomerFormData;
|
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() {
|
export function useCreateCustomer() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const dataSource = useDataSource();
|
const dataSource = useDataSource();
|
||||||
|
|||||||
@ -3,9 +3,9 @@ import { ValidationErrorCollection } from "@repo/rdx-ddd";
|
|||||||
import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { UpdateCustomerByIdRequestSchema } from "../../common";
|
import { UpdateCustomerByIdRequestSchema } from "../../common";
|
||||||
|
import { toValidationErrors } from "../common/hooks/toValidationErrors";
|
||||||
import type { Customer, CustomerFormData } from "../schemas";
|
import type { Customer, CustomerFormData } from "../schemas";
|
||||||
|
|
||||||
import { toValidationErrors } from "./use-create-customer-mutation";
|
|
||||||
import {
|
import {
|
||||||
invalidateCustomerListCache,
|
invalidateCustomerListCache,
|
||||||
upsertCustomerIntoListCaches,
|
upsertCustomerIntoListCaches,
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type { CustomerSummaryPage } from "../api";
|
import type { CustomerSummaryPage } from "../../common";
|
||||||
import type { CustomerSummaryPageData } from "../types";
|
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 { useDebounce } from "@repo/rdx-ui/components";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
|
|
||||||
|
import { useCustomerListQuery } from "../../common";
|
||||||
import { CustomerSummaryDtoAdapter } from "../adapters";
|
import { CustomerSummaryDtoAdapter } from "../adapters";
|
||||||
import { useCustomerListQuery } from "../hooks";
|
|
||||||
|
|
||||||
export const useCustomerListController = () => {
|
export const useCustomerListController = () => {
|
||||||
const [pageIndex, setPageIndex] = useState(0);
|
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 { CustomerSummary, CustomerSummaryPage } from "../../common";
|
||||||
import type { CustomerSummaryPage } from "../api";
|
|
||||||
|
|
||||||
export type CustomerSummaryData = CustomerSummary;
|
export type CustomerSummaryData = CustomerSummary;
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
|
export * from "./address-cell";
|
||||||
|
export * from "./contact-cell";
|
||||||
export * from "./initials";
|
export * from "./initials";
|
||||||
export * from "./kind-badge";
|
export * from "./kind-badge";
|
||||||
export * from "./soft";
|
export * from "./soft";
|
||||||
export * from "./contact-cell";
|
|
||||||
export * from "./address-cell";
|
|
||||||
@ -1,3 +1 @@
|
|||||||
//export * from "./blocks";
|
|
||||||
//export * from "./components";
|
|
||||||
export * from "./pages";
|
export * from "./pages";
|
||||||
|
|||||||
@ -1,4 +1,2 @@
|
|||||||
export * from "./create";
|
export * from "./create";
|
||||||
export * from "./list";
|
|
||||||
export * from "./update";
|
export * from "./update";
|
||||||
export * from "./view";
|
|
||||||
|
|||||||
@ -9,8 +9,7 @@ import {
|
|||||||
NotFoundCard,
|
NotFoundCard,
|
||||||
} from "../../components";
|
} from "../../components";
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
|
import { useCustomerUpdateController } from "../../update/controllers/use-customer-update-page.controller";
|
||||||
import { useCustomerUpdateController } from "./use-customer-update-controller";
|
|
||||||
|
|
||||||
export const CustomerUpdatePage = () => {
|
export const CustomerUpdatePage = () => {
|
||||||
const customerId = useUrlParamId();
|
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 { useEffect, useId, useMemo } from "react";
|
||||||
import { type FieldErrors, FormProvider } from "react-hook-form";
|
import { type FieldErrors, FormProvider } from "react-hook-form";
|
||||||
|
|
||||||
import { useCustomerQuery, useUpdateCustomer } from "../../hooks";
|
import { useCustomerGetQuery, useCustomerUpdateMutation } from "../../common";
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
import {
|
import {
|
||||||
type Customer,
|
type Customer,
|
||||||
@ -34,7 +34,7 @@ export const useCustomerUpdateController = (
|
|||||||
isLoading,
|
isLoading,
|
||||||
isError: isLoadError,
|
isError: isLoadError,
|
||||||
error: loadError,
|
error: loadError,
|
||||||
} = useCustomerQuery(customerId, { enabled: Boolean(customerId) });
|
} = useCustomerGetQuery(customerId, { enabled: Boolean(customerId) });
|
||||||
|
|
||||||
// 2) Estado de creación (mutación)
|
// 2) Estado de creación (mutación)
|
||||||
const {
|
const {
|
||||||
@ -42,7 +42,7 @@ export const useCustomerUpdateController = (
|
|||||||
isPending: isUpdating,
|
isPending: isUpdating,
|
||||||
isError: isUpdateError,
|
isError: isUpdateError,
|
||||||
error: updateError,
|
error: updateError,
|
||||||
} = useUpdateCustomer();
|
} = useCustomerUpdateMutation();
|
||||||
|
|
||||||
const initialValues = useMemo(() => customerData ?? defaultCustomerFormData, [customerData]);
|
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";
|
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