Facturas de cliente y clientes
This commit is contained in:
parent
5b7ee437ff
commit
4b93815985
@ -74,7 +74,7 @@ export class CreateAccountUseCase {
|
|||||||
|
|
||||||
const validatedData: IAccountProps = {
|
const validatedData: IAccountProps = {
|
||||||
status: AccountStatus.createInactive(),
|
status: AccountStatus.createInactive(),
|
||||||
isFreelancer: dto.is_freelancer,
|
isFreelancer: dto.is_companyr,
|
||||||
name: dto.name,
|
name: dto.name,
|
||||||
tradeName: dto.trade_name ? Maybe.some(dto.trade_name) : Maybe.none(),
|
tradeName: dto.trade_name ? Maybe.some(dto.trade_name) : Maybe.none(),
|
||||||
tin: tinOrError.data,
|
tin: tinOrError.data,
|
||||||
|
|||||||
@ -46,8 +46,8 @@ export class UpdateAccountUseCase {
|
|||||||
const errors: Error[] = [];
|
const errors: Error[] = [];
|
||||||
const validatedData: Partial<IAccountProps> = {};
|
const validatedData: Partial<IAccountProps> = {};
|
||||||
|
|
||||||
if (dto.is_freelancer) {
|
if (dto.is_companyr) {
|
||||||
validatedData.isFreelancer = dto.is_freelancer;
|
validatedData.isFreelancer = dto.is_companyr;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dto.name) {
|
if (dto.name) {
|
||||||
|
|||||||
@ -22,7 +22,7 @@ const mockAccountRepository: IAccountRepository = {
|
|||||||
|
|
||||||
const sampleAccountPrimitives = {
|
const sampleAccountPrimitives = {
|
||||||
id: "c5743279-e1cf-4dd5-baae-6698c8c6183c",
|
id: "c5743279-e1cf-4dd5-baae-6698c8c6183c",
|
||||||
is_freelancer: false,
|
is_companyr: false,
|
||||||
name: "Empresa XYZ",
|
name: "Empresa XYZ",
|
||||||
trade_name: "XYZ Trading",
|
trade_name: "XYZ Trading",
|
||||||
tin: "123456789",
|
tin: "123456789",
|
||||||
@ -72,7 +72,7 @@ const accountBuilder = (accountData: any) => {
|
|||||||
|
|
||||||
const validatedData: IAccountProps = {
|
const validatedData: IAccountProps = {
|
||||||
status: AccountStatus.createInactive(),
|
status: AccountStatus.createInactive(),
|
||||||
isFreelancer: sampleAccountPrimitives.is_freelancer,
|
isFreelancer: sampleAccountPrimitives.is_companyr,
|
||||||
name: sampleAccountPrimitives.name,
|
name: sampleAccountPrimitives.name,
|
||||||
tradeName: sampleAccountPrimitives.trade_name
|
tradeName: sampleAccountPrimitives.trade_name
|
||||||
? Maybe.some(sampleAccountPrimitives.trade_name)
|
? Maybe.some(sampleAccountPrimitives.trade_name)
|
||||||
|
|||||||
@ -14,7 +14,7 @@ const mockAccountRepository: IAccountRepository = {
|
|||||||
|
|
||||||
const sampleAccount = {
|
const sampleAccount = {
|
||||||
id: "c5743279-e1cf-4dd5-baae-6698c8c6183c",
|
id: "c5743279-e1cf-4dd5-baae-6698c8c6183c",
|
||||||
is_freelancer: false,
|
is_companyr: false,
|
||||||
name: "Empresa XYZ",
|
name: "Empresa XYZ",
|
||||||
trade_name: "XYZ Trading",
|
trade_name: "XYZ Trading",
|
||||||
tin: "123456789",
|
tin: "123456789",
|
||||||
|
|||||||
@ -53,7 +53,7 @@ export class AccountMapper
|
|||||||
return Account.create(
|
return Account.create(
|
||||||
{
|
{
|
||||||
status: statusOrError.data,
|
status: statusOrError.data,
|
||||||
isFreelancer: source.is_freelancer,
|
isFreelancer: source.is_companyr,
|
||||||
name: source.name,
|
name: source.name,
|
||||||
tradeName: source.trade_name ? Maybe.some(source.trade_name) : Maybe.none(),
|
tradeName: source.trade_name ? Maybe.some(source.trade_name) : Maybe.none(),
|
||||||
tin: tinOrError.data,
|
tin: tinOrError.data,
|
||||||
@ -75,7 +75,7 @@ export class AccountMapper
|
|||||||
public mapToPersistence(source: Account, params?: MapperParamsType): AccountCreationAttributes {
|
public mapToPersistence(source: Account, params?: MapperParamsType): AccountCreationAttributes {
|
||||||
return {
|
return {
|
||||||
id: source.id.toPrimitive(),
|
id: source.id.toPrimitive(),
|
||||||
is_freelancer: source.isFreelancer,
|
is_companyr: source.isFreelancer,
|
||||||
name: source.name,
|
name: source.name,
|
||||||
trade_name: source.tradeName.getOrUndefined(),
|
trade_name: source.tradeName.getOrUndefined(),
|
||||||
tin: source.tin.toPrimitive(),
|
tin: source.tin.toPrimitive(),
|
||||||
|
|||||||
@ -17,7 +17,7 @@ export class AccountModel extends Model<InferAttributes<AccountModel>, AccountCr
|
|||||||
|
|
||||||
declare id: string;
|
declare id: string;
|
||||||
|
|
||||||
declare is_freelancer: boolean;
|
declare is_companyr: boolean;
|
||||||
declare name: string;
|
declare name: string;
|
||||||
declare trade_name: CreationOptional<string>;
|
declare trade_name: CreationOptional<string>;
|
||||||
declare tin: string;
|
declare tin: string;
|
||||||
@ -49,7 +49,7 @@ export default (sequelize: Sequelize) => {
|
|||||||
type: DataTypes.UUID,
|
type: DataTypes.UUID,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
},
|
},
|
||||||
is_freelancer: {
|
is_companyr: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -10,7 +10,7 @@ export const createAccountPresenter: ICreateAccountPresenter = {
|
|||||||
toDTO: (account: Account): ICreateAccountResponseDTO => ({
|
toDTO: (account: Account): ICreateAccountResponseDTO => ({
|
||||||
id: ensureString(account.id.toString()),
|
id: ensureString(account.id.toString()),
|
||||||
|
|
||||||
is_freelancer: ensureBoolean(account.isFreelancer),
|
is_companyr: ensureBoolean(account.isFreelancer),
|
||||||
name: ensureString(account.name),
|
name: ensureString(account.name),
|
||||||
trade_name: ensureString(account.tradeName.getOrUndefined()),
|
trade_name: ensureString(account.tradeName.getOrUndefined()),
|
||||||
tin: ensureString(account.tin.toString()),
|
tin: ensureString(account.tin.toString()),
|
||||||
|
|||||||
@ -10,7 +10,7 @@ export const getAccountPresenter: IGetAccountPresenter = {
|
|||||||
toDTO: (account: Account): IGetAccountResponseDTO => ({
|
toDTO: (account: Account): IGetAccountResponseDTO => ({
|
||||||
id: ensureString(account.id.toPrimitive()),
|
id: ensureString(account.id.toPrimitive()),
|
||||||
|
|
||||||
is_freelancer: ensureBoolean(account.isFreelancer),
|
is_companyr: ensureBoolean(account.isFreelancer),
|
||||||
name: ensureString(account.name),
|
name: ensureString(account.name),
|
||||||
trade_name: ensureString(account.tradeName.getOrUndefined()),
|
trade_name: ensureString(account.tradeName.getOrUndefined()),
|
||||||
tin: ensureString(account.tin.toPrimitive()),
|
tin: ensureString(account.tin.toPrimitive()),
|
||||||
|
|||||||
@ -11,7 +11,7 @@ export const listAccountsPresenter: IListAccountsPresenter = {
|
|||||||
accounts.map((account) => ({
|
accounts.map((account) => ({
|
||||||
id: ensureString(account.id.toString()),
|
id: ensureString(account.id.toString()),
|
||||||
|
|
||||||
is_freelancer: ensureBoolean(account.isFreelancer),
|
is_companyr: ensureBoolean(account.isFreelancer),
|
||||||
name: ensureString(account.name),
|
name: ensureString(account.name),
|
||||||
trade_name: ensureString(account.tradeName.getOrUndefined()),
|
trade_name: ensureString(account.tradeName.getOrUndefined()),
|
||||||
tin: ensureString(account.tin.toString()),
|
tin: ensureString(account.tin.toString()),
|
||||||
|
|||||||
@ -10,7 +10,7 @@ export const updateAccountPresenter: IUpdateAccountPresenter = {
|
|||||||
toDTO: (account: Account): IUpdateAccountResponseDTO => ({
|
toDTO: (account: Account): IUpdateAccountResponseDTO => ({
|
||||||
id: ensureString(account.id.toString()),
|
id: ensureString(account.id.toString()),
|
||||||
|
|
||||||
is_freelancer: ensureBoolean(account.isFreelancer),
|
is_companyr: ensureBoolean(account.isFreelancer),
|
||||||
name: ensureString(account.name),
|
name: ensureString(account.name),
|
||||||
trade_name: ensureString(account.tradeName.getOrUndefined()),
|
trade_name: ensureString(account.tradeName.getOrUndefined()),
|
||||||
tin: ensureString(account.tin.toString()),
|
tin: ensureString(account.tin.toString()),
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
export type IListAccountsRequestDTO = {}
|
export type IListAccountsRequestDTO = {};
|
||||||
|
|
||||||
export interface ICreateAccountRequestDTO {
|
export interface ICreateAccountRequestDTO {
|
||||||
id: string;
|
id: string;
|
||||||
is_freelancer: boolean;
|
is_companyr: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
trade_name: string;
|
trade_name: string;
|
||||||
tin: string;
|
tin: string;
|
||||||
@ -27,7 +27,7 @@ export interface ICreateAccountRequestDTO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IUpdateAccountRequestDTO {
|
export interface IUpdateAccountRequestDTO {
|
||||||
is_freelancer: boolean;
|
is_companyr: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
trade_name: string;
|
trade_name: string;
|
||||||
tin: string;
|
tin: string;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
export interface IListAccountsResponseDTO {
|
export interface IListAccountsResponseDTO {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
is_freelancer: boolean;
|
is_companyr: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
trade_name: string;
|
trade_name: string;
|
||||||
tin: string;
|
tin: string;
|
||||||
@ -29,7 +29,7 @@ export interface IListAccountsResponseDTO {
|
|||||||
export interface IGetAccountResponseDTO {
|
export interface IGetAccountResponseDTO {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
is_freelancer: boolean;
|
is_companyr: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
trade_name: string;
|
trade_name: string;
|
||||||
tin: string;
|
tin: string;
|
||||||
@ -57,7 +57,7 @@ export interface IGetAccountResponseDTO {
|
|||||||
export interface ICreateAccountResponseDTO {
|
export interface ICreateAccountResponseDTO {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
is_freelancer: boolean;
|
is_companyr: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
trade_name: string;
|
trade_name: string;
|
||||||
tin: string;
|
tin: string;
|
||||||
@ -88,7 +88,7 @@ export interface ICreateAccountResponseDTO {
|
|||||||
export interface IUpdateAccountResponseDTO {
|
export interface IUpdateAccountResponseDTO {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
is_freelancer: boolean;
|
is_companyr: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
trade_name: string;
|
trade_name: string;
|
||||||
tin: string;
|
tin: string;
|
||||||
|
|||||||
@ -7,7 +7,7 @@ export const IGetAccountRequestSchema = z.object({});
|
|||||||
export const ICreateAccountRequestSchema = z.object({
|
export const ICreateAccountRequestSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
|
|
||||||
is_freelancer: z.boolean(),
|
is_companyr: z.boolean(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
trade_name: z.string(),
|
trade_name: z.string(),
|
||||||
tin: z.string(),
|
tin: z.string(),
|
||||||
@ -35,7 +35,7 @@ export const ICreateAccountRequestSchema = z.object({
|
|||||||
export const IUpdateAccountRequestSchema = z.object({
|
export const IUpdateAccountRequestSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
|
|
||||||
is_freelancer: z.boolean(),
|
is_companyr: z.boolean(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
trade_name: z.string(),
|
trade_name: z.string(),
|
||||||
tin: z.string(),
|
tin: z.string(),
|
||||||
|
|||||||
@ -50,7 +50,7 @@ export class ContactMapper
|
|||||||
|
|
||||||
return Contact.create(
|
return Contact.create(
|
||||||
{
|
{
|
||||||
isFreelancer: source.is_freelancer,
|
isFreelancer: source.is_companyr,
|
||||||
reference: source.reference,
|
reference: source.reference,
|
||||||
name: source.name,
|
name: source.name,
|
||||||
tradeName: source.trade_name ? Maybe.some(source.trade_name) : Maybe.none(),
|
tradeName: source.trade_name ? Maybe.some(source.trade_name) : Maybe.none(),
|
||||||
@ -77,7 +77,7 @@ export class ContactMapper
|
|||||||
return Result.ok({
|
return Result.ok({
|
||||||
id: source.id.toString(),
|
id: source.id.toString(),
|
||||||
reference: source.reference,
|
reference: source.reference,
|
||||||
is_freelancer: source.isFreelancer,
|
is_companyr: source.isFreelancer,
|
||||||
name: source.name,
|
name: source.name,
|
||||||
trade_name: source.tradeName.getOrUndefined(),
|
trade_name: source.tradeName.getOrUndefined(),
|
||||||
tin: source.tin.toString(),
|
tin: source.tin.toString(),
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export class ContactModel extends Model<
|
|||||||
declare id: string;
|
declare id: string;
|
||||||
declare reference: CreationOptional<string>;
|
declare reference: CreationOptional<string>;
|
||||||
|
|
||||||
declare is_freelancer: boolean;
|
declare is_companyr: boolean;
|
||||||
declare name: string;
|
declare name: string;
|
||||||
declare trade_name: CreationOptional<string>;
|
declare trade_name: CreationOptional<string>;
|
||||||
declare tin: string;
|
declare tin: string;
|
||||||
@ -56,7 +56,7 @@ export default (sequelize: Sequelize) => {
|
|||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
},
|
},
|
||||||
is_freelancer: {
|
is_companyr: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -12,7 +12,7 @@ export const listContactsPresenter: IListContactsPresenter = {
|
|||||||
id: ensureString(contact.id.toString()),
|
id: ensureString(contact.id.toString()),
|
||||||
reference: ensureString(contact.reference),
|
reference: ensureString(contact.reference),
|
||||||
|
|
||||||
is_freelancer: ensureBoolean(contact.isFreelancer),
|
is_companyr: ensureBoolean(contact.isFreelancer),
|
||||||
name: ensureString(contact.name),
|
name: ensureString(contact.name),
|
||||||
trade_name: ensureString(contact.tradeName.getOrUndefined()),
|
trade_name: ensureString(contact.tradeName.getOrUndefined()),
|
||||||
tin: ensureString(contact.tin.toString()),
|
tin: ensureString(contact.tin.toString()),
|
||||||
|
|||||||
@ -2,7 +2,7 @@ export interface IListContactsResponseDTO {
|
|||||||
id: string;
|
id: string;
|
||||||
reference: string;
|
reference: string;
|
||||||
|
|
||||||
is_freelancer: boolean;
|
is_companyr: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
trade_name: string;
|
trade_name: string;
|
||||||
tin: string;
|
tin: string;
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export class CustomerModel extends Model<
|
|||||||
declare id: string;
|
declare id: string;
|
||||||
declare reference: CreationOptional<string>;
|
declare reference: CreationOptional<string>;
|
||||||
|
|
||||||
declare is_freelancer: boolean;
|
declare is_companyr: boolean;
|
||||||
declare name: string;
|
declare name: string;
|
||||||
declare trade_name: CreationOptional<string>;
|
declare trade_name: CreationOptional<string>;
|
||||||
declare tin: string;
|
declare tin: string;
|
||||||
@ -56,7 +56,7 @@ export default (sequelize: Sequelize) => {
|
|||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
},
|
},
|
||||||
is_freelancer: {
|
is_companyr: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -13,7 +13,7 @@ export const listCustomerInvoicesPresenter: IListCustomerInvoicesPresenter = {
|
|||||||
id: ensureString(customer.id.toString()),
|
id: ensureString(customer.id.toString()),
|
||||||
/*reference: ensureString(customer.),
|
/*reference: ensureString(customer.),
|
||||||
|
|
||||||
is_freelancer: ensureBoolean(customer.isFreelancer),
|
is_companyr: ensureBoolean(customer.isFreelancer),
|
||||||
name: ensureString(customer.name),
|
name: ensureString(customer.name),
|
||||||
trade_name: ensureString(customer.tradeName.getValue()),
|
trade_name: ensureString(customer.tradeName.getValue()),
|
||||||
tin: ensureString(customer.tin.toString()),
|
tin: ensureString(customer.tin.toString()),
|
||||||
|
|||||||
@ -2,7 +2,7 @@ export interface IListCustomerInvoicesResponseDTO {
|
|||||||
id: string;
|
id: string;
|
||||||
/*reference: string;
|
/*reference: string;
|
||||||
|
|
||||||
is_freelancer: boolean;
|
is_companyr: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
trade_name: string;
|
trade_name: string;
|
||||||
tin: string;
|
tin: string;
|
||||||
@ -26,4 +26,4 @@ export interface IListCustomerInvoicesResponseDTO {
|
|||||||
currency_code: string;*/
|
currency_code: string;*/
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IGetCustomerInvoiceResponseDTO = {}
|
export type IGetCustomerInvoiceResponseDTO = {};
|
||||||
|
|||||||
@ -2,7 +2,7 @@ export interface IListCustomersResponseDTO {
|
|||||||
id: string;
|
id: string;
|
||||||
reference: string;
|
reference: string;
|
||||||
|
|
||||||
is_freelancer: boolean;
|
is_companyr: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
trade_name: string;
|
trade_name: string;
|
||||||
tin: string;
|
tin: string;
|
||||||
|
|||||||
@ -79,7 +79,7 @@
|
|||||||
"entry": ["src/index.ts"],
|
"entry": ["src/index.ts"],
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"format": ["esm", "cjs"],
|
"format": ["esm", "cjs"],
|
||||||
"target": "es2020",
|
"target": "ES2022",
|
||||||
"sourcemap": true,
|
"sourcemap": true,
|
||||||
"clean": true,
|
"clean": true,
|
||||||
"dts": true,
|
"dts": true,
|
||||||
|
|||||||
@ -33,7 +33,7 @@ export const App = () => {
|
|||||||
baseURL: import.meta.env.VITE_API_SERVER_URL,
|
baseURL: import.meta.env.VITE_API_SERVER_URL,
|
||||||
getAccessToken,
|
getAccessToken,
|
||||||
onAuthError: () => {
|
onAuthError: () => {
|
||||||
console.error("Error de autenticación");
|
console.error("APP, Error de autenticación");
|
||||||
clearAccessToken();
|
clearAccessToken();
|
||||||
//window.location.href = "/login"; // o usar navegación programática
|
//window.location.href = "/login"; // o usar navegación programática
|
||||||
},
|
},
|
||||||
|
|||||||
@ -5,9 +5,9 @@
|
|||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
},
|
},
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
"target": "ES2020",
|
"target": "ES2022",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
export interface IListInvoicesRequestDTO {}
|
export type IListInvoicesRequestDTO = {};
|
||||||
|
|
||||||
export interface ICreateInvoiceRequestDTO {
|
export interface ICreateInvoiceRequestDTO {
|
||||||
id: string;
|
id: string;
|
||||||
@ -12,7 +12,7 @@ export interface ICreateInvoiceRequestDTO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IUpdateInvoiceRequestDTO {
|
export interface IUpdateInvoiceRequestDTO {
|
||||||
is_freelancer: boolean;
|
is_companyr: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
trade_name: string;
|
trade_name: string;
|
||||||
tin: string;
|
tin: string;
|
||||||
|
|||||||
@ -7,9 +7,9 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
"target": "ES2020",
|
"target": "ES2022",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
|||||||
12
modules/core/src/api/application/errors/application-error.ts
Normal file
12
modules/core/src/api/application/errors/application-error.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* Errores de capa de aplicación. No deben "filtrarse" a cliente tal cual.
|
||||||
|
*
|
||||||
|
* */
|
||||||
|
|
||||||
|
export class ApplicationError extends Error {
|
||||||
|
public readonly layer = "application" as const;
|
||||||
|
constructor(message: string, options?: ErrorOptions) {
|
||||||
|
super(message, options);
|
||||||
|
Object.setPrototypeOf(this, new.target.prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
modules/core/src/api/application/errors/index.ts
Normal file
1
modules/core/src/api/application/errors/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./application-error";
|
||||||
1
modules/core/src/api/application/index.ts
Normal file
1
modules/core/src/api/application/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./errors";
|
||||||
12
modules/core/src/api/domain/errors/domain-error.ts
Normal file
12
modules/core/src/api/domain/errors/domain-error.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* Errores de capa de dominio. No deben "filtrarse" a cliente tal cual.
|
||||||
|
*
|
||||||
|
* */
|
||||||
|
|
||||||
|
export class DomainError extends Error {
|
||||||
|
public readonly layer = "domain" as const;
|
||||||
|
constructor(message: string, options?: ErrorOptions) {
|
||||||
|
super(message, options);
|
||||||
|
Object.setPrototypeOf(this, new.target.prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* Clase DomainValidationError
|
||||||
|
* Representa un error de validación de dominio.
|
||||||
|
*
|
||||||
|
* Esta clase extiende la clase Error de JavaScript y se utiliza para manejar errores
|
||||||
|
* específicos de validación dentro del dominio de la aplicación. Permite identificar
|
||||||
|
* el código de error, el campo afectado y un detalle descriptivo del error.
|
||||||
|
*
|
||||||
|
* @class DomainValidationError
|
||||||
|
* @extends {Error}
|
||||||
|
* @property {string} code - Código del error de validación.
|
||||||
|
* @property {string} field - Campo afectado por el error de validación.
|
||||||
|
* @property {string} detail - Detalle descriptivo del error de validación.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const error = new DomainValidationError("INVALID_EMAIL", "email", "El email no es válido");
|
||||||
|
* console.error(error);
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { DomainError } from "./domain-error";
|
||||||
|
|
||||||
|
export class DomainValidationError extends DomainError {
|
||||||
|
// Discriminante estable para mapeo/telemetría
|
||||||
|
public readonly kind = "VALIDATION" as const;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public readonly code: string, // id de regla del negocio (ej. 'INVALID_FORMAT')
|
||||||
|
public readonly field: string, // path: 'number' | 'date' | 'lines[0].quantity'
|
||||||
|
public readonly detail: string, // mensaje legible del negocio
|
||||||
|
options?: ErrorOptions
|
||||||
|
) {
|
||||||
|
super(`[${field}] ${detail}`, options);
|
||||||
|
this.name = "DomainValidationError";
|
||||||
|
Object.freeze(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructores rápidos
|
||||||
|
static required(field: string, options?: ErrorOptions) {
|
||||||
|
return new DomainValidationError("REQUIRED", field, "cannot be empty", options);
|
||||||
|
}
|
||||||
|
static invalidFormat(field: string, detail = "invalid format", options?: ErrorOptions) {
|
||||||
|
return new DomainValidationError("INVALID_FORMAT", field, detail, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proyección útil para Problem+JSON o colecciones
|
||||||
|
toDetail() {
|
||||||
|
return { path: this.field, message: this.detail, rule: this.code };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isDomainValidationError = (e: unknown): e is DomainValidationError =>
|
||||||
|
e instanceof DomainValidationError;
|
||||||
11
modules/core/src/api/domain/errors/duplicate-entity-error.ts
Normal file
11
modules/core/src/api/domain/errors/duplicate-entity-error.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { DomainError } from "./domain-error";
|
||||||
|
|
||||||
|
export class DuplicateEntityError extends DomainError {
|
||||||
|
constructor(entity: string, field: string, value: string, options?: ErrorOptions) {
|
||||||
|
super(`Entity '${entity}' with field '${field}' and value '${value}' already exists.`, options);
|
||||||
|
this.name = "DuplicateEntityError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isDuplicateEntityError = (e: unknown): e is DuplicateEntityError =>
|
||||||
|
e instanceof DuplicateEntityError;
|
||||||
11
modules/core/src/api/domain/errors/entity-not-found-error.ts
Normal file
11
modules/core/src/api/domain/errors/entity-not-found-error.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { DomainError } from "./domain-error";
|
||||||
|
|
||||||
|
export class EntityNotFoundError extends DomainError {
|
||||||
|
constructor(entity: string, field: string, value: string, options?: ErrorOptions) {
|
||||||
|
super(`Entity '${entity}' with ${field} '${value}' was not found.`, options);
|
||||||
|
this.name = "EntityNotFoundError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isEntityNotFoundError = (e: unknown): e is EntityNotFoundError =>
|
||||||
|
e instanceof EntityNotFoundError;
|
||||||
5
modules/core/src/api/domain/errors/index.ts
Normal file
5
modules/core/src/api/domain/errors/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export * from "./domain-error";
|
||||||
|
export * from "./domain-validation-error";
|
||||||
|
export * from "./duplicate-entity-error";
|
||||||
|
export * from "./entity-not-found-error";
|
||||||
|
export * from "./validation-error-collection";
|
||||||
@ -15,19 +15,30 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { DomainError } from "./domain-error";
|
||||||
|
|
||||||
export interface ValidationErrorDetail {
|
export interface ValidationErrorDetail {
|
||||||
path: string; // ejemplo: "lines[1].unitPrice.amount"
|
path: string; // ejemplo: "lines[1].unitPrice.amount"
|
||||||
message: string; // ejemplo: "Amount must be a positive number"
|
message: string; // ejemplo: "Amount must be a positive number",
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ValidationErrorCollection extends Error {
|
/**
|
||||||
|
* Error de validación múltiple. Agrega varios fallos de una sola vez.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class ValidationErrorCollection extends DomainError {
|
||||||
|
public readonly code = "VALIDATION" as const;
|
||||||
public readonly details: ValidationErrorDetail[];
|
public readonly details: ValidationErrorDetail[];
|
||||||
|
|
||||||
constructor(details: ValidationErrorDetail[]) {
|
constructor(message: string, details: ValidationErrorDetail[], options?: ErrorOptions) {
|
||||||
super("Validation failed");
|
super(message, options);
|
||||||
Object.setPrototypeOf(this, ValidationErrorCollection.prototype);
|
Object.setPrototypeOf(this, ValidationErrorCollection.prototype);
|
||||||
|
|
||||||
this.name = "ValidationErrorCollection";
|
this.name = "ValidationErrorCollection";
|
||||||
this.details = details;
|
this.details = details;
|
||||||
|
Object.freeze(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isValidationErrorCollection = (e: unknown): e is ValidationErrorCollection =>
|
||||||
|
e instanceof ValidationErrorCollection;
|
||||||
1
modules/core/src/api/domain/index.ts
Normal file
1
modules/core/src/api/domain/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./errors";
|
||||||
@ -1,29 +0,0 @@
|
|||||||
/**
|
|
||||||
* Clase DomainValidationError
|
|
||||||
* Representa un error de validación de dominio.
|
|
||||||
*
|
|
||||||
* Esta clase extiende la clase Error de JavaScript y se utiliza para manejar errores
|
|
||||||
* específicos de validación dentro del dominio de la aplicación. Permite identificar
|
|
||||||
* el código de error, el campo afectado y un detalle descriptivo del error.
|
|
||||||
*
|
|
||||||
* @class DomainValidationError
|
|
||||||
* @extends {Error}
|
|
||||||
* @property {string} code - Código del error de validación.
|
|
||||||
* @property {string} field - Campo afectado por el error de validación.
|
|
||||||
* @property {string} detail - Detalle descriptivo del error de validación.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const error = new DomainValidationError("INVALID_EMAIL", "email", "El email no es válido");
|
|
||||||
* console.error(error);
|
|
||||||
*/
|
|
||||||
|
|
||||||
export class DomainValidationError extends Error {
|
|
||||||
constructor(
|
|
||||||
public readonly code: string,
|
|
||||||
public readonly field: string,
|
|
||||||
public readonly detail: string
|
|
||||||
) {
|
|
||||||
super(`[${field}] ${detail}`);
|
|
||||||
this.name = "DomainValidationError";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
export class DuplicateEntityError extends Error {
|
|
||||||
constructor(entity: string, id: string) {
|
|
||||||
super(`Entity '${entity}' with ID '${id}' already exists.`);
|
|
||||||
this.name = "DuplicateEntityError";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
export class EntityNotFoundError extends Error {
|
|
||||||
constructor(entity: string, id: string | number) {
|
|
||||||
super(`Entity '${entity}' with ID '${id}' was not found.`);
|
|
||||||
this.name = "EntityNotFoundError";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,136 +0,0 @@
|
|||||||
import {
|
|
||||||
ConnectionError,
|
|
||||||
DatabaseError,
|
|
||||||
ForeignKeyConstraintError,
|
|
||||||
UniqueConstraintError,
|
|
||||||
ValidationError,
|
|
||||||
} from "sequelize";
|
|
||||||
|
|
||||||
import { ApiError } from "./api-error";
|
|
||||||
import { ConflictApiError } from "./conflict-api-error";
|
|
||||||
import { DomainValidationError } from "./domain-validation-error";
|
|
||||||
import { DuplicateEntityError } from "./duplicate-entity-error";
|
|
||||||
import { EntityNotFoundError } from "./entity-not-found-error";
|
|
||||||
import { ForbiddenApiError } from "./forbidden-api-error";
|
|
||||||
import { InternalApiError } from "./internal-api-error";
|
|
||||||
import { NotFoundApiError } from "./not-found-api-error";
|
|
||||||
import { UnauthorizedApiError } from "./unauthorized-api-error";
|
|
||||||
import { UnavailableApiError } from "./unavailable-api-error";
|
|
||||||
import { ValidationApiError } from "./validation-api-error";
|
|
||||||
import { ValidationErrorCollection } from "./validation-error-collection";
|
|
||||||
|
|
||||||
export const errorMapper = {
|
|
||||||
toDomainError(error: unknown): Error {
|
|
||||||
if (error instanceof UniqueConstraintError) {
|
|
||||||
const field = error.errors[0]?.path || "unknown_field";
|
|
||||||
return new Error(`A record with this ${field} already exists.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error instanceof ForeignKeyConstraintError) {
|
|
||||||
return new Error("A referenced entity was not found or is invalid.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error instanceof ValidationError) {
|
|
||||||
return new Error(`Invalid data provided: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error instanceof DatabaseError) {
|
|
||||||
return new Error("Database error occurred.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error instanceof Error) {
|
|
||||||
return error; // Fallback a error estándar
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Error("Unknown persistence error.");
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mapea errores de la aplicación a errores de la API.
|
|
||||||
*
|
|
||||||
* Esta función toma un error de la aplicación y lo convierte en un objeto ApiError
|
|
||||||
* adecuado para enviar como respuesta HTTP. Maneja errores comunes como validación,
|
|
||||||
* conflictos, no encontrados, autenticación y errores de infraestructura.
|
|
||||||
*
|
|
||||||
* @param error - El error de la aplicación a mapear.
|
|
||||||
* @returns Un objeto ApiError que representa el error mapeado.
|
|
||||||
* @example
|
|
||||||
* const error = new Error("Invalid input");
|
|
||||||
* const apiError = errorMapper.toApiError(error);
|
|
||||||
* console.log(apiError);
|
|
||||||
* // Output: ValidationApiError { status: 422, title: 'Validation Failed', detail: 'Invalid input', type: 'https://httpstatuses.com/422' }
|
|
||||||
* @throws {ApiError} Si el error no puede ser mapeado a un tipo conocido.
|
|
||||||
* @see ApiError
|
|
||||||
* @see ValidationApiError
|
|
||||||
*/
|
|
||||||
toApiError: (error: Error): ApiError => {
|
|
||||||
const message = error.message || "An unexpected error occurred";
|
|
||||||
|
|
||||||
// 1. 🔍 Errores de validación complejos (agrupados)
|
|
||||||
if (error instanceof ValidationErrorCollection) {
|
|
||||||
return new ValidationApiError(error.message, error.details);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 🔍 Errores individuales de validación de dominio
|
|
||||||
if (error instanceof DomainValidationError) {
|
|
||||||
return new ValidationApiError(error.detail, [{ path: error.field, message: error.detail }]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error instanceof DuplicateEntityError) {
|
|
||||||
return new ConflictApiError(error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error instanceof EntityNotFoundError) {
|
|
||||||
return new NotFoundApiError(error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 🔍 Errores individuales de validación
|
|
||||||
if (
|
|
||||||
message.includes("invalid") ||
|
|
||||||
message.includes("is not valid") ||
|
|
||||||
message.includes("must be") ||
|
|
||||||
message.includes("cannot be") ||
|
|
||||||
message.includes("empty")
|
|
||||||
) {
|
|
||||||
return new ValidationApiError(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 🔍 Recurso no encontrado
|
|
||||||
if (error.name === "NotFoundError" || message.includes("not found")) {
|
|
||||||
return new NotFoundApiError(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 🔍 Conflicto (por ejemplo, duplicado)
|
|
||||||
if (
|
|
||||||
error.name === "ConflictError" ||
|
|
||||||
error instanceof UniqueConstraintError ||
|
|
||||||
message.includes("already exists") ||
|
|
||||||
message.includes("duplicate key")
|
|
||||||
) {
|
|
||||||
return new ConflictApiError(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. 🔍 No autenticado
|
|
||||||
if (error.name === "UnauthorizedError" || message.includes("unauthorized")) {
|
|
||||||
return new UnauthorizedApiError(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7. 🔍 Prohibido
|
|
||||||
if (error.name === "ForbiddenError" || message.includes("forbidden")) {
|
|
||||||
return new ForbiddenApiError(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 8. 🔍 Error de conexión o indisponibilidad de servicio
|
|
||||||
if (
|
|
||||||
error instanceof ConnectionError ||
|
|
||||||
message.includes("Database connection lost") ||
|
|
||||||
message.includes("timeout") ||
|
|
||||||
message.includes("ECONNREFUSED")
|
|
||||||
) {
|
|
||||||
return new UnavailableApiError("Service temporarily unavailable.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 9. 🔍 Fallback: error no identificado
|
|
||||||
return new InternalApiError(`Unexpected error: ${message}`);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,4 +1,5 @@
|
|||||||
export * from "./errors";
|
export * from "./application";
|
||||||
|
export * from "./domain";
|
||||||
export * from "./infrastructure";
|
export * from "./infrastructure";
|
||||||
export * from "./logger";
|
export * from "./logger";
|
||||||
export * from "./modules";
|
export * from "./modules";
|
||||||
|
|||||||
3
modules/core/src/api/infrastructure/errors/index.ts
Normal file
3
modules/core/src/api/infrastructure/errors/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from "./infrastructure-errors";
|
||||||
|
export * from "./infrastructure-repository-error";
|
||||||
|
export * from "./infrastructure-unavailable-error";
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* Errores de capa de infraestructura. No deben "filtrarse" a cliente tal cual.
|
||||||
|
* Se usan para decidir el código HTTP y para observabilidad (logs/tracing). */
|
||||||
|
|
||||||
|
export class InfrastructureError extends Error {
|
||||||
|
public readonly layer = "infrastructure" as const;
|
||||||
|
constructor(message: string, options?: ErrorOptions) {
|
||||||
|
super(message, options);
|
||||||
|
Object.setPrototypeOf(this, new.target.prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
import { InfrastructureError } from "./infrastructure-errors";
|
||||||
|
|
||||||
|
export class InfrastructureRepositoryError extends InfrastructureError {
|
||||||
|
public readonly code = "REPOSITORY_ERROR" as const;
|
||||||
|
constructor(message = "Repository operation failed", options?: ErrorOptions) {
|
||||||
|
super(message, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isInfrastructureRepositoryError = (e: unknown): e is InfrastructureRepositoryError =>
|
||||||
|
e instanceof InfrastructureRepositoryError;
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
import { InfrastructureError } from "./infrastructure-errors";
|
||||||
|
|
||||||
|
export class InfrastructureUnavailableError extends InfrastructureError {
|
||||||
|
public readonly code = "UNAVAILABLE" as const;
|
||||||
|
constructor(message = "Underlying service temporarily unavailable", options?: ErrorOptions) {
|
||||||
|
super(message, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isInfrastructureUnavailableError = (e: unknown): e is InfrastructureUnavailableError =>
|
||||||
|
e instanceof InfrastructureUnavailableError;
|
||||||
188
modules/core/src/api/infrastructure/express/api-error-mapper.ts
Normal file
188
modules/core/src/api/infrastructure/express/api-error-mapper.ts
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
// Clase para mapear errores de Dominio/Aplicación/Infraestructura → ApiError (Problem+JSON).
|
||||||
|
// - Inmutable (register() devuelve una NUEVA instancia).
|
||||||
|
// - Extensible por reglas con prioridad.
|
||||||
|
// - Sin dependencias de vendors (p.ej. Sequelize).
|
||||||
|
//
|
||||||
|
// ─ Convenciones de carpetas sugeridas:
|
||||||
|
// domain/errors/* → errores semánticos (DDD)
|
||||||
|
// application/errors/* → errores de aplicación (servicios, lógica de negocio)
|
||||||
|
// infrastructure/errors/* → errores técnicos (DB, red, timeouts)
|
||||||
|
// infrastructure/express/errors/* → ApiError (RFC7807) y familia
|
||||||
|
//
|
||||||
|
// Nota: Todos los nombres de tipos/clases/archivos en inglés; comentarios en castellano.
|
||||||
|
|
||||||
|
import {
|
||||||
|
DomainValidationError,
|
||||||
|
DuplicateEntityError,
|
||||||
|
EntityNotFoundError,
|
||||||
|
ValidationErrorCollection,
|
||||||
|
isDomainValidationError,
|
||||||
|
isDuplicateEntityError,
|
||||||
|
isEntityNotFoundError,
|
||||||
|
isValidationErrorCollection,
|
||||||
|
} from "../../domain";
|
||||||
|
import { isInfrastructureRepositoryError, isInfrastructureUnavailableError } from "../errors";
|
||||||
|
import {
|
||||||
|
ApiError,
|
||||||
|
ConflictApiError,
|
||||||
|
ForbiddenApiError,
|
||||||
|
InternalApiError,
|
||||||
|
NotFoundApiError,
|
||||||
|
UnauthorizedApiError,
|
||||||
|
UnavailableApiError,
|
||||||
|
ValidationApiError,
|
||||||
|
} from "./errors";
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Contexto opcional para enriquecer Problem+JSON (útil en middleware Express)
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────
|
||||||
|
export interface ApiErrorContext {
|
||||||
|
instance?: string; // p.ej. req.originalUrl
|
||||||
|
correlationId?: string; // p.ej. header 'x-correlation-id'
|
||||||
|
method?: string; // GET/POST/PUT/DELETE
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Regla de mapeo: cómo reconocer y construir un ApiError
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────
|
||||||
|
export interface ErrorToApiRule {
|
||||||
|
priority?: number; // mayor valor ⇒ se evalúa antes (default 0)
|
||||||
|
matches: (e: unknown) => boolean;
|
||||||
|
build: (e: unknown, ctx?: ApiErrorContext) => ApiError;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────
|
||||||
|
// ApiErrorMapper (inmutable)
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────
|
||||||
|
export class ApiErrorMapper {
|
||||||
|
private readonly rules: ReadonlyArray<ErrorToApiRule>;
|
||||||
|
private readonly fallback: (e: unknown, ctx?: ApiErrorContext) => ApiError;
|
||||||
|
|
||||||
|
private constructor(
|
||||||
|
rules: ReadonlyArray<ErrorToApiRule>,
|
||||||
|
fallback: (e: unknown, ctx?: ApiErrorContext) => ApiError
|
||||||
|
) {
|
||||||
|
this.rules = [...rules].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
||||||
|
this.fallback = fallback;
|
||||||
|
Object.freeze(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crea un mapper con reglas por defecto (cubren casos comunes)
|
||||||
|
static default(): ApiErrorMapper {
|
||||||
|
return new ApiErrorMapper(defaultRules, defaultFallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registra una regla adicional devolviendo una NUEVA instancia
|
||||||
|
register(rule: ErrorToApiRule): ApiErrorMapper {
|
||||||
|
return new ApiErrorMapper([...this.rules, rule], this.fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mapea un error a un ApiError evaluando reglas por prioridad
|
||||||
|
map(error: unknown, ctx?: ApiErrorContext): ApiError {
|
||||||
|
for (const rule of this.rules) {
|
||||||
|
try {
|
||||||
|
if (rule.matches(error)) {
|
||||||
|
return rule.build(error, ctx);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ⚠️ Una regla no debe tumbar el mapper; continuamos con la siguiente.
|
||||||
|
// continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.fallback(error, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Útil en tests / introspección
|
||||||
|
getRules(): ReadonlyArray<ErrorToApiRule> {
|
||||||
|
return this.rules;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Reglas por defecto (prioridad alta a más específicas)
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────
|
||||||
|
const defaultRules: ReadonlyArray<ErrorToApiRule> = [
|
||||||
|
// 1) Validación múltiple (colección)
|
||||||
|
{
|
||||||
|
priority: 100,
|
||||||
|
matches: (e) => isValidationErrorCollection(e),
|
||||||
|
build: (e) =>
|
||||||
|
new ValidationApiError(
|
||||||
|
(e as ValidationErrorCollection).message,
|
||||||
|
(e as ValidationErrorCollection).details
|
||||||
|
),
|
||||||
|
},
|
||||||
|
|
||||||
|
// 2) Validación de dominio unitaria
|
||||||
|
{
|
||||||
|
priority: 90,
|
||||||
|
matches: (e) => isDomainValidationError(e),
|
||||||
|
build: (e) =>
|
||||||
|
new ValidationApiError((e as DomainValidationError).detail, [
|
||||||
|
{ path: (e as DomainValidationError).field, message: (e as DomainValidationError).detail },
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
|
||||||
|
// 3) Duplicados / conflictos de unicidad
|
||||||
|
{
|
||||||
|
priority: 80,
|
||||||
|
matches: (e) => isDuplicateEntityError(e),
|
||||||
|
build: (e) => new ConflictApiError((e as DuplicateEntityError).message),
|
||||||
|
},
|
||||||
|
|
||||||
|
// 4) No encontrado
|
||||||
|
{
|
||||||
|
priority: 70,
|
||||||
|
matches: (e) => isEntityNotFoundError(e),
|
||||||
|
build: (e) => new NotFoundApiError((e as EntityNotFoundError).message),
|
||||||
|
},
|
||||||
|
|
||||||
|
// 5) Infra transitoria (DB/servicio caído, timeouts)
|
||||||
|
{
|
||||||
|
priority: 60,
|
||||||
|
matches: (e) => isInfrastructureUnavailableError(e),
|
||||||
|
build: () => new UnavailableApiError("Service temporarily unavailable."),
|
||||||
|
},
|
||||||
|
|
||||||
|
// 6) Infra no transitoria (errores de repositorio inesperados)
|
||||||
|
{
|
||||||
|
priority: 50,
|
||||||
|
matches: (e) => isInfrastructureRepositoryError(e),
|
||||||
|
build: () => new InternalApiError("Unexpected repository error."),
|
||||||
|
},
|
||||||
|
|
||||||
|
// 7) Autenticación/autorización por nombre (si no tienes clases dedicadas)
|
||||||
|
{
|
||||||
|
priority: 40,
|
||||||
|
matches: (e): e is Error => e instanceof Error && e.name === "UnauthorizedError",
|
||||||
|
build: (e) => new UnauthorizedApiError((e as Error).message || "Unauthorized"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
priority: 40,
|
||||||
|
matches: (e): e is Error => e instanceof Error && e.name === "ForbiddenError",
|
||||||
|
build: (e) => new ForbiddenApiError((e as Error).message || "Forbidden"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Fallback genérico (500)
|
||||||
|
function defaultFallback(e: unknown): ApiError {
|
||||||
|
const message = typeof (e as any)?.message === "string" ? (e as any).message : "Unexpected error";
|
||||||
|
return new InternalApiError(`Unexpected error: ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Serializador opcional a Problem+JSON (si tu ApiError no lo trae ya)
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────────
|
||||||
|
export function toProblemJson(apiError: ApiError, ctx?: ApiErrorContext) {
|
||||||
|
const maybeErrors = (apiError as any).errors ? { errors: (apiError as any).errors } : {};
|
||||||
|
return {
|
||||||
|
type: apiError.type,
|
||||||
|
title: apiError.title,
|
||||||
|
status: apiError.status,
|
||||||
|
detail: apiError.detail,
|
||||||
|
...(ctx?.instance ? { instance: ctx.instance } : {}),
|
||||||
|
...(ctx?.correlationId ? { correlationId: ctx.correlationId } : {}),
|
||||||
|
...(ctx?.method ? { method: ctx.method } : {}),
|
||||||
|
...maybeErrors,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { InfrastructureError } from "../../errors";
|
||||||
|
|
||||||
interface IApiErrorOptions {
|
interface IApiErrorOptions {
|
||||||
status: number;
|
status: number;
|
||||||
title: string;
|
title: string;
|
||||||
@ -8,7 +10,7 @@ interface IApiErrorOptions {
|
|||||||
[key: string]: any; // Para permitir añadir campos extra
|
[key: string]: any; // Para permitir añadir campos extra
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ApiError extends Error {
|
export class ApiError extends InfrastructureError {
|
||||||
public status: number;
|
public status: number;
|
||||||
public title: string;
|
public title: string;
|
||||||
public detail: string;
|
public detail: string;
|
||||||
@ -1,13 +1,8 @@
|
|||||||
export * from "./api-error";
|
export * from "./api-error";
|
||||||
export * from "./conflict-api-error";
|
export * from "./conflict-api-error";
|
||||||
export * from "./domain-validation-error";
|
|
||||||
export * from "./duplicate-entity-error";
|
|
||||||
export * from "./entity-not-found-error";
|
|
||||||
export * from "./error-mapper";
|
|
||||||
export * from "./forbidden-api-error";
|
export * from "./forbidden-api-error";
|
||||||
export * from "./internal-api-error";
|
export * from "./internal-api-error";
|
||||||
export * from "./not-found-api-error";
|
export * from "./not-found-api-error";
|
||||||
export * from "./unauthorized-api-error";
|
export * from "./unauthorized-api-error";
|
||||||
export * from "./unavailable-api-error";
|
export * from "./unavailable-api-error";
|
||||||
export * from "./validation-api-error";
|
export * from "./validation-api-error";
|
||||||
export * from "./validation-error-collection";
|
|
||||||
@ -10,7 +10,7 @@ import {
|
|||||||
UnauthorizedApiError,
|
UnauthorizedApiError,
|
||||||
UnavailableApiError,
|
UnavailableApiError,
|
||||||
ValidationApiError,
|
ValidationApiError,
|
||||||
} from "../../errors";
|
} from "./errors";
|
||||||
|
|
||||||
type GuardResultLike = { isFailure: boolean; error?: ApiError };
|
type GuardResultLike = { isFailure: boolean; error?: ApiError };
|
||||||
export type GuardContext = {
|
export type GuardContext = {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { ForbiddenApiError, UnauthorizedApiError, ValidationApiError } from "../../errors";
|
import { ForbiddenApiError, UnauthorizedApiError, ValidationApiError } from "./errors";
|
||||||
import { GuardContext, GuardFn, guardFail, guardOk } from "./express-controller";
|
import { GuardContext, GuardFn, guardFail, guardOk } from "./express-controller";
|
||||||
|
|
||||||
// ───────────────────────────────────────────────────────────────────────────
|
// ───────────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
export * from "./api-error-mapper";
|
||||||
|
export * from "./errors";
|
||||||
export * from "./express-controller";
|
export * from "./express-controller";
|
||||||
export * from "./express-guards";
|
export * from "./express-guards";
|
||||||
export * from "./middlewares";
|
export * from "./middlewares";
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import { ApiError } from "../../../errors/api-error";
|
import { ApiErrorMapper, toProblemJson } from "../api-error-mapper";
|
||||||
|
import { ApiError } from "../errors/api-error";
|
||||||
|
|
||||||
export const globalErrorHandler = async (
|
export const globalErrorHandler = async (
|
||||||
error: Error,
|
error: Error,
|
||||||
@ -11,6 +12,19 @@ export const globalErrorHandler = async (
|
|||||||
if (res.headersSent) {
|
if (res.headersSent) {
|
||||||
return next(error);
|
return next(error);
|
||||||
}
|
}
|
||||||
|
const ctx = {
|
||||||
|
instance: req.originalUrl,
|
||||||
|
correlationId: (req.headers["x-correlation-id"] as string) || undefined,
|
||||||
|
method: req.method,
|
||||||
|
};
|
||||||
|
|
||||||
|
const apiError = ApiErrorMapper.map(err, ctx);
|
||||||
|
const body = toProblemJson(apiError, ctx);
|
||||||
|
|
||||||
|
// 👇 Log interno con cause/traza (no lo exponemos al cliente)
|
||||||
|
// logger.error({ err, cause: (err as any)?.cause, ...ctx }, `❌ Unhandled API error: ${error.message}`);
|
||||||
|
|
||||||
|
res.status(apiError.status).json(body);
|
||||||
|
|
||||||
//logger.error(`❌ Unhandled API error: ${error.message}`);
|
//logger.error(`❌ Unhandled API error: ${error.message}`);
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { RequestHandler } from "express";
|
import { RequestHandler } from "express";
|
||||||
import { ZodSchema } from "zod/v4";
|
import { ZodSchema } from "zod/v4";
|
||||||
import { InternalApiError, ValidationApiError } from "../../../errors";
|
import { InternalApiError, ValidationApiError } from "../errors";
|
||||||
import { ExpressController } from "../express-controller";
|
import { ExpressController } from "../express-controller";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
export * from "./database";
|
export * from "./database";
|
||||||
|
export * from "./errors";
|
||||||
export * from "./express";
|
export * from "./express";
|
||||||
export * from "./sequelize";
|
export * from "./sequelize";
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
export * from "./sequelize-error-translator";
|
||||||
export * from "./sequelize-mapper";
|
export * from "./sequelize-mapper";
|
||||||
export * from "./sequelize-repository";
|
export * from "./sequelize-repository";
|
||||||
export * from "./sequelize-transaction-manager";
|
export * from "./sequelize-transaction-manager";
|
||||||
|
|||||||
@ -0,0 +1,77 @@
|
|||||||
|
import {
|
||||||
|
ConnectionError,
|
||||||
|
DatabaseError,
|
||||||
|
ForeignKeyConstraintError,
|
||||||
|
ValidationError as SequelizeValidationError,
|
||||||
|
UniqueConstraintError,
|
||||||
|
} from "sequelize";
|
||||||
|
import {
|
||||||
|
DomainValidationError,
|
||||||
|
DuplicateEntityError,
|
||||||
|
EntityNotFoundError,
|
||||||
|
ValidationErrorCollection,
|
||||||
|
} from "../../domain";
|
||||||
|
import { InfrastructureRepositoryError } from "../errors/infrastructure-repository-error";
|
||||||
|
import { InfrastructureUnavailableError } from "../errors/infrastructure-unavailable-error";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Traduce errores específicos de Sequelize a errores de dominio/infraestructura
|
||||||
|
* entendibles por el resto de capas.
|
||||||
|
*
|
||||||
|
* 👉 Este traductor pertenece a la infraestructura (persistencia)
|
||||||
|
*/
|
||||||
|
export function translateSequelizeError(err: unknown): Error {
|
||||||
|
// 1) Duplicados (índices únicos)
|
||||||
|
if (err instanceof UniqueConstraintError) {
|
||||||
|
// Tomamos el primer detalle (puede haber varios)
|
||||||
|
const detail = err.errors?.[0];
|
||||||
|
const entity = detail?.instance?.constructor.name ?? "unknown_entity";
|
||||||
|
const value = detail?.value ?? "unknown_value";
|
||||||
|
const field = detail?.path ?? "unknown_field";
|
||||||
|
|
||||||
|
// ⚠️ Si los nombres de campo son sensibles, normaliza/whitelistea antes de exponerlos a dominio
|
||||||
|
return new DuplicateEntityError(entity, field, value, { cause: err });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Violación de FK → error de validación de dominio (referencia inválida)
|
||||||
|
if (err instanceof ForeignKeyConstraintError) {
|
||||||
|
// Sequelize expone `fields` (obj) y `index`. Extraemos el campo si está.
|
||||||
|
const entity = err.index ?? "unknown_entity";
|
||||||
|
const field = err.fields ? Object.keys(err.fields)[0] : "foreign_key";
|
||||||
|
const value = err.fields ? Object.values(err.fields)[0] : "unknown_value";
|
||||||
|
return new EntityNotFoundError(entity, field, value, { cause: err });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Validaciones de Sequelize (pueden venir varias)
|
||||||
|
if (err instanceof SequelizeValidationError) {
|
||||||
|
const details = (err.errors ?? []).map((e) => ({
|
||||||
|
path: e.path ?? "unknown",
|
||||||
|
message: e.message,
|
||||||
|
// Algunas props útiles: e.validatorKey / e.validatorName
|
||||||
|
rule: (e as any).validatorKey ?? undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Si sólo hay 1, puedes preferir DomainValidationError
|
||||||
|
if (details.length === 1) {
|
||||||
|
const d = details[0];
|
||||||
|
return DomainValidationError.invalidFormat(d.path, d.message, { cause: err });
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ValidationErrorCollection("Invalid data provided", details, { cause: err });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Conectividad / indisponibilidad (transitorio)
|
||||||
|
if (err instanceof ConnectionError) {
|
||||||
|
return new InfrastructureUnavailableError("Database connection unavailable", { cause: err });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5) Otros errores de base de datos (no transitorios)
|
||||||
|
if (err instanceof DatabaseError) {
|
||||||
|
return new InfrastructureRepositoryError("Database error occurred", { cause: err });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6) Fallback: deja pasar si ya es un Error tipado de tu app, si no wrap
|
||||||
|
if (err instanceof Error) return err;
|
||||||
|
|
||||||
|
return new InfrastructureRepositoryError("Unknown persistence error", { cause: err as any });
|
||||||
|
}
|
||||||
@ -7,9 +7,9 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
"target": "ES2020",
|
"target": "ES2022",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
|||||||
@ -33,7 +33,7 @@ export class DeleteCustomerInvoiceUseCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!existsCheck.data) {
|
if (!existsCheck.data) {
|
||||||
return Result.fail(new EntityNotFoundError("CustomerInvoice", id.toString()));
|
return Result.fail(new EntityNotFoundError("CustomerInvoice", "id", id.toString()));
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.service.deleteById(invoiceId, transaction);
|
return await this.service.deleteById(invoiceId, transaction);
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
// Ejemplo: regla específica para Billing → InvoiceIdAlreadyExistsError
|
||||||
|
// (si defines un error más ubicuo dentro del BC con su propia clase)
|
||||||
|
|
||||||
|
import { DomainError } from "@erp/core/api";
|
||||||
|
|
||||||
|
// Suponemos que existe esta clase en tu dominio de Billing:
|
||||||
|
export class CustomerInvoiceIdAlreadyExistsError extends DomainError {
|
||||||
|
public readonly code = "DUPLICATE_INVOICE_ID" as const;
|
||||||
|
}
|
||||||
1
modules/customer-invoices/src/api/domain/errors/index.ts
Normal file
1
modules/customer-invoices/src/api/domain/errors/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./customer-invoice-id-already-exits-error";
|
||||||
@ -1,5 +1,6 @@
|
|||||||
export * from "./aggregates";
|
export * from "./aggregates";
|
||||||
export * from "./entities";
|
export * from "./entities";
|
||||||
|
export * from "./errors";
|
||||||
export * from "./repositories";
|
export * from "./repositories";
|
||||||
export * from "./services";
|
export * from "./services";
|
||||||
export * from "./value-objects";
|
export * from "./value-objects";
|
||||||
|
|||||||
@ -0,0 +1,21 @@
|
|||||||
|
// Ejemplo: regla específica para Billing → InvoiceIdAlreadyExistsError
|
||||||
|
// (si defines un error más ubicuo dentro del BC con su propia clase)
|
||||||
|
|
||||||
|
import { ApiErrorMapper, ConflictApiError, ErrorToApiRule } from "@erp/core/api";
|
||||||
|
import { CustomerInvoiceIdAlreadyExistsError } from "../../domain";
|
||||||
|
|
||||||
|
// Crea una regla específica (prioridad alta para sobreescribir mensajes)
|
||||||
|
const invoiceDuplicateRule: ErrorToApiRule = {
|
||||||
|
priority: 120,
|
||||||
|
matches: (e): e is CustomerInvoiceIdAlreadyExistsError =>
|
||||||
|
e instanceof CustomerInvoiceIdAlreadyExistsError,
|
||||||
|
build: (e) =>
|
||||||
|
new ConflictApiError(
|
||||||
|
(e as CustomerInvoiceIdAlreadyExistsError).message ||
|
||||||
|
"Invoice with the provided id already exists."
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cómo aplicarla: crea una nueva instancia del mapper con la regla extra
|
||||||
|
export const customerInvoicesApiErrorMapper: ApiErrorMapper =
|
||||||
|
ApiErrorMapper.default().register(invoiceDuplicateRule);
|
||||||
@ -7,9 +7,9 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
"target": "ES2020",
|
"target": "ES2022",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,11 @@ import { ICustomerService } from "../../domain";
|
|||||||
import { mapDTOToCustomerProps } from "../helpers";
|
import { mapDTOToCustomerProps } from "../helpers";
|
||||||
import { CreateCustomersAssembler } from "./assembler";
|
import { CreateCustomersAssembler } from "./assembler";
|
||||||
|
|
||||||
|
type CreateCustomerUseCaseInput = {
|
||||||
|
tenantId: string;
|
||||||
|
dto: CreateCustomerCommandDTO;
|
||||||
|
};
|
||||||
|
|
||||||
export class CreateCustomerUseCase {
|
export class CreateCustomerUseCase {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly service: ICustomerService,
|
private readonly service: ICustomerService,
|
||||||
@ -13,7 +18,9 @@ export class CreateCustomerUseCase {
|
|||||||
private readonly assembler: CreateCustomersAssembler
|
private readonly assembler: CreateCustomersAssembler
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public execute(dto: CreateCustomerCommandDTO) {
|
public execute(params: CreateCustomerUseCaseInput) {
|
||||||
|
const { dto, tenantId: companyId } = params;
|
||||||
|
|
||||||
const customerPropsOrError = mapDTOToCustomerProps(dto);
|
const customerPropsOrError = mapDTOToCustomerProps(dto);
|
||||||
|
|
||||||
if (customerPropsOrError.isFailure) {
|
if (customerPropsOrError.isFailure) {
|
||||||
@ -42,7 +49,7 @@ export class CreateCustomerUseCase {
|
|||||||
return Result.fail(new DuplicateEntityError("Customer", id.toString()));
|
return Result.fail(new DuplicateEntityError("Customer", id.toString()));
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await this.service.save(newCustomer, transaction);
|
const result = await this.service.save(newCustomer, idCompany, transaction);
|
||||||
if (result.isFailure) {
|
if (result.isFailure) {
|
||||||
return Result.fail(result.error);
|
return Result.fail(result.error);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { EntityNotFoundError, ITransactionManager } from "@erp/core/api";
|
import { EntityNotFoundError, ITransactionManager } from "@erp/core/api";
|
||||||
import { DeleteCustomerByIdQueryDTO } from "@erp/customers/common/dto";
|
|
||||||
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 { ICustomerService } from "../../domain";
|
import { ICustomerService } from "../../domain";
|
||||||
@ -21,17 +20,17 @@ export class DeleteCustomerUseCase {
|
|||||||
|
|
||||||
return this.transactionManager.complete(async (transaction) => {
|
return this.transactionManager.complete(async (transaction) => {
|
||||||
try {
|
try {
|
||||||
const existsCheck = await this.service.existsById(id, transaction);
|
const existsCheck = await this.service.existsByIdInCompany(id, transaction);
|
||||||
|
|
||||||
if (existsCheck.isFailure) {
|
if (existsCheck.isFailure) {
|
||||||
return Result.fail(existsCheck.error);
|
return Result.fail(existsCheck.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!existsCheck.data) {
|
if (!existsCheck.data) {
|
||||||
return Result.fail(new EntityNotFoundError("Customer", id.toString()));
|
return Result.fail(new EntityNotFoundError("Customer", "id", id.toString()));
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.service.deleteById(id, transaction);
|
return await this.service.deleteCustomerByIdInCompany(id, transaction);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
return Result.fail(error as Error);
|
return Result.fail(error as Error);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ export class GetCustomerAssembler {
|
|||||||
id: customer.id.toPrimitive(),
|
id: customer.id.toPrimitive(),
|
||||||
reference: customer.reference,
|
reference: customer.reference,
|
||||||
|
|
||||||
is_freelancer: customer.isFreelancer,
|
is_companyr: customer.isFreelancer,
|
||||||
name: customer.name,
|
name: customer.name,
|
||||||
trade_name: customer.tradeName.getOrUndefined(),
|
trade_name: customer.tradeName.getOrUndefined(),
|
||||||
tin: customer.tin.toPrimitive(),
|
tin: customer.tin.toPrimitive(),
|
||||||
|
|||||||
@ -1,10 +1,14 @@
|
|||||||
import { ITransactionManager } from "@erp/core/api";
|
import { ITransactionManager } from "@erp/core/api";
|
||||||
import { GetCustomerByIdQueryDTO } from "@erp/customers/common/dto";
|
|
||||||
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 { ICustomerService } from "../../domain";
|
import { ICustomerService } from "../../domain";
|
||||||
import { GetCustomerAssembler } from "./assembler";
|
import { GetCustomerAssembler } from "./assembler";
|
||||||
|
|
||||||
|
type GetCustomerUseCaseInput = {
|
||||||
|
tenantId: string;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
export class GetCustomerUseCase {
|
export class GetCustomerUseCase {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly service: ICustomerService,
|
private readonly service: ICustomerService,
|
||||||
@ -12,16 +16,28 @@ export class GetCustomerUseCase {
|
|||||||
private readonly assembler: GetCustomerAssembler
|
private readonly assembler: GetCustomerAssembler
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public execute(dto: GetCustomerByIdQueryDTO) {
|
public execute(params: GetCustomerUseCaseInput) {
|
||||||
const idOrError = UniqueID.create(dto.id);
|
const { id, tenantId: companyId } = params;
|
||||||
|
|
||||||
|
const idOrError = UniqueID.create(id);
|
||||||
|
|
||||||
if (idOrError.isFailure) {
|
if (idOrError.isFailure) {
|
||||||
return Result.fail(idOrError.error);
|
return Result.fail(idOrError.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const companyIdOrError = UniqueID.create(companyId);
|
||||||
|
|
||||||
|
if (companyIdOrError.isFailure) {
|
||||||
|
return Result.fail(companyIdOrError.error);
|
||||||
|
}
|
||||||
|
|
||||||
return this.transactionManager.complete(async (transaction) => {
|
return this.transactionManager.complete(async (transaction) => {
|
||||||
try {
|
try {
|
||||||
const customerOrError = await this.service.getById(idOrError.data, transaction);
|
const customerOrError = await this.service.getCustomerByIdInCompany(
|
||||||
|
companyIdOrError.data,
|
||||||
|
idOrError.data,
|
||||||
|
transaction
|
||||||
|
);
|
||||||
if (customerOrError.isFailure) {
|
if (customerOrError.isFailure) {
|
||||||
return Result.fail(customerOrError.error);
|
return Result.fail(customerOrError.error);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ export class ListCustomersAssembler {
|
|||||||
id: customer.id.toPrimitive(),
|
id: customer.id.toPrimitive(),
|
||||||
reference: customer.reference,
|
reference: customer.reference,
|
||||||
|
|
||||||
is_freelancer: customer.isFreelancer,
|
is_companyr: customer.isFreelancer,
|
||||||
name: customer.name,
|
name: customer.name,
|
||||||
trade_name: customer.tradeName.getOrUndefined() || "",
|
trade_name: customer.tradeName.getOrUndefined() || "",
|
||||||
tin: customer.tin.toString(),
|
tin: customer.tin.toString(),
|
||||||
|
|||||||
@ -9,8 +9,9 @@ import {
|
|||||||
import { Maybe, Result } from "@repo/rdx-utils";
|
import { Maybe, Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
export interface CustomerProps {
|
export interface CustomerProps {
|
||||||
|
companyId: UniqueID;
|
||||||
reference: string;
|
reference: string;
|
||||||
isFreelancer: boolean;
|
isCompany: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
tin: TINNumber;
|
tin: TINNumber;
|
||||||
address: PostalAddress;
|
address: PostalAddress;
|
||||||
@ -29,6 +30,7 @@ export interface CustomerProps {
|
|||||||
|
|
||||||
export interface ICustomer {
|
export interface ICustomer {
|
||||||
id: UniqueID;
|
id: UniqueID;
|
||||||
|
companyId: UniqueID;
|
||||||
reference: string;
|
reference: string;
|
||||||
name: string;
|
name: string;
|
||||||
tin: TINNumber;
|
tin: TINNumber;
|
||||||
@ -44,8 +46,8 @@ export interface ICustomer {
|
|||||||
fax: Maybe<PhoneNumber>;
|
fax: Maybe<PhoneNumber>;
|
||||||
website: Maybe<string>;
|
website: Maybe<string>;
|
||||||
|
|
||||||
isCustomer: boolean;
|
isIndividual: boolean;
|
||||||
isFreelancer: boolean;
|
isCompany: boolean;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,6 +66,15 @@ export class Customer extends AggregateRoot<CustomerProps> implements ICustomer
|
|||||||
return Result.ok(contact);
|
return Result.ok(contact);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
update(partial: Partial<Omit<CustomerProps, "companyId">>): Result<Customer, Error> {
|
||||||
|
const updatedCustomer = new Customer({ ...this.props, ...partial }, this.id);
|
||||||
|
return Result.ok(updatedCustomer);
|
||||||
|
}
|
||||||
|
|
||||||
|
get companyId(): UniqueID {
|
||||||
|
return this.props.companyId;
|
||||||
|
}
|
||||||
|
|
||||||
get reference() {
|
get reference() {
|
||||||
return this.props.reference;
|
return this.props.reference;
|
||||||
}
|
}
|
||||||
@ -116,12 +127,12 @@ export class Customer extends AggregateRoot<CustomerProps> implements ICustomer
|
|||||||
return this.props.currencyCode;
|
return this.props.currencyCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isCustomer(): boolean {
|
get isIndividual(): boolean {
|
||||||
return !this.props.isFreelancer;
|
return !this.props.isCompany;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isFreelancer(): boolean {
|
get isCompany(): boolean {
|
||||||
return this.props.isFreelancer;
|
return this.props.isCompany;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isActive(): boolean {
|
get isActive(): boolean {
|
||||||
|
|||||||
@ -3,48 +3,50 @@ import { UniqueID } from "@repo/rdx-ddd";
|
|||||||
import { Collection, Result } from "@repo/rdx-utils";
|
import { Collection, Result } from "@repo/rdx-utils";
|
||||||
import { Customer } from "../aggregates";
|
import { Customer } from "../aggregates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contrato del repositorio de Customers.
|
||||||
|
* Define la interfaz de persistencia para el agregado `Customer`.
|
||||||
|
* El escopado multitenant está representado por `companyId`.
|
||||||
|
*/
|
||||||
export interface ICustomerRepository {
|
export interface ICustomerRepository {
|
||||||
existsById(id: UniqueID, transaction?: any): Promise<Result<boolean, Error>>;
|
/**
|
||||||
|
* Guarda (crea o actualiza) un Customer en la base de datos.
|
||||||
|
* Retorna el objeto actualizado tras la operación.
|
||||||
|
*/
|
||||||
|
save(customer: Customer, transaction?: any): Promise<Result<Customer, Error>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Comprueba si existe un Customer con un `id` dentro de una `company`.
|
||||||
* Persiste una nueva factura o actualiza una existente.
|
|
||||||
*
|
|
||||||
* @param customer - El agregado a guardar.
|
|
||||||
* @param transaction - Transacción activa para la operación.
|
|
||||||
* @returns Result<Customer, Error>
|
|
||||||
*/
|
*/
|
||||||
save(customer: Customer, transaction: any): Promise<Result<Customer, Error>>;
|
existsByIdInCompany(
|
||||||
|
companyId: UniqueID,
|
||||||
|
id: UniqueID,
|
||||||
|
transaction?: any
|
||||||
|
): Promise<Result<boolean, Error>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Recupera un Customer por su ID y companyId.
|
||||||
* Busca una factura por su identificador único.
|
* Devuelve un `NotFoundError` si no se encuentra.
|
||||||
* @param id - UUID de la factura.
|
|
||||||
* @param transaction - Transacción activa para la operación.
|
|
||||||
* @returns Result<Customer, Error>
|
|
||||||
*/
|
*/
|
||||||
findById(id: UniqueID, transaction: any): Promise<Result<Customer, Error>>;
|
getByIdInCompany(
|
||||||
|
companyId: UniqueID,
|
||||||
|
id: UniqueID,
|
||||||
|
transaction?: any
|
||||||
|
): Promise<Result<Customer, Error>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Recupera múltiples customers dentro de una empresa según un criterio dinámico (búsqueda, paginación, etc.).
|
||||||
* Consulta facturas usando un objeto Criteria (filtros, orden, paginación).
|
* El resultado está encapsulado en un objeto `Collection<T>`.
|
||||||
* @param criteria - Criterios de búsqueda.
|
|
||||||
* @param transaction - Transacción activa para la operación.
|
|
||||||
* @returns Result<Customer[], Error>
|
|
||||||
*
|
|
||||||
* @see Criteria
|
|
||||||
*/
|
*/
|
||||||
findByCriteria(
|
findByCriteriaInCompany(
|
||||||
|
companyId: UniqueID,
|
||||||
criteria: Criteria,
|
criteria: Criteria,
|
||||||
transaction: any
|
transaction?: any
|
||||||
): Promise<Result<Collection<Customer>, Error>>;
|
): Promise<Result<Collection<Customer>>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Elimina un Customer por su ID, dentro de una empresa.
|
||||||
* Elimina o marca como eliminada una factura.
|
* Retorna `void` si se elimina correctamente, o `NotFoundError` si no existía.
|
||||||
* @param id - UUID de la factura a eliminar.
|
|
||||||
* @param transaction - Transacción activa para la operación.
|
|
||||||
* @returns Result<void, Error>
|
|
||||||
*/
|
*/
|
||||||
deleteById(id: UniqueID, transaction: any): Promise<Result<void, Error>>;
|
deleteByIdInCompany(companyId: UniqueID, id: UniqueID, transaction?: any): Promise<Result<void>>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,30 +4,63 @@ import { Collection, Result } from "@repo/rdx-utils";
|
|||||||
import { Customer, CustomerProps } from "../aggregates";
|
import { Customer, CustomerProps } from "../aggregates";
|
||||||
|
|
||||||
export interface ICustomerService {
|
export interface ICustomerService {
|
||||||
build(props: CustomerProps, id?: UniqueID): Result<Customer, Error>;
|
/**
|
||||||
|
* Construye un nuevo Customer validando todos sus value objects.
|
||||||
|
*/
|
||||||
|
buildCustomerInCompany(
|
||||||
|
companyId: UniqueID,
|
||||||
|
props: Omit<CustomerProps, "companyId">,
|
||||||
|
customerId?: UniqueID
|
||||||
|
): Result<Customer, Error>;
|
||||||
|
|
||||||
save(invoice: Customer, transaction: any): Promise<Result<Customer, Error>>;
|
/**
|
||||||
|
* Guarda un Customer (nuevo o modificado) en base de datos.
|
||||||
|
*/
|
||||||
|
saveCustomerInCompany(customer: Customer, transaction: any): Promise<Result<Customer, Error>>;
|
||||||
|
|
||||||
existsById(id: UniqueID, transaction?: any): Promise<Result<boolean, Error>>;
|
/**
|
||||||
|
* Comprueba si existe un Customer con ese ID en la empresa indicada.
|
||||||
|
*/
|
||||||
|
existsByIdInCompany(
|
||||||
|
companyId: UniqueID,
|
||||||
|
customerId: UniqueID,
|
||||||
|
transaction?: any
|
||||||
|
): Promise<Result<boolean, Error>>;
|
||||||
|
|
||||||
findByCriteria(
|
/**
|
||||||
|
* Lista todos los customers que cumplan el criterio, dentro de una empresa.
|
||||||
|
*/
|
||||||
|
findCustomerByCriteriaInCompany(
|
||||||
|
companyId: UniqueID,
|
||||||
criteria: Criteria,
|
criteria: Criteria,
|
||||||
transaction?: any
|
transaction?: any
|
||||||
): Promise<Result<Collection<Customer>, Error>>;
|
): Promise<Result<Collection<Customer>, Error>>;
|
||||||
|
|
||||||
getById(id: UniqueID, transaction?: any): Promise<Result<Customer>>;
|
/**
|
||||||
|
* Recupera un Customer por su ID dentro de una empresa.
|
||||||
|
*/
|
||||||
|
getCustomerByIdInCompany(
|
||||||
|
companyId: UniqueID,
|
||||||
|
customerId: UniqueID,
|
||||||
|
transaction?: any
|
||||||
|
): Promise<Result<Customer>>;
|
||||||
|
|
||||||
updateById(
|
/**
|
||||||
id: UniqueID,
|
* Actualiza parcialmente los datos de un Customer.
|
||||||
data: Partial<CustomerProps>,
|
*/
|
||||||
|
updateCustomerByIdInCompany(
|
||||||
|
companyId: UniqueID,
|
||||||
|
customerId: UniqueID,
|
||||||
|
partial: Partial<Omit<CustomerProps, "companyId">>,
|
||||||
transaction?: any
|
transaction?: any
|
||||||
): Promise<Result<Customer, Error>>;
|
): Promise<Result<Customer, Error>>;
|
||||||
|
|
||||||
createCustomer(
|
/**
|
||||||
id: UniqueID,
|
* Elimina un Customer por ID dentro de una empresa.
|
||||||
data: CustomerProps,
|
*/
|
||||||
|
deleteCustomerByIdInCompany(
|
||||||
|
companyId: UniqueID,
|
||||||
|
customerId: UniqueID,
|
||||||
transaction?: any
|
transaction?: any
|
||||||
): Promise<Result<Customer, Error>>;
|
): Promise<Result<void, Error>>;
|
||||||
|
|
||||||
deleteById(id: UniqueID, transaction?: any): Promise<Result<void, Error>>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,23 +1,34 @@
|
|||||||
import { Criteria } from "@repo/rdx-criteria/server";
|
import { Criteria } from "@repo/rdx-criteria/server";
|
||||||
import { UniqueID } from "@repo/rdx-ddd";
|
import { UniqueID } from "@repo/rdx-ddd";
|
||||||
import { Collection, Result } from "@repo/rdx-utils";
|
import { Collection, Result } from "@repo/rdx-utils";
|
||||||
import { Transaction } from "sequelize";
|
|
||||||
import { Customer, CustomerProps } from "../aggregates";
|
import { Customer, CustomerProps } from "../aggregates";
|
||||||
import { ICustomerRepository } from "../repositories";
|
import { ICustomerRepository } from "../repositories";
|
||||||
import { ICustomerService } from "./customer-service.interface";
|
import { ICustomerService } from "./customer-service.interface";
|
||||||
|
|
||||||
export class CustomerService implements ICustomerService {
|
export class CustomerService implements ICustomerService {
|
||||||
constructor(private readonly repository: ICustomerRepository) {}
|
constructor(private readonly repository: ICustomerRepository) {}
|
||||||
|
findCustomerByCriteriaInCompany(
|
||||||
|
companyId: UniqueID,
|
||||||
|
criteria: Criteria,
|
||||||
|
transaction?: any
|
||||||
|
): Promise<Result<Collection<Customer>, Error>> {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construye un nuevo agregado Customer a partir de props validadas.
|
* Construye un nuevo agregado Customer a partir de props validadas.
|
||||||
*
|
*
|
||||||
* @param props - Las propiedades ya validadas para crear la factura.
|
* @param companyId - Identificador de la empresa a la que pertenece el cliente.
|
||||||
* @param id - Identificador UUID de la factura (opcional).
|
* @param props - Las propiedades ya validadas para crear el cliente.
|
||||||
|
* @param customerId - Identificador UUID del cliente (opcional).
|
||||||
* @returns Result<Customer, Error> - El agregado construido o un error si falla la creación.
|
* @returns Result<Customer, Error> - El agregado construido o un error si falla la creación.
|
||||||
*/
|
*/
|
||||||
build(props: CustomerProps, id?: UniqueID): Result<Customer, Error> {
|
buildCustomerInCompany(
|
||||||
return Customer.create(props, id);
|
companyId: UniqueID,
|
||||||
|
props: Omit<CustomerProps, "companyId">,
|
||||||
|
customerId?: UniqueID
|
||||||
|
): Result<Customer, Error> {
|
||||||
|
return Customer.create({ ...props, companyId }, customerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -27,123 +38,107 @@ export class CustomerService implements ICustomerService {
|
|||||||
* @param transaction - Transacción activa para la operación.
|
* @param transaction - Transacción activa para la operación.
|
||||||
* @returns Result<Customer, Error> - El agregado guardado o un error si falla la operación.
|
* @returns Result<Customer, Error> - El agregado guardado o un error si falla la operación.
|
||||||
*/
|
*/
|
||||||
async save(invoice: Customer, transaction: any): Promise<Result<Customer, Error>> {
|
async saveCustomerInCompany(
|
||||||
const saved = await this.repository.save(invoice, transaction);
|
customer: Customer,
|
||||||
return saved.isSuccess ? Result.ok(invoice) : Result.fail(saved.error);
|
transaction: any
|
||||||
|
): Promise<Result<Customer, Error>> {
|
||||||
|
return this.repository.save(customer, transaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* Comprueba si existe o no en persistencia una factura con el ID proporcionado
|
* Comprueba si existe o no en persistencia un cliente con el ID proporcionado
|
||||||
*
|
*
|
||||||
* @param id - Identificador UUID de la factura.
|
* @param companyId - Identificador UUID de la empresa a la que pertenece el cliente.
|
||||||
|
* @param customerId - Identificador UUID del cliente
|
||||||
* @param transaction - Transacción activa para la operación.
|
* @param transaction - Transacción activa para la operación.
|
||||||
* @returns Result<Boolean, Error> - Existe la factura o no.
|
* @returns Result<Boolean, Error> - Existe el cliente o no.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async existsById(id: UniqueID, transaction?: any): Promise<Result<boolean, Error>> {
|
existsByIdInCompany(
|
||||||
return this.repository.existsById(id, transaction);
|
companyId: UniqueID,
|
||||||
|
customerId: UniqueID,
|
||||||
|
transaction?: any
|
||||||
|
): Promise<Result<boolean, Error>> {
|
||||||
|
return this.repository.existsByIdInCompany(companyId, customerId, transaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtiene una colección de facturas que cumplen con los filtros definidos en un objeto Criteria.
|
* Obtiene una colección de clientes que cumplen con los filtros definidos en un objeto Criteria.
|
||||||
*
|
*
|
||||||
|
* @param companyId - Identificador UUID de la empresa a la que pertenece el cliente.
|
||||||
* @param criteria - Objeto con condiciones de filtro, paginación y orden.
|
* @param criteria - Objeto con condiciones de filtro, paginación y orden.
|
||||||
* @param transaction - Transacción activa para la operación.
|
* @param transaction - Transacción activa para la operación.
|
||||||
* @returns Result<Collection<Customer>, Error> - Colección de facturas o error.
|
* @returns Result<Collection<Customer>, Error> - Colección de clientes o error.
|
||||||
*/
|
*/
|
||||||
async findByCriteria(
|
async findCustomersByCriteriaInCompany(
|
||||||
|
companyId: UniqueID,
|
||||||
criteria: Criteria,
|
criteria: Criteria,
|
||||||
transaction?: Transaction
|
transaction?: any
|
||||||
): Promise<Result<Collection<Customer>, Error>> {
|
): Promise<Result<Collection<Customer>>> {
|
||||||
const customersOrError = await this.repository.findByCriteria(criteria, transaction);
|
return this.repository.findByCriteriaInCompany(companyId, criteria, transaction);
|
||||||
if (customersOrError.isFailure) {
|
|
||||||
console.error(customersOrError.error);
|
|
||||||
return Result.fail(customersOrError.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Solo devolver usuarios activos
|
|
||||||
//const allCustomers = customersOrError.data.filter((customer) => customer.isActive);
|
|
||||||
//return Result.ok(new Collection(allCustomers));
|
|
||||||
|
|
||||||
return customersOrError;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recupera una factura por su identificador único.
|
* Recupera un cliente por su identificador único.
|
||||||
*
|
*
|
||||||
* @param id - Identificador UUID de la factura.
|
* @param companyId - Identificador UUID de la empresa a la que pertenece el cliente.
|
||||||
|
* @param customerId - Identificador UUID del cliente.
|
||||||
* @param transaction - Transacción activa para la operación.
|
* @param transaction - Transacción activa para la operación.
|
||||||
* @returns Result<Customer, Error> - Factura encontrada o error.
|
* @returns Result<Customer, Error> - Cliente encontradoF o error.
|
||||||
*/
|
*/
|
||||||
async getById(id: UniqueID, transaction?: Transaction): Promise<Result<Customer>> {
|
async getCustomerByIdInCompany(
|
||||||
return await this.repository.findById(id, transaction);
|
companyId: UniqueID,
|
||||||
|
customerId: UniqueID,
|
||||||
|
transaction?: any
|
||||||
|
): Promise<Result<Customer>> {
|
||||||
|
return this.repository.getByIdInCompany(companyId, customerId, transaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Actualiza parcialmente una factura existente con nuevos datos.
|
* Actualiza parcialmente un cliente existente con nuevos datos.
|
||||||
*
|
*
|
||||||
* @param id - Identificador de la factura a actualizar.
|
* @param companyId - Identificador de la empresa a la que pertenece el cliente.
|
||||||
* @param changes - Subconjunto de props válidas para aplicar.
|
* @param customerId - Identificador del cliente a actualizar.
|
||||||
|
* @param partial - Subconjunto de props válidas para aplicar.
|
||||||
* @param transaction - Transacción activa para la operación.
|
* @param transaction - Transacción activa para la operación.
|
||||||
* @returns Result<Customer, Error> - Factura actualizada o error.
|
* @returns Result<Customer, Error> - Cliente actualizado o error.
|
||||||
*/
|
*/
|
||||||
async updateById(
|
async updateCustomerByIdInCompany(
|
||||||
|
companyId: UniqueID,
|
||||||
customerId: UniqueID,
|
customerId: UniqueID,
|
||||||
changes: Partial<CustomerProps>,
|
partial: Partial<Omit<CustomerProps, "companyId">>,
|
||||||
transaction?: Transaction
|
transaction?: any
|
||||||
): Promise<Result<Customer, Error>> {
|
): Promise<Result<Customer>> {
|
||||||
// Verificar si la factura existe
|
const customerResult = await this.getCustomerByIdInCompany(companyId, customerId, transaction);
|
||||||
const customerOrError = await this.repository.findById(customerId, transaction);
|
|
||||||
if (customerOrError.isFailure) {
|
if (customerResult.isFailure) {
|
||||||
return Result.fail(new Error("Customer not found"));
|
return Result.fail(customerResult.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.fail(new Error("No implementado"));
|
const customer = customerResult.data;
|
||||||
|
const updatedCustomer = customer.update(partial);
|
||||||
|
|
||||||
/*const updatedCustomerOrError = Customer.update(customerOrError.data, data);
|
if (updatedCustomer.isFailure) {
|
||||||
if (updatedCustomerOrError.isFailure) {
|
return Result.fail(updatedCustomer.error);
|
||||||
return Result.fail(
|
|
||||||
new Error(`Error updating customer: ${updatedCustomerOrError.error.message}`)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateCustomer = updatedCustomerOrError.data;
|
return this.saveCustomerInCompany(updatedCustomer.data, transaction);
|
||||||
|
|
||||||
await this.repo.update(updateCustomer, transaction);
|
|
||||||
return Result.ok(updateCustomer);*/
|
|
||||||
}
|
|
||||||
|
|
||||||
async createCustomer(
|
|
||||||
customerId: UniqueID,
|
|
||||||
data: CustomerProps,
|
|
||||||
transaction?: Transaction
|
|
||||||
): Promise<Result<Customer, Error>> {
|
|
||||||
// Verificar si la factura existe
|
|
||||||
const customerOrError = await this.repository.findById(customerId, transaction);
|
|
||||||
if (customerOrError.isSuccess) {
|
|
||||||
return Result.fail(new Error("Customer exists"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const newCustomerOrError = Customer.create(data, customerId);
|
|
||||||
if (newCustomerOrError.isFailure) {
|
|
||||||
return Result.fail(new Error(`Error creating customer: ${newCustomerOrError.error.message}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
const newCustomer = newCustomerOrError.data;
|
|
||||||
|
|
||||||
await this.repository.create(newCustomer, transaction);
|
|
||||||
return Result.ok(newCustomer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Elimina (o marca como eliminada) una factura según su ID.
|
* Elimina (o marca como eliminado) un cliente según su ID.
|
||||||
*
|
*
|
||||||
* @param id - Identificador UUID de la factura.
|
* @param companyId - Identificador de la empresa a la que pertenece el cliente.
|
||||||
|
* @param customerId - Identificador UUID del cliente.
|
||||||
* @param transaction - Transacción activa para la operación.
|
* @param transaction - Transacción activa para la operación.
|
||||||
* @returns Result<boolean, Error> - Resultado de la operación.
|
* @returns Result<boolean, Error> - Resultado de la operación.
|
||||||
*/
|
*/
|
||||||
async deleteById(id: UniqueID, transaction?: Transaction): Promise<Result<void, Error>> {
|
async deleteCustomerByIdInCompany(
|
||||||
return this.repository.deleteById(id, transaction);
|
companyId: UniqueID,
|
||||||
|
customerId: UniqueID,
|
||||||
|
transaction?: any
|
||||||
|
): Promise<Result<void>> {
|
||||||
|
return this.repository.deleteByIdInCompany(companyId, customerId, transaction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import {
|
|||||||
ListCustomersAssembler,
|
ListCustomersAssembler,
|
||||||
ListCustomersUseCase,
|
ListCustomersUseCase,
|
||||||
} from "../application";
|
} from "../application";
|
||||||
import { CustomerService } from "../domain";
|
import { CustomerService, ICustomerService } from "../domain";
|
||||||
import { CustomerMapper } from "./mappers";
|
import { CustomerMapper } from "./mappers";
|
||||||
import { CustomerRepository } from "./sequelize";
|
import { CustomerRepository } from "./sequelize";
|
||||||
|
|
||||||
@ -39,7 +39,7 @@ type CustomerDeps = {
|
|||||||
|
|
||||||
let _repo: CustomerRepository | null = null;
|
let _repo: CustomerRepository | null = null;
|
||||||
let _mapper: CustomerMapper | null = null;
|
let _mapper: CustomerMapper | null = null;
|
||||||
let _service: CustomerService | null = null;
|
let _service: ICustomerService | null = null;
|
||||||
let _assemblers: CustomerDeps["assemblers"] | null = null;
|
let _assemblers: CustomerDeps["assemblers"] | null = null;
|
||||||
|
|
||||||
export function getCustomerDependencies(params: ModuleParams): CustomerDeps {
|
export function getCustomerDependencies(params: ModuleParams): CustomerDeps {
|
||||||
|
|||||||
@ -23,7 +23,7 @@ export class CreateCustomerController extends ExpressController {
|
|||||||
dto.customerCompanyId = user.companyId;
|
dto.customerCompanyId = user.companyId;
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const result = await this.useCase.execute(dto);
|
const result = await this.useCase.execute({ tenantId, dto });
|
||||||
|
|
||||||
return result.match(
|
return result.match(
|
||||||
(data) => this.created(data),
|
(data) => this.created(data),
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { enforceTenant } from "@erp/auth/api";
|
|
||||||
import { ILogger, ModuleParams, validateRequest } from "@erp/core/api";
|
import { ILogger, ModuleParams, validateRequest } from "@erp/core/api";
|
||||||
import { Application, NextFunction, Request, Response, Router } from "express";
|
import { Application, NextFunction, Request, Response, Router } from "express";
|
||||||
import { Sequelize } from "sequelize";
|
import { Sequelize } from "sequelize";
|
||||||
@ -28,7 +27,7 @@ export const customersRouter = (params: ModuleParams) => {
|
|||||||
const deps = getCustomerDependencies(params);
|
const deps = getCustomerDependencies(params);
|
||||||
|
|
||||||
// 🔐 Autenticación + Tenancy para TODO el router
|
// 🔐 Autenticación + Tenancy para TODO el router
|
||||||
router.use(/* authenticateJWT(), */ enforceTenant() /*checkTabContext*/);
|
//router.use(/* authenticateJWT(), */ enforceTenant() /*checkTabContext*/);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
"/",
|
"/",
|
||||||
|
|||||||
@ -19,9 +19,10 @@ export class CustomerModel extends Model<
|
|||||||
}*/
|
}*/
|
||||||
|
|
||||||
declare id: string;
|
declare id: string;
|
||||||
|
declare company_id: string;
|
||||||
declare reference: CreationOptional<string>;
|
declare reference: CreationOptional<string>;
|
||||||
|
|
||||||
declare is_freelancer: boolean;
|
declare is_company: boolean;
|
||||||
declare name: string;
|
declare name: string;
|
||||||
declare trade_name: CreationOptional<string>;
|
declare trade_name: CreationOptional<string>;
|
||||||
declare tin: string;
|
declare tin: string;
|
||||||
@ -43,20 +44,28 @@ export class CustomerModel extends Model<
|
|||||||
declare status: string;
|
declare status: string;
|
||||||
declare lang_code: string;
|
declare lang_code: string;
|
||||||
declare currency_code: string;
|
declare currency_code: string;
|
||||||
|
|
||||||
|
static associate(database: Sequelize) {}
|
||||||
|
|
||||||
|
static hooks(database: Sequelize) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (sequelize: Sequelize) => {
|
export default (database: Sequelize) => {
|
||||||
CustomerModel.init(
|
CustomerModel.init(
|
||||||
{
|
{
|
||||||
id: {
|
id: {
|
||||||
type: DataTypes.UUID,
|
type: DataTypes.UUID,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
},
|
},
|
||||||
|
company_id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
reference: {
|
reference: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
},
|
},
|
||||||
is_freelancer: {
|
is_company: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
},
|
},
|
||||||
@ -149,7 +158,7 @@ export default (sequelize: Sequelize) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sequelize,
|
sequelize: database,
|
||||||
tableName: "customers",
|
tableName: "customers",
|
||||||
|
|
||||||
paranoid: true, // softs deletes
|
paranoid: true, // softs deletes
|
||||||
@ -160,8 +169,8 @@ export default (sequelize: Sequelize) => {
|
|||||||
deletedAt: "deleted_at",
|
deletedAt: "deleted_at",
|
||||||
|
|
||||||
indexes: [
|
indexes: [
|
||||||
|
{ name: "company_idx", fields: ["company_id"], unique: false },
|
||||||
{ name: "email_idx", fields: ["email"], unique: true },
|
{ name: "email_idx", fields: ["email"], unique: true },
|
||||||
{ name: "reference_idx", fields: ["reference"], unique: true },
|
|
||||||
],
|
],
|
||||||
|
|
||||||
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope
|
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope
|
||||||
|
|||||||
@ -19,81 +19,108 @@ export class CustomerRepository
|
|||||||
this.mapper = mapper;
|
this.mapper = mapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
async existsById(id: UniqueID, transaction?: Transaction): Promise<Result<boolean, Error>> {
|
/**
|
||||||
|
*
|
||||||
|
* Guarda un nuevo cliente o actualiza uno existente.
|
||||||
|
*
|
||||||
|
* @param customer - El cliente a guardar.
|
||||||
|
* @param transaction - Transacción activa para la operación.
|
||||||
|
* @returns Result<Customer, Error>
|
||||||
|
*/
|
||||||
|
async save(customer: Customer, transaction: Transaction): Promise<Result<Customer, Error>> {
|
||||||
try {
|
try {
|
||||||
const result = await this._exists(CustomerModel, "id", id.toString(), transaction);
|
const data = this.mapper.mapToPersistence(customer);
|
||||||
|
const [instance] = await CustomerModel.upsert(data, { transaction, returning: true });
|
||||||
return Result.ok(Boolean(result));
|
return this.mapper.mapToDomain(instance);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
return Result.fail(errorMapper.toDomainError(err));
|
return Result.fail(errorMapper.toDomainError(err));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Comprueba si existe un Customer con un `id` dentro de una `company`.
|
||||||
*
|
*
|
||||||
* Persiste una nueva factura o actualiza una existente.
|
* @param companyId - Identificador UUID de la empresa a la que pertenece el cliente.
|
||||||
*
|
* @param id - Identificador UUID del cliente.
|
||||||
* @param invoice - El agregado a guardar.
|
|
||||||
* @param transaction - Transacción activa para la operación.
|
* @param transaction - Transacción activa para la operación.
|
||||||
* @returns Result<Customer, Error>
|
* @returns Result<boolean, Error>
|
||||||
*/
|
*/
|
||||||
async save(invoice: Customer, transaction: Transaction): Promise<Result<Customer, Error>> {
|
async existsByIdInCompany(
|
||||||
|
companyId: UniqueID,
|
||||||
|
id: UniqueID,
|
||||||
|
transaction?: any
|
||||||
|
): Promise<Result<boolean, Error>> {
|
||||||
try {
|
try {
|
||||||
const data = this.mapper.mapToPersistence(invoice);
|
const count = await CustomerModel.count({
|
||||||
await CustomerModel.upsert(data, { transaction });
|
where: { id: id.toString(), company_id: companyId.toString() },
|
||||||
return Result.ok(invoice);
|
transaction,
|
||||||
} catch (err: unknown) {
|
});
|
||||||
return Result.fail(errorMapper.toDomainError(err));
|
return Result.ok(Boolean(count > 0));
|
||||||
|
} catch (error: any) {
|
||||||
|
return Result.fail(errorMapper.toDomainError(error));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Recupera un cliente por su ID y companyId.
|
||||||
*
|
*
|
||||||
* Busca una factura por su identificador único.
|
* @param companyId - Identificador UUID de la empresa a la que pertenece el cliente.
|
||||||
* @param id - UUID de la factura.
|
* @param id - Identificador UUID del cliente.
|
||||||
* @param transaction - Transacción activa para la operación.
|
* @param transaction - Transacción activa para la operación.
|
||||||
* @returns Result<Customer, Error>
|
* @returns Result<Customer, Error>
|
||||||
*/
|
*/
|
||||||
async findById(id: UniqueID, transaction: Transaction): Promise<Result<Customer, Error>> {
|
async getByIdInCompany(
|
||||||
|
companyId: UniqueID,
|
||||||
|
id: UniqueID,
|
||||||
|
transaction?: any
|
||||||
|
): Promise<Result<Customer, Error>> {
|
||||||
try {
|
try {
|
||||||
const rawData = await this._findById(CustomerModel, id.toString(), { transaction });
|
const row = await CustomerModel.findOne({
|
||||||
|
where: { id: id.toString(), company_id: companyId.toString() },
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
if (!rawData) {
|
if (!row) {
|
||||||
return Result.fail(new Error(`Invoice with id ${id} not found.`));
|
return Result.fail(new Error(`Customer ${id.toString()} not found`));
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.mapper.mapToDomain(rawData);
|
return this.mapper.mapToDomain(row);
|
||||||
} catch (err: unknown) {
|
} catch (error: any) {
|
||||||
return Result.fail(errorMapper.toDomainError(err));
|
return Result.fail(errorMapper.toDomainError(error));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Recupera múltiples customers dentro de una empresa según un criterio dinámico (búsqueda, paginación, etc.).
|
||||||
*
|
*
|
||||||
* Consulta facturas usando un objeto Criteria (filtros, orden, paginación).
|
* @param companyId - Identificador UUID de la empresa a la que pertenece el cliente.
|
||||||
* @param criteria - Criterios de búsqueda.
|
* @param criteria - Criterios de búsqueda.
|
||||||
* @param transaction - Transacción activa para la operación.
|
* @param transaction - Transacción activa para la operación.
|
||||||
* @returns Result<Customer[], Error>
|
* @returns Result<Collection<Customer>, Error>
|
||||||
*
|
*
|
||||||
* @see Criteria
|
* @see Criteria
|
||||||
*/
|
*/
|
||||||
public async findByCriteria(
|
async findByCriteriaInCompany(
|
||||||
|
companyId: UniqueID,
|
||||||
criteria: Criteria,
|
criteria: Criteria,
|
||||||
transaction: Transaction
|
transaction?: any
|
||||||
): Promise<Result<Collection<Customer>, Error>> {
|
): Promise<Result<Collection<Customer>>> {
|
||||||
try {
|
try {
|
||||||
const converter = new CriteriaToSequelizeConverter();
|
const converter = new CriteriaToSequelizeConverter();
|
||||||
const query = converter.convert(criteria);
|
const query = converter.convert(criteria);
|
||||||
|
|
||||||
console.debug({ criteria, transaction, query, CustomerModel });
|
query.where = {
|
||||||
|
...query.where,
|
||||||
|
company_id: companyId.toString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
console.debug({ model: "CustomerModel", criteria, query });
|
||||||
|
|
||||||
const instances = await CustomerModel.findAll({
|
const instances = await CustomerModel.findAll({
|
||||||
...query,
|
...query,
|
||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.debug(instances);
|
|
||||||
|
|
||||||
return this.mapper.mapArrayToDomain(instances);
|
return this.mapper.mapArrayToDomain(instances);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@ -103,16 +130,30 @@ export class CustomerRepository
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* Elimina o marca como eliminada una factura.
|
* Elimina o marca como eliminado un cliente.
|
||||||
* @param id - UUID de la factura a eliminar.
|
*
|
||||||
|
* @param companyId - Identificador UUID de la empresa a la que pertenece el cliente.
|
||||||
|
* @param id - UUID del cliente a eliminar.
|
||||||
* @param transaction - Transacción activa para la operación.
|
* @param transaction - Transacción activa para la operación.
|
||||||
* @returns Result<void, Error>
|
* @returns Result<void, Error>
|
||||||
*/
|
*/
|
||||||
async deleteById(id: UniqueID, transaction: any): Promise<Result<void, Error>> {
|
async deleteByIdInCompany(
|
||||||
|
companyId: UniqueID,
|
||||||
|
id: UniqueID,
|
||||||
|
transaction?: any
|
||||||
|
): Promise<Result<void>> {
|
||||||
try {
|
try {
|
||||||
await this._deleteById(CustomerModel, id, false, transaction);
|
const deleted = await CustomerModel.destroy({
|
||||||
|
where: { id: id.toString(), company_id: companyId.toString() },
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (deleted === 0) {
|
||||||
|
return Result.fail(new Error(`Customer with id ${id} not found in company ${companyId}.`));
|
||||||
|
}
|
||||||
return Result.ok<void>();
|
return Result.ok<void>();
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
|
// , `Error deleting customer ${id} in company ${companyId}`
|
||||||
return Result.fail(errorMapper.toDomainError(err));
|
return Result.fail(errorMapper.toDomainError(err));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,9 +2,9 @@ import * as z from "zod/v4";
|
|||||||
|
|
||||||
export const CreateCustomerRequestSchema = z.object({
|
export const CreateCustomerRequestSchema = z.object({
|
||||||
id: z.uuid(),
|
id: z.uuid(),
|
||||||
reference: z.string(),
|
reference: z.string().optional(),
|
||||||
|
|
||||||
is_freelancer: z.boolean(),
|
is_company: z.boolean(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
trade_name: z.string(),
|
trade_name: z.string(),
|
||||||
tin: z.string(),
|
tin: z.string(),
|
||||||
@ -22,7 +22,7 @@ export const CreateCustomerRequestSchema = z.object({
|
|||||||
|
|
||||||
legal_record: z.string(),
|
legal_record: z.string(),
|
||||||
|
|
||||||
default_tax: z.number(),
|
default_tax: z.array(z.string()),
|
||||||
status: z.string(),
|
status: z.string(),
|
||||||
lang_code: z.string(),
|
lang_code: z.string(),
|
||||||
currency_code: z.string(),
|
currency_code: z.string(),
|
||||||
|
|||||||
@ -5,7 +5,7 @@ export const CustomerCreationResponseSchema = z.object({
|
|||||||
id: z.uuid(),
|
id: z.uuid(),
|
||||||
reference: z.string(),
|
reference: z.string(),
|
||||||
|
|
||||||
is_freelancer: z.boolean(),
|
is_companyr: z.boolean(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
trade_name: z.string(),
|
trade_name: z.string(),
|
||||||
tin: z.string(),
|
tin: z.string(),
|
||||||
|
|||||||
@ -6,7 +6,7 @@ export const CustomerListResponseSchema = createListViewResponseSchema(
|
|||||||
id: z.uuid(),
|
id: z.uuid(),
|
||||||
reference: z.string(),
|
reference: z.string(),
|
||||||
|
|
||||||
is_freelancer: z.boolean(),
|
is_companyr: z.boolean(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
trade_name: z.string(),
|
trade_name: z.string(),
|
||||||
tin: z.string(),
|
tin: z.string(),
|
||||||
|
|||||||
@ -5,7 +5,7 @@ export const GetCustomerByIdResponseSchema = z.object({
|
|||||||
id: z.uuid(),
|
id: z.uuid(),
|
||||||
reference: z.string(),
|
reference: z.string(),
|
||||||
|
|
||||||
is_freelancer: z.boolean(),
|
is_companyr: z.boolean(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
trade_name: z.string(),
|
trade_name: z.string(),
|
||||||
tin: z.string(),
|
tin: z.string(),
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { AppBreadcrumb, AppContent } from "@repo/rdx-ui/components";
|
import { AppBreadcrumb, AppContent, BackHistoryButton } from "@repo/rdx-ui/components";
|
||||||
import { Button } from "@repo/shadcn-ui/components";
|
import { Button } from "@repo/shadcn-ui/components";
|
||||||
import { useBlocker, useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { useCreateCustomerMutation } from "../../hooks/use-create-customer-mutation";
|
import { useCreateCustomerMutation } from "../../hooks/use-create-customer-mutation";
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
@ -9,7 +9,6 @@ import { CustomerEditForm } from "./customer-edit-form";
|
|||||||
export const CustomerCreate = () => {
|
export const CustomerCreate = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { block, unblock } = useBlocker(1);
|
|
||||||
|
|
||||||
const { mutate, isPending, isError, error } = useCreateCustomerMutation();
|
const { mutate, isPending, isError, error } = useCreateCustomerMutation();
|
||||||
|
|
||||||
@ -56,12 +55,17 @@ export const CustomerCreate = () => {
|
|||||||
<AppContent>
|
<AppContent>
|
||||||
<div className='flex items-center justify-between space-y-2'>
|
<div className='flex items-center justify-between space-y-2'>
|
||||||
<div>
|
<div>
|
||||||
<h2 className='text-2xl font-bold tracking-tight'>{t("pages.create.title")}</h2>
|
<h2 className='text-2xl font-bold tracking-tight text-balance scroll-m-2'>
|
||||||
<p className='text-muted-foreground'>{t("pages.create.description")}</p>
|
{t("pages.create.title")}
|
||||||
|
</h2>
|
||||||
|
<p className='text-muted-foreground scroll-m-20 tracking-tight text-balance'>
|
||||||
|
{t("pages.create.description")}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center justify-end mb-4'>
|
<div className='flex items-center justify-end mb-4'>
|
||||||
<Button className='cursor-pointer' onClick={() => navigate("/customers/list")}>
|
<BackHistoryButton />
|
||||||
{t("pages.create.back_to_list")}
|
<Button type='submit' className='cursor-pointer'>
|
||||||
|
{t("pages.create.submit")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -4,13 +4,18 @@ import { useForm } from "react-hook-form";
|
|||||||
import { TaxesMultiSelectField } from "@erp/core/components";
|
import { TaxesMultiSelectField } from "@erp/core/components";
|
||||||
import { SelectField, TextAreaField, TextField } from "@repo/rdx-ui/components";
|
import { SelectField, TextAreaField, TextField } from "@repo/rdx-ui/components";
|
||||||
import {
|
import {
|
||||||
|
Button,
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
Form,
|
Form,
|
||||||
Label,
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
RadioGroupItem,
|
RadioGroupItem,
|
||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
@ -21,10 +26,24 @@ import { CustomerData, CustomerDataFormSchema } from "./customer.schema";
|
|||||||
|
|
||||||
const defaultCustomerData = {
|
const defaultCustomerData = {
|
||||||
id: "5e4dc5b3-96b9-4968-9490-14bd032fec5f",
|
id: "5e4dc5b3-96b9-4968-9490-14bd032fec5f",
|
||||||
|
is_company: true,
|
||||||
status: "active",
|
status: "active",
|
||||||
name: "1",
|
tin: "B12345678",
|
||||||
language_code: "ES",
|
name: "Pepe",
|
||||||
currency: "EUR",
|
trade_name: "Pepe's Shop",
|
||||||
|
email: "pepe@example.com",
|
||||||
|
phone: "+34 123 456 789",
|
||||||
|
website: "https://pepe.com",
|
||||||
|
fax: "+34 123 456 789",
|
||||||
|
street: "Calle Falsa 123",
|
||||||
|
city: "Madrid",
|
||||||
|
country: "ES",
|
||||||
|
postal_code: "28080",
|
||||||
|
state: "Madrid",
|
||||||
|
lang_code: "es",
|
||||||
|
currency_code: "EUR",
|
||||||
|
legal_record: "Registro Mercantil de Madrid, Tomo 12345, Folio 67, Hoja M-123456",
|
||||||
|
default_tax: ["iva_21", "rec_5_2"],
|
||||||
};
|
};
|
||||||
|
|
||||||
interface CustomerFormProps {
|
interface CustomerFormProps {
|
||||||
@ -71,44 +90,49 @@ export const CustomerEditForm = ({
|
|||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(handleSubmit, handleError)}>
|
<form onSubmit={form.handleSubmit(handleSubmit, handleError)}>
|
||||||
<div className='grid grid-cols-1 space-y-6'>
|
<div className='w-full grid grid-cols-1 space-y-8 space-x-8 xl:grid-cols-2'>
|
||||||
{/* Información básica */}
|
{/* Información básica */}
|
||||||
<Card className='border-0 shadow-none'>
|
<Card className='shadow-none'>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{t("form_groups.basic_info.title")}</CardTitle>
|
<CardTitle>{t("form_groups.basic_info.title")}</CardTitle>
|
||||||
<CardDescription>{t("form_groups.basic_info.description")}</CardDescription>
|
<CardDescription>{t("form_groups.basic_info.description")}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className='grid grid-cols-1 gap-y-8 gap-x-6 @xl:grid-cols-2'>
|
<CardContent className='grid grid-cols-1 gap-y-8 gap-x-6 @xl:grid-cols-2'>
|
||||||
<div className='space-y-3 xl:col-span-2'>
|
<FormField
|
||||||
<Label className='text-sm font-medium'>
|
control={form.control}
|
||||||
{t("form_fields.customer_type.label")}
|
name='is_company'
|
||||||
</Label>
|
render={({ field }) => (
|
||||||
<RadioGroup
|
<FormItem className='space-y-3'>
|
||||||
value={"customer_type"}
|
<FormLabel>{t("form_fields.customer_type.label")}</FormLabel>
|
||||||
onValueChange={(value: "company" | "individual") => {
|
<FormControl>
|
||||||
// Usar setValue del form
|
<RadioGroup
|
||||||
form.setValue("customer_type", value);
|
onValueChange={field.onChange}
|
||||||
}}
|
defaultValue={field.value ? "1" : "0"}
|
||||||
className='flex gap-6'
|
className='flex gap-6'
|
||||||
>
|
>
|
||||||
<div className='flex items-center space-x-2'>
|
<FormItem className='flex items-center space-x-2'>
|
||||||
<RadioGroupItem
|
<FormControl>
|
||||||
value='company'
|
<RadioGroupItem value={"1"} />
|
||||||
id='company'
|
</FormControl>
|
||||||
{...form.register("customer_type")}
|
<FormLabel className='font-normal'>
|
||||||
/>
|
{t("form_fields.customer_type.company")}
|
||||||
<Label htmlFor='company'>{t("form_fields.customer_type.company")}</Label>
|
</FormLabel>
|
||||||
</div>
|
</FormItem>
|
||||||
<div className='flex items-center space-x-2'>
|
|
||||||
<RadioGroupItem
|
<FormItem className='flex items-center space-x-2'>
|
||||||
value='individual'
|
<FormControl>
|
||||||
id='individual'
|
<RadioGroupItem value={"0"} />
|
||||||
{...form.register("customer_type")}
|
</FormControl>
|
||||||
/>
|
<FormLabel className='font-normal'>
|
||||||
<Label htmlFor='individual'>{t("form_fields.customer_type.individual")}</Label>
|
{t("form_fields.customer_type.individual")}
|
||||||
</div>
|
</FormLabel>
|
||||||
</RadioGroup>
|
</FormItem>
|
||||||
</div>
|
</RadioGroup>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@ -140,7 +164,6 @@ export const CustomerEditForm = ({
|
|||||||
<TextField
|
<TextField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name='reference'
|
name='reference'
|
||||||
required
|
|
||||||
label={t("form_fields.reference.label")}
|
label={t("form_fields.reference.label")}
|
||||||
placeholder={t("form_fields.reference.placeholder")}
|
placeholder={t("form_fields.reference.placeholder")}
|
||||||
description={t("form_fields.reference.description")}
|
description={t("form_fields.reference.description")}
|
||||||
@ -149,7 +172,7 @@ export const CustomerEditForm = ({
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Dirección */}
|
{/* Dirección */}
|
||||||
<Card className='border-0 shadow-none'>
|
<Card className='shadow-none'>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{t("form_groups.address.title")}</CardTitle>
|
<CardTitle>{t("form_groups.address.title")}</CardTitle>
|
||||||
<CardDescription>{t("form_groups.address.description")}</CardDescription>
|
<CardDescription>{t("form_groups.address.description")}</CardDescription>
|
||||||
@ -214,7 +237,7 @@ export const CustomerEditForm = ({
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Contacto */}
|
{/* Contacto */}
|
||||||
<Card className='border-0 shadow-none'>
|
<Card className='shadow-none'>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{t("form_groups.contact_info.title")}</CardTitle>
|
<CardTitle>{t("form_groups.contact_info.title")}</CardTitle>
|
||||||
<CardDescription>{t("form_groups.contact_info.description")}</CardDescription>
|
<CardDescription>{t("form_groups.contact_info.description")}</CardDescription>
|
||||||
@ -259,7 +282,7 @@ export const CustomerEditForm = ({
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Configuraciones Adicionales */}
|
{/* Configuraciones Adicionales */}
|
||||||
<Card className='border-0 shadow-none'>
|
<Card className='shadow-none'>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{t("form_groups.additional_config.title")}</CardTitle>
|
<CardTitle>{t("form_groups.additional_config.title")}</CardTitle>
|
||||||
<CardDescription>{t("form_groups.additional_config.description")}</CardDescription>
|
<CardDescription>{t("form_groups.additional_config.description")}</CardDescription>
|
||||||
@ -320,6 +343,7 @@ export const CustomerEditForm = ({
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
<Button type='submit'>Submit</Button>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -7,9 +7,9 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
"target": "ES2020",
|
"target": "ES2022",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
|||||||
@ -15,6 +15,6 @@
|
|||||||
"incremental": false,
|
"incremental": false,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"exactOptionalPropertyTypes": true,
|
"exactOptionalPropertyTypes": true,
|
||||||
"target": "es2020"
|
"target": "ES2022"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -120,17 +120,17 @@ const data = {
|
|||||||
],
|
],
|
||||||
navSecondary: [
|
navSecondary: [
|
||||||
{
|
{
|
||||||
title: "Settings",
|
title: "Ajustes",
|
||||||
url: "#",
|
url: "#",
|
||||||
icon: SettingsIcon,
|
icon: SettingsIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Get Help",
|
title: "Soporte",
|
||||||
url: "#",
|
url: "#",
|
||||||
icon: HelpCircleIcon,
|
icon: HelpCircleIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Search",
|
title: "Buscar",
|
||||||
url: "#",
|
url: "#",
|
||||||
icon: SearchIcon,
|
icon: SearchIcon,
|
||||||
},
|
},
|
||||||
@ -180,8 +180,8 @@ const data2 = {
|
|||||||
],
|
],
|
||||||
navMain: [
|
navMain: [
|
||||||
{
|
{
|
||||||
title: "Playground",
|
title: "Clientes",
|
||||||
url: "#",
|
url: "/customers",
|
||||||
icon: SquareTerminal,
|
icon: SquareTerminal,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
items: [
|
items: [
|
||||||
@ -200,8 +200,8 @@ const data2 = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Models",
|
title: "Facturas de cliente",
|
||||||
url: "#",
|
url: "/customer-invoices",
|
||||||
icon: Bot,
|
icon: Bot,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -21,7 +21,7 @@ import {
|
|||||||
ToggleGroup,
|
ToggleGroup,
|
||||||
ToggleGroupItem,
|
ToggleGroupItem,
|
||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
import { useIsMobile } from "@repo/shadcn-ui/hooks";
|
import { useIsMobile } from "@repo/shadcn-ui/hooks/";
|
||||||
|
|
||||||
const chartData = [
|
const chartData = [
|
||||||
{ date: "2024-04-01", desktop: 222, mobile: 150 },
|
{ date: "2024-04-01", desktop: 222, mobile: 150 },
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user