Suppliers
This commit is contained in:
parent
5ac24e161d
commit
366f90d403
@ -38,6 +38,7 @@
|
|||||||
"@erp/customers": "workspace:*",
|
"@erp/customers": "workspace:*",
|
||||||
"@erp/customer-invoices": "workspace:*",
|
"@erp/customer-invoices": "workspace:*",
|
||||||
"@erp/factuges": "workspace:*",
|
"@erp/factuges": "workspace:*",
|
||||||
|
"@erp/suppliers": "workspace:*",
|
||||||
"@repo/rdx-logger": "workspace:*",
|
"@repo/rdx-logger": "workspace:*",
|
||||||
"@repo/rdx-utils": "workspace:*",
|
"@repo/rdx-utils": "workspace:*",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import customerInvoicesAPIModule from "@erp/customer-invoices/api";
|
import customerInvoicesAPIModule from "@erp/customer-invoices/api";
|
||||||
import customersAPIModule from "@erp/customers/api";
|
import customersAPIModule from "@erp/customers/api";
|
||||||
import factuGESAPIModule from "@erp/factuges/api";
|
import factuGESAPIModule from "@erp/factuges/api";
|
||||||
|
import suppliersAPIModule from "@erp/suppliers/api";
|
||||||
|
|
||||||
import { registerModule } from "./lib";
|
import { registerModule } from "./lib";
|
||||||
|
|
||||||
@ -9,4 +10,5 @@ export const registerModules = () => {
|
|||||||
registerModule(customersAPIModule);
|
registerModule(customersAPIModule);
|
||||||
registerModule(customerInvoicesAPIModule);
|
registerModule(customerInvoicesAPIModule);
|
||||||
registerModule(factuGESAPIModule);
|
registerModule(factuGESAPIModule);
|
||||||
|
registerModule(suppliersAPIModule);
|
||||||
};
|
};
|
||||||
|
|||||||
33
modules/supplier/package.json
Normal file
33
modules/supplier/package.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "@erp/suppliers",
|
||||||
|
"description": "Suppliers",
|
||||||
|
"version": "0.5.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"sideEffects": false,
|
||||||
|
"scripts": {
|
||||||
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||||
|
"clean": "rimraf .turbo node_modules dist"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": "./src/common/index.ts",
|
||||||
|
"./common": "./src/common/index.ts",
|
||||||
|
"./api": "./src/api/index.ts"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@erp/auth": "workspace:*",
|
||||||
|
"@erp/core": "workspace:*",
|
||||||
|
"@repo/i18next": "workspace:*",
|
||||||
|
"@repo/rdx-criteria": "workspace:*",
|
||||||
|
"@repo/rdx-ddd": "workspace:*",
|
||||||
|
"@repo/rdx-logger": "workspace:*",
|
||||||
|
"@repo/rdx-utils": "workspace:*",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"sequelize": "^6.37.5",
|
||||||
|
"zod": "^4.1.11"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
modules/supplier/src/api/application/di/index.ts
Normal file
6
modules/supplier/src/api/application/di/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export * from "./supplier-creator.di";
|
||||||
|
export * from "./supplier-finder.di";
|
||||||
|
export * from "./supplier-input-mappers.di";
|
||||||
|
export * from "./supplier-snapshot-builders.di";
|
||||||
|
export * from "./supplier-updater.di";
|
||||||
|
export * from "./supplier-use-cases.di";
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
import type { ISupplierRepository } from "../repositories";
|
||||||
|
import { type ISupplierCreator, SupplierCreator } from "../services";
|
||||||
|
|
||||||
|
export const buildSupplierCreator = (params: {
|
||||||
|
repository: ISupplierRepository;
|
||||||
|
}): ISupplierCreator => {
|
||||||
|
const { repository } = params;
|
||||||
|
|
||||||
|
return new SupplierCreator({
|
||||||
|
repository,
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
import type { ISupplierRepository } from "../repositories";
|
||||||
|
import { type ISupplierFinder, SupplierFinder } from "../services";
|
||||||
|
|
||||||
|
export function buildSupplierFinder(params: { repository: ISupplierRepository }): ISupplierFinder {
|
||||||
|
const { repository } = params;
|
||||||
|
|
||||||
|
return new SupplierFinder(repository);
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
import type { ICatalogs } from "@erp/core/api";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CreateSupplierInputMapper,
|
||||||
|
type ICreateSupplierInputMapper,
|
||||||
|
type IUpdateSupplierInputMapper,
|
||||||
|
UpdateSupplierInputMapper,
|
||||||
|
} from "../mappers";
|
||||||
|
|
||||||
|
export interface ISupplierInputMappers {
|
||||||
|
createInputMapper: ICreateSupplierInputMapper;
|
||||||
|
updateInputMapper: IUpdateSupplierInputMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const buildSupplierInputMappers = (catalogs: ICatalogs): ISupplierInputMappers => {
|
||||||
|
const { taxCatalog } = catalogs;
|
||||||
|
|
||||||
|
// Mappers el DTO a las props validadas (SupplierProps) y luego construir agregado
|
||||||
|
const createInputMapper = new CreateSupplierInputMapper({ taxCatalog });
|
||||||
|
const updateInputMapper = new UpdateSupplierInputMapper();
|
||||||
|
|
||||||
|
return {
|
||||||
|
createInputMapper,
|
||||||
|
updateInputMapper,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
import { SupplierFullSnapshotBuilder, SupplierSummarySnapshotBuilder } from "../snapshot-builders";
|
||||||
|
|
||||||
|
export function buildSupplierSnapshotBuilders() {
|
||||||
|
const fullSnapshotBuilder = new SupplierFullSnapshotBuilder();
|
||||||
|
const summarySnapshotBuilder = new SupplierSummarySnapshotBuilder();
|
||||||
|
|
||||||
|
return {
|
||||||
|
full: fullSnapshotBuilder,
|
||||||
|
summary: summarySnapshotBuilder,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
import type { ISupplierRepository } from "../repositories";
|
||||||
|
import { type ISupplierUpdater, SupplierUpdater } from "../services";
|
||||||
|
|
||||||
|
export const buildSupplierUpdater = (params: {
|
||||||
|
repository: ISupplierRepository;
|
||||||
|
}): ISupplierUpdater => {
|
||||||
|
const { repository } = params;
|
||||||
|
|
||||||
|
return new SupplierUpdater({
|
||||||
|
repository,
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -0,0 +1,95 @@
|
|||||||
|
import type { ITransactionManager } from "@erp/core/api";
|
||||||
|
|
||||||
|
import type { ICreateSupplierInputMapper, IUpdateSupplierInputMapper } from "../mappers";
|
||||||
|
import type { ISupplierCreator, ISupplierFinder, ISupplierUpdater } from "../services";
|
||||||
|
import type {
|
||||||
|
ISupplierFullSnapshotBuilder,
|
||||||
|
ISupplierSummarySnapshotBuilder,
|
||||||
|
} from "../snapshot-builders";
|
||||||
|
import {
|
||||||
|
CreateSupplierUseCase,
|
||||||
|
GetSupplierByIdUseCase,
|
||||||
|
ListSuppliersUseCase,
|
||||||
|
UpdateSupplierUseCase,
|
||||||
|
} from "../use-cases";
|
||||||
|
|
||||||
|
export function buildGetSupplierByIdUseCase(deps: {
|
||||||
|
finder: ISupplierFinder;
|
||||||
|
fullSnapshotBuilder: ISupplierFullSnapshotBuilder;
|
||||||
|
transactionManager: ITransactionManager;
|
||||||
|
}) {
|
||||||
|
return new GetSupplierByIdUseCase(deps.finder, deps.fullSnapshotBuilder, deps.transactionManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildListSuppliersUseCase(deps: {
|
||||||
|
finder: ISupplierFinder;
|
||||||
|
summarySnapshotBuilder: ISupplierSummarySnapshotBuilder;
|
||||||
|
transactionManager: ITransactionManager;
|
||||||
|
}) {
|
||||||
|
return new ListSuppliersUseCase(
|
||||||
|
deps.finder,
|
||||||
|
deps.summarySnapshotBuilder,
|
||||||
|
deps.transactionManager
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCreateSupplierUseCase(deps: {
|
||||||
|
creator: ISupplierCreator;
|
||||||
|
dtoMapper: ICreateSupplierInputMapper;
|
||||||
|
fullSnapshotBuilder: ISupplierFullSnapshotBuilder;
|
||||||
|
transactionManager: ITransactionManager;
|
||||||
|
}) {
|
||||||
|
return new CreateSupplierUseCase({
|
||||||
|
dtoMapper: deps.dtoMapper,
|
||||||
|
creator: deps.creator,
|
||||||
|
fullSnapshotBuilder: deps.fullSnapshotBuilder,
|
||||||
|
transactionManager: deps.transactionManager,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildUpdateSupplierUseCase(deps: {
|
||||||
|
updater: ISupplierUpdater;
|
||||||
|
dtoMapper: IUpdateSupplierInputMapper;
|
||||||
|
fullSnapshotBuilder: ISupplierFullSnapshotBuilder;
|
||||||
|
transactionManager: ITransactionManager;
|
||||||
|
}) {
|
||||||
|
return new UpdateSupplierUseCase({
|
||||||
|
dtoMapper: deps.dtoMapper,
|
||||||
|
updater: deps.updater,
|
||||||
|
fullSnapshotBuilder: deps.fullSnapshotBuilder,
|
||||||
|
transactionManager: deps.transactionManager,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*export function buildReportSupplierUseCase(deps: {
|
||||||
|
finder: ISupplierFinder;
|
||||||
|
fullSnapshotBuilder: ISupplierFullSnapshotBuilder;
|
||||||
|
reportSnapshotBuilder: ISupplierReportSnapshotBuilder;
|
||||||
|
documentService: SupplierDocumentGeneratorService;
|
||||||
|
transactionManager: ITransactionManager;
|
||||||
|
}) {
|
||||||
|
return new ReportSupplierUseCase(
|
||||||
|
deps.finder,
|
||||||
|
deps.fullSnapshotBuilder,
|
||||||
|
deps.reportSnapshotBuilder,
|
||||||
|
deps.documentService,
|
||||||
|
deps.transactionManager
|
||||||
|
);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
export function buildDeleteSupplierUseCase(deps: { finder: ISupplierFinder }) {
|
||||||
|
return new DeleteSupplierUseCase(deps.finder);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildIssueSupplierUseCase(deps: { finder: ISupplierFinder }) {
|
||||||
|
return new IssueSupplierUseCase(deps.finder);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildChangeStatusSupplierUseCase(deps: {
|
||||||
|
finder: ISupplierFinder;
|
||||||
|
transactionManager: ITransactionManager;
|
||||||
|
}) {
|
||||||
|
return new ChangeStatusSupplierUseCase(deps.finder, deps.transactionManager);
|
||||||
|
}*/
|
||||||
6
modules/supplier/src/api/application/index.ts
Normal file
6
modules/supplier/src/api/application/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export * from "./di";
|
||||||
|
export * from "./models";
|
||||||
|
export * from "./repositories";
|
||||||
|
export * from "./services";
|
||||||
|
export * from "./snapshot-builders";
|
||||||
|
export * from "./use-cases";
|
||||||
@ -0,0 +1,217 @@
|
|||||||
|
import type { JsonTaxCatalogProvider } from "@erp/core";
|
||||||
|
import {
|
||||||
|
City,
|
||||||
|
Country,
|
||||||
|
CurrencyCode,
|
||||||
|
DomainError,
|
||||||
|
EmailAddress,
|
||||||
|
LanguageCode,
|
||||||
|
Name,
|
||||||
|
PhoneNumber,
|
||||||
|
type PostalAddressProps,
|
||||||
|
PostalCode,
|
||||||
|
Province,
|
||||||
|
Street,
|
||||||
|
TINNumber,
|
||||||
|
URLAddress,
|
||||||
|
UniqueID,
|
||||||
|
ValidationErrorCollection,
|
||||||
|
type ValidationErrorDetail,
|
||||||
|
extractOrPushError,
|
||||||
|
maybeFromNullableResult,
|
||||||
|
} from "@repo/rdx-ddd";
|
||||||
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
import type { CreateSupplierRequestDTO } from "../../../common";
|
||||||
|
import { type ISupplierCreateProps, SupplierStatus } from "../../domain";
|
||||||
|
|
||||||
|
export interface ICreateSupplierInputMapper {
|
||||||
|
map(
|
||||||
|
dto: CreateSupplierRequestDTO,
|
||||||
|
params: { companyId: UniqueID }
|
||||||
|
): Result<{ id: UniqueID; props: ISupplierCreateProps }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CreateSupplierInputMapper implements ICreateSupplierInputMapper {
|
||||||
|
private readonly taxCatalog: JsonTaxCatalogProvider;
|
||||||
|
|
||||||
|
constructor(params: { taxCatalog: JsonTaxCatalogProvider }) {
|
||||||
|
this.taxCatalog = params.taxCatalog;
|
||||||
|
}
|
||||||
|
|
||||||
|
public map(
|
||||||
|
dto: CreateSupplierRequestDTO,
|
||||||
|
params: { companyId: UniqueID }
|
||||||
|
): Result<{ id: UniqueID; props: ISupplierCreateProps }> {
|
||||||
|
try {
|
||||||
|
const errors: ValidationErrorDetail[] = [];
|
||||||
|
const { companyId } = params;
|
||||||
|
|
||||||
|
const supplierId = extractOrPushError(UniqueID.create(dto.id), "id", errors);
|
||||||
|
|
||||||
|
const status = SupplierStatus.active();
|
||||||
|
const isCompany = dto.is_company === "true";
|
||||||
|
|
||||||
|
const reference = extractOrPushError(
|
||||||
|
maybeFromNullableResult(dto.reference, (value) => Name.create(value)),
|
||||||
|
"reference",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const name = extractOrPushError(Name.create(dto.name), "name", errors);
|
||||||
|
|
||||||
|
const tradeName = extractOrPushError(
|
||||||
|
maybeFromNullableResult(dto.trade_name, (value) => Name.create(value)),
|
||||||
|
"trade_name",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const tinNumber = extractOrPushError(
|
||||||
|
maybeFromNullableResult(dto.tin, (value) => TINNumber.create(value)),
|
||||||
|
"tin",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const street = extractOrPushError(
|
||||||
|
maybeFromNullableResult(dto.street, (value) => Street.create(value)),
|
||||||
|
"street",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const street2 = extractOrPushError(
|
||||||
|
maybeFromNullableResult(dto.street2, (value) => Street.create(value)),
|
||||||
|
"street2",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const city = extractOrPushError(
|
||||||
|
maybeFromNullableResult(dto.city, (value) => City.create(value)),
|
||||||
|
"city",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const province = extractOrPushError(
|
||||||
|
maybeFromNullableResult(dto.province, (value) => Province.create(value)),
|
||||||
|
"province",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const postalCode = extractOrPushError(
|
||||||
|
maybeFromNullableResult(dto.postal_code, (value) => PostalCode.create(value)),
|
||||||
|
"postal_code",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const country = extractOrPushError(
|
||||||
|
maybeFromNullableResult(dto.country, (value) => Country.create(value)),
|
||||||
|
"country",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const primaryEmailAddress = extractOrPushError(
|
||||||
|
maybeFromNullableResult(dto.email_primary, (value) => EmailAddress.create(value)),
|
||||||
|
"email_primary",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const secondaryEmailAddress = extractOrPushError(
|
||||||
|
maybeFromNullableResult(dto.email_secondary, (value) => EmailAddress.create(value)),
|
||||||
|
"email_secondary",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const primaryPhoneNumber = extractOrPushError(
|
||||||
|
maybeFromNullableResult(dto.phone_primary, (value) => PhoneNumber.create(value)),
|
||||||
|
"phone_primary",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const secondaryPhoneNumber = extractOrPushError(
|
||||||
|
maybeFromNullableResult(dto.phone_secondary, (value) => PhoneNumber.create(value)),
|
||||||
|
"phone_secondary",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const primaryMobileNumber = extractOrPushError(
|
||||||
|
maybeFromNullableResult(dto.mobile_primary, (value) => PhoneNumber.create(value)),
|
||||||
|
"mobile_primary",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const secondaryMobileNumber = extractOrPushError(
|
||||||
|
maybeFromNullableResult(dto.mobile_secondary, (value) => PhoneNumber.create(value)),
|
||||||
|
"mobile_secondary",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const faxNumber = extractOrPushError(
|
||||||
|
maybeFromNullableResult(dto.fax, (value) => PhoneNumber.create(value)),
|
||||||
|
"fax",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const website = extractOrPushError(
|
||||||
|
maybeFromNullableResult(dto.website, (value) => URLAddress.create(value)),
|
||||||
|
"website",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const languageCode = extractOrPushError(
|
||||||
|
LanguageCode.create(dto.language_code),
|
||||||
|
"language_code",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const currencyCode = extractOrPushError(
|
||||||
|
CurrencyCode.create(dto.currency_code),
|
||||||
|
"currency_code",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
return Result.fail(new ValidationErrorCollection("Supplier props mapping failed", errors));
|
||||||
|
}
|
||||||
|
|
||||||
|
const postalAddressProps: PostalAddressProps = {
|
||||||
|
street: street!,
|
||||||
|
street2: street2!,
|
||||||
|
city: city!,
|
||||||
|
postalCode: postalCode!,
|
||||||
|
province: province!,
|
||||||
|
country: country!,
|
||||||
|
};
|
||||||
|
|
||||||
|
const supplierProps: ISupplierCreateProps = {
|
||||||
|
companyId,
|
||||||
|
status: status!,
|
||||||
|
reference: reference!,
|
||||||
|
|
||||||
|
isCompany: isCompany,
|
||||||
|
name: name!,
|
||||||
|
tradeName: tradeName!,
|
||||||
|
tin: tinNumber!,
|
||||||
|
|
||||||
|
address: postalAddressProps!,
|
||||||
|
|
||||||
|
emailPrimary: primaryEmailAddress!,
|
||||||
|
emailSecondary: secondaryEmailAddress!,
|
||||||
|
|
||||||
|
phonePrimary: primaryPhoneNumber!,
|
||||||
|
phoneSecondary: secondaryPhoneNumber!,
|
||||||
|
|
||||||
|
mobilePrimary: primaryMobileNumber!,
|
||||||
|
mobileSecondary: secondaryMobileNumber!,
|
||||||
|
|
||||||
|
fax: faxNumber!,
|
||||||
|
website: website!,
|
||||||
|
|
||||||
|
languageCode: languageCode!,
|
||||||
|
currencyCode: currencyCode!,
|
||||||
|
};
|
||||||
|
|
||||||
|
return Result.ok({ id: supplierId!, props: supplierProps });
|
||||||
|
} catch (err: unknown) {
|
||||||
|
return Result.fail(new DomainError("Supplier props mapping failed", { cause: err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
modules/supplier/src/api/application/mappers/index.ts
Normal file
2
modules/supplier/src/api/application/mappers/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./create-supplier-input.mapper";
|
||||||
|
export * from "./update-supplier-input.mapper";
|
||||||
@ -0,0 +1,262 @@
|
|||||||
|
import {
|
||||||
|
City,
|
||||||
|
Country,
|
||||||
|
CurrencyCode,
|
||||||
|
DomainError,
|
||||||
|
EmailAddress,
|
||||||
|
LanguageCode,
|
||||||
|
Name,
|
||||||
|
PhoneNumber,
|
||||||
|
type PostalAddressPatchProps,
|
||||||
|
PostalCode,
|
||||||
|
Province,
|
||||||
|
Street,
|
||||||
|
TINNumber,
|
||||||
|
URLAddress,
|
||||||
|
type UniqueID,
|
||||||
|
ValidationErrorCollection,
|
||||||
|
type ValidationErrorDetail,
|
||||||
|
extractOrPushError,
|
||||||
|
maybeFromNullableResult,
|
||||||
|
} from "@repo/rdx-ddd";
|
||||||
|
import { Result, isNullishOrEmpty, toPatchField } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
import type { UpdateSupplierByIdRequestDTO } from "../../../common";
|
||||||
|
import type { SupplierPatchProps } from "../../domain";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UpdateSupplierInputMapper
|
||||||
|
* Convierte el DTO a las props validadas (SupplierProps).
|
||||||
|
* No construye directamente el agregado.
|
||||||
|
* Tri-estado:
|
||||||
|
* - campo omitido → no se cambia
|
||||||
|
* - campo con valor null/"" → se quita el valor -> set(None()),
|
||||||
|
* - campo con valor no-vacío → se pone el nuevo valor -> set(Some(VO)).
|
||||||
|
*
|
||||||
|
* @param dto - DTO con los datos a cambiar en el cliente
|
||||||
|
* @returns Cambios en las propiedades del cliente
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface IUpdateSupplierInputMapper {
|
||||||
|
map(
|
||||||
|
dto: UpdateSupplierByIdRequestDTO,
|
||||||
|
params: { companyId: UniqueID }
|
||||||
|
): Result<SupplierPatchProps>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdateSupplierInputMapper implements IUpdateSupplierInputMapper {
|
||||||
|
public map(dto: UpdateSupplierByIdRequestDTO, params: { companyId: UniqueID }) {
|
||||||
|
try {
|
||||||
|
const errors: ValidationErrorDetail[] = [];
|
||||||
|
const supplierPatchProps: SupplierPatchProps = {};
|
||||||
|
|
||||||
|
toPatchField(dto.reference).ifSet((reference) => {
|
||||||
|
supplierPatchProps.reference = extractOrPushError(
|
||||||
|
maybeFromNullableResult(reference, (value) => Name.create(value)),
|
||||||
|
"reference",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
toPatchField(dto.is_company).ifSet((is_company) => {
|
||||||
|
if (isNullishOrEmpty(is_company)) {
|
||||||
|
errors.push({ path: "is_company", message: "is_company cannot be empty" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
supplierPatchProps.isCompany = extractOrPushError(
|
||||||
|
Result.ok(Boolean(is_company!)),
|
||||||
|
"is_company",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
toPatchField(dto.name).ifSet((name) => {
|
||||||
|
if (isNullishOrEmpty(name)) {
|
||||||
|
errors.push({ path: "name", message: "Name cannot be empty" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
supplierPatchProps.name = extractOrPushError(Name.create(name!), "name", errors);
|
||||||
|
});
|
||||||
|
|
||||||
|
toPatchField(dto.trade_name).ifSet((trade_name) => {
|
||||||
|
supplierPatchProps.tradeName = extractOrPushError(
|
||||||
|
maybeFromNullableResult(trade_name, (value) => Name.create(value)),
|
||||||
|
"trade_name",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
toPatchField(dto.tin).ifSet((tin) => {
|
||||||
|
supplierPatchProps.tin = extractOrPushError(
|
||||||
|
maybeFromNullableResult(tin, (value) => TINNumber.create(value)),
|
||||||
|
"tin",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
toPatchField(dto.email_primary).ifSet((email_primary) => {
|
||||||
|
supplierPatchProps.emailPrimary = extractOrPushError(
|
||||||
|
maybeFromNullableResult(email_primary, (value) => EmailAddress.create(value)),
|
||||||
|
"email_primary",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
toPatchField(dto.email_secondary).ifSet((email_secondary) => {
|
||||||
|
supplierPatchProps.emailSecondary = extractOrPushError(
|
||||||
|
maybeFromNullableResult(email_secondary, (value) => EmailAddress.create(value)),
|
||||||
|
"email_secondary",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
toPatchField(dto.mobile_primary).ifSet((mobile_primary) => {
|
||||||
|
supplierPatchProps.mobilePrimary = extractOrPushError(
|
||||||
|
maybeFromNullableResult(mobile_primary, (value) => PhoneNumber.create(value)),
|
||||||
|
"mobile_primary",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
toPatchField(dto.mobile_secondary).ifSet((mobile_secondary) => {
|
||||||
|
supplierPatchProps.mobilePrimary = extractOrPushError(
|
||||||
|
maybeFromNullableResult(mobile_secondary, (value) => PhoneNumber.create(value)),
|
||||||
|
"mobile_secondary",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
toPatchField(dto.phone_primary).ifSet((phone_primary) => {
|
||||||
|
supplierPatchProps.phonePrimary = extractOrPushError(
|
||||||
|
maybeFromNullableResult(phone_primary, (value) => PhoneNumber.create(value)),
|
||||||
|
"phone_primary",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
toPatchField(dto.phone_secondary).ifSet((phone_secondary) => {
|
||||||
|
supplierPatchProps.phoneSecondary = extractOrPushError(
|
||||||
|
maybeFromNullableResult(phone_secondary, (value) => PhoneNumber.create(value)),
|
||||||
|
"phone_secondary",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
toPatchField(dto.fax).ifSet((fax) => {
|
||||||
|
supplierPatchProps.fax = extractOrPushError(
|
||||||
|
maybeFromNullableResult(fax, (value) => PhoneNumber.create(value)),
|
||||||
|
"fax",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
toPatchField(dto.website).ifSet((website) => {
|
||||||
|
supplierPatchProps.website = extractOrPushError(
|
||||||
|
maybeFromNullableResult(website, (value) => URLAddress.create(value)),
|
||||||
|
"website",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
toPatchField(dto.language_code).ifSet((languageCode) => {
|
||||||
|
if (isNullishOrEmpty(languageCode)) {
|
||||||
|
errors.push({ path: "language_code", message: "Language code cannot be empty" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
supplierPatchProps.languageCode = extractOrPushError(
|
||||||
|
LanguageCode.create(languageCode!),
|
||||||
|
"language_code",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
toPatchField(dto.currency_code).ifSet((currencyCode) => {
|
||||||
|
if (isNullishOrEmpty(currencyCode)) {
|
||||||
|
errors.push({ path: "currency_code", message: "Currency code cannot be empty" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
supplierPatchProps.currencyCode = extractOrPushError(
|
||||||
|
CurrencyCode.create(currencyCode!),
|
||||||
|
"currency_code",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// PostalAddress
|
||||||
|
const addressPatchProps = this.mapPostalAddress(dto, errors);
|
||||||
|
if (addressPatchProps) {
|
||||||
|
supplierPatchProps.address = addressPatchProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
return Result.fail(
|
||||||
|
new ValidationErrorCollection("Supplier props mapping failed (update)", errors)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok(supplierPatchProps);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
return Result.fail(new DomainError("Supplier props mapping failed", { cause: err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public mapPostalAddress(
|
||||||
|
dto: UpdateSupplierByIdRequestDTO,
|
||||||
|
errors: ValidationErrorDetail[]
|
||||||
|
): PostalAddressPatchProps | undefined {
|
||||||
|
const postalAddressPatchProps: PostalAddressPatchProps = {};
|
||||||
|
|
||||||
|
toPatchField(dto.street).ifSet((street) => {
|
||||||
|
postalAddressPatchProps.street = extractOrPushError(
|
||||||
|
maybeFromNullableResult(street, (value) => Street.create(value)),
|
||||||
|
"street",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
toPatchField(dto.street2).ifSet((street2) => {
|
||||||
|
postalAddressPatchProps.street2 = extractOrPushError(
|
||||||
|
maybeFromNullableResult(street2, (value) => Street.create(value)),
|
||||||
|
"street2",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
toPatchField(dto.city).ifSet((city) => {
|
||||||
|
postalAddressPatchProps.city = extractOrPushError(
|
||||||
|
maybeFromNullableResult(city, (value) => City.create(value)),
|
||||||
|
"city",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
toPatchField(dto.province).ifSet((province) => {
|
||||||
|
postalAddressPatchProps.province = extractOrPushError(
|
||||||
|
maybeFromNullableResult(province, (value) => Province.create(value)),
|
||||||
|
"province",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
toPatchField(dto.postal_code).ifSet((postalCode) => {
|
||||||
|
postalAddressPatchProps.postalCode = extractOrPushError(
|
||||||
|
maybeFromNullableResult(postalCode, (value) => PostalCode.create(value)),
|
||||||
|
"postal_code",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
toPatchField(dto.country).ifSet((country) => {
|
||||||
|
postalAddressPatchProps.country = extractOrPushError(
|
||||||
|
maybeFromNullableResult(country, (value) => Country.create(value)),
|
||||||
|
"country",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.keys(postalAddressPatchProps).length > 0 ? postalAddressPatchProps : undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
1
modules/supplier/src/api/application/models/index.ts
Normal file
1
modules/supplier/src/api/application/models/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./supplier-summary";
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
import type {
|
||||||
|
CurrencyCode,
|
||||||
|
EmailAddress,
|
||||||
|
LanguageCode,
|
||||||
|
Name,
|
||||||
|
PhoneNumber,
|
||||||
|
PostalAddress,
|
||||||
|
TINNumber,
|
||||||
|
URLAddress,
|
||||||
|
UniqueID,
|
||||||
|
} from "@repo/rdx-ddd";
|
||||||
|
import type { Maybe } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
export type SupplierSummary = {
|
||||||
|
id: UniqueID;
|
||||||
|
companyId: UniqueID;
|
||||||
|
isActive: boolean;
|
||||||
|
reference: Maybe<Name>;
|
||||||
|
|
||||||
|
isCompany: boolean;
|
||||||
|
name: Name;
|
||||||
|
tradeName: Maybe<Name>;
|
||||||
|
tin: Maybe<TINNumber>;
|
||||||
|
|
||||||
|
address: PostalAddress;
|
||||||
|
|
||||||
|
emailPrimary: Maybe<EmailAddress>;
|
||||||
|
emailSecondary: Maybe<EmailAddress>;
|
||||||
|
phonePrimary: Maybe<PhoneNumber>;
|
||||||
|
phoneSecondary: Maybe<PhoneNumber>;
|
||||||
|
mobilePrimary: Maybe<PhoneNumber>;
|
||||||
|
mobileSecondary: Maybe<PhoneNumber>;
|
||||||
|
|
||||||
|
fax: Maybe<PhoneNumber>;
|
||||||
|
website: Maybe<URLAddress>;
|
||||||
|
|
||||||
|
languageCode: LanguageCode;
|
||||||
|
currencyCode: CurrencyCode;
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from './supplier-repository.interface';
|
||||||
@ -0,0 +1,82 @@
|
|||||||
|
import type { Criteria } from "@repo/rdx-criteria/server";
|
||||||
|
import type { TINNumber, UniqueID } from "@repo/rdx-ddd";
|
||||||
|
import type { Collection, Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
import type { Supplier } from "../../domain/aggregates";
|
||||||
|
import type { SupplierSummary } from "../models";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interfaz del repositorio para el agregado `Supplier`.
|
||||||
|
* El escopado multitenant está representado por `companyId`.
|
||||||
|
*/
|
||||||
|
export interface ISupplierRepository {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Crea un nuevo proveedor
|
||||||
|
*
|
||||||
|
* @param supplier - El proveedor nuevo a guardar.
|
||||||
|
* @param transaction - Transacción activa para la operación.
|
||||||
|
* @returns Result<void, Error>
|
||||||
|
*/
|
||||||
|
create(supplier: Supplier, transaction: unknown): Promise<Result<void, Error>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actualiza un proveedor existente.
|
||||||
|
*
|
||||||
|
* @param supplier - El proveedor a actualizar.
|
||||||
|
* @param transaction - Transacción activa para la operación.
|
||||||
|
* @returns Result<void, Error>
|
||||||
|
*/
|
||||||
|
update(supplier: Supplier, transaction: unknown): Promise<Result<void, Error>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Comprueba si existe un Supplier con un `id` dentro de una `company`.
|
||||||
|
*/
|
||||||
|
existsByIdInCompany(
|
||||||
|
companyId: UniqueID,
|
||||||
|
id: UniqueID,
|
||||||
|
transaction: unknown
|
||||||
|
): Promise<Result<boolean, Error>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recupera un Supplier por su ID y companyId.
|
||||||
|
* Devuelve un `NotFoundError` si no se encuentra.
|
||||||
|
*/
|
||||||
|
getByIdInCompany(
|
||||||
|
companyId: UniqueID,
|
||||||
|
id: UniqueID,
|
||||||
|
transaction: unknown
|
||||||
|
): Promise<Result<Supplier, Error>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recupera un Supplier por su TIN y companyId.
|
||||||
|
* Devuelve un `NotFoundError` si no se encuentra.
|
||||||
|
*/
|
||||||
|
getByTINInCompany(
|
||||||
|
companyId: UniqueID,
|
||||||
|
tin: TINNumber,
|
||||||
|
transaction?: unknown
|
||||||
|
): Promise<Result<Supplier, Error>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recupera múltiples suppliers dentro de una empresa
|
||||||
|
* según un criterio dinámico (búsqueda, paginación, etc.).
|
||||||
|
* El resultado está encapsulado en un objeto `Collection<T>`.
|
||||||
|
*/
|
||||||
|
findByCriteriaInCompany(
|
||||||
|
companyId: UniqueID,
|
||||||
|
criteria: Criteria,
|
||||||
|
transaction: unknown
|
||||||
|
): Promise<Result<Collection<SupplierSummary>, Error>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Elimina un Supplier por su ID, dentro de una empresa.
|
||||||
|
* Retorna `void` si se elimina correctamente, o `NotFoundError` si no existía.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
deleteByIdInCompany(
|
||||||
|
companyId: UniqueID,
|
||||||
|
id: UniqueID,
|
||||||
|
transaction: unknown
|
||||||
|
): Promise<Result<boolean, Error>>;
|
||||||
|
}
|
||||||
4
modules/supplier/src/api/application/services/index.ts
Normal file
4
modules/supplier/src/api/application/services/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export * from "./supplier-creator.service";
|
||||||
|
export * from "./supplier-finder.service";
|
||||||
|
export * from "./supplier-public-services.interface";
|
||||||
|
export * from "./supplier-updater.service";
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
import { DuplicateEntityError } from "@erp/core/api";
|
||||||
|
import type { UniqueID } from "@repo/rdx-ddd";
|
||||||
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
import { type ISupplierCreateProps, Supplier } from "../../domain";
|
||||||
|
import type { ISupplierRepository } from "../repositories";
|
||||||
|
import { SupplierNotExistsInCompanySpecification } from "../specs";
|
||||||
|
|
||||||
|
export interface ISupplierCreator {
|
||||||
|
create(params: {
|
||||||
|
companyId: UniqueID;
|
||||||
|
id: UniqueID;
|
||||||
|
props: ISupplierCreateProps;
|
||||||
|
unknown: unknown;
|
||||||
|
}): Promise<Result<Supplier, Error>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type SupplierCreatorDeps = {
|
||||||
|
repository: ISupplierRepository;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class SupplierCreator implements ISupplierCreator {
|
||||||
|
private readonly repository: ISupplierRepository;
|
||||||
|
|
||||||
|
constructor(deps: SupplierCreatorDeps) {
|
||||||
|
this.repository = deps.repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(params: {
|
||||||
|
companyId: UniqueID;
|
||||||
|
id: UniqueID;
|
||||||
|
props: ISupplierCreateProps;
|
||||||
|
unknown: unknown;
|
||||||
|
}): Promise<Result<Supplier, Error>> {
|
||||||
|
const { companyId, id, props, unknown } = params;
|
||||||
|
|
||||||
|
// 1. Verificar unicidad
|
||||||
|
const spec = new SupplierNotExistsInCompanySpecification(this.repository, companyId, unknown);
|
||||||
|
|
||||||
|
const isNew = await spec.isSatisfiedBy(id);
|
||||||
|
|
||||||
|
if (!isNew) {
|
||||||
|
return Result.fail(new DuplicateEntityError("Supplier", "id", String(id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Crear agregado
|
||||||
|
const createResult = Supplier.create(
|
||||||
|
{
|
||||||
|
...props,
|
||||||
|
companyId,
|
||||||
|
},
|
||||||
|
id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (createResult.isFailure) {
|
||||||
|
return createResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSupplier = createResult.data;
|
||||||
|
|
||||||
|
// 3. Persistir agregado
|
||||||
|
const saveResult = await this.repository.create(newSupplier, unknown);
|
||||||
|
|
||||||
|
if (saveResult.isFailure) {
|
||||||
|
return Result.fail(saveResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok(newSupplier);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
import type { Criteria } from "@repo/rdx-criteria/server";
|
||||||
|
import type { TINNumber, UniqueID } from "@repo/rdx-ddd";
|
||||||
|
import type { Collection, Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
import type { Supplier } from "../../domain";
|
||||||
|
import type { SupplierSummary } from "../models";
|
||||||
|
import type { ISupplierRepository } from "../repositories";
|
||||||
|
|
||||||
|
export interface ISupplierFinder {
|
||||||
|
findSupplierById(
|
||||||
|
companyId: UniqueID,
|
||||||
|
supplierId: UniqueID,
|
||||||
|
unknown?: unknown
|
||||||
|
): Promise<Result<Supplier, Error>>;
|
||||||
|
|
||||||
|
findSupplierByTIN(
|
||||||
|
companyId: UniqueID,
|
||||||
|
tin: TINNumber,
|
||||||
|
unknown?: unknown
|
||||||
|
): Promise<Result<Supplier, Error>>;
|
||||||
|
|
||||||
|
supplierExists(
|
||||||
|
companyId: UniqueID,
|
||||||
|
invoiceId: UniqueID,
|
||||||
|
unknown?: unknown
|
||||||
|
): Promise<Result<boolean, Error>>;
|
||||||
|
|
||||||
|
findSuppliersByCriteria(
|
||||||
|
companyId: UniqueID,
|
||||||
|
criteria: Criteria,
|
||||||
|
unknown?: unknown
|
||||||
|
): Promise<Result<Collection<SupplierSummary>, Error>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SupplierFinder implements ISupplierFinder {
|
||||||
|
constructor(private readonly repository: ISupplierRepository) {}
|
||||||
|
|
||||||
|
async findSupplierById(
|
||||||
|
companyId: UniqueID,
|
||||||
|
supplierId: UniqueID,
|
||||||
|
unknown?: unknown
|
||||||
|
): Promise<Result<Supplier, Error>> {
|
||||||
|
return this.repository.getByIdInCompany(companyId, supplierId, unknown);
|
||||||
|
}
|
||||||
|
|
||||||
|
findSupplierByTIN(
|
||||||
|
companyId: UniqueID,
|
||||||
|
tin: TINNumber,
|
||||||
|
unknown?: unknown
|
||||||
|
): Promise<Result<Supplier, Error>> {
|
||||||
|
return this.repository.getByTINInCompany(companyId, tin, unknown);
|
||||||
|
}
|
||||||
|
|
||||||
|
async supplierExists(
|
||||||
|
companyId: UniqueID,
|
||||||
|
supplierId: UniqueID,
|
||||||
|
unknown?: unknown
|
||||||
|
): Promise<Result<boolean, Error>> {
|
||||||
|
return this.repository.existsByIdInCompany(companyId, supplierId, unknown);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findSuppliersByCriteria(
|
||||||
|
companyId: UniqueID,
|
||||||
|
criteria: Criteria,
|
||||||
|
unknown?: unknown
|
||||||
|
): Promise<Result<Collection<SupplierSummary>, Error>> {
|
||||||
|
return this.repository.findByCriteriaInCompany(companyId, criteria, unknown);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import type { TINNumber, UniqueID } from "@repo/rdx-ddd";
|
||||||
|
import type { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
import type { ISupplierCreateProps, Supplier } from "../../domain";
|
||||||
|
|
||||||
|
export interface ISupplierServicesContext {
|
||||||
|
transaction: unknown;
|
||||||
|
companyId: UniqueID;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISupplierPublicServices {
|
||||||
|
findSupplierByTIN: (
|
||||||
|
tin: TINNumber,
|
||||||
|
context: ISupplierServicesContext
|
||||||
|
) => Promise<Result<Supplier, Error>>;
|
||||||
|
|
||||||
|
createSupplier: (
|
||||||
|
id: UniqueID,
|
||||||
|
props: ISupplierCreateProps,
|
||||||
|
context: ISupplierServicesContext
|
||||||
|
) => Promise<Result<Supplier, Error>>;
|
||||||
|
}
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
import type { UniqueID } from "@repo/rdx-ddd";
|
||||||
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
import type { Supplier, SupplierPatchProps } from "../../domain";
|
||||||
|
import type { ISupplierRepository } from "../repositories";
|
||||||
|
|
||||||
|
export interface ISupplierUpdater {
|
||||||
|
update(params: {
|
||||||
|
companyId: UniqueID;
|
||||||
|
id: UniqueID;
|
||||||
|
props: SupplierPatchProps;
|
||||||
|
transaction: unknown;
|
||||||
|
}): Promise<Result<Supplier, Error>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type SupplierUpdaterDeps = {
|
||||||
|
repository: ISupplierRepository;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class SupplierUpdater implements ISupplierUpdater {
|
||||||
|
private readonly repository: ISupplierRepository;
|
||||||
|
|
||||||
|
constructor(deps: SupplierUpdaterDeps) {
|
||||||
|
this.repository = deps.repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(params: {
|
||||||
|
companyId: UniqueID;
|
||||||
|
id: UniqueID;
|
||||||
|
props: SupplierPatchProps;
|
||||||
|
transaction: unknown;
|
||||||
|
}): Promise<Result<Supplier, Error>> {
|
||||||
|
const { companyId, id, props, transaction } = params;
|
||||||
|
|
||||||
|
// Recuperar agregado existente
|
||||||
|
const existingResult = await this.repository.getByIdInCompany(companyId, id, transaction);
|
||||||
|
|
||||||
|
if (existingResult.isFailure) {
|
||||||
|
return Result.fail(existingResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const supplier = existingResult.data;
|
||||||
|
|
||||||
|
// Aplicar cambios en el agregado
|
||||||
|
const updateResult = supplier.update(props);
|
||||||
|
|
||||||
|
if (updateResult.isFailure) {
|
||||||
|
return Result.fail(updateResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persistir cambios
|
||||||
|
const saveResult = await this.repository.update(supplier, transaction);
|
||||||
|
|
||||||
|
if (saveResult.isFailure) {
|
||||||
|
return Result.fail(saveResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok(supplier);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./supplier-snapshot.interface";
|
||||||
|
export * from "./supplier-snapshot-builder";
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
import type { ISnapshotBuilder } from "@erp/core/api";
|
||||||
|
import { maybeToEmptyString } from "@repo/rdx-ddd";
|
||||||
|
|
||||||
|
import type { Supplier } from "../../../domain";
|
||||||
|
|
||||||
|
import type { ISupplierFullSnapshot } from "./supplier-snapshot.interface";
|
||||||
|
|
||||||
|
export interface ISupplierFullSnapshotBuilder
|
||||||
|
extends ISnapshotBuilder<Supplier, ISupplierFullSnapshot> {}
|
||||||
|
|
||||||
|
export class SupplierFullSnapshotBuilder implements ISupplierFullSnapshotBuilder {
|
||||||
|
toOutput(supplier: Supplier): ISupplierFullSnapshot {
|
||||||
|
const address = supplier.address.toPrimitive();
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: supplier.id.toPrimitive(),
|
||||||
|
company_id: supplier.companyId.toPrimitive(),
|
||||||
|
status: supplier.isActive ? "active" : "inactive",
|
||||||
|
reference: maybeToEmptyString(supplier.reference, (value) => value.toPrimitive()),
|
||||||
|
|
||||||
|
is_company: String(supplier.isCompany),
|
||||||
|
name: supplier.name.toPrimitive(),
|
||||||
|
trade_name: maybeToEmptyString(supplier.tradeName, (value) => value.toPrimitive()),
|
||||||
|
tin: maybeToEmptyString(supplier.tin, (value) => value.toPrimitive()),
|
||||||
|
|
||||||
|
street: maybeToEmptyString(address.street, (value) => value.toPrimitive()),
|
||||||
|
street2: maybeToEmptyString(address.street2, (value) => value.toPrimitive()),
|
||||||
|
city: maybeToEmptyString(address.city, (value) => value.toPrimitive()),
|
||||||
|
province: maybeToEmptyString(address.province, (value) => value.toPrimitive()),
|
||||||
|
postal_code: maybeToEmptyString(address.postalCode, (value) => value.toPrimitive()),
|
||||||
|
country: maybeToEmptyString(address.country, (value) => value.toPrimitive()),
|
||||||
|
|
||||||
|
email_primary: maybeToEmptyString(supplier.emailPrimary, (value) => value.toPrimitive()),
|
||||||
|
email_secondary: maybeToEmptyString(supplier.emailSecondary, (value) => value.toPrimitive()),
|
||||||
|
|
||||||
|
phone_primary: maybeToEmptyString(supplier.phonePrimary, (value) => value.toPrimitive()),
|
||||||
|
phone_secondary: maybeToEmptyString(supplier.phoneSecondary, (value) => value.toPrimitive()),
|
||||||
|
|
||||||
|
mobile_primary: maybeToEmptyString(supplier.mobilePrimary, (value) => value.toPrimitive()),
|
||||||
|
mobile_secondary: maybeToEmptyString(supplier.mobileSecondary, (value) =>
|
||||||
|
value.toPrimitive()
|
||||||
|
),
|
||||||
|
|
||||||
|
fax: maybeToEmptyString(supplier.fax, (value) => value.toPrimitive()),
|
||||||
|
website: maybeToEmptyString(supplier.website, (value) => value.toPrimitive()),
|
||||||
|
|
||||||
|
language_code: supplier.languageCode.toPrimitive(),
|
||||||
|
currency_code: supplier.currencyCode.toPrimitive(),
|
||||||
|
|
||||||
|
metadata: {
|
||||||
|
entity: "supplier",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
export interface ISupplierFullSnapshot {
|
||||||
|
id: string;
|
||||||
|
company_id: string;
|
||||||
|
status: string;
|
||||||
|
reference: string;
|
||||||
|
|
||||||
|
is_company: string;
|
||||||
|
name: string;
|
||||||
|
trade_name: string;
|
||||||
|
tin: string;
|
||||||
|
|
||||||
|
street: string;
|
||||||
|
street2: string;
|
||||||
|
city: string;
|
||||||
|
province: string;
|
||||||
|
postal_code: string;
|
||||||
|
country: string;
|
||||||
|
|
||||||
|
email_primary: string;
|
||||||
|
email_secondary: string;
|
||||||
|
|
||||||
|
phone_primary: string;
|
||||||
|
phone_secondary: string;
|
||||||
|
|
||||||
|
mobile_primary: string;
|
||||||
|
mobile_secondary: string;
|
||||||
|
|
||||||
|
fax: string;
|
||||||
|
website: string;
|
||||||
|
|
||||||
|
language_code: string;
|
||||||
|
currency_code: string;
|
||||||
|
|
||||||
|
metadata?: Record<string, string>;
|
||||||
|
}
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./domain";
|
||||||
|
export * from "./summary";
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./supplier-summary-snapshot.interface";
|
||||||
|
export * from "./supplier-summary-snapshot-builder";
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
import type { ISnapshotBuilder } from "@erp/core/api";
|
||||||
|
import { maybeToEmptyString } from "@repo/rdx-ddd";
|
||||||
|
|
||||||
|
import type { SupplierSummary } from "../../models";
|
||||||
|
|
||||||
|
import type { ISupplierSummarySnapshot } from "./supplier-summary-snapshot.interface";
|
||||||
|
|
||||||
|
export interface ISupplierSummarySnapshotBuilder
|
||||||
|
extends ISnapshotBuilder<SupplierSummary, ISupplierSummarySnapshot> {}
|
||||||
|
|
||||||
|
export class SupplierSummarySnapshotBuilder implements ISupplierSummarySnapshotBuilder {
|
||||||
|
toOutput(supplier: SupplierSummary): ISupplierSummarySnapshot {
|
||||||
|
const { address } = supplier;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: supplier.id.toString(),
|
||||||
|
company_id: supplier.companyId.toString(),
|
||||||
|
status: supplier.isActive ? "active" : "inactive",
|
||||||
|
reference: maybeToEmptyString(supplier.reference, (value) => value.toString()),
|
||||||
|
|
||||||
|
is_company: String(supplier.isCompany),
|
||||||
|
name: supplier.name.toString(),
|
||||||
|
trade_name: maybeToEmptyString(supplier.tradeName, (value) => value.toString()),
|
||||||
|
tin: maybeToEmptyString(supplier.tin, (value) => value.toString()),
|
||||||
|
|
||||||
|
street: maybeToEmptyString(address.street, (value) => value.toString()),
|
||||||
|
street2: maybeToEmptyString(address.street2, (value) => value.toString()),
|
||||||
|
city: maybeToEmptyString(address.city, (value) => value.toString()),
|
||||||
|
postal_code: maybeToEmptyString(address.postalCode, (value) => value.toString()),
|
||||||
|
province: maybeToEmptyString(address.province, (value) => value.toString()),
|
||||||
|
country: maybeToEmptyString(address.country, (value) => value.toString()),
|
||||||
|
|
||||||
|
email_primary: maybeToEmptyString(supplier.emailPrimary, (value) => value.toString()),
|
||||||
|
email_secondary: maybeToEmptyString(supplier.emailSecondary, (value) => value.toString()),
|
||||||
|
|
||||||
|
phone_primary: maybeToEmptyString(supplier.phonePrimary, (value) => value.toString()),
|
||||||
|
phone_secondary: maybeToEmptyString(supplier.phoneSecondary, (value) => value.toString()),
|
||||||
|
|
||||||
|
mobile_primary: maybeToEmptyString(supplier.mobilePrimary, (value) => value.toString()),
|
||||||
|
mobile_secondary: maybeToEmptyString(supplier.mobileSecondary, (value) => value.toString()),
|
||||||
|
|
||||||
|
fax: maybeToEmptyString(supplier.fax, (value) => value.toString()),
|
||||||
|
website: maybeToEmptyString(supplier.website, (value) => value.toString()),
|
||||||
|
|
||||||
|
language_code: supplier.languageCode.code,
|
||||||
|
currency_code: supplier.currencyCode.code,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
export type ISupplierSummarySnapshot = {
|
||||||
|
id: string;
|
||||||
|
company_id: string;
|
||||||
|
status: string;
|
||||||
|
reference: string;
|
||||||
|
|
||||||
|
is_company: string;
|
||||||
|
name: string;
|
||||||
|
trade_name: string;
|
||||||
|
tin: string;
|
||||||
|
|
||||||
|
street: string;
|
||||||
|
street2: string;
|
||||||
|
city: string;
|
||||||
|
postal_code: string;
|
||||||
|
province: string;
|
||||||
|
country: string;
|
||||||
|
|
||||||
|
email_primary: string;
|
||||||
|
email_secondary: string;
|
||||||
|
|
||||||
|
phone_primary: string;
|
||||||
|
phone_secondary: string;
|
||||||
|
|
||||||
|
mobile_primary: string;
|
||||||
|
mobile_secondary: string;
|
||||||
|
|
||||||
|
fax: string;
|
||||||
|
website: string;
|
||||||
|
|
||||||
|
language_code: string;
|
||||||
|
currency_code: string;
|
||||||
|
|
||||||
|
metadata?: Record<string, string>;
|
||||||
|
};
|
||||||
1
modules/supplier/src/api/application/specs/index.ts
Normal file
1
modules/supplier/src/api/application/specs/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./supplier-not-exists.spec";
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
import { CompositeSpecification, type UniqueID } from "@repo/rdx-ddd";
|
||||||
|
|
||||||
|
import type { ISupplierRepository } from "../../application";
|
||||||
|
|
||||||
|
export class SupplierNotExistsInCompanySpecification extends CompositeSpecification<UniqueID> {
|
||||||
|
constructor(
|
||||||
|
private readonly repository: ISupplierRepository,
|
||||||
|
private readonly companyId: UniqueID,
|
||||||
|
private readonly transaction?: unknown
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async isSatisfiedBy(supplierId: UniqueID): Promise<boolean> {
|
||||||
|
const existsCheck = await this.repository.existsByIdInCompany(
|
||||||
|
this.companyId,
|
||||||
|
supplierId,
|
||||||
|
this.transaction
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existsCheck.isFailure) {
|
||||||
|
throw existsCheck.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const supplierExists = existsCheck.data;
|
||||||
|
/*logger.debug(
|
||||||
|
`supplierExists => ${supplierExists}, ${JSON.stringify({ supplierId, companyId: this.companyId }, null, 2)}`,
|
||||||
|
{
|
||||||
|
label: "SupplierNotExistsInCompanySpecification",
|
||||||
|
}
|
||||||
|
);*/
|
||||||
|
|
||||||
|
return supplierExists === false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,65 @@
|
|||||||
|
import type { ITransactionManager } from "@erp/core/api";
|
||||||
|
import type { UniqueID } from "@repo/rdx-ddd";
|
||||||
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
import type { CreateSupplierRequestDTO } from "../../../common";
|
||||||
|
import type { ISupplierCreator } from "../services";
|
||||||
|
import type {
|
||||||
|
ICreateSupplierInputMapper,
|
||||||
|
ISupplierFullSnapshotBuilder,
|
||||||
|
} from "../snapshot-builders";
|
||||||
|
|
||||||
|
type CreateSupplierUseCaseInput = {
|
||||||
|
companyId: UniqueID;
|
||||||
|
dto: CreateSupplierRequestDTO;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CreateSupplierUseCaseDeps = {
|
||||||
|
dtoMapper: ICreateSupplierInputMapper;
|
||||||
|
creator: ISupplierCreator;
|
||||||
|
fullSnapshotBuilder: ISupplierFullSnapshotBuilder;
|
||||||
|
transactionManager: ITransactionManager;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class CreateSupplierUseCase {
|
||||||
|
private readonly dtoMapper: ICreateSupplierInputMapper;
|
||||||
|
private readonly creator: ISupplierCreator;
|
||||||
|
private readonly fullSnapshotBuilder: ISupplierFullSnapshotBuilder;
|
||||||
|
private readonly transactionManager: ITransactionManager;
|
||||||
|
|
||||||
|
constructor(deps: CreateSupplierUseCaseDeps) {
|
||||||
|
this.dtoMapper = deps.dtoMapper;
|
||||||
|
this.creator = deps.creator;
|
||||||
|
this.fullSnapshotBuilder = deps.fullSnapshotBuilder;
|
||||||
|
this.transactionManager = deps.transactionManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public execute(params: CreateSupplierUseCaseInput) {
|
||||||
|
const { dto, companyId } = params;
|
||||||
|
|
||||||
|
const mappedPropsResult = this.dtoMapper.map(dto, { companyId });
|
||||||
|
if (mappedPropsResult.isFailure) {
|
||||||
|
return mappedPropsResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { props, id } = mappedPropsResult.data;
|
||||||
|
|
||||||
|
return this.transactionManager.complete(async (transaction: unknown) => {
|
||||||
|
try {
|
||||||
|
const createResult = await this.creator.create({ companyId, id, props, transaction });
|
||||||
|
|
||||||
|
if (createResult.isFailure) {
|
||||||
|
return createResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSupplier = createResult.data;
|
||||||
|
|
||||||
|
const snapshot = this.fullSnapshotBuilder.toOutput(newSupplier);
|
||||||
|
|
||||||
|
return Result.ok(snapshot);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
return Result.fail(error as Error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
import type { ITransactionManager } from "@erp/core/api";
|
||||||
|
import { UniqueID } from "@repo/rdx-ddd";
|
||||||
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
import type { ISupplierFinder } from "../services";
|
||||||
|
import type { ISupplierFullSnapshotBuilder } from "../snapshot-builders";
|
||||||
|
|
||||||
|
type GetSupplierUseCaseInput = {
|
||||||
|
companyId: UniqueID;
|
||||||
|
supplier_id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class GetSupplierByIdUseCase {
|
||||||
|
constructor(
|
||||||
|
private readonly finder: ISupplierFinder,
|
||||||
|
private readonly fullSnapshotBuilder: ISupplierFullSnapshotBuilder,
|
||||||
|
private readonly transactionManager: ITransactionManager
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public execute(params: GetSupplierUseCaseInput) {
|
||||||
|
const { supplier_id, companyId } = params;
|
||||||
|
|
||||||
|
const idOrError = UniqueID.create(supplier_id);
|
||||||
|
if (idOrError.isFailure) {
|
||||||
|
return Result.fail(idOrError.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const supplierId = idOrError.data;
|
||||||
|
|
||||||
|
return this.transactionManager.complete(async (transaction) => {
|
||||||
|
try {
|
||||||
|
const supplierResult = await this.finder.findSupplierById(
|
||||||
|
companyId,
|
||||||
|
supplierId,
|
||||||
|
transaction
|
||||||
|
);
|
||||||
|
|
||||||
|
if (supplierResult.isFailure) {
|
||||||
|
return Result.fail(supplierResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullSnapshot = this.fullSnapshotBuilder.toOutput(supplierResult.data);
|
||||||
|
|
||||||
|
return Result.ok(fullSnapshot);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
return Result.fail(error as Error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
6
modules/supplier/src/api/application/use-cases/index.ts
Normal file
6
modules/supplier/src/api/application/use-cases/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export * from './activate-supplier.use-case';
|
||||||
|
export * from './create-supplier.use-case';
|
||||||
|
export * from './deactivate-supplier.use-case';
|
||||||
|
export * from './get-supplier.use-case';
|
||||||
|
export * from './list-suppliers.use-case';
|
||||||
|
export * from './update-supplier.use-case';
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
import type { ITransactionManager } from "@erp/core/api";
|
||||||
|
import type { Criteria } from "@repo/rdx-criteria/server";
|
||||||
|
import type { UniqueID } from "@repo/rdx-ddd";
|
||||||
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
import type { ISupplierFinder } from "../services";
|
||||||
|
import type { ISupplierSummarySnapshotBuilder } from "../snapshot-builders/summary";
|
||||||
|
|
||||||
|
type ListSuppliersUseCaseInput = {
|
||||||
|
companyId: UniqueID;
|
||||||
|
criteria: Criteria;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ListSuppliersUseCase {
|
||||||
|
constructor(
|
||||||
|
private readonly finder: ISupplierFinder,
|
||||||
|
private readonly summarySnapshotBuilder: ISupplierSummarySnapshotBuilder,
|
||||||
|
private readonly transactionManager: ITransactionManager
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public execute(params: ListSuppliersUseCaseInput) {
|
||||||
|
const { criteria, companyId } = params;
|
||||||
|
|
||||||
|
return this.transactionManager.complete(async (transaction: unknown) => {
|
||||||
|
try {
|
||||||
|
const result = await this.finder.findSuppliersByCriteria(companyId, criteria, transaction);
|
||||||
|
|
||||||
|
if (result.isFailure) {
|
||||||
|
return Result.fail(result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const suppliers = result.data;
|
||||||
|
const totalSuppliers = suppliers.total();
|
||||||
|
|
||||||
|
const items = suppliers.map((item) => this.summarySnapshotBuilder.toOutput(item));
|
||||||
|
|
||||||
|
const snapshot = {
|
||||||
|
page: criteria.pageNumber,
|
||||||
|
per_page: criteria.pageSize,
|
||||||
|
total_pages: Math.ceil(totalSuppliers / criteria.pageSize),
|
||||||
|
total_items: totalSuppliers,
|
||||||
|
items: items,
|
||||||
|
metadata: {
|
||||||
|
entity: "suppliers",
|
||||||
|
criteria: criteria.toJSON(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return Result.ok(snapshot);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
return Result.fail(error as Error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
93
modules/supplier/src/api/create-supplier-module.sh
Normal file
93
modules/supplier/src/api/create-supplier-module.sh
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MODULE_ROOT="./"
|
||||||
|
|
||||||
|
DIRS=(
|
||||||
|
"$MODULE_ROOT"
|
||||||
|
"$MODULE_ROOT/application"
|
||||||
|
"$MODULE_ROOT/application/di"
|
||||||
|
"$MODULE_ROOT/application/dtos"
|
||||||
|
"$MODULE_ROOT/application/mappers"
|
||||||
|
"$MODULE_ROOT/application/repositories"
|
||||||
|
"$MODULE_ROOT/application/services"
|
||||||
|
"$MODULE_ROOT/application/use-cases"
|
||||||
|
"$MODULE_ROOT/domain"
|
||||||
|
"$MODULE_ROOT/domain/aggregates"
|
||||||
|
"$MODULE_ROOT/domain/collections"
|
||||||
|
"$MODULE_ROOT/domain/errors"
|
||||||
|
"$MODULE_ROOT/domain/value-objects"
|
||||||
|
"$MODULE_ROOT/infrastructure"
|
||||||
|
"$MODULE_ROOT/infrastructure/di"
|
||||||
|
"$MODULE_ROOT/infrastructure/express"
|
||||||
|
"$MODULE_ROOT/infrastructure/express/controllers"
|
||||||
|
"$MODULE_ROOT/infrastructure/persistence"
|
||||||
|
"$MODULE_ROOT/infrastructure/persistence/sequelize"
|
||||||
|
"$MODULE_ROOT/infrastructure/persistence/sequelize/mappers"
|
||||||
|
"$MODULE_ROOT/infrastructure/persistence/sequelize/models"
|
||||||
|
"$MODULE_ROOT/infrastructure/persistence/sequelize/repositories"
|
||||||
|
)
|
||||||
|
|
||||||
|
FILES=(
|
||||||
|
"$MODULE_ROOT/application/repositories/supplier-repository.interface.ts"
|
||||||
|
|
||||||
|
"$MODULE_ROOT/application/services/supplier-creator.service.ts"
|
||||||
|
"$MODULE_ROOT/application/services/supplier-updater.service.ts"
|
||||||
|
"$MODULE_ROOT/application/services/supplier-finder.service.ts"
|
||||||
|
|
||||||
|
"$MODULE_ROOT/application/use-cases/create-supplier.use-case.ts"
|
||||||
|
"$MODULE_ROOT/application/use-cases/update-supplier.use-case.ts"
|
||||||
|
"$MODULE_ROOT/application/use-cases/get-supplier.use-case.ts"
|
||||||
|
"$MODULE_ROOT/application/use-cases/list-suppliers.use-case.ts"
|
||||||
|
"$MODULE_ROOT/application/use-cases/activate-supplier.use-case.ts"
|
||||||
|
"$MODULE_ROOT/application/use-cases/deactivate-supplier.use-case.ts"
|
||||||
|
|
||||||
|
"$MODULE_ROOT/domain/aggregates/supplier.aggregate.ts"
|
||||||
|
"$MODULE_ROOT/domain/collections/supplier-taxes.collection.ts"
|
||||||
|
"$MODULE_ROOT/domain/errors/supplier-domain.errors.ts"
|
||||||
|
"$MODULE_ROOT/domain/value-objects/supplier-status.value-object.ts"
|
||||||
|
|
||||||
|
"$MODULE_ROOT/infrastructure/express/supplier.routes.ts"
|
||||||
|
"$MODULE_ROOT/infrastructure/persistence/sequelize/models/supplier-sequelize.model.ts"
|
||||||
|
"$MODULE_ROOT/infrastructure/persistence/sequelize/mappers/sequelize-supplier-domain.mapper.ts"
|
||||||
|
"$MODULE_ROOT/infrastructure/persistence/sequelize/mappers/supplier-summary.mapper.ts"
|
||||||
|
"$MODULE_ROOT/infrastructure/persistence/sequelize/repositories/sequelize-supplier.repository.ts"
|
||||||
|
)
|
||||||
|
|
||||||
|
mkdir -p "${DIRS[@]}"
|
||||||
|
touch "${FILES[@]}"
|
||||||
|
|
||||||
|
create_index() {
|
||||||
|
local dir="$1"
|
||||||
|
local index_file="$dir/index.ts"
|
||||||
|
|
||||||
|
: > "$index_file"
|
||||||
|
|
||||||
|
mapfile -t ts_files < <(
|
||||||
|
find "$dir" -maxdepth 1 -type f -name "*.ts" ! -name "index.ts" \
|
||||||
|
| sort
|
||||||
|
)
|
||||||
|
|
||||||
|
mapfile -t subdirs < <(
|
||||||
|
find "$dir" -maxdepth 1 -mindepth 1 -type d \
|
||||||
|
| sort
|
||||||
|
)
|
||||||
|
|
||||||
|
for file in "${ts_files[@]}"; do
|
||||||
|
local base
|
||||||
|
base="$(basename "$file" .ts)"
|
||||||
|
echo "export * from './$base';" >> "$index_file"
|
||||||
|
done
|
||||||
|
|
||||||
|
for subdir in "${subdirs[@]}"; do
|
||||||
|
local name
|
||||||
|
name="$(basename "$subdir")"
|
||||||
|
echo "export * from './$name';" >> "$index_file"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
for dir in "${DIRS[@]}"; do
|
||||||
|
create_index "$dir"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Módulo supplier creado correctamente en $MODULE_ROOT"
|
||||||
1
modules/supplier/src/api/domain/aggregates/index.ts
Normal file
1
modules/supplier/src/api/domain/aggregates/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './supplier.aggregate';
|
||||||
250
modules/supplier/src/api/domain/aggregates/supplier.aggregate.ts
Normal file
250
modules/supplier/src/api/domain/aggregates/supplier.aggregate.ts
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
import {
|
||||||
|
AggregateRoot,
|
||||||
|
type CurrencyCode,
|
||||||
|
type EmailAddress,
|
||||||
|
type LanguageCode,
|
||||||
|
type Name,
|
||||||
|
type PhoneNumber,
|
||||||
|
type PostalAddress,
|
||||||
|
type PostalAddressPatchProps,
|
||||||
|
type PostalAddressProps,
|
||||||
|
type TINNumber,
|
||||||
|
type URLAddress,
|
||||||
|
type UniqueID,
|
||||||
|
} from "@repo/rdx-ddd";
|
||||||
|
import { type Maybe, Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
import { SupplierStatus } from "../value-objects/supplier-status.value-object";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props de creación
|
||||||
|
*/
|
||||||
|
export interface ISupplierCreateProps {
|
||||||
|
companyId: UniqueID;
|
||||||
|
status?: SupplierStatus;
|
||||||
|
reference: Maybe<Name>;
|
||||||
|
|
||||||
|
isCompany: boolean;
|
||||||
|
name: Name;
|
||||||
|
tradeName: Maybe<Name>;
|
||||||
|
tin: Maybe<TINNumber>;
|
||||||
|
|
||||||
|
address: PostalAddressProps;
|
||||||
|
|
||||||
|
emailPrimary: Maybe<EmailAddress>;
|
||||||
|
emailSecondary: Maybe<EmailAddress>;
|
||||||
|
|
||||||
|
phonePrimary: Maybe<PhoneNumber>;
|
||||||
|
phoneSecondary: Maybe<PhoneNumber>;
|
||||||
|
|
||||||
|
mobilePrimary: Maybe<PhoneNumber>;
|
||||||
|
mobileSecondary: Maybe<PhoneNumber>;
|
||||||
|
|
||||||
|
fax: Maybe<PhoneNumber>;
|
||||||
|
website: Maybe<URLAddress>;
|
||||||
|
|
||||||
|
languageCode: LanguageCode;
|
||||||
|
currencyCode: CurrencyCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SupplierPatchProps = Partial<
|
||||||
|
Omit<ISupplierCreateProps, "companyId" | "address" | "status">
|
||||||
|
> & {
|
||||||
|
address?: PostalAddressPatchProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Supplier
|
||||||
|
export interface ISupplier {
|
||||||
|
// comportamiento
|
||||||
|
update(partialSupplier: SupplierPatchProps): Result<Supplier, Error>;
|
||||||
|
|
||||||
|
// propiedades (getters)
|
||||||
|
readonly isIndividual: boolean;
|
||||||
|
readonly isCompany: boolean;
|
||||||
|
readonly isActive: boolean;
|
||||||
|
|
||||||
|
readonly companyId: UniqueID;
|
||||||
|
|
||||||
|
readonly reference: Maybe<Name>;
|
||||||
|
readonly name: Name;
|
||||||
|
readonly tradeName: Maybe<Name>;
|
||||||
|
readonly tin: Maybe<TINNumber>;
|
||||||
|
|
||||||
|
readonly address: PostalAddress;
|
||||||
|
|
||||||
|
readonly emailPrimary: Maybe<EmailAddress>;
|
||||||
|
readonly emailSecondary: Maybe<EmailAddress>;
|
||||||
|
|
||||||
|
readonly phonePrimary: Maybe<PhoneNumber>;
|
||||||
|
readonly phoneSecondary: Maybe<PhoneNumber>;
|
||||||
|
readonly mobilePrimary: Maybe<PhoneNumber>;
|
||||||
|
readonly mobileSecondary: Maybe<PhoneNumber>;
|
||||||
|
|
||||||
|
readonly fax: Maybe<PhoneNumber>;
|
||||||
|
readonly website: Maybe<URLAddress>;
|
||||||
|
|
||||||
|
readonly languageCode: LanguageCode;
|
||||||
|
readonly currencyCode: CurrencyCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
type SupplierInternalProps = Omit<ISupplierCreateProps, "address"> & {
|
||||||
|
readonly address: PostalAddress;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aggregate Root: Supplier
|
||||||
|
*/
|
||||||
|
export class Supplier extends AggregateRoot<SupplierInternalProps> implements ISupplier {
|
||||||
|
static create(props: ISupplierCreateProps, id?: UniqueID): Result<Supplier, Error> {
|
||||||
|
const validationResult = Supplier.validateCreateProps(props);
|
||||||
|
|
||||||
|
if (validationResult.isFailure) {
|
||||||
|
return Result.fail(validationResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { address, ...internalProps } = props;
|
||||||
|
|
||||||
|
const postalAddressResult = PostalAddress.create(address);
|
||||||
|
|
||||||
|
if (postalAddressResult.isFailure) {
|
||||||
|
return Result.fail(postalAddressResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const contact = new Supplier(
|
||||||
|
{
|
||||||
|
...internalProps,
|
||||||
|
address: postalAddressResult.data,
|
||||||
|
},
|
||||||
|
id
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reglas de negocio / validaciones
|
||||||
|
// ...
|
||||||
|
// ...
|
||||||
|
|
||||||
|
// Disparar eventos de dominio
|
||||||
|
// ...
|
||||||
|
// ...
|
||||||
|
|
||||||
|
return Result.ok(contact);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static validateCreateProps(props: ISupplierCreateProps): Result<void, Error> {
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rehidratación desde persistencia
|
||||||
|
static rehydrate(props: SupplierInternalProps, id: UniqueID): Supplier {
|
||||||
|
return new Supplier(props, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// BEHAVIOUR
|
||||||
|
// -------------------------
|
||||||
|
|
||||||
|
public update(partialSupplier: SupplierPatchProps): Result<Supplier, Error> {
|
||||||
|
const { address: partialAddress, ...rest } = partialSupplier;
|
||||||
|
|
||||||
|
Object.assign(this.props, rest);
|
||||||
|
|
||||||
|
if (partialAddress) {
|
||||||
|
const addressResult = this.address.update(partialAddress);
|
||||||
|
|
||||||
|
if (addressResult.isFailure) {
|
||||||
|
return Result.fail(addressResult.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
public activate(): Result<Supplier, Error> {
|
||||||
|
this.props.status = SupplierStatus.active();
|
||||||
|
return Result.ok(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public deactivate(): Result<Supplier, Error> {
|
||||||
|
this.props.status = SupplierStatus.inactive();
|
||||||
|
return Result.ok(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// GETTERS
|
||||||
|
// -------------------------
|
||||||
|
|
||||||
|
public get isIndividual(): boolean {
|
||||||
|
return !this.props.isCompany;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isCompany(): boolean {
|
||||||
|
return this.props.isCompany;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isActive(): boolean {
|
||||||
|
return this.props.status.isActive();
|
||||||
|
}
|
||||||
|
|
||||||
|
public get companyId(): UniqueID {
|
||||||
|
return this.props.companyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get reference(): Maybe<Name> {
|
||||||
|
return this.props.reference;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get name(): Name {
|
||||||
|
return this.props.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get tradeName(): Maybe<Name> {
|
||||||
|
return this.props.tradeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get tin(): Maybe<TINNumber> {
|
||||||
|
return this.props.tin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get address(): PostalAddress {
|
||||||
|
return this.props.address;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get emailPrimary(): Maybe<EmailAddress> {
|
||||||
|
return this.props.emailPrimary;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get emailSecondary(): Maybe<EmailAddress> {
|
||||||
|
return this.props.emailSecondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get phonePrimary(): Maybe<PhoneNumber> {
|
||||||
|
return this.props.phonePrimary;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get phoneSecondary(): Maybe<PhoneNumber> {
|
||||||
|
return this.props.phoneSecondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get mobilePrimary(): Maybe<PhoneNumber> {
|
||||||
|
return this.props.mobilePrimary;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get mobileSecondary(): Maybe<PhoneNumber> {
|
||||||
|
return this.props.mobileSecondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get fax(): Maybe<PhoneNumber> {
|
||||||
|
return this.props.fax;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get website(): Maybe<URLAddress> {
|
||||||
|
return this.props.website;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get languageCode(): LanguageCode {
|
||||||
|
return this.props.languageCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get currencyCode(): CurrencyCode {
|
||||||
|
return this.props.currencyCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
1
modules/supplier/src/api/domain/collections/index.ts
Normal file
1
modules/supplier/src/api/domain/collections/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './supplier-taxes.collection';
|
||||||
1
modules/supplier/src/api/domain/errors/index.ts
Normal file
1
modules/supplier/src/api/domain/errors/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./supplier-domain.errors";
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
import { DomainError } from "@repo/rdx-ddd";
|
||||||
|
|
||||||
|
export class SupplierNotFoundError extends DomainError {
|
||||||
|
public readonly code = "SUPPLIER_ID" as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isSupplierNotFoundError = (e: unknown): e is SupplierNotFoundError =>
|
||||||
|
e instanceof SupplierNotFoundError;
|
||||||
4
modules/supplier/src/api/domain/index.ts
Normal file
4
modules/supplier/src/api/domain/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export * from './aggregates';
|
||||||
|
export * from './collections';
|
||||||
|
export * from './errors';
|
||||||
|
export * from './value-objects';
|
||||||
1
modules/supplier/src/api/domain/value-objects/index.ts
Normal file
1
modules/supplier/src/api/domain/value-objects/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './supplier-status.value-object';
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
import { ValueObject } from "@repo/rdx-ddd";
|
||||||
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estados permitidos
|
||||||
|
*/
|
||||||
|
export enum SUPPLIER_STATUS {
|
||||||
|
ACTIVE = "ACTIVE",
|
||||||
|
INACTIVE = "INACTIVE",
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ISupplierStatusProps {
|
||||||
|
value: SUPPLIER_STATUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SupplierStatus extends ValueObject<ISupplierStatusProps> {
|
||||||
|
private constructor(props: ISupplierStatusProps) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static create(value: string): Result<SupplierStatus, Error> {
|
||||||
|
if (!Object.values(SUPPLIER_STATUS).includes(value as SUPPLIER_STATUS)) {
|
||||||
|
return Result.fail(new Error(`Invalid supplier status: ${value}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok(new SupplierStatus({ value: value as SUPPLIER_STATUS }));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static active(): SupplierStatus {
|
||||||
|
return new SupplierStatus({ value: SUPPLIER_STATUS.ACTIVE });
|
||||||
|
}
|
||||||
|
|
||||||
|
public static inactive(): SupplierStatus {
|
||||||
|
return new SupplierStatus({ value: SUPPLIER_STATUS.INACTIVE });
|
||||||
|
}
|
||||||
|
|
||||||
|
public isActive(): boolean {
|
||||||
|
return this.props.value === SUPPLIER_STATUS.ACTIVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isInactive(): boolean {
|
||||||
|
return this.props.value === SUPPLIER_STATUS.INACTIVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public toPrimitive(): string {
|
||||||
|
return this.props.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
getProps(): string {
|
||||||
|
return this.props.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
78
modules/supplier/src/api/index.ts
Normal file
78
modules/supplier/src/api/index.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import type { IModuleServer } from "@erp/core/api";
|
||||||
|
|
||||||
|
import type { ISupplierPublicServices } from "./application";
|
||||||
|
import { models, suppliersRouter } from "./infrastructure";
|
||||||
|
import { buildSupplierPublicServices, buildSuppliersDependencies } from "./infrastructure/di";
|
||||||
|
|
||||||
|
export * from "./infrastructure/persistence/sequelize";
|
||||||
|
|
||||||
|
export const suppliersAPIModule: IModuleServer = {
|
||||||
|
name: "suppliers",
|
||||||
|
version: "1.0.0",
|
||||||
|
dependencies: [],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fase de SETUP
|
||||||
|
* ----------------
|
||||||
|
* - Construye el dominio (una sola vez)
|
||||||
|
* - Define qué expone el módulo
|
||||||
|
* - NO conecta infraestructura
|
||||||
|
*/
|
||||||
|
async setup(params) {
|
||||||
|
const { env: ENV, app, database, baseRoutePath: API_BASE_PATH, logger } = params;
|
||||||
|
|
||||||
|
// 1) Dominio interno
|
||||||
|
const internal = buildSuppliersDependencies(params);
|
||||||
|
|
||||||
|
// 2) Servicios públicos (Application Services)
|
||||||
|
const suppliersServices: ISupplierPublicServices = buildSupplierPublicServices(
|
||||||
|
params,
|
||||||
|
internal
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info("🚀 Suppliers module dependencies registered", {
|
||||||
|
label: this.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Modelos Sequelize del módulo
|
||||||
|
models,
|
||||||
|
|
||||||
|
// Servicios expuestos a otros módulos
|
||||||
|
services: {
|
||||||
|
general: suppliersServices, // 'suppliers:general'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Implementación privada del módulo
|
||||||
|
internal,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fase de START
|
||||||
|
* -------------
|
||||||
|
* - Conecta el módulo al runtime
|
||||||
|
* - Puede usar servicios e internals ya construidos
|
||||||
|
* - NO construye dominio
|
||||||
|
*/
|
||||||
|
async start(params) {
|
||||||
|
const { app, baseRoutePath, logger, getInternal } = params;
|
||||||
|
|
||||||
|
// Registro de rutas HTTP
|
||||||
|
suppliersRouter(params);
|
||||||
|
|
||||||
|
logger.info("🚀 Suppliers module started", {
|
||||||
|
label: this.name,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Warmup opcional (si lo necesitas en el futuro)
|
||||||
|
* ----------------------------------------------
|
||||||
|
* warmup(params) {
|
||||||
|
* ...
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
};
|
||||||
|
|
||||||
|
export default suppliersAPIModule;
|
||||||
2
modules/supplier/src/api/infrastructure/di/index.ts
Normal file
2
modules/supplier/src/api/infrastructure/di/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./supplier-public-services";
|
||||||
|
export * from "./suppliers.di";
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
import type { ICatalogs } from "@erp/core/api";
|
||||||
|
|
||||||
|
import { SequelizeSupplierDomainMapper, SequelizeSupplierSummaryMapper } from "../persistence";
|
||||||
|
|
||||||
|
export interface ISupplierPersistenceMappers {
|
||||||
|
domainMapper: SequelizeSupplierDomainMapper;
|
||||||
|
summaryMapper: SequelizeSupplierSummaryMapper;
|
||||||
|
|
||||||
|
//createMapper: CreateSupplierInputMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const buildSupplierPersistenceMappers = (
|
||||||
|
catalogs: ICatalogs
|
||||||
|
): ISupplierPersistenceMappers => {
|
||||||
|
const { taxCatalog } = catalogs;
|
||||||
|
|
||||||
|
// Mappers para el repositorio
|
||||||
|
const domainMapper = new SequelizeSupplierDomainMapper({ taxCatalog });
|
||||||
|
const summaryMapper = new SequelizeSupplierSummaryMapper();
|
||||||
|
|
||||||
|
// Mappers el DTO a las props validadas (SupplierProps) y luego construir agregado
|
||||||
|
//const createMapper = new CreateSupplierInputMapper();
|
||||||
|
|
||||||
|
return {
|
||||||
|
domainMapper,
|
||||||
|
summaryMapper,
|
||||||
|
|
||||||
|
//createMapper,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,65 @@
|
|||||||
|
import { type SetupParams, buildCatalogs } from "@erp/core/api";
|
||||||
|
import type { TINNumber, UniqueID } from "@repo/rdx-ddd";
|
||||||
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
import {
|
||||||
|
type ISupplierPublicServices,
|
||||||
|
type ISupplierServicesContext,
|
||||||
|
buildSupplierCreator,
|
||||||
|
buildSupplierFinder,
|
||||||
|
} from "../../application";
|
||||||
|
import type { ISupplierCreateProps } from "../../domain";
|
||||||
|
|
||||||
|
import { buildSupplierPersistenceMappers } from "./supplier-persistence-mappers.di";
|
||||||
|
import { buildSupplierRepository } from "./supplier-repositories.di";
|
||||||
|
import type { SuppliersInternalDeps } from "./suppliers.di";
|
||||||
|
|
||||||
|
export function buildSupplierPublicServices(
|
||||||
|
params: SetupParams,
|
||||||
|
deps: SuppliersInternalDeps
|
||||||
|
): ISupplierPublicServices {
|
||||||
|
const { database } = params;
|
||||||
|
const catalogs = buildCatalogs();
|
||||||
|
|
||||||
|
// Infrastructure
|
||||||
|
const persistenceMappers = buildSupplierPersistenceMappers(catalogs);
|
||||||
|
const repository = buildSupplierRepository({ database, mappers: persistenceMappers });
|
||||||
|
|
||||||
|
const finder = buildSupplierFinder({ repository });
|
||||||
|
const creator = buildSupplierCreator({ repository });
|
||||||
|
|
||||||
|
return {
|
||||||
|
findSupplierByTIN: async (tin: TINNumber, context: ISupplierServicesContext) => {
|
||||||
|
const { companyId, transaction } = context;
|
||||||
|
|
||||||
|
const supplierResult = await finder.findSupplierByTIN(companyId, tin, transaction);
|
||||||
|
|
||||||
|
if (supplierResult.isFailure) {
|
||||||
|
return Result.fail(supplierResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok(supplierResult.data);
|
||||||
|
},
|
||||||
|
|
||||||
|
createSupplier: async (
|
||||||
|
id: UniqueID,
|
||||||
|
props: ISupplierCreateProps,
|
||||||
|
context: ISupplierServicesContext
|
||||||
|
) => {
|
||||||
|
const { companyId, transaction } = context;
|
||||||
|
|
||||||
|
const supplierResult = await creator.create({
|
||||||
|
companyId,
|
||||||
|
id,
|
||||||
|
props,
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (supplierResult.isFailure) {
|
||||||
|
return Result.fail(supplierResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok(supplierResult.data);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import type { Sequelize } from "sequelize";
|
||||||
|
|
||||||
|
import { SupplierRepository } from "../persistence";
|
||||||
|
|
||||||
|
import type { ISupplierPersistenceMappers } from "./supplier-persistence-mappers.di";
|
||||||
|
|
||||||
|
export const buildSupplierRepository = (params: {
|
||||||
|
database: Sequelize;
|
||||||
|
mappers: ISupplierPersistenceMappers;
|
||||||
|
}) => {
|
||||||
|
const { database, mappers } = params;
|
||||||
|
|
||||||
|
return new SupplierRepository(mappers.domainMapper, mappers.summaryMapper, database);
|
||||||
|
};
|
||||||
88
modules/supplier/src/api/infrastructure/di/suppliers.di.ts
Normal file
88
modules/supplier/src/api/infrastructure/di/suppliers.di.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { type ModuleParams, buildCatalogs, buildTransactionManager } from "@erp/core/api";
|
||||||
|
|
||||||
|
import {
|
||||||
|
type CreateSupplierUseCase,
|
||||||
|
type GetSupplierByIdUseCase,
|
||||||
|
type ListSuppliersUseCase,
|
||||||
|
buildCreateSupplierUseCase,
|
||||||
|
buildGetSupplierByIdUseCase,
|
||||||
|
buildListSuppliersUseCase,
|
||||||
|
buildSupplierCreator,
|
||||||
|
buildSupplierFinder,
|
||||||
|
buildSupplierInputMappers,
|
||||||
|
buildSupplierSnapshotBuilders,
|
||||||
|
buildSupplierUpdater,
|
||||||
|
} from "../../application";
|
||||||
|
|
||||||
|
import { buildSupplierPersistenceMappers } from "./supplier-persistence-mappers.di";
|
||||||
|
import { buildSupplierRepository } from "./supplier-repositories.di";
|
||||||
|
|
||||||
|
export type SuppliersInternalDeps = {
|
||||||
|
useCases: {
|
||||||
|
listSuppliers: () => ListSuppliersUseCase;
|
||||||
|
getSupplierById: () => GetSupplierByIdUseCase;
|
||||||
|
createSupplier: () => CreateSupplierUseCase;
|
||||||
|
//updateSupplier: () => UpdateSupplierUseCase;
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
deleteSupplier: () => DeleteSupplierUseCase;
|
||||||
|
changeStatusSupplier: () => ChangeStatusSupplierUseCase;*/
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function buildSuppliersDependencies(params: ModuleParams): SuppliersInternalDeps {
|
||||||
|
const { database } = params;
|
||||||
|
|
||||||
|
// Infrastructure
|
||||||
|
const transactionManager = buildTransactionManager(database);
|
||||||
|
const catalogs = buildCatalogs();
|
||||||
|
const persistenceMappers = buildSupplierPersistenceMappers(catalogs);
|
||||||
|
|
||||||
|
const repository = buildSupplierRepository({ database, mappers: persistenceMappers });
|
||||||
|
//const numberService = buildSupplierNumberGenerator();
|
||||||
|
|
||||||
|
// Application helpers
|
||||||
|
const inputMappers = buildSupplierInputMappers(catalogs);
|
||||||
|
const finder = buildSupplierFinder({ repository });
|
||||||
|
const creator = buildSupplierCreator({ repository });
|
||||||
|
const updater = buildSupplierUpdater({ repository });
|
||||||
|
|
||||||
|
const snapshotBuilders = buildSupplierSnapshotBuilders();
|
||||||
|
//const documentGeneratorPipeline = buildSupplierDocumentService(params);
|
||||||
|
|
||||||
|
// Internal use cases (factories)
|
||||||
|
return {
|
||||||
|
useCases: {
|
||||||
|
listSuppliers: () =>
|
||||||
|
buildListSuppliersUseCase({
|
||||||
|
finder,
|
||||||
|
summarySnapshotBuilder: snapshotBuilders.summary,
|
||||||
|
transactionManager,
|
||||||
|
}),
|
||||||
|
|
||||||
|
getSupplierById: () =>
|
||||||
|
buildGetSupplierByIdUseCase({
|
||||||
|
finder,
|
||||||
|
fullSnapshotBuilder: snapshotBuilders.full,
|
||||||
|
transactionManager,
|
||||||
|
}),
|
||||||
|
|
||||||
|
createSupplier: () =>
|
||||||
|
buildCreateSupplierUseCase({
|
||||||
|
creator,
|
||||||
|
dtoMapper: inputMappers.createInputMapper,
|
||||||
|
fullSnapshotBuilder: snapshotBuilders.full,
|
||||||
|
transactionManager,
|
||||||
|
}),
|
||||||
|
|
||||||
|
/*updateSupplier: () =>
|
||||||
|
buildUpdateSupplierUseCase({
|
||||||
|
updater,
|
||||||
|
dtoMapper: inputMappers.updateInputMapper,
|
||||||
|
fullSnapshotBuilder: snapshotBuilders.full,
|
||||||
|
transactionManager,
|
||||||
|
}),*/
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
import {
|
||||||
|
ExpressController,
|
||||||
|
forbidQueryFieldGuard,
|
||||||
|
requireAuthenticatedGuard,
|
||||||
|
requireCompanyContextGuard,
|
||||||
|
} from "@erp/core/api";
|
||||||
|
|
||||||
|
import type { CreateSupplierRequestDTO } from "../../../../common/dto";
|
||||||
|
import type { CreateSupplierUseCase } from "../../../application";
|
||||||
|
import { suppliersApiErrorMapper } from "../supplier-api-error-mapper";
|
||||||
|
|
||||||
|
export class CreateSupplierController extends ExpressController {
|
||||||
|
public constructor(private readonly useCase: CreateSupplierUseCase) {
|
||||||
|
super();
|
||||||
|
this.errorMapper = suppliersApiErrorMapper;
|
||||||
|
|
||||||
|
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||||
|
this.registerGuards(
|
||||||
|
requireAuthenticatedGuard(),
|
||||||
|
requireCompanyContextGuard(),
|
||||||
|
forbidQueryFieldGuard("companyId")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async executeImpl() {
|
||||||
|
const companyId = this.getTenantId();
|
||||||
|
if (!companyId) {
|
||||||
|
return this.forbiddenError("Tenant ID not found");
|
||||||
|
}
|
||||||
|
const dto = this.req.body as CreateSupplierRequestDTO;
|
||||||
|
|
||||||
|
const result = await this.useCase.execute({ dto, companyId });
|
||||||
|
|
||||||
|
return result.match(
|
||||||
|
(data) => this.created(data),
|
||||||
|
(err) => this.handleError(err)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
import {
|
||||||
|
ExpressController,
|
||||||
|
forbidQueryFieldGuard,
|
||||||
|
requireAuthenticatedGuard,
|
||||||
|
requireCompanyContextGuard,
|
||||||
|
} from "@erp/core/api";
|
||||||
|
|
||||||
|
import type { DeleteSupplierUseCase } from "../../../application";
|
||||||
|
import { suppliersApiErrorMapper } from "../supplier-api-error-mapper";
|
||||||
|
|
||||||
|
export class DeleteSupplierController extends ExpressController {
|
||||||
|
public constructor(private readonly useCase: DeleteSupplierUseCase) {
|
||||||
|
super();
|
||||||
|
this.errorMapper = suppliersApiErrorMapper;
|
||||||
|
|
||||||
|
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||||
|
this.registerGuards(
|
||||||
|
requireAuthenticatedGuard(),
|
||||||
|
requireCompanyContextGuard(),
|
||||||
|
forbidQueryFieldGuard("companyId")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeImpl(): Promise<any> {
|
||||||
|
const companyId = this.getTenantId();
|
||||||
|
if (!companyId) {
|
||||||
|
return this.forbiddenError("Tenant ID not found");
|
||||||
|
}
|
||||||
|
const { supplier_id } = this.req.params;
|
||||||
|
|
||||||
|
const result = await this.useCase.execute({ supplier_id, companyId });
|
||||||
|
|
||||||
|
return result.match(
|
||||||
|
(data) => this.ok(data),
|
||||||
|
(err) => this.handleError(err)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
import {
|
||||||
|
ExpressController,
|
||||||
|
forbidQueryFieldGuard,
|
||||||
|
requireAuthenticatedGuard,
|
||||||
|
requireCompanyContextGuard,
|
||||||
|
} from "@erp/core/api";
|
||||||
|
|
||||||
|
import type { GetSupplierUseCase } from "../../../application";
|
||||||
|
import { suppliersApiErrorMapper } from "../supplier-api-error-mapper";
|
||||||
|
|
||||||
|
export class GetSupplierController extends ExpressController {
|
||||||
|
public constructor(private readonly useCase: GetSupplierUseCase) {
|
||||||
|
super();
|
||||||
|
this.errorMapper = suppliersApiErrorMapper;
|
||||||
|
|
||||||
|
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||||
|
this.registerGuards(
|
||||||
|
requireAuthenticatedGuard(),
|
||||||
|
requireCompanyContextGuard(),
|
||||||
|
forbidQueryFieldGuard("companyId")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async executeImpl() {
|
||||||
|
const companyId = this.getTenantId();
|
||||||
|
if (!companyId) {
|
||||||
|
return this.forbiddenError("Tenant ID not found");
|
||||||
|
}
|
||||||
|
const { supplier_id } = this.req.params;
|
||||||
|
|
||||||
|
const result = await this.useCase.execute({ supplier_id, companyId });
|
||||||
|
|
||||||
|
return result.match(
|
||||||
|
(data) => this.ok(data),
|
||||||
|
(err) => this.handleError(err)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
export * from "./create-supplier.controller";
|
||||||
|
export * from "./delete-supplier.controller";
|
||||||
|
export * from "./get-supplier.controller";
|
||||||
|
export * from "./list-suppliers.controller";
|
||||||
|
export * from "./update-supplier.controller";
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
import {
|
||||||
|
ExpressController,
|
||||||
|
forbidQueryFieldGuard,
|
||||||
|
requireAuthenticatedGuard,
|
||||||
|
requireCompanyContextGuard,
|
||||||
|
} from "@erp/core/api";
|
||||||
|
import { Criteria } from "@repo/rdx-criteria/server";
|
||||||
|
|
||||||
|
import type { ListSuppliersUseCase } from "../../../application";
|
||||||
|
import { suppliersApiErrorMapper } from "../supplier-api-error-mapper";
|
||||||
|
|
||||||
|
export class ListSuppliersController extends ExpressController {
|
||||||
|
public constructor(private readonly listSuppliers: ListSuppliersUseCase) {
|
||||||
|
super();
|
||||||
|
this.errorMapper = suppliersApiErrorMapper;
|
||||||
|
|
||||||
|
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||||
|
this.registerGuards(
|
||||||
|
requireAuthenticatedGuard(),
|
||||||
|
requireCompanyContextGuard(),
|
||||||
|
forbidQueryFieldGuard("companyId")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCriteriaWithDefaultOrder() {
|
||||||
|
if (this.criteria.hasOrder()) {
|
||||||
|
return this.criteria;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { q: quicksearch, filters, pageSize, pageNumber } = this.criteria.toPrimitives();
|
||||||
|
return Criteria.fromPrimitives(filters, "name", "ASC", pageSize, pageNumber, quicksearch);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async executeImpl() {
|
||||||
|
const companyId = this.getTenantId();
|
||||||
|
if (!companyId) {
|
||||||
|
return this.forbiddenError("Tenant ID not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const criteria = this.getCriteriaWithDefaultOrder();
|
||||||
|
const result = await this.listSuppliers.execute({ criteria, companyId });
|
||||||
|
|
||||||
|
return result.match(
|
||||||
|
(data) =>
|
||||||
|
this.ok(data, {
|
||||||
|
"X-Total-Count": String(data.total_items),
|
||||||
|
"Pagination-Count": String(data.total_pages),
|
||||||
|
"Pagination-Page": String(data.page),
|
||||||
|
"Pagination-Limit": String(data.per_page),
|
||||||
|
}),
|
||||||
|
(err) => this.handleError(err)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
import {
|
||||||
|
ExpressController,
|
||||||
|
forbidQueryFieldGuard,
|
||||||
|
requireAuthenticatedGuard,
|
||||||
|
requireCompanyContextGuard,
|
||||||
|
} from "@erp/core/api";
|
||||||
|
|
||||||
|
import type { UpdateSupplierByIdRequestDTO } from "../../../../common/dto";
|
||||||
|
import type { UpdateSupplierUseCase } from "../../../application";
|
||||||
|
import { suppliersApiErrorMapper } from "../supplier-api-error-mapper";
|
||||||
|
|
||||||
|
export class UpdateSupplierController extends ExpressController {
|
||||||
|
public constructor(private readonly useCase: UpdateSupplierUseCase) {
|
||||||
|
super();
|
||||||
|
this.errorMapper = suppliersApiErrorMapper;
|
||||||
|
|
||||||
|
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||||
|
this.registerGuards(
|
||||||
|
requireAuthenticatedGuard(),
|
||||||
|
requireCompanyContextGuard(),
|
||||||
|
forbidQueryFieldGuard("companyId")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async executeImpl() {
|
||||||
|
const companyId = this.getTenantId();
|
||||||
|
if (!companyId) {
|
||||||
|
return this.forbiddenError("Tenant ID not found");
|
||||||
|
}
|
||||||
|
const { supplier_id } = this.req.params;
|
||||||
|
const dto = this.req.body as UpdateSupplierByIdRequestDTO;
|
||||||
|
|
||||||
|
const result = await this.useCase.execute({ supplier_id, companyId, dto });
|
||||||
|
|
||||||
|
return result.match(
|
||||||
|
(data) => this.ok(data),
|
||||||
|
(err) => this.handleError(err)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
modules/supplier/src/api/infrastructure/express/index.ts
Normal file
1
modules/supplier/src/api/infrastructure/express/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./suppliers.routes";
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
import { ApiErrorMapper, type ErrorToApiRule, NotFoundApiError } from "@erp/core/api";
|
||||||
|
|
||||||
|
import { type SupplierNotFoundError, isSupplierNotFoundError } from "../../domain";
|
||||||
|
|
||||||
|
// Crea una regla específica (prioridad alta para sobreescribir mensajes)
|
||||||
|
const supplierNotFoundRule: ErrorToApiRule = {
|
||||||
|
priority: 120,
|
||||||
|
matches: (e) => isSupplierNotFoundError(e),
|
||||||
|
build: (e) =>
|
||||||
|
new NotFoundApiError(
|
||||||
|
(e as SupplierNotFoundError).message || "Supplier with the provided id not exists."
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cómo aplicarla: crea una nueva instancia del mapper con la regla extra
|
||||||
|
export const suppliersApiErrorMapper: ApiErrorMapper =
|
||||||
|
ApiErrorMapper.default().register(supplierNotFoundRule);
|
||||||
@ -0,0 +1,112 @@
|
|||||||
|
import { mockUser, requireAuthenticated, requireCompanyContext } from "@erp/auth/api";
|
||||||
|
import { type RequestWithAuth, type StartParams, validateRequest } from "@erp/core/api";
|
||||||
|
import { type NextFunction, type Request, type Response, Router } from "express";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CreateSupplierRequestSchema,
|
||||||
|
GetSupplierByIdRequestSchema,
|
||||||
|
SupplierListRequestSchema,
|
||||||
|
UpdateSupplierByIdParamsRequestSchema,
|
||||||
|
UpdateSupplierByIdRequestSchema,
|
||||||
|
} from "../../../common";
|
||||||
|
import type { SuppliersInternalDeps } from "../di";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CreateSupplierController,
|
||||||
|
GetSupplierController,
|
||||||
|
ListSuppliersController,
|
||||||
|
UpdateSupplierController,
|
||||||
|
} from "./controllers";
|
||||||
|
|
||||||
|
export const suppliersRouter = (params: StartParams) => {
|
||||||
|
const { app, config, getInternal } = params;
|
||||||
|
|
||||||
|
// Recuperamos el dominio interno del módulo
|
||||||
|
const deps = getInternal<SuppliersInternalDeps>("suppliers");
|
||||||
|
|
||||||
|
const router: Router = Router({ mergeParams: true });
|
||||||
|
|
||||||
|
// ----------------------------------------------
|
||||||
|
|
||||||
|
// 🔐 Autenticación + Tenancy para TODO el router
|
||||||
|
if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "production") {
|
||||||
|
router.use(
|
||||||
|
(req: Request, res: Response, next: NextFunction) =>
|
||||||
|
mockUser(req as RequestWithAuth, res, next) // Debe ir antes de las rutas protegidas
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//router.use(/*authenticateJWT(),*/ enforceTenant() /*checkTabContext*/);
|
||||||
|
router.use([
|
||||||
|
(req: Request, res: Response, next: NextFunction) =>
|
||||||
|
requireAuthenticated()(req as RequestWithAuth, res, next), // Debe ir antes de las rutas protegidas
|
||||||
|
|
||||||
|
(req: Request, res: Response, next: NextFunction) =>
|
||||||
|
requireCompanyContext()(req as RequestWithAuth, res, next), // Debe ir antes de las rutas protegidas
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ----------------------------------------------
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
"/",
|
||||||
|
//checkTabContext,
|
||||||
|
|
||||||
|
validateRequest(SupplierListRequestSchema, "params"),
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const useCase = deps.useCases.listSuppliers();
|
||||||
|
const controller = new ListSuppliersController(useCase /*, deps.presenters.list */);
|
||||||
|
return controller.execute(req, res, next);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
"/:supplier_id",
|
||||||
|
//checkTabContext,
|
||||||
|
|
||||||
|
validateRequest(GetSupplierByIdRequestSchema, "params"),
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const useCase = deps.useCases.getSupplierById();
|
||||||
|
const controller = new GetSupplierController(useCase);
|
||||||
|
return controller.execute(req, res, next);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"/",
|
||||||
|
//checkTabContext,
|
||||||
|
|
||||||
|
validateRequest(CreateSupplierRequestSchema, "body"),
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const useCase = deps.useCases.createSupplier();
|
||||||
|
const controller = new CreateSupplierController(useCase);
|
||||||
|
return controller.execute(req, res, next);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.put(
|
||||||
|
"/:supplier_id",
|
||||||
|
//checkTabContext,
|
||||||
|
|
||||||
|
validateRequest(UpdateSupplierByIdParamsRequestSchema, "params"),
|
||||||
|
validateRequest(UpdateSupplierByIdRequestSchema, "body"),
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const useCase = deps.useCases.updateSupplier();
|
||||||
|
const controller = new UpdateSupplierController(useCase);
|
||||||
|
return controller.execute(req, res, next);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/*router.delete(
|
||||||
|
"/:supplier_id",
|
||||||
|
//checkTabContext,
|
||||||
|
|
||||||
|
validateRequest(DeleteSupplierByIdRequestSchema, "params"),
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const useCase = deps.build.delete();
|
||||||
|
const controller = new DeleteSupplierController(useCase);
|
||||||
|
return controller.execute(req, res, next);
|
||||||
|
}
|
||||||
|
);*/
|
||||||
|
|
||||||
|
app.use(`${config.server.apiBasePath}/suppliers`, router);
|
||||||
|
};
|
||||||
3
modules/supplier/src/api/infrastructure/index.ts
Normal file
3
modules/supplier/src/api/infrastructure/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from "./di";
|
||||||
|
export * from "./express";
|
||||||
|
export * from "./persistence";
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from './sequelize';
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
import supplierModelInit from "./models/sequelize-supplier.model";
|
||||||
|
|
||||||
|
export * from "./mappers";
|
||||||
|
export * from "./models";
|
||||||
|
export * from "./repositories";
|
||||||
|
|
||||||
|
// Array de inicializadores para que registerModels() lo use
|
||||||
|
export const models = [supplierModelInit];
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./sequelize-supplier.mapper";
|
||||||
@ -0,0 +1,265 @@
|
|||||||
|
import type { TaxCatalogProvider } from "@erp/core";
|
||||||
|
import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
|
||||||
|
import {
|
||||||
|
City,
|
||||||
|
Country,
|
||||||
|
CurrencyCode,
|
||||||
|
EmailAddress,
|
||||||
|
LanguageCode,
|
||||||
|
Name,
|
||||||
|
PhoneNumber,
|
||||||
|
PostalAddress,
|
||||||
|
PostalCode,
|
||||||
|
Province,
|
||||||
|
Street,
|
||||||
|
TINNumber,
|
||||||
|
URLAddress,
|
||||||
|
UniqueID,
|
||||||
|
ValidationErrorCollection,
|
||||||
|
type ValidationErrorDetail,
|
||||||
|
extractOrPushError,
|
||||||
|
maybeFromNullableResult,
|
||||||
|
maybeToNullable,
|
||||||
|
} from "@repo/rdx-ddd";
|
||||||
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
import { type ISupplierCreateProps, Supplier, SupplierStatus } from "../../../../../domain";
|
||||||
|
import type { SupplierCreationAttributes, SupplierModel } from "../../../sequelize";
|
||||||
|
|
||||||
|
export class SequelizeSupplierDomainMapper extends SequelizeDomainMapper<
|
||||||
|
SupplierModel,
|
||||||
|
SupplierCreationAttributes,
|
||||||
|
Supplier
|
||||||
|
> {
|
||||||
|
private readonly taxCatalog: TaxCatalogProvider;
|
||||||
|
|
||||||
|
constructor(params: { taxCatalog: TaxCatalogProvider }) {
|
||||||
|
super();
|
||||||
|
this.taxCatalog = params.taxCatalog;
|
||||||
|
}
|
||||||
|
|
||||||
|
public mapToDomain(source: SupplierModel, params?: MapperParamsType): Result<Supplier, Error> {
|
||||||
|
try {
|
||||||
|
const errors: ValidationErrorDetail[] = [];
|
||||||
|
|
||||||
|
const supplierId = extractOrPushError(UniqueID.create(source.id), "id", errors);
|
||||||
|
const companyId = extractOrPushError(
|
||||||
|
UniqueID.create(source.company_id),
|
||||||
|
"company_id",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const isCompany = source.is_company;
|
||||||
|
const status = extractOrPushError(SupplierStatus.create(source.status), "status", errors);
|
||||||
|
const reference = extractOrPushError(
|
||||||
|
maybeFromNullableResult(source.reference, (value) => Name.create(value)),
|
||||||
|
"reference",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const name = extractOrPushError(Name.create(source.name), "name", errors);
|
||||||
|
|
||||||
|
const tradeName = extractOrPushError(
|
||||||
|
maybeFromNullableResult(source.trade_name, (value) => Name.create(value)),
|
||||||
|
"trade_name",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const tinNumber = extractOrPushError(
|
||||||
|
maybeFromNullableResult(source.tin, (value) => TINNumber.create(value)),
|
||||||
|
"tin",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const street = extractOrPushError(
|
||||||
|
maybeFromNullableResult(source.street, (value) => Street.create(value)),
|
||||||
|
"street",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const street2 = extractOrPushError(
|
||||||
|
maybeFromNullableResult(source.street2, (value) => Street.create(value)),
|
||||||
|
"street2",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const city = extractOrPushError(
|
||||||
|
maybeFromNullableResult(source.city, (value) => City.create(value)),
|
||||||
|
"city",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const province = extractOrPushError(
|
||||||
|
maybeFromNullableResult(source.province, (value) => Province.create(value)),
|
||||||
|
"province",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const postalCode = extractOrPushError(
|
||||||
|
maybeFromNullableResult(source.postal_code, (value) => PostalCode.create(value)),
|
||||||
|
"postal_code",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const country = extractOrPushError(
|
||||||
|
maybeFromNullableResult(source.country, (value) => Country.create(value)),
|
||||||
|
"country",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const emailPrimaryAddress = extractOrPushError(
|
||||||
|
maybeFromNullableResult(source.email_primary, (value) => EmailAddress.create(value)),
|
||||||
|
"email_primary",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const emailSecondaryAddress = extractOrPushError(
|
||||||
|
maybeFromNullableResult(source.email_secondary, (value) => EmailAddress.create(value)),
|
||||||
|
"email_secondary",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const phonePrimaryNumber = extractOrPushError(
|
||||||
|
maybeFromNullableResult(source.phone_primary, (value) => PhoneNumber.create(value)),
|
||||||
|
"phone_primary",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const phoneSecondaryNumber = extractOrPushError(
|
||||||
|
maybeFromNullableResult(source.phone_secondary, (value) => PhoneNumber.create(value)),
|
||||||
|
"phone_secondary",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const mobilePrimaryNumber = extractOrPushError(
|
||||||
|
maybeFromNullableResult(source.mobile_primary, (value) => PhoneNumber.create(value)),
|
||||||
|
"mobile_primary",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const mobileSecondaryNumber = extractOrPushError(
|
||||||
|
maybeFromNullableResult(source.mobile_secondary, (value) => PhoneNumber.create(value)),
|
||||||
|
"mobile_secondary",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const faxNumber = extractOrPushError(
|
||||||
|
maybeFromNullableResult(source.fax, (value) => PhoneNumber.create(value)),
|
||||||
|
"fax",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const website = extractOrPushError(
|
||||||
|
maybeFromNullableResult(source.website, (value) => URLAddress.create(value)),
|
||||||
|
"website",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const languageCode = extractOrPushError(
|
||||||
|
LanguageCode.create(source.language_code),
|
||||||
|
"language_code",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const currencyCode = extractOrPushError(
|
||||||
|
CurrencyCode.create(source.currency_code),
|
||||||
|
"currency_code",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
// Now, create the PostalAddress VO
|
||||||
|
const postalAddressProps = {
|
||||||
|
street: street!,
|
||||||
|
street2: street2!,
|
||||||
|
city: city!,
|
||||||
|
postalCode: postalCode!,
|
||||||
|
province: province!,
|
||||||
|
country: country!,
|
||||||
|
};
|
||||||
|
|
||||||
|
const postalAddress = extractOrPushError(
|
||||||
|
PostalAddress.create(postalAddressProps),
|
||||||
|
"address",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
// Si hubo errores de mapeo, devolvemos colección de validación
|
||||||
|
if (errors.length > 0) {
|
||||||
|
return Result.fail(new ValidationErrorCollection("Supplier props mapping failed", errors));
|
||||||
|
}
|
||||||
|
|
||||||
|
const supplierProps: ISupplierCreateProps = {
|
||||||
|
companyId: companyId!,
|
||||||
|
status: status!,
|
||||||
|
reference: reference!,
|
||||||
|
|
||||||
|
isCompany: isCompany,
|
||||||
|
name: name!,
|
||||||
|
tradeName: tradeName!,
|
||||||
|
tin: tinNumber!,
|
||||||
|
|
||||||
|
address: postalAddress!,
|
||||||
|
|
||||||
|
emailPrimary: emailPrimaryAddress!,
|
||||||
|
emailSecondary: emailSecondaryAddress!,
|
||||||
|
phonePrimary: phonePrimaryNumber!,
|
||||||
|
phoneSecondary: phoneSecondaryNumber!,
|
||||||
|
mobilePrimary: mobilePrimaryNumber!,
|
||||||
|
mobileSecondary: mobileSecondaryNumber!,
|
||||||
|
fax: faxNumber!,
|
||||||
|
website: website!,
|
||||||
|
|
||||||
|
languageCode: languageCode!,
|
||||||
|
currencyCode: currencyCode!,
|
||||||
|
};
|
||||||
|
|
||||||
|
return Supplier.create(supplierProps, supplierId);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
return Result.fail(err as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public mapToPersistence(
|
||||||
|
source: Supplier,
|
||||||
|
params?: MapperParamsType
|
||||||
|
): Result<SupplierCreationAttributes, Error> {
|
||||||
|
const supplierValues: Partial<SupplierCreationAttributes> = {
|
||||||
|
id: source.id.toPrimitive(),
|
||||||
|
company_id: source.companyId.toPrimitive(),
|
||||||
|
|
||||||
|
reference: maybeToNullable(source.reference, (reference) => reference.toPrimitive()),
|
||||||
|
is_company: source.isCompany,
|
||||||
|
name: source.name.toPrimitive(),
|
||||||
|
trade_name: maybeToNullable(source.tradeName, (tradeName) => tradeName.toPrimitive()),
|
||||||
|
tin: maybeToNullable(source.tin, (tin) => tin.toPrimitive()),
|
||||||
|
|
||||||
|
email_primary: maybeToNullable(source.emailPrimary, (email) => email.toPrimitive()),
|
||||||
|
email_secondary: maybeToNullable(source.emailSecondary, (email) => email.toPrimitive()),
|
||||||
|
phone_primary: maybeToNullable(source.phonePrimary, (phone) => phone.toPrimitive()),
|
||||||
|
phone_secondary: maybeToNullable(source.phoneSecondary, (phone) => phone.toPrimitive()),
|
||||||
|
mobile_primary: maybeToNullable(source.mobilePrimary, (mobile) => mobile.toPrimitive()),
|
||||||
|
mobile_secondary: maybeToNullable(source.mobileSecondary, (mobile) => mobile.toPrimitive()),
|
||||||
|
fax: maybeToNullable(source.fax, (fax) => fax.toPrimitive()),
|
||||||
|
website: maybeToNullable(source.website, (website) => website.toPrimitive()),
|
||||||
|
|
||||||
|
status: source.isActive ? "active" : "inactive",
|
||||||
|
language_code: source.languageCode.toPrimitive(),
|
||||||
|
currency_code: source.currencyCode.toPrimitive(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (source.address) {
|
||||||
|
Object.assign(supplierValues, {
|
||||||
|
street: maybeToNullable(source.address.street, (street) => street.toPrimitive()),
|
||||||
|
street2: maybeToNullable(source.address.street2, (street2) => street2.toPrimitive()),
|
||||||
|
city: maybeToNullable(source.address.city, (city) => city.toPrimitive()),
|
||||||
|
province: maybeToNullable(source.address.province, (province) => province.toPrimitive()),
|
||||||
|
postal_code: maybeToNullable(source.address.postalCode, (postalCode) =>
|
||||||
|
postalCode.toPrimitive()
|
||||||
|
),
|
||||||
|
country: maybeToNullable(source.address.country, (country) => country.toPrimitive()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok<SupplierCreationAttributes>(supplierValues as SupplierCreationAttributes);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./domain";
|
||||||
|
export * from "./summary";
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./sequelize-supplier-summary.mapper";
|
||||||
@ -0,0 +1,209 @@
|
|||||||
|
import { type MapperParamsType, SequelizeQueryMapper } from "@erp/core/api";
|
||||||
|
import {
|
||||||
|
City,
|
||||||
|
Country,
|
||||||
|
CurrencyCode,
|
||||||
|
EmailAddress,
|
||||||
|
LanguageCode,
|
||||||
|
Name,
|
||||||
|
PhoneNumber,
|
||||||
|
PostalAddress,
|
||||||
|
PostalCode,
|
||||||
|
Province,
|
||||||
|
Street,
|
||||||
|
TINNumber,
|
||||||
|
URLAddress,
|
||||||
|
UniqueID,
|
||||||
|
ValidationErrorCollection,
|
||||||
|
type ValidationErrorDetail,
|
||||||
|
extractOrPushError,
|
||||||
|
maybeFromNullableResult,
|
||||||
|
} from "@repo/rdx-ddd";
|
||||||
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
import type { SupplierSummary } from "../../../../../application";
|
||||||
|
import { SupplierStatus } from "../../../../../domain";
|
||||||
|
import type { SupplierModel } from "../../models";
|
||||||
|
|
||||||
|
export class SequelizeSupplierSummaryMapper extends SequelizeQueryMapper<
|
||||||
|
SupplierModel,
|
||||||
|
SupplierSummary
|
||||||
|
> {
|
||||||
|
public mapToReadModel(
|
||||||
|
raw: SupplierModel,
|
||||||
|
params?: MapperParamsType
|
||||||
|
): Result<SupplierSummary, Error> {
|
||||||
|
const errors: ValidationErrorDetail[] = [];
|
||||||
|
|
||||||
|
// 1) Valores escalares (atributos generales)
|
||||||
|
const supplierId = extractOrPushError(UniqueID.create(raw.id), "id", errors);
|
||||||
|
const companyId = extractOrPushError(UniqueID.create(raw.company_id), "company_id", errors);
|
||||||
|
|
||||||
|
const isCompany = raw.is_company;
|
||||||
|
const status = extractOrPushError(SupplierStatus.create(raw.status), "status", errors);
|
||||||
|
const reference = extractOrPushError(
|
||||||
|
maybeFromNullableResult(raw.reference, (value) => Name.create(value)),
|
||||||
|
"reference",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const name = extractOrPushError(Name.create(raw.name), "name", errors);
|
||||||
|
|
||||||
|
const tradeName = extractOrPushError(
|
||||||
|
maybeFromNullableResult(raw.trade_name, (value) => Name.create(value)),
|
||||||
|
"trade_name",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const tinNumber = extractOrPushError(
|
||||||
|
maybeFromNullableResult(raw.tin, (value) => TINNumber.create(value)),
|
||||||
|
"tin",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const street = extractOrPushError(
|
||||||
|
maybeFromNullableResult(raw.street, (value) => Street.create(value)),
|
||||||
|
"street",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const street2 = extractOrPushError(
|
||||||
|
maybeFromNullableResult(raw.street2, (value) => Street.create(value)),
|
||||||
|
"street2",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const city = extractOrPushError(
|
||||||
|
maybeFromNullableResult(raw.city, (value) => City.create(value)),
|
||||||
|
"city",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const province = extractOrPushError(
|
||||||
|
maybeFromNullableResult(raw.province, (value) => Province.create(value)),
|
||||||
|
"province",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const postalCode = extractOrPushError(
|
||||||
|
maybeFromNullableResult(raw.postal_code, (value) => PostalCode.create(value)),
|
||||||
|
"postal_code",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const country = extractOrPushError(
|
||||||
|
maybeFromNullableResult(raw.country, (value) => Country.create(value)),
|
||||||
|
"country",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const emailPrimaryAddress = extractOrPushError(
|
||||||
|
maybeFromNullableResult(raw.email_primary, (value) => EmailAddress.create(value)),
|
||||||
|
"email_primary",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const emailSecondaryAddress = extractOrPushError(
|
||||||
|
maybeFromNullableResult(raw.email_secondary, (value) => EmailAddress.create(value)),
|
||||||
|
"email_secondary",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const phonePrimaryNumber = extractOrPushError(
|
||||||
|
maybeFromNullableResult(raw.phone_primary, (value) => PhoneNumber.create(value)),
|
||||||
|
"phone_primary",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const phoneSecondaryNumber = extractOrPushError(
|
||||||
|
maybeFromNullableResult(raw.phone_secondary, (value) => PhoneNumber.create(value)),
|
||||||
|
"phone_secondary",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const mobilePrimaryNumber = extractOrPushError(
|
||||||
|
maybeFromNullableResult(raw.mobile_primary, (value) => PhoneNumber.create(value)),
|
||||||
|
"mobile_primary",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const mobileSecondaryNumber = extractOrPushError(
|
||||||
|
maybeFromNullableResult(raw.mobile_secondary, (value) => PhoneNumber.create(value)),
|
||||||
|
"mobile_secondary",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const faxNumber = extractOrPushError(
|
||||||
|
maybeFromNullableResult(raw.fax, (value) => PhoneNumber.create(value)),
|
||||||
|
"fax",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const website = extractOrPushError(
|
||||||
|
maybeFromNullableResult(raw.website, (value) => URLAddress.create(value)),
|
||||||
|
"website",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const languageCode = extractOrPushError(
|
||||||
|
LanguageCode.create(raw.language_code),
|
||||||
|
"language_code",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const currencyCode = extractOrPushError(
|
||||||
|
CurrencyCode.create(raw.currency_code),
|
||||||
|
"currency_code",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
// Valores compuestos (objetos de valor / agregados)
|
||||||
|
const postalAddressProps = {
|
||||||
|
street: street!,
|
||||||
|
street2: street2!,
|
||||||
|
city: city!,
|
||||||
|
postalCode: postalCode!,
|
||||||
|
province: province!,
|
||||||
|
country: country!,
|
||||||
|
};
|
||||||
|
|
||||||
|
const postalAddress = extractOrPushError(
|
||||||
|
PostalAddress.create(postalAddressProps),
|
||||||
|
"address",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
// Si hubo errores de mapeo, devolvemos colección de validación
|
||||||
|
if (errors.length > 0) {
|
||||||
|
return Result.fail(
|
||||||
|
new ValidationErrorCollection("Supplier mapping failed [mapToDTO]", errors)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok<SupplierSummary>({
|
||||||
|
id: supplierId!,
|
||||||
|
companyId: companyId!,
|
||||||
|
isActive: status!.isActive(),
|
||||||
|
reference: reference!,
|
||||||
|
|
||||||
|
isCompany: isCompany,
|
||||||
|
name: name!,
|
||||||
|
tradeName: tradeName!,
|
||||||
|
tin: tinNumber!,
|
||||||
|
|
||||||
|
address: postalAddress!,
|
||||||
|
|
||||||
|
emailPrimary: emailPrimaryAddress!,
|
||||||
|
emailSecondary: emailSecondaryAddress!,
|
||||||
|
phonePrimary: phonePrimaryNumber!,
|
||||||
|
phoneSecondary: phoneSecondaryNumber!,
|
||||||
|
mobilePrimary: mobilePrimaryNumber!,
|
||||||
|
mobileSecondary: mobileSecondaryNumber!,
|
||||||
|
fax: faxNumber!,
|
||||||
|
website: website!,
|
||||||
|
|
||||||
|
languageCode: languageCode!,
|
||||||
|
currencyCode: currencyCode!,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./sequelize-supplier.model";
|
||||||
@ -0,0 +1,236 @@
|
|||||||
|
import {
|
||||||
|
type CreationOptional,
|
||||||
|
DataTypes,
|
||||||
|
type InferAttributes,
|
||||||
|
type InferCreationAttributes,
|
||||||
|
Model,
|
||||||
|
type Sequelize,
|
||||||
|
} from "sequelize";
|
||||||
|
|
||||||
|
export type SupplierCreationAttributes = InferCreationAttributes<SupplierModel, {}> & {};
|
||||||
|
|
||||||
|
export class SupplierModel extends Model<
|
||||||
|
InferAttributes<SupplierModel>,
|
||||||
|
InferCreationAttributes<SupplierModel>
|
||||||
|
> {
|
||||||
|
// To avoid table creation
|
||||||
|
/*static async sync(): Promise<any> {
|
||||||
|
return Promise.resolve();
|
||||||
|
}*/
|
||||||
|
|
||||||
|
declare id: string;
|
||||||
|
declare company_id: string;
|
||||||
|
declare reference: CreationOptional<string | null>;
|
||||||
|
|
||||||
|
declare is_company: boolean;
|
||||||
|
declare name: string;
|
||||||
|
declare trade_name: CreationOptional<string | null>;
|
||||||
|
declare tin: CreationOptional<string | null>;
|
||||||
|
|
||||||
|
declare street: CreationOptional<string | null>;
|
||||||
|
declare street2: CreationOptional<string | null>;
|
||||||
|
declare city: CreationOptional<string | null>;
|
||||||
|
declare province: CreationOptional<string | null>;
|
||||||
|
declare postal_code: CreationOptional<string | null>;
|
||||||
|
declare country: CreationOptional<string | null>;
|
||||||
|
|
||||||
|
// Correos electrónicos
|
||||||
|
declare email_primary: CreationOptional<string | null>;
|
||||||
|
declare email_secondary: CreationOptional<string | null>;
|
||||||
|
|
||||||
|
// Teléfonos fijos
|
||||||
|
declare phone_primary: CreationOptional<string | null>;
|
||||||
|
declare phone_secondary: CreationOptional<string | null>;
|
||||||
|
|
||||||
|
// Móviles
|
||||||
|
declare mobile_primary: CreationOptional<string | null>;
|
||||||
|
declare mobile_secondary: CreationOptional<string | null>;
|
||||||
|
|
||||||
|
declare fax: CreationOptional<string | null>;
|
||||||
|
declare website: CreationOptional<string | null>;
|
||||||
|
|
||||||
|
declare status: string;
|
||||||
|
declare language_code: CreationOptional<string>;
|
||||||
|
declare currency_code: CreationOptional<string>;
|
||||||
|
|
||||||
|
static associate(_database: Sequelize) {}
|
||||||
|
|
||||||
|
static hooks(_database: Sequelize) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (database: Sequelize) => {
|
||||||
|
SupplierModel.init(
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
primaryKey: true,
|
||||||
|
},
|
||||||
|
company_id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
reference: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null,
|
||||||
|
},
|
||||||
|
is_company: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
trade_name: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null,
|
||||||
|
},
|
||||||
|
tin: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null,
|
||||||
|
},
|
||||||
|
|
||||||
|
street: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null,
|
||||||
|
},
|
||||||
|
street2: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null,
|
||||||
|
},
|
||||||
|
city: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null,
|
||||||
|
},
|
||||||
|
province: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null,
|
||||||
|
},
|
||||||
|
postal_code: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null,
|
||||||
|
},
|
||||||
|
country: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null,
|
||||||
|
},
|
||||||
|
|
||||||
|
email_primary: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null,
|
||||||
|
validate: {
|
||||||
|
isEmail: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
email_secondary: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null,
|
||||||
|
validate: {
|
||||||
|
isEmail: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
phone_primary: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null,
|
||||||
|
},
|
||||||
|
phone_secondary: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null,
|
||||||
|
},
|
||||||
|
|
||||||
|
mobile_primary: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null,
|
||||||
|
},
|
||||||
|
mobile_secondary: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null,
|
||||||
|
},
|
||||||
|
|
||||||
|
fax: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null,
|
||||||
|
},
|
||||||
|
website: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null,
|
||||||
|
validate: {
|
||||||
|
isUrl: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
status: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: "active",
|
||||||
|
},
|
||||||
|
|
||||||
|
language_code: {
|
||||||
|
type: DataTypes.STRING(2),
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: "es",
|
||||||
|
},
|
||||||
|
|
||||||
|
currency_code: {
|
||||||
|
type: new DataTypes.STRING(3),
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: "EUR",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sequelize: database,
|
||||||
|
modelName: "SupplierModel",
|
||||||
|
tableName: "suppliers",
|
||||||
|
|
||||||
|
underscored: true,
|
||||||
|
paranoid: true, // softs deletes
|
||||||
|
timestamps: true,
|
||||||
|
|
||||||
|
createdAt: "created_at",
|
||||||
|
updatedAt: "updated_at",
|
||||||
|
deletedAt: "deleted_at",
|
||||||
|
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
name: "idx_suppliers_company_name",
|
||||||
|
fields: ["company_id", "deleted_at", "name"],
|
||||||
|
},
|
||||||
|
{ name: "idx_name", fields: ["name"] }, // <- para ordenación
|
||||||
|
{ name: "idx_tin", fields: ["tin"] }, // <- para servicios externos
|
||||||
|
{ name: "idx_company_idx", fields: ["id", "company_id"], unique: true }, // <- para consulta get
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "ft_supplier",
|
||||||
|
type: "FULLTEXT",
|
||||||
|
fields: ["name", "trade_name", "reference", "tin", "email_primary", "mobile_primary"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope
|
||||||
|
|
||||||
|
defaultScope: {},
|
||||||
|
|
||||||
|
scopes: {},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return SupplierModel;
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from './sequelize-supplier.repository';
|
||||||
@ -0,0 +1,334 @@
|
|||||||
|
import {
|
||||||
|
EntityNotFoundError,
|
||||||
|
InfrastructureRepositoryError,
|
||||||
|
SequelizeRepository,
|
||||||
|
translateSequelizeError,
|
||||||
|
} from "@erp/core/api";
|
||||||
|
import { type Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server";
|
||||||
|
import type { TINNumber, UniqueID } from "@repo/rdx-ddd";
|
||||||
|
import { type Collection, Result } from "@repo/rdx-utils";
|
||||||
|
import type { FindOptions, InferAttributes, OrderItem, Sequelize, Transaction } from "sequelize";
|
||||||
|
|
||||||
|
import type { ISupplierRepository, SupplierSummary } from "../../../../application";
|
||||||
|
import type { Supplier } from "../../../../domain";
|
||||||
|
import type { SequelizeSupplierDomainMapper, SequelizeSupplierSummaryMapper } from "../../mappers";
|
||||||
|
import { SupplierModel } from "../models";
|
||||||
|
|
||||||
|
export class SupplierRepository
|
||||||
|
extends SequelizeRepository<Supplier>
|
||||||
|
implements ISupplierRepository
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
private readonly domainMapper: SequelizeSupplierDomainMapper,
|
||||||
|
private readonly summaryMapper: SequelizeSupplierSummaryMapper,
|
||||||
|
database: Sequelize
|
||||||
|
) {
|
||||||
|
super({ database });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Crea un nuevo proveedor
|
||||||
|
*
|
||||||
|
* @param supplier - El proveedor nuevo a guardar.
|
||||||
|
* @param transaction - Transacción activa para la operación.
|
||||||
|
* @returns Result<void, Error>
|
||||||
|
*/
|
||||||
|
async create(supplier: Supplier, transaction?: Transaction): Promise<Result<void, Error>> {
|
||||||
|
try {
|
||||||
|
const dtoResult = this.domainMapper.mapToPersistence(supplier);
|
||||||
|
|
||||||
|
if (dtoResult.isFailure) {
|
||||||
|
return Result.fail(dtoResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dto = dtoResult.data;
|
||||||
|
|
||||||
|
await SupplierModel.create(dto, {
|
||||||
|
include: [{ all: true }],
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
return Result.ok();
|
||||||
|
} catch (err: unknown) {
|
||||||
|
return Result.fail(translateSequelizeError(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actualiza un proveedor existente.
|
||||||
|
*
|
||||||
|
* @param supplier - El proveedor a actualizar.
|
||||||
|
* @param transaction - Transacción activa para la operación.
|
||||||
|
* @returns Result<void, Error>
|
||||||
|
*/
|
||||||
|
async update(supplier: Supplier, transaction?: Transaction): Promise<Result<void, Error>> {
|
||||||
|
try {
|
||||||
|
const dtoResult = this.domainMapper.mapToPersistence(supplier);
|
||||||
|
|
||||||
|
const { id, ...updatePayload } = dtoResult.data;
|
||||||
|
const [affected] = await SupplierModel.update(updatePayload, {
|
||||||
|
where: { id /*, version */ },
|
||||||
|
//fields: Object.keys(updatePayload),
|
||||||
|
transaction,
|
||||||
|
individualHooks: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (affected === 0) {
|
||||||
|
return Result.fail(
|
||||||
|
new InfrastructureRepositoryError("Concurrency conflict or not found update supplier")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok();
|
||||||
|
} catch (err: unknown) {
|
||||||
|
return Result.fail(translateSequelizeError(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Comprueba si existe un Supplier con un `id` dentro de una `company`.
|
||||||
|
*
|
||||||
|
* @param companyId - Identificador UUID de la empresa a la que pertenece el proveedor.
|
||||||
|
* @param id - Identificador UUID del proveedor.
|
||||||
|
* @param transaction - Transacción activa para la operación.
|
||||||
|
* @returns Result<boolean, Error>
|
||||||
|
*/
|
||||||
|
async existsByIdInCompany(
|
||||||
|
companyId: UniqueID,
|
||||||
|
id: UniqueID,
|
||||||
|
transaction?: Transaction
|
||||||
|
): Promise<Result<boolean, Error>> {
|
||||||
|
try {
|
||||||
|
const count = await SupplierModel.count({
|
||||||
|
where: { id: id.toString(), company_id: companyId.toString() },
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
return Result.ok(Boolean(count > 0));
|
||||||
|
} catch (error: unknown) {
|
||||||
|
return Result.fail(translateSequelizeError(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recupera un proveedor por su ID y companyId.
|
||||||
|
*
|
||||||
|
* @param companyId - Identificador UUID de la empresa a la que pertenece el proveedor.
|
||||||
|
* @param id - Identificador UUID del proveedor.
|
||||||
|
* @param transaction - Transacción activa para la operación.
|
||||||
|
* @returns Result<Supplier, Error>
|
||||||
|
*/
|
||||||
|
async getByIdInCompany(
|
||||||
|
companyId: UniqueID,
|
||||||
|
id: UniqueID,
|
||||||
|
transaction?: Transaction,
|
||||||
|
options: FindOptions<InferAttributes<SupplierModel>> = {}
|
||||||
|
): Promise<Result<Supplier, Error>> {
|
||||||
|
try {
|
||||||
|
// Normalización defensiva de order/include
|
||||||
|
const normalizedOrder = Array.isArray(options.order)
|
||||||
|
? options.order
|
||||||
|
: options.order
|
||||||
|
? [options.order]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const normalizedInclude = Array.isArray(options.include)
|
||||||
|
? options.include
|
||||||
|
: options.include
|
||||||
|
? [options.include]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const mergedOptions: FindOptions<InferAttributes<SupplierModel>> = {
|
||||||
|
...options,
|
||||||
|
where: {
|
||||||
|
...(options.where ?? {}),
|
||||||
|
id: id.toString(),
|
||||||
|
company_id: companyId.toString(),
|
||||||
|
},
|
||||||
|
order: normalizedOrder,
|
||||||
|
include: normalizedInclude,
|
||||||
|
transaction,
|
||||||
|
};
|
||||||
|
|
||||||
|
const row = await SupplierModel.findOne(mergedOptions);
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
return Result.fail(new EntityNotFoundError("Supplier", "id", id.toString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
const supplier = this.domainMapper.mapToDomain(row);
|
||||||
|
return supplier;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
return Result.fail(translateSequelizeError(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recupera un proveedor por su ID y companyId.
|
||||||
|
*
|
||||||
|
* @param companyId - Identificador UUID de la empresa a la que pertenece el proveedor.
|
||||||
|
* @param tin - TIN del proveedor.
|
||||||
|
* @param transaction - Transacción activa para la operación.
|
||||||
|
* @returns Result<Supplier, Error>
|
||||||
|
*/
|
||||||
|
async getByTINInCompany(
|
||||||
|
companyId: UniqueID,
|
||||||
|
tin: TINNumber,
|
||||||
|
transaction?: Transaction,
|
||||||
|
options: FindOptions<InferAttributes<SupplierModel>> = {}
|
||||||
|
): Promise<Result<Supplier, Error>> {
|
||||||
|
try {
|
||||||
|
// Normalización defensiva de order/include
|
||||||
|
const normalizedOrder = Array.isArray(options.order)
|
||||||
|
? options.order
|
||||||
|
: options.order
|
||||||
|
? [options.order]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const normalizedInclude = Array.isArray(options.include)
|
||||||
|
? options.include
|
||||||
|
: options.include
|
||||||
|
? [options.include]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const mergedOptions: FindOptions<InferAttributes<SupplierModel>> = {
|
||||||
|
...options,
|
||||||
|
where: {
|
||||||
|
...(options.where ?? {}),
|
||||||
|
tin: tin.toString(),
|
||||||
|
company_id: companyId.toString(),
|
||||||
|
},
|
||||||
|
order: normalizedOrder,
|
||||||
|
include: normalizedInclude,
|
||||||
|
transaction,
|
||||||
|
};
|
||||||
|
|
||||||
|
const row = await SupplierModel.findOne(mergedOptions);
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
return Result.fail(new EntityNotFoundError("Supplier", "tin", tin.toString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
const supplier = this.domainMapper.mapToDomain(row);
|
||||||
|
return supplier;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
return Result.fail(translateSequelizeError(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recupera múltiples suppliers dentro de una empresa según un criterio dinámico (búsqueda, paginación, etc.).
|
||||||
|
*
|
||||||
|
* @param companyId - Identificador UUID de la empresa a la que pertenece el proveedor.
|
||||||
|
* @param criteria - Criterios de búsqueda.
|
||||||
|
* @param transaction - Transacción activa para la operación.
|
||||||
|
* @returns Result<Collection<SupplierListDTO>, Error>
|
||||||
|
*
|
||||||
|
* @see Criteria
|
||||||
|
*/
|
||||||
|
async findByCriteriaInCompany(
|
||||||
|
companyId: UniqueID,
|
||||||
|
criteria: Criteria,
|
||||||
|
transaction?: Transaction,
|
||||||
|
options: FindOptions<InferAttributes<SupplierModel>> = {}
|
||||||
|
): Promise<Result<Collection<SupplierSummary>, Error>> {
|
||||||
|
try {
|
||||||
|
const criteriaConverter = new CriteriaToSequelizeConverter();
|
||||||
|
const query = criteriaConverter.convert(criteria, {
|
||||||
|
searchableFields: [
|
||||||
|
"name",
|
||||||
|
"trade_name",
|
||||||
|
"reference",
|
||||||
|
"tin",
|
||||||
|
"email_primary",
|
||||||
|
"mobile_primary",
|
||||||
|
],
|
||||||
|
allowedFields: [
|
||||||
|
"name",
|
||||||
|
"trade_name",
|
||||||
|
"reference",
|
||||||
|
"tin",
|
||||||
|
"email_primary",
|
||||||
|
"mobile_primary",
|
||||||
|
],
|
||||||
|
enableFullText: true,
|
||||||
|
database: this.database,
|
||||||
|
strictMode: true, // fuerza error si ORDER BY no permitido
|
||||||
|
});
|
||||||
|
|
||||||
|
// Normalización defensiva de order/include
|
||||||
|
const normalizedOrder = Array.isArray(options.order)
|
||||||
|
? options.order
|
||||||
|
: options.order
|
||||||
|
? [options.order]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const normalizedInclude = Array.isArray(options.include)
|
||||||
|
? options.include
|
||||||
|
: options.include
|
||||||
|
? [options.include]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
query.where = {
|
||||||
|
...query.where,
|
||||||
|
company_id: companyId.toString(),
|
||||||
|
deleted_at: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
query.order = [...(query.order as OrderItem[]), ...normalizedOrder];
|
||||||
|
|
||||||
|
query.include = normalizedInclude;
|
||||||
|
|
||||||
|
// Reemplazar findAndCountAll por findAll + count (más control y mejor rendimiento)
|
||||||
|
/*
|
||||||
|
const { rows, count } = await SupplierModel.findAndCountAll({
|
||||||
|
...query,
|
||||||
|
transaction,
|
||||||
|
});*/
|
||||||
|
const [rows, count] = await Promise.all([
|
||||||
|
SupplierModel.findAll({
|
||||||
|
...query,
|
||||||
|
transaction,
|
||||||
|
}),
|
||||||
|
SupplierModel.count({
|
||||||
|
where: query.where,
|
||||||
|
distinct: true, // evita duplicados por LEFT JOIN
|
||||||
|
transaction,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return this.summaryMapper.mapToReadModelCollection(rows, count);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
return Result.fail(translateSequelizeError(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Elimina o marca como eliminado un proveedor.
|
||||||
|
*
|
||||||
|
* @param companyId - Identificador UUID de la empresa a la que pertenece el proveedor.
|
||||||
|
* @param id - UUID del proveedor a eliminar.
|
||||||
|
* @param transaction - Transacción activa para la operación.
|
||||||
|
* @returns Result<boolean, Error>
|
||||||
|
*/
|
||||||
|
async deleteByIdInCompany(
|
||||||
|
companyId: UniqueID,
|
||||||
|
id: UniqueID,
|
||||||
|
transaction: Transaction
|
||||||
|
): Promise<Result<boolean, Error>> {
|
||||||
|
try {
|
||||||
|
const deleted = await SupplierModel.destroy({
|
||||||
|
where: { id: id.toString(), company_id: companyId.toString() },
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (deleted === 0) {
|
||||||
|
return Result.fail(new EntityNotFoundError("Supplier", "id", id.toString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok(true);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
return Result.fail(translateSequelizeError(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
modules/supplier/src/common/dto/index.ts
Normal file
2
modules/supplier/src/common/dto/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./request";
|
||||||
|
export * from "./response";
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
export const CreateSupplierRequestSchema = z.object({
|
||||||
|
id: z.string().nonempty(),
|
||||||
|
|
||||||
|
reference: z.string().optional(),
|
||||||
|
is_company: z.string().toLowerCase().default("true"),
|
||||||
|
name: z.string(),
|
||||||
|
trade_name: z.string().optional(),
|
||||||
|
tin: z.string().optional(),
|
||||||
|
|
||||||
|
street: z.string().optional(),
|
||||||
|
street2: z.string().optional(),
|
||||||
|
city: z.string().optional(),
|
||||||
|
province: z.string().optional(),
|
||||||
|
postal_code: z.string().optional(),
|
||||||
|
country: z.string().toLowerCase().default("es").optional(),
|
||||||
|
|
||||||
|
email_primary: z.string().optional(),
|
||||||
|
email_secondary: z.string().optional(),
|
||||||
|
phone_primary: z.string().optional(),
|
||||||
|
phone_secondary: z.string().optional(),
|
||||||
|
mobile_primary: z.string().optional(),
|
||||||
|
mobile_secondary: z.string().optional(),
|
||||||
|
|
||||||
|
fax: z.string().optional(),
|
||||||
|
website: z.string().optional(),
|
||||||
|
|
||||||
|
language_code: z.string().toLowerCase().default("es"),
|
||||||
|
currency_code: z.string().toUpperCase().default("EUR"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CreateSupplierRequestDTO = z.infer<typeof CreateSupplierRequestSchema>;
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Este DTO es utilizado por el endpoint:
|
||||||
|
* `DELETE /suppliers/:id` (eliminar una factura por ID).
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const DeleteSupplierByIdRequestSchema = z.object({
|
||||||
|
supplier_id: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type DeleteSupplierByIdRequestDTO = z.infer<typeof DeleteSupplierByIdRequestSchema>;
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Este DTO es utilizado por el endpoint:
|
||||||
|
* `GET /suppliers/:supplier_id` (consultar una factura por ID).
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const GetSupplierByIdRequestSchema = z.object({
|
||||||
|
supplier_id: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type GetSupplierByIdRequestDTO = z.infer<typeof GetSupplierByIdRequestSchema>;
|
||||||
5
modules/supplier/src/common/dto/request/index.ts
Normal file
5
modules/supplier/src/common/dto/request/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export * from "./create-supplier.request.dto";
|
||||||
|
export * from "./delete-supplier-by-id.request.dto";
|
||||||
|
export * from "./get-supplier-by-id.request.dto";
|
||||||
|
export * from "./supplier-list.request.dto";
|
||||||
|
export * from "./update-supplier-by-id.request.dto";
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
import { CriteriaSchema } from "@erp/core";
|
||||||
|
import type { z } from "zod/v4";
|
||||||
|
|
||||||
|
export const SupplierListRequestSchema = CriteriaSchema;
|
||||||
|
export type SupplierListRequestDTO = z.infer<typeof SupplierListRequestSchema>;
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
export const UpdateSupplierByIdParamsRequestSchema = z.object({
|
||||||
|
supplier_id: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateSupplierByIdRequestSchema = z.object({
|
||||||
|
reference: z.string().optional(),
|
||||||
|
|
||||||
|
is_company: z.string().optional(),
|
||||||
|
name: z.string().optional(),
|
||||||
|
trade_name: z.string().optional(),
|
||||||
|
tin: z.string().optional(),
|
||||||
|
|
||||||
|
street: z.string().optional(),
|
||||||
|
street2: z.string().optional(),
|
||||||
|
city: z.string().optional(),
|
||||||
|
province: z.string().optional(),
|
||||||
|
postal_code: z.string().optional(),
|
||||||
|
country: z.string().optional(),
|
||||||
|
|
||||||
|
email_primary: z.string().optional(),
|
||||||
|
email_secondary: z.string().optional(),
|
||||||
|
phone_primary: z.string().optional(),
|
||||||
|
phone_secondary: z.string().optional(),
|
||||||
|
mobile_primary: z.string().optional(),
|
||||||
|
mobile_secondary: z.string().optional(),
|
||||||
|
|
||||||
|
fax: z.string().optional(),
|
||||||
|
website: z.string().optional(),
|
||||||
|
|
||||||
|
language_code: z.string().optional(),
|
||||||
|
currency_code: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type UpdateSupplierByIdRequestDTO = Partial<z.infer<typeof UpdateSupplierByIdRequestSchema>>;
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import {
|
||||||
|
type GetSupplierByIdResponseDTO,
|
||||||
|
GetSupplierByIdResponseSchema,
|
||||||
|
} from "./get-supplier-by-id.response.dto";
|
||||||
|
|
||||||
|
export const CreateSupplierResponseSchema = GetSupplierByIdResponseSchema;
|
||||||
|
export type SupplierCreationResponseDTO = GetSupplierByIdResponseDTO;
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
import { MetadataSchema } from "@erp/core";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
export const GetSupplierByIdResponseSchema = z.object({
|
||||||
|
id: z.uuid(),
|
||||||
|
company_id: z.uuid(),
|
||||||
|
reference: z.string(),
|
||||||
|
|
||||||
|
is_company: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
trade_name: z.string(),
|
||||||
|
tin: z.string(),
|
||||||
|
|
||||||
|
street: z.string(),
|
||||||
|
street2: z.string(),
|
||||||
|
city: z.string(),
|
||||||
|
province: z.string(),
|
||||||
|
postal_code: z.string(),
|
||||||
|
country: z.string(),
|
||||||
|
|
||||||
|
email_primary: z.string(),
|
||||||
|
email_secondary: z.string(),
|
||||||
|
phone_primary: z.string(),
|
||||||
|
phone_secondary: z.string(),
|
||||||
|
mobile_primary: z.string(),
|
||||||
|
mobile_secondary: z.string(),
|
||||||
|
|
||||||
|
fax: z.string(),
|
||||||
|
website: z.string(),
|
||||||
|
|
||||||
|
legal_record: z.string(),
|
||||||
|
|
||||||
|
default_taxes: z.array(z.string()),
|
||||||
|
status: z.string(),
|
||||||
|
language_code: z.string(),
|
||||||
|
currency_code: z.string(),
|
||||||
|
|
||||||
|
metadata: MetadataSchema.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type GetSupplierByIdResponseDTO = z.infer<typeof GetSupplierByIdResponseSchema>;
|
||||||
4
modules/supplier/src/common/dto/response/index.ts
Normal file
4
modules/supplier/src/common/dto/response/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export * from "./create-supplier.result.dto";
|
||||||
|
export * from "./get-supplier-by-id.response.dto";
|
||||||
|
export * from "./list-suppliers.response.dto";
|
||||||
|
export * from "./update-supplier-by-id.response.dto";
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
import { MetadataSchema, createPaginatedListSchema } from "@erp/core";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
export const ListSuppliersResponseSchema = createPaginatedListSchema(
|
||||||
|
z.object({
|
||||||
|
id: z.uuid(),
|
||||||
|
company_id: z.uuid(),
|
||||||
|
status: z.string(),
|
||||||
|
reference: z.string(),
|
||||||
|
|
||||||
|
is_company: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
trade_name: z.string(),
|
||||||
|
tin: z.string(),
|
||||||
|
|
||||||
|
street: z.string(),
|
||||||
|
street2: z.string(),
|
||||||
|
city: z.string(),
|
||||||
|
province: z.string(),
|
||||||
|
postal_code: z.string(),
|
||||||
|
country: z.string(),
|
||||||
|
|
||||||
|
email_primary: z.string(),
|
||||||
|
email_secondary: z.string(),
|
||||||
|
phone_primary: z.string(),
|
||||||
|
phone_secondary: z.string(),
|
||||||
|
mobile_primary: z.string(),
|
||||||
|
mobile_secondary: z.string(),
|
||||||
|
fax: z.string(),
|
||||||
|
website: z.string(),
|
||||||
|
|
||||||
|
language_code: z.string(),
|
||||||
|
currency_code: z.string(),
|
||||||
|
|
||||||
|
metadata: MetadataSchema.optional(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export type ListSuppliersResponseDTO = z.infer<typeof ListSuppliersResponseSchema>;
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
import { MetadataSchema } from "@erp/core";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
export const UpdateSupplierByIdResponseSchema = z.object({
|
||||||
|
id: z.uuid(),
|
||||||
|
company_id: z.uuid(),
|
||||||
|
reference: z.string(),
|
||||||
|
|
||||||
|
is_company: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
trade_name: z.string(),
|
||||||
|
tin: z.string(),
|
||||||
|
|
||||||
|
street: z.string(),
|
||||||
|
street2: z.string(),
|
||||||
|
city: z.string(),
|
||||||
|
province: z.string(),
|
||||||
|
postal_code: z.string(),
|
||||||
|
country: z.string(),
|
||||||
|
|
||||||
|
email_primary: z.string(),
|
||||||
|
email_secondary: z.string(),
|
||||||
|
phone_primary: z.string(),
|
||||||
|
phone_secondary: z.string(),
|
||||||
|
mobile_primary: z.string(),
|
||||||
|
mobile_secondary: z.string(),
|
||||||
|
|
||||||
|
fax: z.string(),
|
||||||
|
website: z.string(),
|
||||||
|
|
||||||
|
language_code: z.string(),
|
||||||
|
currency_code: z.string(),
|
||||||
|
|
||||||
|
metadata: MetadataSchema.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type UpdateSupplierByIdResponseDTO = z.infer<typeof UpdateSupplierByIdResponseSchema>;
|
||||||
1
modules/supplier/src/common/index.ts
Normal file
1
modules/supplier/src/common/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./dto";
|
||||||
33
modules/supplier/tsconfig.json
Normal file
33
modules/supplier/tsconfig.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@erp/suppliers/*": ["./src/*"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
@ -62,6 +62,9 @@ importers:
|
|||||||
'@erp/factuges':
|
'@erp/factuges':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../modules/factuges
|
version: link:../../modules/factuges
|
||||||
|
'@erp/suppliers':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../modules/supplier
|
||||||
'@repo/rdx-logger':
|
'@repo/rdx-logger':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/rdx-logger
|
version: link:../../packages/rdx-logger
|
||||||
@ -691,6 +694,46 @@ importers:
|
|||||||
specifier: ^5.9.3
|
specifier: ^5.9.3
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
|
|
||||||
|
modules/supplier:
|
||||||
|
dependencies:
|
||||||
|
'@erp/auth':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../auth
|
||||||
|
'@erp/core':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../core
|
||||||
|
'@repo/i18next':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/i18n
|
||||||
|
'@repo/rdx-criteria':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/rdx-criteria
|
||||||
|
'@repo/rdx-ddd':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/rdx-ddd
|
||||||
|
'@repo/rdx-logger':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/rdx-logger
|
||||||
|
'@repo/rdx-utils':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/rdx-utils
|
||||||
|
express:
|
||||||
|
specifier: ^4.18.2
|
||||||
|
version: 4.21.2
|
||||||
|
sequelize:
|
||||||
|
specifier: ^6.37.5
|
||||||
|
version: 6.37.7(mysql2@3.15.3)(pg-hstore@2.3.4)
|
||||||
|
zod:
|
||||||
|
specifier: ^4.1.11
|
||||||
|
version: 4.1.12
|
||||||
|
devDependencies:
|
||||||
|
'@types/express':
|
||||||
|
specifier: ^4.17.21
|
||||||
|
version: 4.17.25
|
||||||
|
typescript:
|
||||||
|
specifier: ^5.9.3
|
||||||
|
version: 5.9.3
|
||||||
|
|
||||||
modules/supplier-invoices:
|
modules/supplier-invoices:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@erp/auth':
|
'@erp/auth':
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user