This commit is contained in:
David Arranz 2025-03-18 09:05:00 +01:00
parent d2e4c44474
commit 7def4f7dc5
101 changed files with 4533 additions and 41 deletions

View File

@ -4,6 +4,9 @@ import { IAccountRepository } from "../repositories";
import { AccountService } from "./account.service";
const mockAccountRepository: IAccountRepository = {
accountExists: jest.fn(),
findAll: jest.fn(),
findByEmail: jest.fn(),
findById: jest.fn(),
create: jest.fn(),
update: jest.fn(),

View File

@ -1,8 +1,10 @@
import { z } from "zod";
export const ListAccountsSchema = z.object({});
export const ListAccountsRequestSchema = z.object({});
export const IGetAcccountResponseDTOSchema = z.object({
export const IGetAccountRequestSchema = z.object({});
export const ICreateAccountRequestSchema = z.object({
id: z.string(),
is_freelancer: z.boolean(),
@ -30,7 +32,7 @@ export const IGetAcccountResponseDTOSchema = z.object({
logo: z.string(),
});
export const ICreateAcccountResponseDTOSchema = z.object({
export const IUpdateAccountRequestSchema = z.object({
id: z.string(),
is_freelancer: z.boolean(),
@ -58,30 +60,4 @@ export const ICreateAcccountResponseDTOSchema = z.object({
logo: z.string(),
});
export const IUpdateAcccountResponseDTOSchema = z.object({
id: z.string(),
is_freelancer: z.boolean(),
name: z.string(),
trade_name: z.string(),
tin: z.string(),
street: z.string(),
city: z.string(),
state: z.string(),
postal_code: z.string(),
country: z.string(),
email: z.string().email(), // Validación específica para email
phone: z.string(),
fax: z.string(),
website: z.string().url(), // Validación específica para URL
legal_record: z.string(),
default_tax: z.number(),
status: z.string(),
lang_code: z.string(),
currency_code: z.string(),
logo: z.string(),
});
export const IDeleteAccountRequestSchema = z.object({});

View File

@ -1,3 +1,3 @@
export * from "./accounts.request.dto";
export * from "./accounts.response.dto";
export * from "./accounts.validation.dto";
export * from "./accounts.schemas";

View File

@ -0,0 +1,109 @@
import { UniqueID } from "@common/domain";
import { Result } from "@common/helpers";
import { ITransactionManager } from "@common/infrastructure/database";
import { logger } from "@common/infrastructure/logger";
import { IInvoiceProps, IInvoiceService, Invoice, InvoiceStatus } from "@contexts/invoices/domain";
import { ICreateInvoiceRequestDTO } from "../presentation";
export class CreateInvoiceUseCase {
constructor(
private readonly invoiceService: IInvoiceService,
private readonly transactionManager: ITransactionManager
) {}
public execute(
invoiceID: UniqueID,
dto: ICreateInvoiceRequestDTO
): Promise<Result<Invoice, Error>> {
return this.transactionManager.complete(async (transaction) => {
try {
const validOrErrors = this.validateInvoiceData(dto);
if (validOrErrors.isFailure) {
return Result.fail(validOrErrors.error);
}
const data = validOrErrors.data;
// Update invoice with dto
return await this.invoiceService.createInvoice(invoiceID, data, transaction);
} catch (error: unknown) {
logger.error(error as Error);
return Result.fail(error as Error);
}
});
}
private validateInvoiceData(dto: ICreateInvoiceRequestDTO): Result<IInvoiceProps, Error> {
const errors: Error[] = [];
let invoice_status = InvoiceStatus.create(invoiceDTO.status).object;
if (invoice_status.isEmpty()) {
invoice_status = InvoiceStatus.createDraft();
}
let invoice_series = InvoiceSeries.create(invoiceDTO.invoice_series).object;
if (invoice_series.isEmpty()) {
invoice_series = InvoiceSeries.create(invoiceDTO.invoice_series).object;
}
let issue_date = InvoiceDate.create(invoiceDTO.issue_date).object;
if (issue_date.isEmpty()) {
issue_date = InvoiceDate.createCurrentDate().object;
}
let operation_date = InvoiceDate.create(invoiceDTO.operation_date).object;
if (operation_date.isEmpty()) {
operation_date = InvoiceDate.createCurrentDate().object;
}
let invoiceCurrency = Currency.createFromCode(invoiceDTO.currency).object;
if (invoiceCurrency.isEmpty()) {
invoiceCurrency = Currency.createDefaultCode().object;
}
let invoiceLanguage = Language.createFromCode(invoiceDTO.language_code).object;
if (invoiceLanguage.isEmpty()) {
invoiceLanguage = Language.createDefaultCode().object;
}
const items = new Collection<InvoiceItem>(
invoiceDTO.items?.map(
(item) =>
InvoiceSimpleItem.create({
description: Description.create(item.description).object,
quantity: Quantity.create(item.quantity).object,
unitPrice: UnitPrice.create({
amount: item.unit_price.amount,
currencyCode: item.unit_price.currency,
precision: item.unit_price.precision,
}).object,
}).object
)
);
if (!invoice_status.isDraft()) {
throw Error("Error al crear una factura que no es borrador");
}
return DraftInvoice.create(
{
invoiceSeries: invoice_series,
issueDate: issue_date,
operationDate: operation_date,
invoiceCurrency,
language: invoiceLanguage,
invoiceNumber: InvoiceNumber.create(undefined).object,
//notes: Note.create(invoiceDTO.notes).object,
//senderId: UniqueID.create(null).object,
recipient,
items,
},
invoiceId
);
}
}

View File

@ -0,0 +1,23 @@
import { UniqueID } from "@common/domain";
import { Result } from "@common/helpers";
import { ITransactionManager } from "@common/infrastructure/database";
import { logger } from "@common/infrastructure/logger";
import { IInvoiceService, Invoice } from "../domain";
export class DeleteInvoiceUseCase {
constructor(
private readonly invoiceService: IInvoiceService,
private readonly transactionManager: ITransactionManager
) {}
public execute(invoiceID: UniqueID): Promise<Result<Invoice, Error>> {
return this.transactionManager.complete(async (transaction) => {
try {
return await this.invoiceService.deleteInvoiceById(invoiceID, transaction);
} catch (error: unknown) {
logger.error(error as Error);
return Result.fail(error as Error);
}
});
}
}

View File

@ -0,0 +1,23 @@
import { UniqueID } from "@common/domain";
import { Result } from "@common/helpers";
import { ITransactionManager } from "@common/infrastructure/database";
import { logger } from "@common/infrastructure/logger";
import { IInvoiceService, Invoice } from "../domain";
export class GetInvoiceUseCase {
constructor(
private readonly invoiceService: IInvoiceService,
private readonly transactionManager: ITransactionManager
) {}
public execute(invoiceID: UniqueID): Promise<Result<Invoice, Error>> {
return this.transactionManager.complete(async (transaction) => {
try {
return await this.invoiceService.findInvoiceById(invoiceID, transaction);
} catch (error: unknown) {
logger.error(error as Error);
return Result.fail(error as Error);
}
});
}
}

View File

@ -0,0 +1,5 @@
export * from "./create-invoice.use-case";
export * from "./delete-invoice.use-case";
export * from "./get-invoice.use-case";
export * from "./list-invoices.use-case";
export * from "./update-invoice.use-case";

View File

@ -0,0 +1,22 @@
import { Collection, Result } from "@common/helpers";
import { ITransactionManager } from "@common/infrastructure/database";
import { logger } from "@common/infrastructure/logger";
import { Invoice } from "../domain";
export class ListInvoicesUseCase {
constructor(
private readonly invoiceService: IInvoiceService,
private readonly transactionManager: ITransactionManager
) {}
public execute(): Promise<Result<Collection<Invoice>, Error>> {
return this.transactionManager.complete(async (transaction) => {
try {
return await this.invoiceService.findInvoices(transaction);
} catch (error: unknown) {
logger.error(error as Error);
return Result.fail(error as Error);
}
});
}
}

View File

@ -0,0 +1,2 @@
export * from "./participantAddressFinder";
export * from "./participantFinder";

View File

@ -0,0 +1,70 @@
import {
ApplicationServiceError,
IApplicationServiceError,
} from "@/contexts/common/application/services/ApplicationServiceError";
import { IAdapter, RepositoryBuilder } from "@/contexts/common/domain";
import { Result, UniqueID } from "@shared/contexts";
import { NullOr } from "@shared/utilities";
import {
IInvoiceParticipantAddress,
IInvoiceParticipantAddressRepository,
} from "../../domain";
export const participantAddressFinder = async (
addressId: UniqueID,
adapter: IAdapter,
repository: RepositoryBuilder<IInvoiceParticipantAddressRepository>,
) => {
if (addressId.isNull()) {
return Result.fail<IApplicationServiceError>(
ApplicationServiceError.create(
ApplicationServiceError.INVALID_REQUEST_PARAM,
`Participant address ID required`,
),
);
}
const transaction = adapter.startTransaction();
let address: NullOr<IInvoiceParticipantAddress> = null;
try {
await transaction.complete(async (t) => {
address = await repository({ transaction: t }).getById(addressId);
});
if (address === null) {
return Result.fail<IApplicationServiceError>(
ApplicationServiceError.create(
ApplicationServiceError.NOT_FOUND_ERROR,
"",
{
id: addressId.toString(),
entity: "participant address",
},
),
);
}
return Result.ok<IInvoiceParticipantAddress>(address);
} catch (error: unknown) {
const _error = error as Error;
if (repository().isRepositoryError(_error)) {
return Result.fail<IApplicationServiceError>(
ApplicationServiceError.create(
ApplicationServiceError.REPOSITORY_ERROR,
_error.message,
_error,
),
);
}
return Result.fail<IApplicationServiceError>(
ApplicationServiceError.create(
ApplicationServiceError.UNEXCEPTED_ERROR,
_error.message,
_error,
),
);
}
};

View File

@ -0,0 +1,20 @@
import { IAdapter, RepositoryBuilder } from "@/contexts/common/domain";
import { UniqueID } from "@shared/contexts";
import { IInvoiceParticipantRepository } from "../../domain";
import { InvoiceParticipant } from "../../domain/InvoiceParticipant/InvoiceParticipant";
export const participantFinder = async (
participantId: UniqueID,
adapter: IAdapter,
repository: RepositoryBuilder<IInvoiceParticipantRepository>,
): Promise<InvoiceParticipant | undefined> => {
if (!participantId || (participantId && participantId.isNull())) {
return Promise.resolve(undefined);
}
const participant = await adapter
.startTransaction()
.complete((t) => repository({ transaction: t }).getById(participantId));
return Promise.resolve(participant ? participant : undefined);
};

View File

@ -0,0 +1,398 @@
import { UniqueID } from "@common/domain";
import { Result } from "@common/helpers";
import { ITransactionManager } from "@common/infrastructure/database";
import { logger } from "@common/infrastructure/logger";
import { IUpdateInvoiceRequestDTO } from "../presentation/dto";
export class CreateInvoiceUseCase {
constructor(
private readonly invoiceService: IInvoiceService,
private readonly transactionManager: ITransactionManager
) {}
public execute(
invoiceID: UniqueID,
dto: Partial<IUpdateInvoiceRequestDTO>
): Promise<Result<Invoice, Error>> {
return this.transactionManager.complete(async (transaction) => {
try {
const validOrErrors = this.validateInvoiceData(dto);
if (validOrErrors.isFailure) {
return Result.fail(validOrErrors.error);
}
const data = validOrErrors.data;
// Update invoice with dto
return await this.invoiceService.updateInvoiceById(invoiceID, data, transaction);
} catch (error: unknown) {
logger.error(error as Error);
return Result.fail(error as Error);
}
});
}
private validateInvoiceData(
dto: Partial<IUpdateInvoiceRequestDTO>
): Result<Partial<IInvoiceProps>, Error> {
const errors: Error[] = [];
const validatedData: Partial<IInvoiceProps> = {};
// Create invoice
let invoice_status = InvoiceStatus.create(invoiceDTO.status).object;
if (invoice_status.isEmpty()) {
invoice_status = InvoiceStatus.createDraft();
}
let invoice_series = InvoiceSeries.create(invoiceDTO.invoice_series).object;
if (invoice_series.isEmpty()) {
invoice_series = InvoiceSeries.create(invoiceDTO.invoice_series).object;
}
let issue_date = InvoiceDate.create(invoiceDTO.issue_date).object;
if (issue_date.isEmpty()) {
issue_date = InvoiceDate.createCurrentDate().object;
}
let operation_date = InvoiceDate.create(invoiceDTO.operation_date).object;
if (operation_date.isEmpty()) {
operation_date = InvoiceDate.createCurrentDate().object;
}
let invoiceCurrency = Currency.createFromCode(invoiceDTO.currency).object;
if (invoiceCurrency.isEmpty()) {
invoiceCurrency = Currency.createDefaultCode().object;
}
let invoiceLanguage = Language.createFromCode(invoiceDTO.language_code).object;
if (invoiceLanguage.isEmpty()) {
invoiceLanguage = Language.createDefaultCode().object;
}
const items = new Collection<InvoiceItem>(
invoiceDTO.items?.map(
(item) =>
InvoiceSimpleItem.create({
description: Description.create(item.description).object,
quantity: Quantity.create(item.quantity).object,
unitPrice: UnitPrice.create({
amount: item.unit_price.amount,
currencyCode: item.unit_price.currency,
precision: item.unit_price.precision,
}).object,
}).object
)
);
if (!invoice_status.isDraft()) {
throw Error("Error al crear una factura que no es borrador");
}
return DraftInvoice.create(
{
invoiceSeries: invoice_series,
issueDate: issue_date,
operationDate: operation_date,
invoiceCurrency,
language: invoiceLanguage,
invoiceNumber: InvoiceNumber.create(undefined).object,
//notes: Note.create(invoiceDTO.notes).object,
//senderId: UniqueID.create(null).object,
recipient,
items,
},
invoiceId
);
}
}
export type UpdateInvoiceResponseOrError =
| Result<never, IUseCaseError> // Misc errors (value objects)
| Result<Invoice, never>; // Success!
export class UpdateInvoiceUseCase2
implements
IUseCase<{ id: UniqueID; data: IUpdateInvoice_DTO }, Promise<UpdateInvoiceResponseOrError>>
{
private _context: IInvoicingContext;
private _adapter: ISequelizeAdapter;
private _repositoryManager: IRepositoryManager;
constructor(context: IInvoicingContext) {
this._context = context;
this._adapter = context.adapter;
this._repositoryManager = context.repositoryManager;
}
private getRepository<T>(name: string) {
return this._repositoryManager.getRepository<T>(name);
}
private handleValidationFailure(
validationError: Error,
message?: string
): Result<never, IUseCaseError> {
return Result.fail<IUseCaseError>(
UseCaseError.create(
UseCaseError.INVALID_INPUT_DATA,
message ? message : validationError.message,
validationError
)
);
}
async execute(request: {
id: UniqueID;
data: IUpdateInvoice_DTO;
}): Promise<UpdateInvoiceResponseOrError> {
const { id, data: invoiceDTO } = request;
// Validaciones
const invoiceDTOOrError = ensureUpdateInvoice_DTOIsValid(invoiceDTO);
if (invoiceDTOOrError.isFailure) {
return this.handleValidationFailure(invoiceDTOOrError.error);
}
const transaction = this._adapter.startTransaction();
const invoiceRepoBuilder = this.getRepository<IInvoiceRepository>("Invoice");
let invoice: Invoice | null = null;
try {
await transaction.complete(async (t) => {
invoice = await invoiceRepoBuilder({ transaction: t }).getById(id);
});
if (invoice === null) {
return Result.fail<IUseCaseError>(
UseCaseError.create(UseCaseError.NOT_FOUND_ERROR, `Invoice not found`, {
id: request.id.toString(),
entity: "invoice",
})
);
}
return Result.ok<Invoice>(invoice);
} catch (error: unknown) {
const _error = error as Error;
if (invoiceRepoBuilder().isRepositoryError(_error)) {
return this.handleRepositoryError(error as BaseError, invoiceRepoBuilder());
} else {
return this.handleUnexceptedError(error);
}
}
// Recipient validations
/*const recipientIdOrError = ensureParticipantIdIsValid(
invoiceDTO?.recipient?.id,
);
if (recipientIdOrError.isFailure) {
return this.handleValidationFailure(
recipientIdOrError.error,
"Recipient ID not valid",
);
}
const recipientId = recipientIdOrError.object;
const recipientBillingIdOrError = ensureParticipantAddressIdIsValid(
invoiceDTO?.recipient?.billing_address_id,
);
if (recipientBillingIdOrError.isFailure) {
return this.handleValidationFailure(
recipientBillingIdOrError.error,
"Recipient billing address ID not valid",
);
}
const recipientBillingId = recipientBillingIdOrError.object;
const recipientShippingIdOrError = ensureParticipantAddressIdIsValid(
invoiceDTO?.recipient?.shipping_address_id,
);
if (recipientShippingIdOrError.isFailure) {
return this.handleValidationFailure(
recipientShippingIdOrError.error,
"Recipient shipping address ID not valid",
);
}
const recipientShippingId = recipientShippingIdOrError.object;
const recipientContact = await this.findContact(
recipientId,
recipientBillingId,
recipientShippingId,
);
if (!recipientContact) {
return this.handleValidationFailure(
new Error(`Recipient with ID ${recipientId.toString()} does not exist`),
);
}
// Crear invoice
const invoiceOrError = await this.tryUpdateInvoiceInstance(
invoiceDTO,
invoiceIdOrError.object,
//senderId,
//senderBillingId,
//senderShippingId,
recipientContact,
);
if (invoiceOrError.isFailure) {
const { error: domainError } = invoiceOrError;
let errorCode = "";
let message = "";
switch (domainError.code) {
case Invoice.ERROR_CUSTOMER_WITHOUT_NAME:
errorCode = UseCaseError.INVALID_INPUT_DATA;
message =
"El cliente debe ser una compañía o tener nombre y apellidos.";
break;
default:
errorCode = UseCaseError.UNEXCEPTED_ERROR;
message = "";
break;
}
return Result.fail<IUseCaseError>(
UseCaseError.create(errorCode, message, domainError),
);
}
return this.saveInvoice(invoiceOrError.object);
*/
}
private async tryUpdateInvoiceInstance(invoiceDTO, invoiceId, recipient) {
// Create invoice
let invoice_status = InvoiceStatus.create(invoiceDTO.status).object;
if (invoice_status.isEmpty()) {
invoice_status = InvoiceStatus.createDraft();
}
let invoice_series = InvoiceSeries.create(invoiceDTO.invoice_series).object;
if (invoice_series.isEmpty()) {
invoice_series = InvoiceSeries.create(invoiceDTO.invoice_series).object;
}
let issue_date = InvoiceDate.create(invoiceDTO.issue_date).object;
if (issue_date.isEmpty()) {
issue_date = InvoiceDate.createCurrentDate().object;
}
let operation_date = InvoiceDate.create(invoiceDTO.operation_date).object;
if (operation_date.isEmpty()) {
operation_date = InvoiceDate.createCurrentDate().object;
}
let invoiceCurrency = Currency.createFromCode(invoiceDTO.currency).object;
if (invoiceCurrency.isEmpty()) {
invoiceCurrency = Currency.createDefaultCode().object;
}
let invoiceLanguage = Language.createFromCode(invoiceDTO.language_code).object;
if (invoiceLanguage.isEmpty()) {
invoiceLanguage = Language.createDefaultCode().object;
}
const items = new Collection<InvoiceItem>(
invoiceDTO.items?.map(
(item) =>
InvoiceSimpleItem.create({
description: Description.create(item.description).object,
quantity: Quantity.create(item.quantity).object,
unitPrice: UnitPrice.create({
amount: item.unit_price.amount,
currencyCode: item.unit_price.currency,
precision: item.unit_price.precision,
}).object,
}).object
)
);
if (!invoice_status.isDraft()) {
throw Error("Error al crear una factura que no es borrador");
}
return DraftInvoice.create(
{
invoiceSeries: invoice_series,
issueDate: issue_date,
operationDate: operation_date,
invoiceCurrency,
language: invoiceLanguage,
invoiceNumber: InvoiceNumber.create(undefined).object,
//notes: Note.create(invoiceDTO.notes).object,
//senderId: UniqueID.create(null).object,
recipient,
items,
},
invoiceId
);
}
private async findContact(
contactId: UniqueID,
billingAddressId: UniqueID,
shippingAddressId: UniqueID
) {
const contactRepoBuilder = this.getRepository<IContactRepository>("Contact");
const contact = await contactRepoBuilder().getById2(
contactId,
billingAddressId,
shippingAddressId
);
return contact;
}
private async saveInvoice(invoice: DraftInvoice) {
const transaction = this._adapter.startTransaction();
const invoiceRepoBuilder = this.getRepository<IInvoiceRepository>("Invoice");
try {
await transaction.complete(async (t) => {
const invoiceRepo = invoiceRepoBuilder({ transaction: t });
await invoiceRepo.save(invoice);
});
return Result.ok<DraftInvoice>(invoice);
} catch (error: unknown) {
const _error = error as Error;
if (invoiceRepoBuilder().isRepositoryError(_error)) {
return this.handleRepositoryError(error as BaseError, invoiceRepoBuilder());
} else {
return this.handleUnexceptedError(error);
}
}
}
private handleUnexceptedError(error): Result<never, IUseCaseError> {
return Result.fail<IUseCaseError>(
UseCaseError.create(UseCaseError.UNEXCEPTED_ERROR, error.message, error)
);
}
private handleRepositoryError(
error: BaseError,
repository: IInvoiceRepository
): Result<never, IUseCaseError> {
const { message, details } = repository.handleRepositoryError(error);
return Result.fail<IUseCaseError>(
UseCaseError.create(UseCaseError.REPOSITORY_ERROR, message, details)
);
}
}

View File

@ -0,0 +1,64 @@
import { IDomainError } from "@/contexts/common/domain";
import {
Entity,
GenericAddress,
IGenericAddressProps,
Name,
Result,
TINNumber,
UniqueID,
} from "@shared/contexts";
export interface IContactProps {
tin: TINNumber;
companyName: Name;
firstName: Name;
lastName: Name;
billingAddress: GenericAddress<IGenericAddressProps>;
shippingAddress: GenericAddress<IGenericAddressProps>;
}
export interface IContact {
id: UniqueID;
tin: TINNumber;
companyName: Name;
firstName: Name;
lastName: Name;
billingAddress: GenericAddress<IGenericAddressProps>;
shippingAddress: GenericAddress<IGenericAddressProps>;
}
export class Contact extends Entity<IContactProps> implements IContact {
public static create(
props: IContactProps,
id?: UniqueID,
): Result<Contact, IDomainError> {
const participant = new Contact(props, id);
return Result.ok<Contact>(participant);
}
get tin(): TINNumber {
return this.props.tin;
}
get companyName(): Name {
return this.props.companyName;
}
get firstName(): Name {
return this.props.firstName;
}
get lastName(): Name {
return this.props.lastName;
}
get billingAddress() {
return this.props.billingAddress;
}
get shippingAddress() {
return this.props.shippingAddress;
}
}

View File

@ -0,0 +1,16 @@
/* eslint-disable no-unused-vars */
import { IRepository } from "@/contexts/common/domain/repositories";
import { UniqueID } from "@shared/contexts";
import { Contact } from ".";
export interface IContactRepository extends IRepository<Contact> {
getById(id: UniqueID): Promise<Contact | null>;
getById2(
id: UniqueID,
billingAddressId: UniqueID,
shippingAddressId: UniqueID,
): Promise<Contact | null>;
exists(id: UniqueID): Promise<boolean>;
}

View File

@ -0,0 +1,2 @@
export * from "./Contact";
export * from "./IContactRepository.interface";

View File

@ -0,0 +1,22 @@
import {
GenericAddress,
IGenericAddress,
IGenericAddressProps,
Result,
UniqueID,
} from "@shared/contexts";
export type ContactAddressType = "billing" | "shipping";
export interface IContactAddressProps extends IGenericAddressProps {}
export interface IContactAddress extends IGenericAddress {}
export class ContactAddress
extends GenericAddress<IContactAddressProps>
implements IContactAddress
{
public static create(props: IContactAddressProps, id?: UniqueID) {
return Result.ok(new this(props, id));
}
}

View File

@ -0,0 +1 @@
export * from "./ContactAddress";

View File

@ -0,0 +1,64 @@
import {
Description,
Entity,
IEntityProps,
MoneyValue,
Quantity,
} from "@shared/contexts";
export interface IInvoiceBaseItemProps extends IEntityProps {
description: Description; // Descripción del artículo o servicio
quantity: Quantity; // Cantidad de unidades
unitPrice: MoneyValue; // Precio unitario en la moneda de la factura
//tax: Tax; // Tasa de impuesto en decimal (por ejemplo, 0.15 para 15%)
}
export interface IInvoiceBaseItem {
description: Description;
quantity: Quantity;
unitPrice: MoneyValue;
//unitMeasure: string;
//tax: Tax;
//dto: Percentage | Number(10, 4) ???;
/*calculateSubtotal: () => number;
calculateTaxAmount: () => number;
calculateDtoAmount: () => number;
calculateTotal: () => number;*/
}
export abstract class InvoiceBaseItem<P extends IEntityProps>
extends Entity<P>
implements IInvoiceBaseItem
{
// Método para calcular el total antes de impuestos
calculateSubtotal(): MoneyValue {
return this.unitPrice.multiply(this.quantity.toNumber());
}
// Método para calcular el monto del impuesto
calculateTaxAmount(): MoneyValue {
return MoneyValue.create({ amount: 0, precision: 4 }).object;
}
// Método para calcular el total incluyendo impuestos
calculateTotal(): MoneyValue {
return this.calculateSubtotal().add(this.calculateTaxAmount());
}
// Getters para acceder a los atributos privados
get description(): Description {
return this.props.description;
}
get quantity(): Quantity {
return this.props.quantity;
}
get unitPrice(): MoneyValue {
return this.props.unitPrice;
}
get taxRate(): number {
return this.props.taxRate;
}
}

View File

@ -0,0 +1,15 @@
import {
IInvoiceBaseItem,
IInvoiceBaseItemProps,
InvoiceBaseItem,
} from "./InvoiceBaseItem";
export interface IInvoiceBudgetItemProps extends IInvoiceBaseItemProps {}
export interface IInvoiceBudgetItem extends IInvoiceBaseItem {}
export class InvoiceBudgetItem
extends InvoiceBaseItem<IInvoiceBudgetItemProps>
implements IInvoiceBudgetItem {
//private contents: (InvoiceLineItem | InvoiceChapter)[] = [];
}

View File

@ -0,0 +1,21 @@
export interface WithInvoiceItems {
items: InvoiceItems;
}
export class InvoiceIems<T> {
private items: T[] = [];
public length(): number {
return this.items.length;
}
public lastPosition(): number {}
public positionIsValid(position: number): boolean {}
public delete(position: number): void {
if (position >= 0 && position < this.items.length) {
this.items.splice(position, 1);
}
}
}

View File

@ -0,0 +1,23 @@
import { IDomainError } from "@/contexts/common/domain";
import { Result, UniqueID } from "@shared/contexts";
import {
IInvoiceBaseItem,
IInvoiceBaseItemProps,
InvoiceBaseItem,
} from "./InvoiceBaseItem";
export interface IInvoiceSimpleItemProps extends IInvoiceBaseItemProps {}
export interface IInvoiceSimpleItem extends IInvoiceBaseItem {}
export class InvoiceSimpleItem
extends InvoiceBaseItem<IInvoiceSimpleItemProps>
implements IInvoiceSimpleItem
{
public static create(
props: IInvoiceSimpleItemProps,
id?: UniqueID,
): Result<InvoiceSimpleItem, IDomainError> {
return Result.ok(new InvoiceSimpleItem(props, id));
}
}

View File

@ -0,0 +1,7 @@
import { InvoiceBudgetItem } from "./InvoiceBudgetItem";
import { InvoiceSimpleItem } from "./InvoiceSimpleItem";
export * from "./InvoiceBudgetItem";
export * from "./InvoiceSimpleItem";
export type InvoiceItem = InvoiceSimpleItem | InvoiceBudgetItem;

View File

@ -0,0 +1,10 @@
/* eslint-disable no-unused-vars */
import { IRepository } from "@/contexts/common/domain/repositories";
import { UniqueID } from "@shared/contexts";
import { InvoiceParticipant } from ".";
export interface IInvoiceParticipantRepository
extends IRepository<InvoiceParticipant> {
getById(id: UniqueID): Promise<InvoiceParticipant | null>;
exists(id: UniqueID): Promise<boolean>;
}

View File

@ -0,0 +1,64 @@
import { IDomainError } from "@/contexts/common/domain";
import { Entity, Name, Result, TINNumber, UniqueID } from "@shared/contexts";
import {
InvoiceParticipantBillingAddress,
InvoiceParticipantShippingAddress,
} from "../InvoiceParticipantAddress";
export interface IInvoiceParticipantProps {
tin: TINNumber;
companyName: Name;
firstName: Name;
lastName: Name;
billingAddress?: InvoiceParticipantBillingAddress;
shippingAddress?: InvoiceParticipantShippingAddress;
}
export interface IInvoiceParticipant {
id: UniqueID;
tin: TINNumber;
companyName: Name;
firstName: Name;
lastName: Name;
billingAddress?: InvoiceParticipantBillingAddress;
shippingAddress?: InvoiceParticipantShippingAddress;
}
export class InvoiceParticipant
extends Entity<IInvoiceParticipantProps>
implements IInvoiceParticipant
{
public static create(
props: IInvoiceParticipantProps,
id?: UniqueID,
): Result<InvoiceParticipant, IDomainError> {
const participant = new InvoiceParticipant(props, id);
return Result.ok<InvoiceParticipant>(participant);
}
get tin(): TINNumber {
return this.props.tin;
}
get companyName(): Name {
return this.props.companyName;
}
get firstName(): Name {
return this.props.firstName;
}
get lastName(): Name {
return this.props.lastName;
}
get billingAddress() {
return this.props.billingAddress;
}
get shippingAddress() {
return this.props.shippingAddress;
}
}

View File

@ -0,0 +1,3 @@
import { InvoiceParticipant } from "./InvoiceParticipant";
export class Recipient extends InvoiceParticipant {}

View File

@ -0,0 +1,3 @@
import { InvoiceParticipant } from "./InvoiceParticipant";
export class Supplier extends InvoiceParticipant {}

View File

@ -0,0 +1,4 @@
export * from "./IInvoiceParticipantRepository.interface";
export * from "./InvoiceParticipant";
export * from "./Recipient";
export * from "./Supplier";

View File

@ -0,0 +1,10 @@
/* eslint-disable no-unused-vars */
import { IRepository } from "@/contexts/common/domain/repositories";
import { UniqueID } from "@shared/contexts";
import { InvoiceParticipantAddress } from "./InvoiceParticipantAddress";
export interface IInvoiceParticipantAddressRepository
extends IRepository<InvoiceParticipantAddress> {
getById(id: UniqueID): Promise<InvoiceParticipantAddress | null>;
exists(id: UniqueID): Promise<boolean>;
}

View File

@ -0,0 +1,22 @@
import {
GenericAddress,
IGenericAddress,
IGenericAddressProps,
Result,
UniqueID,
} from "@shared/contexts";
export type InvoiceParticipantAddressType = "billing" | "shipping";
export interface IInvoiceParticipantAddressProps extends IGenericAddressProps {}
export interface IInvoiceParticipantAddress extends IGenericAddress {}
export class InvoiceParticipantAddress
extends GenericAddress<IInvoiceParticipantAddressProps>
implements IInvoiceParticipantAddress
{
public static create(props: IInvoiceParticipantAddressProps, id?: UniqueID) {
return Result.ok(new this(props, id));
}
}

View File

@ -0,0 +1,32 @@
import { Result, UniqueID } from "@shared/contexts";
import {
IInvoiceParticipantAddress,
IInvoiceParticipantAddressProps,
InvoiceParticipantAddress,
} from "./InvoiceParticipantAddress";
export interface IInvoiceParticipantBillingAddressProps
extends Omit<IInvoiceParticipantAddressProps, "type"> {}
export interface IInvoiceParticipantBillingAddress
extends IInvoiceParticipantAddress {}
export class InvoiceParticipantBillingAddress
extends InvoiceParticipantAddress
implements IInvoiceParticipantBillingAddress
{
public static create(
props: IInvoiceParticipantBillingAddressProps,
id?: UniqueID,
) {
const address = new InvoiceParticipantAddress(
{
...props,
type: "billing",
},
id,
);
return Result.ok<InvoiceParticipantBillingAddress>(address);
}
}

View File

@ -0,0 +1,32 @@
import { Result, UniqueID } from "@shared/contexts";
import {
IInvoiceParticipantAddress,
IInvoiceParticipantAddressProps,
InvoiceParticipantAddress,
} from "./InvoiceParticipantAddress";
export interface IInvoiceParticipantShippingAddressProps
extends Omit<IInvoiceParticipantAddressProps, "type"> {}
export interface IInvoiceParticipantShippingAddress
extends IInvoiceParticipantAddress {}
export class InvoiceParticipantShippingAddress
extends InvoiceParticipantAddress
implements IInvoiceParticipantShippingAddress
{
public static create(
props: IInvoiceParticipantShippingAddressProps,
id?: UniqueID,
) {
const address = new InvoiceParticipantAddress(
{
...props,
type: "shipping",
},
id,
);
return Result.ok<InvoiceParticipantShippingAddress>(address);
}
}

View File

@ -0,0 +1,4 @@
export * from "./InvoiceParticipantAddress";
export * from "./InvoiceParticipantAddress.repository.interface";
export * from "./InvoiceParticipantBillingAddress";
export * from "./InvoiceParticipantShippingAddress";

View File

@ -0,0 +1 @@
export * from "./invoice";

View File

@ -0,0 +1,208 @@
import { AggregateRoot, MoneyValue, UniqueID, UtcDate } from "@common/domain";
import { Collection, Result } from "@common/helpers";
import { Currency } from "dinero.js";
import { InvoiceStatus } from "../value-objects";
export interface IInvoiceProps {
invoiceNumber: InvoiceNumber;
invoiceSeries: InvoiceSeries;
issueDate: UtcDate;
operationDate: UtcDate;
//dueDate: UtcDate; // ? --> depende de la forma de pago
//tax: Tax; // ? --> detalles?
invoiceCurrency: Currency;
language: Language;
//purchareOrderNumber: string;
//notes: Note;
//senderId: UniqueID;
recipient: InvoiceParticipant;
//paymentInstructions: Note;
//paymentTerms: string;
items: Collection<InvoiceItem>;
}
export interface IInvoice {
id: UniqueID;
invoiceNumber: InvoiceNumber;
invoiceSeries: InvoiceSeries;
status: InvoiceStatus;
issueDate: UtcDate;
operationDate: UtcDate;
//senderId: UniqueID;
recipient: InvoiceParticipant;
//dueDate
//tax: Tax;
language: Language;
currency: Currency;
//purchareOrderNumber: string;
//notes: Note;
//paymentInstructions: Note;
//paymentTerms: string;
items: Collection<InvoiceItem>;
calculateSubtotal: () => MoneyValue;
calculateTaxTotal: () => MoneyValue;
calculateTotal: () => MoneyValue;
}
export class Invoice extends AggregateRoot<IInvoiceProps> implements IInvoice {
private _items: Collection<InvoiceItem>;
protected _status: InvoiceStatus;
static create(props: IInvoiceProps, id?: UniqueID): Result<Invoice, Error> {
const invoice = new Invoice(props, id);
// Reglas de negocio / validaciones
// ...
// ...
// 🔹 Disparar evento de dominio "InvoiceAuthenticatedEvent"
//const { invoice } = props;
//user.addDomainEvent(new InvoiceAuthenticatedEvent(id, invoice.toString()));
return Result.ok(invoice);
}
get invoiceNumber() {
return this.props.invoiceNumber;
}
get invoiceSeries() {
return this.props.invoiceSeries;
}
get issueDate() {
return this.props.issueDate;
}
/*get senderId(): UniqueID {
return this.props.senderId;
}*/
get recipient(): InvoiceParticipant {
return this.props.recipient;
}
get operationDate() {
return this.props.operationDate;
}
get language() {
return this.props.language;
}
get dueDate() {
return undefined;
}
get tax() {
return undefined;
}
get status() {
return this._status;
}
get items() {
return this._items;
}
/*get purchareOrderNumber() {
return this.props.purchareOrderNumber;
}
get paymentInstructions() {
return this.props.paymentInstructions;
}
get paymentTerms() {
return this.props.paymentTerms;
}
get billTo() {
return this.props.billTo;
}
get shipTo() {
return this.props.shipTo;
}*/
get currency() {
return this.props.invoiceCurrency;
}
/*get notes() {
return this.props.notes;
}*/
// Method to get the complete list of line items
/*get lineItems(): InvoiceLineItem[] {
return this._lineItems;
}
addLineItem(lineItem: InvoiceLineItem, position?: number): void {
if (position === undefined) {
this._lineItems.push(lineItem);
} else {
this._lineItems.splice(position, 0, lineItem);
}
}*/
calculateSubtotal(): MoneyValue {
let subtotal: MoneyValue | null = null;
for (const item of this._items.items) {
if (!subtotal) {
subtotal = item.calculateSubtotal();
} else {
subtotal = subtotal.add(item.calculateSubtotal());
}
}
return subtotal
? subtotal.convertPrecision(2)
: MoneyValue.create({
amount: 0,
currencyCode: this.props.invoiceCurrency.code,
precision: 2,
}).object;
}
// Method to calculate the total tax in the invoice
calculateTaxTotal(): MoneyValue {
let taxTotal = MoneyValue.create({
amount: 0,
currencyCode: this.props.invoiceCurrency.code,
precision: 2,
}).object;
for (const item of this._items.items) {
taxTotal = taxTotal.add(item.calculateTaxAmount());
}
return taxTotal.convertPrecision(2);
}
// Method to calculate the total invoice amount, including taxes
calculateTotal(): MoneyValue {
return this.calculateSubtotal().add(this.calculateTaxTotal()).convertPrecision(2);
}
}

View File

@ -0,0 +1,6 @@
export * from "./aggregates";
export * from "./Contact";
export * from "./ContactAddress.ts";
export * from "./InvoiceItems";
export * from "./InvoiceParticipant";
export * from "./InvoiceParticipantAddress";

View File

@ -0,0 +1 @@
export * from "./invoice-repository.interface";

View File

@ -0,0 +1,12 @@
import { UniqueID } from "@common/domain";
import { Collection, Result } from "@common/helpers";
import { Invoice } from "../aggregates";
export interface IInvoiceRepository {
findAll(transaction?: any): Promise<Result<Collection<Invoice>, Error>>;
findById(id: UniqueID, transaction?: any): Promise<Result<Invoice, Error>>;
deleteById(id: UniqueID, transaction?: any): Promise<Result<boolean, Error>>;
create(invoice: Invoice, transaction?: any): Promise<void>;
update(invoice: Invoice, transaction?: any): Promise<void>;
}

View File

@ -0,0 +1,2 @@
export * from "./invoice-service.interface";
export * from "./invoice.service";

View File

@ -0,0 +1,22 @@
import { UniqueID } from "@common/domain";
import { Collection, Result } from "@common/helpers";
import { IInvoiceProps, Invoice } from "../aggregates";
export interface IInvoiceService {
findInvoices(transaction?: any): Promise<Result<Collection<Invoice>, Error>>;
findInvoiceById(invoiceId: UniqueID, transaction?: any): Promise<Result<Invoice>>;
updateInvoiceById(
invoiceId: UniqueID,
data: Partial<IInvoiceProps>,
transaction?: any
): Promise<Result<Invoice, Error>>;
createInvoice(
invoiceId: UniqueID,
data: IInvoiceProps,
transaction?: any
): Promise<Result<Invoice, Error>>;
deleteInvoiceById(invoiceId: UniqueID, transaction?: any): Promise<Result<Invoice, Error>>;
}

View File

@ -0,0 +1,94 @@
import { UniqueID } from "@common/domain";
import { Collection, Result } from "@common/helpers";
import { Transaction } from "sequelize";
import { IInvoiceProps, Invoice } from "../aggregates";
import { IInvoiceRepository } from "../repositories";
import { IInvoiceService } from "./invoice-service.interface";
export class InvoiceService implements IInvoiceService {
constructor(private readonly repo: IInvoiceRepository) {}
async findInvoices(transaction?: Transaction): Promise<Result<Collection<Invoice>, Error>> {
const invoicesOrError = await this.repo.findAll(transaction);
if (invoicesOrError.isFailure) {
return Result.fail(invoicesOrError.error);
}
// Solo devolver usuarios activos
//const allInvoices = invoicesOrError.data.filter((invoice) => invoice.isActive);
//return Result.ok(new Collection(allInvoices));
return invoicesOrError;
}
async findInvoiceById(invoiceId: UniqueID, transaction?: Transaction): Promise<Result<Invoice>> {
return await this.repo.findById(invoiceId, transaction);
}
async updateInvoiceById(
invoiceId: UniqueID,
data: Partial<IInvoiceProps>,
transaction?: Transaction
): Promise<Result<Invoice, Error>> {
// Verificar si la cuenta existe
const invoiceOrError = await this.repo.findById(invoiceId, transaction);
if (invoiceOrError.isFailure) {
return Result.fail(new Error("Invoice not found"));
}
const updatedInvoiceOrError = Invoice.update(invoiceOrError.data, data);
if (updatedInvoiceOrError.isFailure) {
return Result.fail(
new Error(`Error updating invoice: ${updatedInvoiceOrError.error.message}`)
);
}
const updateInvoice = updatedInvoiceOrError.data;
await this.repo.update(updateInvoice, transaction);
return Result.ok(updateInvoice);
}
async createInvoice(
invoiceId: UniqueID,
data: IInvoiceProps,
transaction?: Transaction
): Promise<Result<Invoice, Error>> {
// Verificar si la cuenta existe
const invoiceOrError = await this.repo.findById(invoiceId, transaction);
if (invoiceOrError.isSuccess) {
return Result.fail(new Error("Invoice exists"));
}
const newInvoiceOrError = Invoice.create(data, invoiceId);
if (newInvoiceOrError.isFailure) {
return Result.fail(new Error(`Error creating invoice: ${newInvoiceOrError.error.message}`));
}
const newInvoice = newInvoiceOrError.data;
await this.repo.create(newInvoice, transaction);
return Result.ok(newInvoice);
}
async deleteInvoiceById(
invoiceId: UniqueID,
transaction?: Transaction
): Promise<Result<Invoice, Error>> {
// Verificar si la cuenta existe
const invoiceOrError = await this.repo.findById(invoiceId, transaction);
if (invoiceOrError.isFailure) {
return Result.fail(new Error("Invoice not exists"));
}
const newInvoiceOrError = Invoice.create(data, invoiceId);
if (newInvoiceOrError.isFailure) {
return Result.fail(new Error(`Error creating invoice: ${newInvoiceOrError.error.message}`));
}
const newInvoice = newInvoiceOrError.data;
await this.repo.create(newInvoice, transaction);
return Result.ok(newInvoice);
}
}

View File

@ -0,0 +1 @@
export * from "./invoice-status";

View File

@ -0,0 +1,41 @@
import Joi from "joi";
import { UndefinedOr } from "../../../../utilities";
import {
IStringValueObjectOptions,
Result,
RuleValidator,
StringValueObject,
} from "../../../common";
export class InvoiceNumber extends StringValueObject {
protected static validate(
value: UndefinedOr<string>,
options: IStringValueObjectOptions,
) {
const rule = Joi.string()
.allow(null, "")
.default("")
.trim()
.label(options.label ? options.label : "value");
return RuleValidator.validate<string>(rule, value);
}
public static create(
value: UndefinedOr<string>,
options: IStringValueObjectOptions = {},
) {
const _options = {
label: "invoice_number",
...options,
};
const validationResult = InvoiceNumber.validate(value, _options);
if (validationResult.isFailure) {
return Result.fail(validationResult.error);
}
return Result.ok(new InvoiceNumber(validationResult.object));
}
}

View File

@ -0,0 +1,41 @@
import Joi from "joi";
import { UndefinedOr } from "../../../../utilities";
import {
IStringValueObjectOptions,
Result,
RuleValidator,
StringValueObject,
} from "../../../common";
export class InvoiceSeries extends StringValueObject {
protected static validate(
value: UndefinedOr<string>,
options: IStringValueObjectOptions,
) {
const rule = Joi.string()
.allow(null, "")
.default("")
.trim()
.label(options.label ? options.label : "value");
return RuleValidator.validate<string>(rule, value);
}
public static create(
value: UndefinedOr<string>,
options: IStringValueObjectOptions = {},
) {
const _options = {
label: "invoice_series",
...options,
};
const validationResult = InvoiceSeries.validate(value, _options);
InvoiceSeries;
if (validationResult.isFailure) {
return Result.fail(validationResult.error);
}
return Result.ok(new InvoiceSeries(validationResult.object));
}
}

View File

@ -0,0 +1,76 @@
import { ValueObject } from "@common/domain";
import { Result } from "@common/helpers";
interface IInvoiceStatusProps {
value: string;
}
export enum INVOICE_STATUS {
DRAFT = "draft",
EMITTED = "emitted",
SENT = "sent",
REJECTED = "rejected",
}
export class InvoiceStatus extends ValueObject<IInvoiceStatusProps> {
private static readonly ALLOWED_STATUSES = ["draft", "emitted", "sent", "rejected"];
private static readonly TRANSITIONS: Record<string, string[]> = {
draft: [INVOICE_STATUS.EMITTED],
emitted: [INVOICE_STATUS.SENT, INVOICE_STATUS.REJECTED, INVOICE_STATUS.DRAFT],
sent: [INVOICE_STATUS.REJECTED],
rejected: [],
};
static create(value: string): Result<InvoiceStatus, Error> {
if (!this.ALLOWED_STATUSES.includes(value)) {
return Result.fail(new Error(`Estado de la factura no válido: ${value}`));
}
return Result.ok(
value === "rejected"
? InvoiceStatus.createRejected()
: value === "sent"
? InvoiceStatus.createSent()
: value === "emitted"
? InvoiceStatus.createSent()
: InvoiceStatus.createDraft()
);
}
public static createDraft(): InvoiceStatus {
return new InvoiceStatus({ value: INVOICE_STATUS.DRAFT });
}
public static createEmitted(): InvoiceStatus {
return new InvoiceStatus({ value: INVOICE_STATUS.EMITTED });
}
public static createSent(): InvoiceStatus {
return new InvoiceStatus({ value: INVOICE_STATUS.SENT });
}
public static createRejected(): InvoiceStatus {
return new InvoiceStatus({ value: INVOICE_STATUS.REJECTED });
}
getValue(): string {
return this.props.value;
}
canTransitionTo(nextStatus: string): boolean {
return InvoiceStatus.TRANSITIONS[this.props.value].includes(nextStatus);
}
transitionTo(nextStatus: string): Result<InvoiceStatus, Error> {
if (!this.canTransitionTo(nextStatus)) {
return Result.fail(
new Error(`Transición no permitida de ${this.props.value} a ${nextStatus}`)
);
}
return InvoiceStatus.create(nextStatus);
}
toString(): string {
return this.getValue();
}
}

View File

@ -0,0 +1,77 @@
import {
ISequelizeAdapter,
SequelizeRepository,
} from "@/contexts/common/infrastructure/sequelize";
import { UniqueID } from "@shared/contexts";
import { Transaction } from "sequelize";
import { Contact, IContactRepository } from "../domain/Contact";
import { IContactMapper } from "./mappers/contact.mapper";
export class ContactRepository
extends SequelizeRepository<Contact>
implements IContactRepository
{
protected mapper: IContactMapper;
public constructor(props: {
mapper: IContactMapper;
adapter: ISequelizeAdapter;
transaction: Transaction;
}) {
const { adapter, mapper, transaction } = props;
super({ adapter, transaction });
this.mapper = mapper;
}
public async getById2(
id: UniqueID,
billingAddressId: UniqueID,
shippingAddressId: UniqueID,
) {
const Contact_Model = this.adapter.getModel("Contact_Model");
const ContactAddress_Model = this.adapter.getModel("ContactAddress_Model");
const rawContact: any = await Contact_Model.findOne({
where: { id: id.toString() },
include: [
{
model: ContactAddress_Model,
as: "billingAddress",
where: {
id: billingAddressId.toString(),
},
},
{
model: ContactAddress_Model,
as: "shippingAddress",
where: {
id: shippingAddressId.toString(),
},
},
],
transaction: this.transaction,
});
if (!rawContact === true) {
return null;
}
return this.mapper.mapToDomain(rawContact);
}
public async getById(id: UniqueID): Promise<Contact | null> {
const rawContact: any = await this._getById("Contact_Model", id, {
include: [{ all: true }],
});
if (!rawContact === true) {
return null;
}
return this.mapper.mapToDomain(rawContact);
}
public async exists(id: UniqueID): Promise<boolean> {
return this._exists("Customer", "id", id.toString());
}
}

View File

@ -0,0 +1,101 @@
import { SequelizeRepository } from "@/contexts/common/infrastructure/sequelize/SequelizeRepository";
import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
import { ICollection, IQueryCriteria, UniqueID } from "@shared/contexts";
import { Transaction } from "sequelize";
import { IInvoiceRepository, Invoice } from "../domain";
import { IInvoiceMapper } from "./mappers";
export type QueryParams = {
pagination: Record<string, any>;
filters: Record<string, any>;
};
export class InvoiceRepository
extends SequelizeRepository<Invoice>
implements IInvoiceRepository
{
protected mapper: IInvoiceMapper;
public constructor(props: {
mapper: IInvoiceMapper;
adapter: ISequelizeAdapter;
transaction: Transaction;
}) {
const { adapter, mapper, transaction } = props;
super({ adapter, transaction });
this.mapper = mapper;
}
public async getById(id: UniqueID): Promise<Invoice | null> {
const rawContact: any = await this._getById("Invoice_Model", id, {
include: [
{ association: "items" },
{
association: "participants",
include: [
{ association: "shippingAddress" },
{ association: "billingAddress" },
],
},
],
});
if (!rawContact === true) {
return null;
}
return this.mapper.mapToDomain(rawContact);
}
public async findAll(
queryCriteria?: IQueryCriteria
): Promise<ICollection<Invoice>> {
const { rows, count } = await this._findAll(
"Invoice_Model",
queryCriteria,
{
include: [
{
association: "participants",
separate: true,
},
],
}
);
return this.mapper.mapArrayAndCountToDomain(rows, count);
}
public async save(invoice: Invoice): Promise<void> {
const { items, participants, ...invoiceData } =
this.mapper.mapToPersistence(invoice);
await this.adapter
.getModel("Invoice_Model")
.create(invoiceData, { transaction: this.transaction });
await this.adapter
.getModel("InvoiceItem_Model")
.bulkCreate(items, { transaction: this.transaction });
await this.adapter
.getModel("InvoiceParticipant_Model")
.bulkCreate(participants, { transaction: this.transaction });
await this.adapter
.getModel("InvoiceParticipantAddress_Model")
.bulkCreate(
[participants[0].billingAddress, participants[0].shippingAddress],
{ transaction: this.transaction }
);
}
public removeById(id: UniqueID): Promise<void> {
return this._removeById("Invoice_Model", id);
}
public async exists(id: UniqueID): Promise<boolean> {
return this._exists("Invoice_Model", "id", id.toString());
}
}

View File

@ -0,0 +1,57 @@
import {
ISequelizeAdapter,
SequelizeRepository,
} from "@/contexts/common/infrastructure/sequelize";
import { Transaction } from "sequelize";
import { InvoiceParticipant } from "../domain";
import { IInvoiceParticipantMapper } from "./mappers";
export class InvoiceParticipantRepository extends SequelizeRepository<InvoiceParticipant> {
protected mapper: IInvoiceParticipantMapper;
public constructor(props: {
mapper: IInvoiceParticipantMapper;
adapter: ISequelizeAdapter;
transaction: Transaction;
}) {
const { adapter, mapper, transaction } = props;
super({ adapter, transaction });
this.mapper = mapper;
}
/*public async getParticipantById(
id: UniqueID,
): Promise<InvoiceParticipant | null> {
const rawParticipant: any = await this._getById(
"InvoiceParticipant_Model",
id,
{
include: [{ all: true }],
raw: true,
},
);
if (!rawParticipant === true) {
return null;
}
return this.mapper.mapToDomain(rawParticipant);
}
public async getContactById(id: UniqueID): Promise<any | null> {
const rawContact: any = await this._getById("Customer", id, {
include: [{ all: true }],
raw: true,
});
if (!rawContact === true) {
return null;
}
return this.mapper.mapToDomain(rawContact);
}
public async exists(id: UniqueID): Promise<boolean> {
return this._exists("Customer", "id", id.toString());
}*/
}

View File

@ -0,0 +1,44 @@
import {
ISequelizeAdapter,
SequelizeRepository,
} from "@/contexts/common/infrastructure/sequelize";
import { UniqueID } from "@shared/contexts";
import { Transaction } from "sequelize";
import { InvoiceParticipantAddress } from "../domain";
import { IInvoiceParticipantAddressMapper } from "./mappers";
export class InvoiceParticipantAddressRepository extends SequelizeRepository<InvoiceParticipantAddress> {
protected mapper: IInvoiceParticipantAddressMapper;
public constructor(props: {
mapper: IInvoiceParticipantAddressMapper;
adapter: ISequelizeAdapter;
transaction: Transaction;
}) {
const { adapter, mapper, transaction } = props;
super({ adapter, transaction });
this.mapper = mapper;
}
public async getById(
id: UniqueID,
): Promise<InvoiceParticipantAddress | null> {
const rawParticipant: any = await this._getById(
"InvoiceParticipantAddress_Model",
id,
{
include: [{ all: true }],
},
);
if (!rawParticipant === true) {
return null;
}
return this.mapper.mapToDomain(rawParticipant);
}
public async exists(id: UniqueID): Promise<boolean> {
return this._exists("CustomerAddress", "id", id.toString());
}
}

View File

@ -0,0 +1,43 @@
import {
IRepositoryManager,
RepositoryManager,
} from "@/contexts/common/domain";
import {
ISequelizeAdapter,
createSequelizeAdapter,
} from "@/contexts/common/infrastructure/sequelize";
import { InvoicingServices, TInvoicingServices } from "../application";
export interface IInvoicingContext {
adapter: ISequelizeAdapter;
repositoryManager: IRepositoryManager;
services: TInvoicingServices;
}
class InvoicingContext {
private static instance: InvoicingContext | null = null;
public static getInstance(): InvoicingContext {
if (!InvoicingContext.instance) {
InvoicingContext.instance = new InvoicingContext();
}
return InvoicingContext.instance;
}
private context: IInvoicingContext;
private constructor() {
this.context = {
adapter: createSequelizeAdapter(),
repositoryManager: RepositoryManager.getInstance(),
services: InvoicingServices,
};
}
public getContext(): IInvoicingContext {
return this.context;
}
}
const sharedInvoicingContext = InvoicingContext.getInstance().getContext();
export { sharedInvoicingContext };

View File

@ -0,0 +1,2 @@
export * from "./mappers";
export * from "./sequelize";

View File

@ -0,0 +1,83 @@
import {
ISequelizeMapper,
SequelizeMapper,
} from "@/contexts/common/infrastructure";
import { Name, TINNumber, UniqueID } from "@shared/contexts";
import { Contact, IContactProps } from "../../domain";
import { IInvoicingContext } from "../InvoicingContext";
import {
Contact_Model,
TCreationContact_Model,
} from "../sequelize/contact.model";
import {
IContactAddressMapper,
createContactAddressMapper,
} from "./contactAddress.mapper";
export interface IContactMapper
extends ISequelizeMapper<Contact_Model, TCreationContact_Model, Contact> {}
class ContactMapper
extends SequelizeMapper<Contact_Model, TCreationContact_Model, Contact>
implements IContactMapper
{
public constructor(props: {
addressMapper: IContactAddressMapper;
context: IInvoicingContext;
}) {
super(props);
}
protected toDomainMappingImpl(source: Contact_Model, params: any): Contact {
if (!source.billingAddress) {
this.handleRequiredFieldError(
"billingAddress",
new Error("Missing participant's billing address")
);
}
if (!source.shippingAddress) {
this.handleRequiredFieldError(
"shippingAddress",
new Error("Missing participant's shipping address")
);
}
const billingAddress = this.props.addressMapper.mapToDomain(
source.billingAddress!,
params
);
const shippingAddress = this.props.addressMapper.mapToDomain(
source.shippingAddress!,
params
);
const props: IContactProps = {
tin: this.mapsValue(source, "tin", TINNumber.create),
firstName: this.mapsValue(source, "first_name", Name.create),
lastName: this.mapsValue(source, "last_name", Name.create),
companyName: this.mapsValue(source, "company_name", Name.create),
billingAddress,
shippingAddress,
};
const id = this.mapsValue(source, "id", UniqueID.create);
const contactOrError = Contact.create(props, id);
if (contactOrError.isFailure) {
throw contactOrError.error;
}
return contactOrError.object;
}
}
export const createContactMapper = (
context: IInvoicingContext
): IContactMapper =>
new ContactMapper({
addressMapper: createContactAddressMapper(context),
context,
});

View File

@ -0,0 +1,65 @@
import {
ISequelizeMapper,
SequelizeMapper,
} from "@/contexts/common/infrastructure";
import {
City,
Country,
Email,
Note,
Phone,
PostalCode,
Province,
Street,
UniqueID,
} from "@shared/contexts";
import { ContactAddress, IContactAddressProps } from "../../domain";
import { IInvoicingContext } from "../InvoicingContext";
import {
ContactAddress_Model,
TCreationContactAddress_Attributes,
} from "../sequelize";
export interface IContactAddressMapper
extends ISequelizeMapper<
ContactAddress_Model,
TCreationContactAddress_Attributes,
ContactAddress
> {}
export const createContactAddressMapper = (
context: IInvoicingContext
): IContactAddressMapper => new ContactAddressMapper({ context });
class ContactAddressMapper
extends SequelizeMapper<
ContactAddress_Model,
TCreationContactAddress_Attributes,
ContactAddress
>
implements IContactAddressMapper
{
protected toDomainMappingImpl(source: ContactAddress_Model, params: any) {
const id = this.mapsValue(source, "id", UniqueID.create);
const props: IContactAddressProps = {
type: source.type,
street: this.mapsValue(source, "street", Street.create),
city: this.mapsValue(source, "city", City.create),
province: this.mapsValue(source, "province", Province.create),
postalCode: this.mapsValue(source, "postal_code", PostalCode.create),
country: this.mapsValue(source, "country", Country.create),
email: this.mapsValue(source, "email", Email.create),
phone: this.mapsValue(source, "phone", Phone.create),
notes: this.mapsValue(source, "notes", Note.create),
};
const addressOrError = ContactAddress.create(props, id);
if (addressOrError.isFailure) {
throw addressOrError.error;
}
return addressOrError.object;
}
}

View File

@ -0,0 +1,6 @@
export * from "./contact.mapper";
export * from "./contactAddress.mapper";
export * from "./invoice.mapper";
export * from "./invoiceItem.mapper";
export * from "./invoiceParticipant.mapper";
export * from "./invoiceParticipantAddress.mapper";

View File

@ -0,0 +1,115 @@
import {
Currency,
InvoiceDate,
InvoiceNumber,
InvoiceSeries,
Language,
UniqueID,
} from "@shared/contexts";
import { ISequelizeMapper, SequelizeMapper } from "@/contexts/common/infrastructure";
import { DraftInvoice, Invoice } from "../../domain";
import { IBaseInvoiceProps } from "../../domain/aggregates/invoice";
import { IInvoicingContext } from "../InvoicingContext";
import { Invoice_Model, TCreationInvoice_Model } from "../sequelize";
import { IInvoiceItemMapper, createInvoiceItemMapper } from "./invoiceItem.mapper";
import {
IInvoiceParticipantMapper,
createInvoiceParticipantMapper,
} from "./invoiceParticipant.mapper";
export interface IInvoiceMapper
extends ISequelizeMapper<Invoice_Model, TCreationInvoice_Model, Invoice> {}
export const createInvoiceMapper = (context: IInvoicingContext): IInvoiceMapper =>
new InvoiceMapper({
context,
invoiceItemMapper: createInvoiceItemMapper(context),
participantMapper: createInvoiceParticipantMapper(context),
});
class InvoiceMapper
extends SequelizeMapper<Invoice_Model, TCreationInvoice_Model, Invoice>
implements IInvoiceMapper
{
public constructor(props: {
invoiceItemMapper: IInvoiceItemMapper;
participantMapper: IInvoiceParticipantMapper;
context: IInvoicingContext;
}) {
super(props);
}
protected toDomainMappingImpl(source: Invoice_Model): Invoice {
const id = this.mapsValue(source, "id", UniqueID.create);
/*if (!source.items) {
this.handleRequiredFieldError(
"items",
new Error("Missing invoice items"),
);
}*/
const items = (this.props.invoiceItemMapper as IInvoiceItemMapper).mapArrayToDomain(
source.items,
{
sourceParent: source,
}
);
const participants = (
this.props.participantMapper as IInvoiceParticipantMapper
).mapArrayToDomain(source.participants);
const props: IBaseInvoiceProps = {
invoiceNumber: this.mapsValue(source, "invoice_number", InvoiceNumber.create),
invoiceSeries: this.mapsValue(source, "invoice_series", InvoiceSeries.create),
issueDate: this.mapsValue(source, "issue_date", InvoiceDate.create),
operationDate: this.mapsValue(source, "operation_date", InvoiceDate.create),
invoiceCurrency: this.mapsValue(source, "invoice_currency", Currency.createFromCode),
language: this.mapsValue(source, "invoice_language", Language.createFromCode),
//recipientId: id,
//senderId: id,
items,
recipient: participants.items[0],
};
const invoiceOrError = DraftInvoice.create(props, id);
if (invoiceOrError.isFailure) {
throw invoiceOrError.error;
}
return invoiceOrError.object;
}
protected toPersistenceMappingImpl(source: Invoice) {
const items = (this.props.invoiceItemMapper as IInvoiceItemMapper).mapCollectionToPersistence(
source.items,
{ sourceParent: source }
);
const recipientData = (
this.props.participantMapper as IInvoiceParticipantMapper
).mapToPersistence(source.recipient, { sourceParent: source });
const invoice: TCreationInvoice_Model = {
id: source.id.toPrimitive(),
invoice_status: source.status.toPrimitive(),
invoice_number: source.invoiceNumber.toPrimitive(),
invoice_series: source.invoiceSeries.toPrimitive(),
invoice_currency: source.currency.toPrimitive(),
invoice_language: source.language.toPrimitive(),
issue_date: source.issueDate.toPrimitive(),
operation_date: source.operationDate.toPrimitive(),
subtotal: source.calculateSubtotal().toPrimitive(),
total: source.calculateTotal().toPrimitive(),
items,
participants: [recipientData],
};
return invoice;
}
}

View File

@ -0,0 +1,87 @@
import {
ISequelizeMapper,
SequelizeMapper,
} from "@/contexts/common/infrastructure";
import { Description, Quantity, UniqueID, UnitPrice } from "@shared/contexts";
import { Invoice } from "../../domain";
import {
IInvoiceSimpleItemProps,
InvoiceItem,
InvoiceSimpleItem,
} from "../../domain/InvoiceItems";
import { IInvoicingContext } from "../InvoicingContext";
import {
InvoiceItem_Model,
Invoice_Model,
TCreationInvoiceItem_Model,
} from "../sequelize";
export interface IInvoiceItemMapper
extends ISequelizeMapper<
InvoiceItem_Model,
TCreationInvoiceItem_Model,
InvoiceItem
> {}
export const createInvoiceItemMapper = (
context: IInvoicingContext,
): IInvoiceItemMapper => new InvoiceItemMapper({ context });
class InvoiceItemMapper
extends SequelizeMapper<
InvoiceItem_Model,
TCreationInvoiceItem_Model,
InvoiceItem
>
implements IInvoiceItemMapper
{
protected toDomainMappingImpl(
source: InvoiceItem_Model,
params: { sourceParent: Invoice_Model },
): InvoiceItem {
const { sourceParent } = params;
const id = this.mapsValue(source, "item_id", UniqueID.create);
const props: IInvoiceSimpleItemProps = {
description: this.mapsValue(source, "description", Description.create),
quantity: this.mapsValue(source, "quantity", Quantity.create),
unitPrice: this.mapsValue(source, "unit_price", (unit_price) =>
UnitPrice.create({
amount: unit_price,
currencyCode: sourceParent.invoice_currency,
precision: 4,
}),
),
};
const invoiceItemOrError = InvoiceSimpleItem.create(props, id);
if (invoiceItemOrError.isFailure) {
throw invoiceItemOrError.error;
}
return invoiceItemOrError.object;
}
protected toPersistenceMappingImpl(
source: InvoiceItem,
params: { index: number; sourceParent: Invoice },
): TCreationInvoiceItem_Model {
const { index, sourceParent } = params;
const lineData = {
parent_id: undefined,
invoice_id: sourceParent.id.toPrimitive(),
item_type: "simple",
position: index,
item_id: source.id.toPrimitive(),
description: source.description.toPrimitive(),
quantity: source.quantity.toPrimitive(),
unit_price: source.unitPrice.toPrimitive(),
subtotal: source.calculateSubtotal().toPrimitive(),
total: source.calculateTotal().toPrimitive(),
};
return lineData;
}
}

View File

@ -0,0 +1,129 @@
import {
ISequelizeMapper,
SequelizeMapper,
} from "@/contexts/common/infrastructure";
import { Name, TINNumber, UniqueID } from "@shared/contexts";
import {
IInvoiceParticipantProps,
Invoice,
InvoiceParticipant,
InvoiceParticipantBillingAddress,
InvoiceParticipantShippingAddress,
} from "../../domain";
import { IInvoicingContext } from "../InvoicingContext";
import {
InvoiceParticipant_Model,
TCreationInvoiceParticipant_Model,
} from "../sequelize";
import {
IInvoiceParticipantAddressMapper,
createInvoiceParticipantAddressMapper,
} from "./invoiceParticipantAddress.mapper";
export interface IInvoiceParticipantMapper
extends ISequelizeMapper<
InvoiceParticipant_Model,
TCreationInvoiceParticipant_Model,
InvoiceParticipant
> {}
export const createInvoiceParticipantMapper = (
context: IInvoicingContext,
): IInvoiceParticipantMapper =>
new InvoiceParticipantMapper({
context,
addressMapper: createInvoiceParticipantAddressMapper(context),
});
class InvoiceParticipantMapper
extends SequelizeMapper<
InvoiceParticipant_Model,
TCreationInvoiceParticipant_Model,
InvoiceParticipant
>
implements IInvoiceParticipantMapper
{
public constructor(props: {
addressMapper: IInvoiceParticipantAddressMapper;
context: IInvoicingContext;
}) {
super(props);
}
protected toDomainMappingImpl(source: InvoiceParticipant_Model, params: any) {
/*if (!source.billingAddress) {
this.handleRequiredFieldError(
"billingAddress",
new Error("Missing participant's billing address"),
);
}
if (!source.shippingAddress) {
this.handleRequiredFieldError(
"shippingAddress",
new Error("Missing participant's shipping address"),
);
}
*/
const billingAddress = source.billingAddress
? ((
this.props.addressMapper as IInvoiceParticipantAddressMapper
).mapToDomain(
source.billingAddress,
params,
) as InvoiceParticipantBillingAddress)
: undefined;
const shippingAddress = source.shippingAddress
? ((
this.props.addressMapper as IInvoiceParticipantAddressMapper
).mapToDomain(
source.shippingAddress,
params,
) as InvoiceParticipantShippingAddress)
: undefined;
const props: IInvoiceParticipantProps = {
tin: this.mapsValue(source, "tin", TINNumber.create),
firstName: this.mapsValue(source, "first_name", Name.create),
lastName: this.mapsValue(source, "last_name", Name.create),
companyName: this.mapsValue(source, "company_name", Name.create),
billingAddress,
shippingAddress,
};
const id = this.mapsValue(source, "participant_id", UniqueID.create);
const participantOrError = InvoiceParticipant.create(props, id);
if (participantOrError.isFailure) {
throw participantOrError.error;
}
return participantOrError.object;
}
protected toPersistenceMappingImpl(
source: InvoiceParticipant,
params: { sourceParent: Invoice },
): TCreationInvoiceParticipant_Model {
const { sourceParent } = params;
return {
invoice_id: sourceParent.id.toPrimitive(),
participant_id: source.id.toPrimitive(),
tin: source.tin.toPrimitive(),
first_name: source.firstName.toPrimitive(),
last_name: source.lastName.toPrimitive(),
company_name: source.companyName.toPrimitive(),
billingAddress: (
this.props.addressMapper as IInvoiceParticipantAddressMapper
).mapToPersistence(source.billingAddress!, { sourceParent: source }),
shippingAddress: (
this.props.addressMapper as IInvoiceParticipantAddressMapper
).mapToPersistence(source.shippingAddress!, { sourceParent: source }),
};
}
}

View File

@ -0,0 +1,94 @@
import {
ISequelizeMapper,
SequelizeMapper,
} from "@/contexts/common/infrastructure";
import {
City,
Country,
Email,
Note,
Phone,
PostalCode,
Province,
Street,
UniqueID,
} from "@shared/contexts";
import {
IInvoiceParticipantAddressProps,
InvoiceParticipant,
InvoiceParticipantAddress,
} from "../../domain";
import { IInvoicingContext } from "../InvoicingContext";
import {
InvoiceParticipantAddress_Model,
TCreationInvoiceParticipantAddress_Model,
} from "../sequelize";
export interface IInvoiceParticipantAddressMapper
extends ISequelizeMapper<
InvoiceParticipantAddress_Model,
TCreationInvoiceParticipantAddress_Model,
InvoiceParticipantAddress
> {}
export const createInvoiceParticipantAddressMapper = (
context: IInvoicingContext
): IInvoiceParticipantAddressMapper =>
new InvoiceParticipantAddressMapper({ context });
class InvoiceParticipantAddressMapper
extends SequelizeMapper<
InvoiceParticipantAddress_Model,
TCreationInvoiceParticipantAddress_Model,
InvoiceParticipantAddress
>
implements IInvoiceParticipantAddressMapper
{
protected toDomainMappingImpl(
source: InvoiceParticipantAddress_Model,
params: any
) {
const id = this.mapsValue(source, "address_id", UniqueID.create);
const props: IInvoiceParticipantAddressProps = {
type: source.type,
street: this.mapsValue(source, "street", Street.create),
city: this.mapsValue(source, "city", City.create),
province: this.mapsValue(source, "province", Province.create),
postalCode: this.mapsValue(source, "postal_code", PostalCode.create),
country: this.mapsValue(source, "country", Country.create),
email: this.mapsValue(source, "email", Email.create),
phone: this.mapsValue(source, "phone", Phone.create),
notes: this.mapsValue(source, "notes", Note.create),
};
const addressOrError = InvoiceParticipantAddress.create(props, id);
if (addressOrError.isFailure) {
throw addressOrError.error;
}
return addressOrError.object;
}
protected toPersistenceMappingImpl(
source: InvoiceParticipantAddress,
params: { sourceParent: InvoiceParticipant }
) {
const { sourceParent } = params;
return {
address_id: source.id.toPrimitive(),
participant_id: sourceParent.id.toPrimitive(),
type: String(source.type),
title: source.title,
street: source.street.toPrimitive(),
city: source.city.toPrimitive(),
postal_code: source.postalCode.toPrimitive(),
province: source.province.toPrimitive(),
country: source.country.toPrimitive(),
email: source.email.toPrimitive(),
phone: source.phone.toPrimitive(),
};
}
}

View File

@ -0,0 +1,93 @@
import {
CreationOptional,
DataTypes,
InferAttributes,
InferCreationAttributes,
Model,
NonAttribute,
Sequelize,
} from "sequelize";
import {
ContactAddress_Model,
TCreationContactAddress_Attributes,
} from "./contactAddress.model";
export type TCreationContact_Model = InferCreationAttributes<
Contact_Model,
{ omit: "shippingAddress" | "billingAddress" }
> & {
billingAddress: TCreationContactAddress_Attributes;
shippingAddress: TCreationContactAddress_Attributes;
};
export class Contact_Model extends Model<
InferAttributes<
Contact_Model,
{ omit: "shippingAddress" | "billingAddress" }
>,
InferCreationAttributes<
Contact_Model,
{ omit: "shippingAddress" | "billingAddress" }
>
> {
// To avoid table creation
static async sync(): Promise<any> {
return Promise.resolve();
}
static associate(connection: Sequelize) {
const { Contact_Model, ContactAddress_Model } = connection.models;
Contact_Model.hasOne(ContactAddress_Model, {
as: "shippingAddress",
foreignKey: "customer_id",
onDelete: "CASCADE",
});
Contact_Model.hasOne(ContactAddress_Model, {
as: "billingAddress",
foreignKey: "customer_id",
onDelete: "CASCADE",
});
}
declare id: string;
declare tin: CreationOptional<string>;
declare company_name: CreationOptional<string>;
declare first_name: CreationOptional<string>;
declare last_name: CreationOptional<string>;
declare shippingAddress?: NonAttribute<ContactAddress_Model>;
declare billingAddress?: NonAttribute<ContactAddress_Model>;
}
export default (sequelize: Sequelize) => {
Contact_Model.init(
{
id: {
type: new DataTypes.UUID(),
primaryKey: true,
},
tin: {
type: new DataTypes.STRING(),
},
company_name: {
type: new DataTypes.STRING(),
},
first_name: {
type: new DataTypes.STRING(),
},
last_name: {
type: new DataTypes.STRING(),
},
},
{
sequelize,
tableName: "customers",
timestamps: false,
}
);
return Contact_Model;
};

View File

@ -0,0 +1,75 @@
import {
CreationOptional,
DataTypes,
ForeignKey,
InferAttributes,
InferCreationAttributes,
Model,
NonAttribute,
Sequelize,
} from "sequelize";
import { Contact_Model } from "./contact.model";
export type TCreationContactAddress_Attributes = InferCreationAttributes<
ContactAddress_Model,
{ omit: "customer" }
>;
export class ContactAddress_Model extends Model<
InferAttributes<ContactAddress_Model, { omit: "customer" }>,
TCreationContactAddress_Attributes
> {
// To avoid table creation
static async sync(): Promise<any> {
return Promise.resolve();
}
static associate(connection: Sequelize) {
const { Contact_Model, ContactAddress_Model } = connection.models;
ContactAddress_Model.belongsTo(Contact_Model, {
as: "customer",
foreignKey: "customer_id",
});
}
declare id: string;
declare customer_id: ForeignKey<Contact_Model["id"]>;
declare type: string;
declare street: CreationOptional<string>;
declare postal_code: CreationOptional<string>;
declare city: CreationOptional<string>;
declare province: CreationOptional<string>;
declare country: CreationOptional<string>;
declare phone: CreationOptional<string>;
declare email: CreationOptional<string>;
declare customer?: NonAttribute<Contact_Model>;
}
export default (sequelize: Sequelize) => {
ContactAddress_Model.init(
{
id: {
type: DataTypes.UUID,
primaryKey: true,
},
customer_id: new DataTypes.UUID(),
type: DataTypes.STRING(),
street: DataTypes.STRING(),
postal_code: DataTypes.STRING(),
city: DataTypes.STRING,
province: DataTypes.STRING,
country: DataTypes.STRING,
email: DataTypes.STRING,
phone: DataTypes.STRING,
},
{
sequelize,
tableName: "customer_addresses",
timestamps: false,
}
);
return ContactAddress_Model;
};

View File

@ -0,0 +1,10 @@
import { IInvoiceRepository } from "@contexts/invoicing/domain";
import { invoiceRepository } from "./invoice.repository";
export * from "./invoice.model";
export * from "./invoice.repository";
export const createInvoiceRepository = (): IInvoiceRepository => {
return invoiceRepository;
};

View File

@ -0,0 +1,146 @@
import {
CreationOptional,
DataTypes,
InferAttributes,
InferCreationAttributes,
Model,
NonAttribute,
Sequelize,
} from "sequelize";
import { SequelizeRevision } from "sequelize-revision";
import {
InvoiceItem_Model,
TCreationInvoiceItem_Model,
} from "./invoiceItem.model";
import {
InvoiceParticipant_Model,
TCreationInvoiceParticipant_Model,
} from "./invoiceParticipant.model";
export type TCreationInvoice_Model = InferCreationAttributes<
Invoice_Model,
{ omit: "items" | "participants" }
> & {
items: TCreationInvoiceItem_Model[];
participants: TCreationInvoiceParticipant_Model[];
};
export class Invoice_Model extends Model<
InferAttributes<Invoice_Model, { omit: "items" | "participants" }>,
InferCreationAttributes<Invoice_Model, { omit: "items" | "participants" }>
> {
static async trackRevision(
connection: Sequelize,
sequelizeRevision: SequelizeRevision<any>
) {
const {
Invoice_Model,
InvoiceItem_Model,
InvoiceParticipant_Model,
InvoiceParticipantAddress_Model,
} = connection.models;
sequelizeRevision.trackRevision(Invoice_Model);
sequelizeRevision.trackRevision(InvoiceItem_Model);
sequelizeRevision.trackRevision(InvoiceParticipant_Model);
sequelizeRevision.trackRevision(InvoiceParticipantAddress_Model);
}
static associate(connection: Sequelize) {
const { Invoice_Model, InvoiceItem_Model, InvoiceParticipant_Model } =
connection.models;
Invoice_Model.hasMany(InvoiceItem_Model, {
as: "items",
foreignKey: "invoice_id",
onDelete: "CASCADE",
});
Invoice_Model.hasMany(InvoiceParticipant_Model, {
as: "participants",
foreignKey: "invoice_id",
onDelete: "CASCADE",
});
}
declare id: string;
declare invoice_status: string;
declare invoice_series: CreationOptional<string>;
declare invoice_number: CreationOptional<string>;
declare issue_date: CreationOptional<string>;
declare operation_date: CreationOptional<string>;
declare invoice_language: string;
declare invoice_currency: string;
declare subtotal: number;
declare total: number;
declare items: NonAttribute<InvoiceItem_Model[]>;
declare participants: NonAttribute<InvoiceParticipant_Model[]>;
}
export default (sequelize: Sequelize) => {
Invoice_Model.init(
{
id: {
type: new DataTypes.UUID(),
primaryKey: true,
},
invoice_status: {
type: new DataTypes.STRING(),
allowNull: false, // Puede ser nulo
},
invoice_series: {
type: new DataTypes.STRING(),
allowNull: true, // Puede ser nulo
},
invoice_number: {
type: new DataTypes.STRING(),
allowNull: true, // Puede ser nulo
},
issue_date: {
type: new DataTypes.DATE(),
allowNull: true, // Puede ser nulo
},
operation_date: {
type: new DataTypes.DATE(),
allowNull: true, // Puede ser nulo
},
invoice_language: {
type: new DataTypes.STRING(),
allowNull: false,
},
invoice_currency: {
type: new DataTypes.STRING(),
allowNull: false,
},
subtotal: {
type: new DataTypes.BIGINT(),
allowNull: true,
},
total: {
type: new DataTypes.BIGINT(),
allowNull: true,
},
},
{
sequelize,
tableName: "invoices",
paranoid: true, // softs deletes
timestamps: true,
//version: true,
createdAt: "created_at",
updatedAt: "updated_at",
deletedAt: "deleted_at",
}
);
return Invoice_Model;
};

View File

@ -0,0 +1,88 @@
import { UniqueID } from "@common/domain";
import { Collection, Result } from "@common/helpers";
import { SequelizeRepository } from "@common/infrastructure";
import { Invoice } from "@contexts/invoices/domain";
import { IInvoiceRepository } from "@contexts/invoices/domain/repositories/invoice-repository.interface";
import { Transaction } from "sequelize";
import { IInvoiceMapper, invoiceMapper } from "../mappers/invoice.mapper";
import { InvoiceModel } from "./invoice.model";
class InvoiceRepository extends SequelizeRepository<Invoice> implements IInvoiceRepository {
private readonly _mapper!: IInvoiceMapper;
/**
* 🔹 Función personalizada para mapear errores de unicidad en autenticación
*/
private _customErrorMapper(error: Error): string | null {
if (error.name === "SequelizeUniqueConstraintError") {
return "Invoice with this email already exists";
}
return null;
}
constructor(mapper: IInvoiceMapper) {
super();
this._mapper = mapper;
}
async invoiceExists(id: UniqueID, transaction?: Transaction): Promise<Result<boolean, Error>> {
try {
const _invoice = await this._getById(InvoiceModel, id, {}, transaction);
return Result.ok(Boolean(id.equals(_invoice.id)));
} catch (error: any) {
return this._handleDatabaseError(error, this._customErrorMapper);
}
}
async findAll(transaction?: Transaction): Promise<Result<Collection<Invoice>, Error>> {
try {
const rawInvoices: any = await this._findAll(InvoiceModel, {}, transaction);
if (!rawInvoices === true) {
return Result.fail(new Error("Invoice with email not exists"));
}
return this._mapper.mapArrayToDomain(rawInvoices);
} catch (error: any) {
return this._handleDatabaseError(error, this._customErrorMapper);
}
}
async findById(id: UniqueID, transaction?: Transaction): Promise<Result<Invoice, Error>> {
try {
const rawInvoice: any = await this._getById(InvoiceModel, id, {}, transaction);
if (!rawInvoice === true) {
return Result.fail(new Error(`Invoice with id ${id.toString()} not exists`));
}
return this._mapper.mapToDomain(rawInvoice);
} catch (error: any) {
return this._handleDatabaseError(error, this._customErrorMapper);
}
}
async deleteById(id: UniqueID, transaction?: Transaction): Promise<Result<boolean, Error>> {
try {
this._deleteById(InvoiceModel, id);
return Result.ok<boolean>(true);
} catch (error: any) {
return this._handleDatabaseError(error, this._customErrorMapper);
}
}
async create(invoice: Invoice, transaction?: Transaction): Promise<void> {
const invoiceData = this._mapper.mapToPersistence(invoice);
await this._save(InvoiceModel, invoice.id, invoiceData, {}, transaction);
}
async update(invoice: Invoice, transaction?: Transaction): Promise<void> {
const invoiceData = this._mapper.mapToPersistence(invoice);
await this._save(InvoiceModel, invoice.id, invoiceData, {}, transaction);
}
}
const invoiceRepository = new InvoiceRepository(invoiceMapper);
export { invoiceRepository };

View File

@ -0,0 +1,114 @@
import {
CreationOptional,
DataTypes,
InferAttributes,
InferCreationAttributes,
Model,
NonAttribute,
Sequelize,
} from "sequelize";
import { Invoice_Model } from "./invoice.model";
export type TCreationInvoiceItem_Model = InferCreationAttributes<
InvoiceItem_Model,
{ omit: "invoice" }
>;
export class InvoiceItem_Model extends Model<
InferAttributes<InvoiceItem_Model, { omit: "invoice" }>,
InferCreationAttributes<InvoiceItem_Model, { omit: "invoice" }>
> {
static associate(connection: Sequelize) {
const { Invoice_Model, InvoiceItem_Model } = connection.models;
InvoiceItem_Model.belongsTo(Invoice_Model, {
as: "invoice",
foreignKey: "invoice_id",
onDelete: "CASCADE",
});
}
declare invoice_id: string;
declare item_id: string;
declare parent_id: CreationOptional<string>;
declare position: number;
declare item_type: string;
declare description: CreationOptional<string>;
declare quantity: CreationOptional<number>;
declare unit_price: CreationOptional<number>;
declare subtotal: CreationOptional<number>;
declare total: CreationOptional<number>;
declare invoice?: NonAttribute<Invoice_Model>;
}
export default (sequelize: Sequelize) => {
InvoiceItem_Model.init(
{
item_id: {
type: new DataTypes.UUID(),
primaryKey: true,
},
invoice_id: {
type: new DataTypes.UUID(),
primaryKey: true,
},
parent_id: {
type: new DataTypes.UUID(),
allowNull: true, // Puede ser nulo para elementos de nivel superior
},
position: {
type: new DataTypes.MEDIUMINT(),
autoIncrement: false,
allowNull: false,
},
item_type: {
type: new DataTypes.STRING(),
allowNull: false,
defaultValue: "simple",
},
description: {
type: new DataTypes.TEXT(),
allowNull: true,
},
quantity: {
type: DataTypes.BIGINT(),
allowNull: true,
},
unit_price: {
type: new DataTypes.BIGINT(),
allowNull: true,
},
/*tax_slug: {
type: new DataTypes.DECIMAL(3, 2),
allowNull: true,
},
tax_rate: {
type: new DataTypes.DECIMAL(3, 2),
allowNull: true,
},
tax_equalization: {
type: new DataTypes.DECIMAL(3, 2),
allowNull: true,
},*/
subtotal: {
type: new DataTypes.BIGINT(),
allowNull: true,
},
/*tax_amount: {
type: new DataTypes.BIGINT(),
allowNull: true,
},*/
total: {
type: new DataTypes.BIGINT(),
allowNull: true,
},
},
{
sequelize,
tableName: "invoice_items",
},
);
return InvoiceItem_Model;
};

View File

@ -0,0 +1,109 @@
import {
CreationOptional,
DataTypes,
InferAttributes,
InferCreationAttributes,
Model,
NonAttribute,
Sequelize,
} from "sequelize";
import { Invoice_Model } from "./invoice.model";
import {
InvoiceParticipantAddress_Model,
TCreationInvoiceParticipantAddress_Model,
} from "./invoiceParticipantAddress.model";
export type TCreationInvoiceParticipant_Model = InferCreationAttributes<
InvoiceParticipant_Model,
{ omit: "shippingAddress" | "billingAddress" | "invoice" }
> & {
billingAddress: TCreationInvoiceParticipantAddress_Model;
shippingAddress: TCreationInvoiceParticipantAddress_Model;
};
export class InvoiceParticipant_Model extends Model<
InferAttributes<
InvoiceParticipant_Model,
{ omit: "shippingAddress" | "billingAddress" | "invoice" }
>,
InferCreationAttributes<
InvoiceParticipant_Model,
{ omit: "shippingAddress" | "billingAddress" | "invoice" }
>
> {
static associate(connection: Sequelize) {
const {
Invoice_Model,
InvoiceParticipantAddress_Model,
InvoiceParticipant_Model,
} = connection.models;
InvoiceParticipant_Model.belongsTo(Invoice_Model, {
as: "invoice",
foreignKey: "invoice_id",
onDelete: "CASCADE",
});
InvoiceParticipant_Model.hasOne(InvoiceParticipantAddress_Model, {
as: "shippingAddress",
foreignKey: "participant_id",
onDelete: "CASCADE",
});
InvoiceParticipant_Model.hasOne(InvoiceParticipantAddress_Model, {
as: "billingAddress",
foreignKey: "participant_id",
onDelete: "CASCADE",
});
}
declare participant_id: string;
declare invoice_id: string;
declare tin: CreationOptional<string>;
declare company_name: CreationOptional<string>;
declare first_name: CreationOptional<string>;
declare last_name: CreationOptional<string>;
declare shippingAddress?: NonAttribute<InvoiceParticipantAddress_Model>;
declare billingAddress?: NonAttribute<InvoiceParticipantAddress_Model>;
declare invoice?: NonAttribute<Invoice_Model>;
}
export default (sequelize: Sequelize) => {
InvoiceParticipant_Model.init(
{
participant_id: {
type: new DataTypes.UUID(),
primaryKey: true,
},
invoice_id: {
type: new DataTypes.UUID(),
primaryKey: true,
},
tin: {
type: new DataTypes.STRING(),
allowNull: true,
},
company_name: {
type: new DataTypes.STRING(),
allowNull: true,
},
first_name: {
type: new DataTypes.STRING(),
allowNull: true,
},
last_name: {
type: new DataTypes.STRING(),
allowNull: true,
},
},
{
sequelize,
tableName: "invoice_participants",
timestamps: false,
},
);
return InvoiceParticipant_Model;
};

View File

@ -0,0 +1,98 @@
import {
CreationOptional,
DataTypes,
InferAttributes,
InferCreationAttributes,
Model,
NonAttribute,
Sequelize,
} from "sequelize";
import { InvoiceParticipant_Model } from "./invoiceParticipant.model";
export type TCreationInvoiceParticipantAddress_Model = InferCreationAttributes<
InvoiceParticipantAddress_Model,
{ omit: "participant" }
>;
export class InvoiceParticipantAddress_Model extends Model<
InferAttributes<InvoiceParticipantAddress_Model, { omit: "participant" }>,
InferCreationAttributes<
InvoiceParticipantAddress_Model,
{ omit: "participant" }
>
> {
static associate(connection: Sequelize) {
const { InvoiceParticipantAddress_Model, InvoiceParticipant_Model } =
connection.models;
InvoiceParticipantAddress_Model.belongsTo(InvoiceParticipant_Model, {
as: "participant",
foreignKey: "participant_id",
});
}
declare address_id: string;
declare participant_id: string;
declare type: string;
declare street: CreationOptional<string>;
declare postal_code: CreationOptional<string>;
declare city: CreationOptional<string>;
declare province: CreationOptional<string>;
declare country: CreationOptional<string>;
declare phone: CreationOptional<string>;
declare email: CreationOptional<string>;
declare participant?: NonAttribute<InvoiceParticipant_Model>;
}
export default (sequelize: Sequelize) => {
InvoiceParticipantAddress_Model.init(
{
address_id: {
type: new DataTypes.UUID(),
primaryKey: true,
},
participant_id: {
type: new DataTypes.UUID(),
primaryKey: true,
},
type: {
type: new DataTypes.STRING(),
allowNull: false,
},
street: {
type: new DataTypes.STRING(),
allowNull: true,
},
postal_code: {
type: new DataTypes.STRING(),
allowNull: true,
},
city: {
type: new DataTypes.STRING(),
allowNull: true,
},
province: {
type: new DataTypes.STRING(),
allowNull: true,
},
country: {
type: new DataTypes.STRING(),
allowNull: true,
},
email: {
type: new DataTypes.STRING(),
allowNull: true,
},
phone: {
type: new DataTypes.STRING(),
allowNull: true,
},
},
{
sequelize,
tableName: "invoice_participant_addresses",
},
);
return InvoiceParticipantAddress_Model;
};

View File

@ -0,0 +1,89 @@
import { UseCaseError } from "@/contexts/common/application/useCases";
import { ExpressController } from "@/contexts/common/infrastructure/express";
import {
CreateInvoiceResponseOrError,
CreateInvoiceUseCase,
} from "@/contexts/invoicing/application";
import {
ICreateInvoice_DTO,
ICreateInvoice_Response_DTO,
ensureCreateInvoice_DTOIsValid,
} from "@shared/contexts";
import { IServerError } from "@/contexts/common/domain/errors";
import { Invoice } from "@/contexts/invoicing/domain";
import { IInvoicingContext } from "../../../InvoicingContext";
import { ICreateInvoicePresenter } from "./presenter";
export class CreateInvoiceController extends ExpressController {
private useCase: CreateInvoiceUseCase;
private presenter: ICreateInvoicePresenter;
private context: IInvoicingContext;
constructor(
props: {
useCase: CreateInvoiceUseCase;
presenter: ICreateInvoicePresenter;
},
context: IInvoicingContext
) {
super();
const { useCase, presenter } = props;
this.useCase = useCase;
this.presenter = presenter;
this.context = context;
}
async executeImpl(): Promise<any> {
try {
const invoiceDTO: ICreateInvoice_DTO = this.req.body;
// Validaciones de DTO
const invoiceDTOOrError = ensureCreateInvoice_DTOIsValid(invoiceDTO);
if (invoiceDTOOrError.isFailure) {
return this.invalidInputError(invoiceDTOOrError.error.message);
}
// Llamar al caso de uso
const result: CreateInvoiceResponseOrError = await this.useCase.execute(
invoiceDTO
);
if (result.isFailure) {
const { error } = result;
switch (error.code) {
case UseCaseError.INVALID_REQUEST_PARAM:
return this.invalidInputError(error.message, error);
case UseCaseError.INVALID_INPUT_DATA:
return this.invalidInputError(error.message, error);
case UseCaseError.UNEXCEPTED_ERROR:
return this.internalServerError(error.message, error);
case UseCaseError.REPOSITORY_ERROR:
return this.conflictError(error, error.details);
case UseCaseError.NOT_FOUND_ERROR:
return this.notFoundError(error.message, error);
case UseCaseError.RESOURCE_ALREADY_EXITS:
return this.conflictError(error);
default:
return this.clientError(error.message);
}
}
const invoice = <Invoice>result.object;
return this.created<ICreateInvoice_Response_DTO>(
this.presenter.map(invoice, this.context)
);
} catch (error: unknown) {
return this.fail(error as IServerError);
}
}
}

View File

@ -0,0 +1,84 @@
import { CreateInvoiceUseCase } from "@/contexts/invoicing/application";
import {
ContactRepository,
IInvoicingContext,
InvoiceParticipantAddressRepository,
InvoiceParticipantRepository,
InvoiceRepository,
} from "../../..";
import {
createInvoiceMapper,
createInvoiceParticipantAddressMapper,
createInvoiceParticipantMapper,
} from "../../../mappers";
import { createContactMapper } from "../../../mappers/contact.mapper";
import { CreateInvoiceController } from "./CreateInvoiceController";
import { createInvoicePresenter } from "./presenter";
export const createInvoiceController = (context: IInvoicingContext) => {
const adapter = context.adapter;
const repoManager = context.repositoryManager;
repoManager.registerRepository(
"Invoice",
(params = { transaction: null }) => {
const { transaction } = params;
return new InvoiceRepository({
transaction,
adapter,
mapper: createInvoiceMapper(context),
});
},
);
repoManager.registerRepository(
"Participant",
(params = { transaction: null }) => {
const { transaction } = params;
return new InvoiceParticipantRepository({
transaction,
adapter,
mapper: createInvoiceParticipantMapper(context),
});
},
);
repoManager.registerRepository(
"ParticipantAddress",
(params = { transaction: null }) => {
const { transaction } = params;
return new InvoiceParticipantAddressRepository({
transaction,
adapter,
mapper: createInvoiceParticipantAddressMapper(context),
});
},
);
repoManager.registerRepository(
"Contact",
(params = { transaction: null }) => {
const { transaction } = params;
return new ContactRepository({
transaction,
adapter,
mapper: createContactMapper(context),
});
},
);
const createInvoiceUseCase = new CreateInvoiceUseCase(context);
return new CreateInvoiceController(
{
useCase: createInvoiceUseCase,
presenter: createInvoicePresenter,
},
context,
);
};

View File

@ -0,0 +1,39 @@
import { Invoice } from "@/contexts/invoicing/domain";
import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext";
import { ICreateInvoice_Response_DTO } from "@shared/contexts";
import { invoiceItemPresenter } from "./InvoiceItem.presenter";
import { InvoiceParticipantPresenter } from "./InvoiceParticipant.presenter";
export interface ICreateInvoicePresenter {
map: (
invoice: Invoice,
context: IInvoicingContext,
) => ICreateInvoice_Response_DTO;
}
export const createInvoicePresenter: ICreateInvoicePresenter = {
map: (
invoice: Invoice,
context: IInvoicingContext,
): ICreateInvoice_Response_DTO => {
return {
id: invoice.id.toString(),
invoice_status: invoice.status.toString(),
invoice_number: invoice.invoiceNumber.toString(),
invoice_series: invoice.invoiceSeries.toString(),
issue_date: invoice.issueDate.toISO8601(),
operation_date: invoice.operationDate.toISO8601(),
language_code: invoice.language.toString(),
currency: invoice.currency.toString(),
subtotal: invoice.calculateSubtotal().toObject(),
total: invoice.calculateTotal().toObject(),
//sender: {}, //await InvoiceParticipantPresenter(invoice.senderId, context),
recipient: InvoiceParticipantPresenter(invoice.recipient, context),
items: invoiceItemPresenter(invoice.items, context),
};
},
};

View File

@ -0,0 +1,19 @@
import { InvoiceItem } from "@/contexts/invoicing/domain/InvoiceItems";
import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext";
import { ICollection, IMoney_Response_DTO } from "@shared/contexts";
export const invoiceItemPresenter = (
items: ICollection<InvoiceItem>,
context: IInvoicingContext,
) =>
items.totalCount > 0
? items.items.map((item: InvoiceItem) => ({
description: item.description.toString(),
quantity: item.quantity.toString(),
unit_measure: "",
unit_price: item.unitPrice.toObject() as IMoney_Response_DTO,
subtotal: item.calculateSubtotal().toObject() as IMoney_Response_DTO,
tax_amount: item.calculateTaxAmount().toObject() as IMoney_Response_DTO,
total: item.calculateTotal().toObject() as IMoney_Response_DTO,
}))
: [];

View File

@ -0,0 +1,26 @@
import { IInvoiceParticipant } from "@/contexts/invoicing/domain";
import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext";
import { ICreateInvoice_Participant_Response_DTO } from "@shared/contexts";
import { InvoiceParticipantAddressPresenter } from "./InvoiceParticipantAddress.presenter";
export const InvoiceParticipantPresenter = (
participant: IInvoiceParticipant,
context: IInvoicingContext,
): ICreateInvoice_Participant_Response_DTO | undefined => {
return {
id: participant.id.toString(),
tin: participant.tin.toString(),
first_name: participant.firstName.toString(),
last_name: participant.lastName.toString(),
company_name: participant.companyName.toString(),
billing_address: InvoiceParticipantAddressPresenter(
participant.billingAddress!,
context,
),
shipping_address: InvoiceParticipantAddressPresenter(
participant.shippingAddress!,
context,
),
};
};

View File

@ -0,0 +1,19 @@
import { InvoiceParticipantAddress } from "@/contexts/invoicing/domain";
import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext";
import { ICreateInvoice_AddressParticipant_Response_DTO } from "@shared/contexts";
export const InvoiceParticipantAddressPresenter = (
address: InvoiceParticipantAddress,
context: IInvoicingContext,
): ICreateInvoice_AddressParticipant_Response_DTO => {
return {
id: address.id.toString(),
street: address.street.toString(),
city: address.city.toString(),
postal_code: address.postalCode.toString(),
province: address.province.toString(),
country: address.country.toString(),
email: address.email.toString(),
phone: address.phone.toString(),
};
};

View File

@ -0,0 +1 @@
export * from "./CreateInvoice.presenter";

View File

@ -0,0 +1,65 @@
import { UseCaseError } from "@/contexts/common/application/useCases";
import { IServerError } from "@/contexts/common/domain";
import { ExpressController } from "@/contexts/common/infrastructure/express";
import { DeleteInvoiceUseCase } from "@/contexts/invoicing/application";
import {
IDeleteInvoiceRequest_DTO,
RuleValidator,
UniqueID,
} from "@shared/contexts";
export class DeleteInvoiceController extends ExpressController {
private useCase: DeleteInvoiceUseCase;
constructor(props: { useCase: DeleteInvoiceUseCase }) {
super();
const { useCase } = props;
this.useCase = useCase;
}
async executeImpl(): Promise<any> {
const { invoiceId } = this.req.params;
if (
RuleValidator.validate(
RuleValidator.RULE_NOT_NULL_OR_UNDEFINED,
invoiceId,
).isFailure
) {
return this.invalidInputError("Invoice Id param is required!");
}
const idOrError = UniqueID.create(invoiceId);
if (idOrError.isFailure) {
return this.invalidInputError("Invalid invoice Id param!");
}
try {
const deleteInvoiceRequest: IDeleteInvoiceRequest_DTO = {
id: idOrError.object,
};
const result = await this.useCase.execute(deleteInvoiceRequest);
if (result.isFailure) {
const { error } = result;
switch (error.code) {
case UseCaseError.NOT_FOUND_ERROR:
return this.notFoundError("Invoice not found", error);
case UseCaseError.UNEXCEPTED_ERROR:
return this.internalServerError(result.error.message, result.error);
default:
return this.clientError(result.error.message);
}
}
return this.noContent();
} catch (e: unknown) {
return this.fail(e as IServerError);
}
}
}

View File

@ -0,0 +1,35 @@
import { RepositoryManager } from "@/contexts/common/domain";
import { createSequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
import { DeleteInvoiceUseCase } from "@/contexts/invoicing/application";
import { IInvoicingContext } from "../../..";
import { InvoiceRepository } from "../../../Invoice.repository";
import { createInvoiceMapper } from "../../../mappers";
import { DeleteInvoiceController } from "./DeleteInvoiceController";
export const createDeleteInvoiceController = (context: IInvoicingContext) => {
const adapter = createSequelizeAdapter();
const repoManager = RepositoryManager.getInstance();
repoManager.registerRepository(
"Invoice",
(params = { transaction: null }) => {
const { transaction } = params;
return new InvoiceRepository({
transaction,
adapter,
mapper: createInvoiceMapper(context),
});
},
);
const deleteInvoiceUseCase = new DeleteInvoiceUseCase({
adapter,
repositoryManager: repoManager,
});
return new DeleteInvoiceController({
useCase: deleteInvoiceUseCase,
});
};

View File

@ -0,0 +1,86 @@
import { UseCaseError } from "@/contexts/common/application/useCases";
import { IServerError } from "@/contexts/common/domain/errors";
import { ExpressController } from "@/contexts/common/infrastructure/express";
import {
IGetInvoice_Request_DTO,
IGetInvoice_Response_DTO,
RuleValidator,
UniqueID,
} from "@shared/contexts";
import { GetInvoiceUseCase } from "@/contexts/invoicing/application";
import { Invoice } from "@/contexts/invoicing/domain";
import { IInvoicingContext } from "../../../InvoicingContext";
import { IGetInvoicePresenter } from "./presenter";
export class GetInvoiceController extends ExpressController {
private useCase: GetInvoiceUseCase;
private presenter: IGetInvoicePresenter;
private context: IInvoicingContext;
constructor(
props: {
useCase: GetInvoiceUseCase;
presenter: IGetInvoicePresenter;
},
context: IInvoicingContext
) {
super();
const { useCase, presenter } = props;
this.useCase = useCase;
this.presenter = presenter;
this.context = context;
}
async executeImpl(): Promise<any> {
const { invoiceId } = this.req.params;
if (
RuleValidator.validate(
RuleValidator.RULE_NOT_NULL_OR_UNDEFINED,
invoiceId
).isFailure
) {
return this.invalidInputError("Invoice Id param is required!");
}
const idOrError = UniqueID.create(invoiceId);
if (idOrError.isFailure) {
return this.invalidInputError("Invalid invoice Id param!");
}
try {
const getInvoiceRequest: IGetInvoice_Request_DTO = {
id: idOrError.object,
};
const result = await this.useCase.execute(getInvoiceRequest);
if (result.isFailure) {
const { error } = result;
switch (error.code) {
case UseCaseError.NOT_FOUND_ERROR:
return this.notFoundError(
`Invoice with id ${idOrError.object} not found`,
error
);
case UseCaseError.UNEXCEPTED_ERROR:
return this.internalServerError(result.error.message, result.error);
default:
return this.clientError(result.error.message);
}
}
const invoice = <Invoice>result.object;
return this.ok<IGetInvoice_Response_DTO>(
await this.presenter.map(invoice, this.context)
);
} catch (e: unknown) {
return this.fail(e as IServerError);
}
}
}

View File

@ -0,0 +1,34 @@
import { GetInvoiceUseCase } from "@/contexts/invoicing/application";
import { InvoiceRepository } from "../../../Invoice.repository";
import { IInvoicingContext } from "../../../InvoicingContext";
import { createInvoiceMapper } from "../../../mappers";
import { GetInvoiceController } from "./GetInvoiceController";
import { getInvoicePresenter } from "./presenter";
export const createGetInvoiceController = (context: IInvoicingContext) => {
const adapter = context.adapter;
const repoManager = context.repositoryManager;
repoManager.registerRepository(
"Invoice",
(params = { transaction: null }) => {
const { transaction } = params;
return new InvoiceRepository({
transaction,
adapter,
mapper: createInvoiceMapper(context),
});
},
);
const getInvoiceUseCase = new GetInvoiceUseCase(context);
return new GetInvoiceController(
{
useCase: getInvoiceUseCase,
presenter: getInvoicePresenter,
},
context,
);
};

View File

@ -0,0 +1,57 @@
import { Invoice } from "@/contexts/invoicing/domain";
import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext";
import { IGetInvoice_Response_DTO } from "@shared/contexts";
import { invoiceItemPresenter } from "./InvoiceItem.presenter";
import { InvoiceParticipantPresenter } from "./InvoiceParticipant.presenter";
export interface IGetInvoicePresenter {
map: (
invoice: Invoice,
context: IInvoicingContext,
) => Promise<IGetInvoice_Response_DTO>;
}
export const getInvoicePresenter: IGetInvoicePresenter = {
map: async (
invoice: Invoice,
context: IInvoicingContext,
): Promise<IGetInvoice_Response_DTO> => {
return {
id: invoice.id.toString(),
invoice_status: invoice.status.toString(),
invoice_number: invoice.invoiceNumber.toString(),
invoice_series: invoice.invoiceSeries.toString(),
issue_date: invoice.issueDate.toISO8601(),
operation_date: invoice.operationDate.toISO8601(),
language_code: invoice.language.toString(),
currency: invoice.currency.toString(),
subtotal: invoice.calculateSubtotal().toObject(),
total: invoice.calculateTotal().toObject(),
//sender: {}, //await InvoiceParticipantPresenter(invoice.senderId, context),
recipient: await InvoiceParticipantPresenter(invoice.recipient, context),
items: invoiceItemPresenter(invoice.items, context),
payment_term: {
payment_type: "",
due_date: "",
},
due_amount: {
currency: invoice.currency.toString(),
precision: 2,
amount: 0,
},
custom_fields: [],
metadata: {
create_time: "",
last_updated_time: "",
delete_time: "",
},
};
},
};

View File

@ -0,0 +1,19 @@
import { InvoiceItem } from "@/contexts/invoicing/domain/InvoiceItems";
import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext";
import { ICollection, IMoney_Response_DTO } from "@shared/contexts";
export const invoiceItemPresenter = (
items: ICollection<InvoiceItem>,
context: IInvoicingContext,
) =>
items.totalCount > 0
? items.items.map((item: InvoiceItem) => ({
description: item.description.toString(),
quantity: item.quantity.toString(),
unit_measure: "",
unit_price: item.unitPrice.toObject() as IMoney_Response_DTO,
subtotal: item.calculateSubtotal().toObject() as IMoney_Response_DTO,
tax_amount: item.calculateTaxAmount().toObject() as IMoney_Response_DTO,
total: item.calculateTotal().toObject() as IMoney_Response_DTO,
}))
: [];

View File

@ -0,0 +1,26 @@
import { IInvoiceParticipant } from "@/contexts/invoicing/domain";
import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext";
import { ICreateInvoice_Participant_Response_DTO } from "@shared/contexts";
import { InvoiceParticipantAddressPresenter } from "./InvoiceParticipantAddress.presenter";
export const InvoiceParticipantPresenter = async (
participant: IInvoiceParticipant,
context: IInvoicingContext,
): Promise<ICreateInvoice_Participant_Response_DTO | undefined> => {
return {
id: participant.id.toString(),
tin: participant.tin.toString(),
first_name: participant.firstName.toString(),
last_name: participant.lastName.toString(),
company_name: participant.companyName.toString(),
billing_address: await InvoiceParticipantAddressPresenter(
participant.billingAddress!,
context,
),
shipping_address: await InvoiceParticipantAddressPresenter(
participant.shippingAddress!,
context,
),
};
};

View File

@ -0,0 +1,19 @@
import { InvoiceParticipantAddress } from "@/contexts/invoicing/domain";
import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext";
import { ICreateInvoice_AddressParticipant_Response_DTO } from "@shared/contexts";
export const InvoiceParticipantAddressPresenter = async (
address: InvoiceParticipantAddress,
context: IInvoicingContext,
): Promise<ICreateInvoice_AddressParticipant_Response_DTO> => {
return {
id: address.id.toString(),
street: address.street.toString(),
city: address.city.toString(),
postal_code: address.postalCode.toString(),
province: address.province.toString(),
country: address.country.toString(),
email: address.email.toString(),
phone: address.phone.toString(),
};
};

View File

@ -0,0 +1 @@
export * from "./GetInvoice.presenter";

View File

@ -0,0 +1,5 @@
export * from "./create-invoice";
export * from "./delete-invoice";
export * from "./get-invoice";
export * from "./list-invoices";
export * from "./update-invoice";

View File

@ -0,0 +1,13 @@
import { SequelizeTransactionManager } from "@common/infrastructure";
import { ListInvoicesController } from "./list-invoices.controller";
import { listInvoicesPresenter } from "./presenter";
export const listInvoicesController = () => {
const transactionManager = new SequelizeTransactionManager();
const invoiceService = new InvoiceService(invoiceRepository);
const useCase = new ListInvoicesUseCase(invoiceService, transactionManager);
const presenter = listInvoicesPresenter;
return new ListInvoicesController(useCase, presenter);
};

View File

@ -0,0 +1,47 @@
import { ListInvoicesUseCase } from "@/contexts/invoicing/application";
import { ExpressController } from "@common/presentation";
import { IListInvoicesPresenter } from "./presenter";
export class ListInvoicesController extends ExpressController {
public constructor(
private readonly listInvoices: ListInvoicesUseCase,
private readonly presenter: IListInvoicesPresenter
) {
super();
}
protected async executeImpl() {
const { query } = this.req;
//const queryCriteria: IQueryCriteria = QueryCriteriaService.parse(query);
const invoicesOrError = await this.listInvoices.execute(/* queryCriteria */);
if (invoicesOrError.isFailure) {
return this.handleError(invoicesOrError.error);
}
return this.ok(
this.presenter.toDTO(
invoicesOrError.data /*, {
page: queryCriteria.pagination.offset,
limit: queryCriteria.pagination.limit,
}*/
)
);
}
private handleError(error: Error) {
const message = error.message;
if (
message.includes("Database connection lost") ||
message.includes("Database request timed out")
) {
return this.unavailableError(
"Database service is currently unavailable. Please try again later."
);
}
return this.conflictError(message);
}
}

View File

@ -0,0 +1,22 @@
import { IInvoiceParticipant } from "@/contexts/invoicing/domain";
import { IListInvoice_Participant_Response_DTO } from "@shared/contexts";
import { InvoiceParticipantAddressPresenter } from "./InvoiceParticipantAddress.presenter";
export const InvoiceParticipantPresenter = (
participant: IInvoiceParticipant,
): IListInvoice_Participant_Response_DTO => {
return {
participant_id: participant?.id?.toString(),
tin: participant?.tin?.toString(),
first_name: participant?.firstName?.toString(),
last_name: participant?.lastName?.toString(),
company_name: participant?.companyName?.toString(),
billing_address: InvoiceParticipantAddressPresenter(
participant?.billingAddress!,
),
shipping_address: InvoiceParticipantAddressPresenter(
participant?.shippingAddress!,
),
};
};

View File

@ -0,0 +1,17 @@
import { InvoiceParticipantAddress } from "@/contexts/invoicing/domain";
import { IListInvoice_AddressParticipant_Response_DTO } from "@shared/contexts";
export const InvoiceParticipantAddressPresenter = (
address: InvoiceParticipantAddress,
): IListInvoice_AddressParticipant_Response_DTO => {
return {
address_id: address?.id.toString(),
street: address?.street.toString(),
city: address?.city.toString(),
postal_code: address?.postalCode.toString(),
province: address?.province.toString(),
country: address?.country.toString(),
email: address?.email.toString(),
phone: address?.phone.toString(),
};
};

View File

@ -0,0 +1 @@
export * from "./list-invoices.presenter";

View File

@ -0,0 +1,33 @@
import { Collection } from "@common/helpers";
import { Invoice } from "@contexts/invoicing/domain";
import { IListInvoicesResponseDTO } from "@contexts/invoicing/presentation/dto";
export interface IListInvoicesPresenter {
toDTO: (invoices: Collection<Invoice>) => IListInvoicesResponseDTO[];
}
export const listInvoicesPresenter: IListInvoicesPresenter = {
toDTO: (invoices: Collection<Invoice>): IListInvoicesResponseDTO[] => {
return invoices.map((invoice) => {
const result = {
id: invoice.id.toPrimitive(),
invoice_status: invoice.status.toPrimitive(),
invoice_number: invoice.invoiceNumber.toPrimitive(),
invoice_series: invoice.invoiceSeries.toPrimitive(),
issue_date: invoice.issueDate.toPrimitive()!,
operation_date: invoice.operationDate.toPrimitive()!,
language_code: invoice.language.toPrimitive(),
currency: invoice.currency.toPrimitive(),
subtotal: invoice.calculateSubtotal().toObject(),
total: invoice.calculateTotal().toObject(),
//recipient: InvoiceParticipantPresenter(invoice.recipient),
};
console.timeEnd("listInvoicesPresenter.map");
return result;
});
},
};

View File

@ -0,0 +1,91 @@
import { UseCaseError } from "@/contexts/common/application/useCases";
import { IServerError } from "@/contexts/common/domain";
import { ExpressController } from "@/contexts/common/infrastructure/express";
import {
IUpdateInvoice_DTO,
IUpdateInvoice_Response_DTO,
RuleValidator,
UniqueID,
} from "@shared/contexts";
import { Invoice } from "@/contexts/invoicing/domain";
import { IInvoicingContext } from "../../../InvoicingContext";
import { UpdateInvoiceUseCase } from "@/contexts/invoicing/application";
import { IUpdateInvoicePresenter } from "./presenter";
export class UpdateInvoiceController extends ExpressController {
private useCase: UpdateInvoiceUseCase;
private presenter: IUpdateInvoicePresenter;
private context: IInvoicingContext;
constructor(
props: {
useCase: UpdateInvoiceUseCase;
presenter: IUpdateInvoicePresenter;
},
context: IInvoicingContext,
) {
super();
const { useCase, presenter } = props;
this.useCase = useCase;
this.presenter = presenter;
this.context = context;
}
async executeImpl(): Promise<any> {
const { invoiceId } = this.req.params;
const request: IUpdateInvoice_DTO = this.req.body;
if (
RuleValidator.validate(
RuleValidator.RULE_NOT_NULL_OR_UNDEFINED,
invoiceId,
).isFailure
) {
return this.invalidInputError("Invoice Id param is required!");
}
const idOrError = UniqueID.create(invoiceId);
if (idOrError.isFailure) {
return this.invalidInputError("Invalid invoice Id param!");
}
try {
const result = await this.useCase.execute({
id: idOrError.object,
data: request,
});
if (result.isFailure) {
const { error } = result;
switch (error.code) {
case UseCaseError.NOT_FOUND_ERROR:
return this.notFoundError("Invoice not found", error);
case UseCaseError.INVALID_INPUT_DATA:
return this.invalidInputError(error.message);
case UseCaseError.UNEXCEPTED_ERROR:
return this.internalServerError(result.error.message, result.error);
case UseCaseError.REPOSITORY_ERROR:
return this.conflictError(result.error, result.error.details);
default:
return this.clientError(result.error.message);
}
}
const invoice = <Invoice>result.object;
return this.ok<IUpdateInvoice_Response_DTO>(
this.presenter.map(invoice, this.context),
);
} catch (e: unknown) {
return this.fail(e as IServerError);
}
}
}

View File

@ -0,0 +1,83 @@
import { UpdateInvoiceUseCase } from "@/contexts/invoicing/application";
import {
ContactRepository,
IInvoicingContext,
InvoiceParticipantAddressRepository,
InvoiceParticipantRepository,
} from "../../..";
import { InvoiceRepository } from "../../../Invoice.repository";
import {
createContactMapper,
createInvoiceMapper,
createInvoiceParticipantAddressMapper,
createInvoiceParticipantMapper,
} from "../../../mappers";
import { UpdateInvoiceController } from "./UpdateInvoiceController";
import { updateInvoicePresenter } from "./presenter";
export const updateInvoiceController = (context: IInvoicingContext) => {
const adapter = context.adapter;
const repoManager = context.repositoryManager;
repoManager.registerRepository(
"Invoice",
(params = { transaction: null }) => {
const { transaction } = params;
return new InvoiceRepository({
transaction,
adapter,
mapper: createInvoiceMapper(context),
});
},
);
repoManager.registerRepository(
"Participant",
(params = { transaction: null }) => {
const { transaction } = params;
return new InvoiceParticipantRepository({
transaction,
adapter,
mapper: createInvoiceParticipantMapper(context),
});
},
);
repoManager.registerRepository(
"ParticipantAddress",
(params = { transaction: null }) => {
const { transaction } = params;
return new InvoiceParticipantAddressRepository({
transaction,
adapter,
mapper: createInvoiceParticipantAddressMapper(context),
});
},
);
repoManager.registerRepository(
"Contact",
(params = { transaction: null }) => {
const { transaction } = params;
return new ContactRepository({
transaction,
adapter,
mapper: createContactMapper(context),
});
},
);
const updateInvoiceUseCase = new UpdateInvoiceUseCase(context);
return new UpdateInvoiceController(
{
useCase: updateInvoiceUseCase,
presenter: updateInvoicePresenter,
},
context,
);
};

View File

@ -0,0 +1,19 @@
import { InvoiceItem } from "@/contexts/invoicing/domain/InvoiceItems";
import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext";
import { ICollection, IMoney_Response_DTO } from "@shared/contexts";
export const invoiceItemPresenter = (
items: ICollection<InvoiceItem>,
context: IInvoicingContext,
) =>
items.totalCount > 0
? items.items.map((item: InvoiceItem) => ({
description: item.description.toString(),
quantity: item.quantity.toString(),
unit_measure: "",
unit_price: item.unitPrice.toObject() as IMoney_Response_DTO,
subtotal: item.calculateSubtotal().toObject() as IMoney_Response_DTO,
tax_amount: item.calculateTaxAmount().toObject() as IMoney_Response_DTO,
total: item.calculateTotal().toObject() as IMoney_Response_DTO,
}))
: [];

View File

@ -0,0 +1,26 @@
import { IInvoiceParticipant } from "@/contexts/invoicing/domain";
import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext";
import { IUpdateInvoice_Participant_Response_DTO } from "@shared/contexts";
import { InvoiceParticipantAddressPresenter } from "./InvoiceParticipantAddress.presenter";
export const InvoiceParticipantPresenter = (
participant: IInvoiceParticipant,
context: IInvoicingContext,
): IUpdateInvoice_Participant_Response_DTO | undefined => {
return {
id: participant.id.toString(),
tin: participant.tin.toString(),
first_name: participant.firstName.toString(),
last_name: participant.lastName.toString(),
company_name: participant.companyName.toString(),
billing_address: InvoiceParticipantAddressPresenter(
participant.billingAddress!,
context,
),
shipping_address: InvoiceParticipantAddressPresenter(
participant.shippingAddress!,
context,
),
};
};

View File

@ -0,0 +1,19 @@
import { InvoiceParticipantAddress } from "@/contexts/invoicing/domain";
import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext";
import { IUpdateInvoice_AddressParticipant_Response_DTO } from "@shared/contexts";
export const InvoiceParticipantAddressPresenter = (
address: InvoiceParticipantAddress,
context: IInvoicingContext,
): IUpdateInvoice_AddressParticipant_Response_DTO => {
return {
id: address.id.toString(),
street: address.street.toString(),
city: address.city.toString(),
postal_code: address.postalCode.toString(),
province: address.province.toString(),
country: address.country.toString(),
email: address.email.toString(),
phone: address.phone.toString(),
};
};

View File

@ -0,0 +1,39 @@
import { Invoice } from "@/contexts/invoicing/domain";
import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext";
import { IUpdateInvoice_Response_DTO } from "@shared/contexts";
import { invoiceItemPresenter } from "./InvoiceItem.presenter";
import { InvoiceParticipantPresenter } from "./InvoiceParticipant.presenter";
export interface IUpdateInvoicePresenter {
map: (
invoice: Invoice,
context: IInvoicingContext,
) => IUpdateInvoice_Response_DTO;
}
export const updateInvoicePresenter: IUpdateInvoicePresenter = {
map: (
invoice: Invoice,
context: IInvoicingContext,
): IUpdateInvoice_Response_DTO => {
return {
id: invoice.id.toString(),
invoice_status: invoice.status.toString(),
invoice_number: invoice.invoiceNumber.toString(),
invoice_series: invoice.invoiceSeries.toString(),
issue_date: invoice.issueDate.toISO8601(),
operation_date: invoice.operationDate.toISO8601(),
language_code: invoice.language.toString(),
currency: invoice.currency.toString(),
subtotal: invoice.calculateSubtotal().toObject(),
total: invoice.calculateTotal().toObject(),
//sender: {}, //await InvoiceParticipantPresenter(invoice.senderId, context),
recipient: InvoiceParticipantPresenter(invoice.recipient, context),
items: invoiceItemPresenter(invoice.items, context),
};
},
};

View File

@ -0,0 +1 @@
export * from "./UpdateInvoice.presenter";

View File

@ -0,0 +1,3 @@
export * from "./invoices.request.dto";
export * from "./invoices.response.dto";
export * from "./invoices.schemas";

View File

@ -0,0 +1,52 @@
export interface IListInvoicesRequestDTO {}
export interface ICreateInvoiceRequestDTO {
id: string;
is_freelancer: boolean;
name: string;
trade_name: string;
tin: string;
street: string;
city: string;
state: string;
postal_code: string;
country: string;
email: string;
phone: string;
fax: string;
website: string;
legal_record: string;
default_tax: number;
lang_code: string;
currency_code: string;
logo: string;
}
export interface IUpdateInvoiceRequestDTO {
is_freelancer: boolean;
name: string;
trade_name: string;
tin: string;
street: string;
city: string;
state: string;
postal_code: string;
country: string;
email: string;
phone: string;
fax: string;
website: string;
legal_record: string;
default_tax: number;
lang_code: string;
currency_code: string;
logo: string;
}

View File

@ -0,0 +1,114 @@
export interface IListInvoicesResponseDTO {
id: string;
is_freelancer: boolean;
name: string;
trade_name: string;
tin: string;
street: string;
city: string;
state: string;
postal_code: string;
country: string;
email: string;
phone: string;
fax: string;
website: string;
legal_record: string;
default_tax: number;
status: string;
lang_code: string;
currency_code: string;
logo: string;
}
export interface IGetInvoiceResponseDTO {
id: string;
is_freelancer: boolean;
name: string;
trade_name: string;
tin: string;
street: string;
city: string;
state: string;
postal_code: string;
country: string;
email: string;
phone: string;
fax: string;
website: string;
legal_record: string;
default_tax: number;
status: string;
lang_code: string;
currency_code: string;
logo: string;
}
export interface ICreateInvoiceResponseDTO {
id: string;
is_freelancer: boolean;
name: string;
trade_name: string;
tin: string;
street: string;
city: string;
state: string;
postal_code: string;
country: string;
email: string;
phone: string;
fax: string;
website: string;
legal_record: string;
default_tax: number;
status: string;
lang_code: string;
currency_code: string;
logo: string;
}
// Inferir el tipo en TypeScript desde el esquema Zod
//export type IUpdateAcccountResponseDTO = z.infer<typeof IUpdateAcccountResponseDTOSchema>;
export interface IUpdateInvoiceResponseDTO {
id: string;
is_freelancer: boolean;
name: string;
trade_name: string;
tin: string;
street: string;
city: string;
state: string;
postal_code: string;
country: string;
email: string;
phone: string;
fax: string;
website: string;
legal_record: string;
default_tax: number;
status: string;
lang_code: string;
currency_code: string;
logo: string;
}

View File

@ -0,0 +1,11 @@
import { z } from "zod";
export const ListInvoicesRequestSchema = z.object({});
export const IGetInvoiceRequestSchema = z.object({});
export const ICreateInvoiceRequestSchema = z.object({});
export const IUpdateInvoiceRequestSchema = z.object({});
export const IDeleteInvoiceRequestSchema = z.object({});

View File

@ -1,9 +1,9 @@
import { validateRequestDTO } from "@common/presentation";
import {
ICreateAcccountResponseDTOSchema,
IGetAcccountResponseDTOSchema,
IUpdateAcccountResponseDTOSchema,
ListAccountsSchema,
ICreateAccountRequestSchema,
IGetAccountRequestSchema,
IUpdateAccountRequestSchema,
ListAccountsRequestSchema,
} from "@contexts/accounts/presentation";
import {
createAccountController,
@ -19,7 +19,7 @@ export const accountsRouter = (appRouter: Router) => {
routes.get(
"/",
validateRequestDTO(ListAccountsSchema),
validateRequestDTO(ListAccountsRequestSchema),
checkTabContext,
//checkUser,
(req: Request, res: Response, next: NextFunction) => {
@ -28,8 +28,8 @@ export const accountsRouter = (appRouter: Router) => {
);
routes.get(
"/:accountId",
validateRequestDTO(IGetAcccountResponseDTOSchema),
"/:invoiceId",
validateRequestDTO(IGetAccountRequestSchema),
checkTabContext,
//checkUser,
(req: Request, res: Response, next: NextFunction) => {
@ -39,7 +39,7 @@ export const accountsRouter = (appRouter: Router) => {
routes.post(
"/",
validateRequestDTO(ICreateAcccountResponseDTOSchema),
validateRequestDTO(ICreateAccountRequestSchema),
checkTabContext,
//checkUser,
(req: Request, res: Response, next: NextFunction) => {
@ -48,8 +48,8 @@ export const accountsRouter = (appRouter: Router) => {
);
routes.put(
"/:accountId",
validateRequestDTO(IUpdateAcccountResponseDTOSchema),
"/:invoiceId",
validateRequestDTO(IUpdateAccountRequestSchema),
checkTabContext,
//checkUser,
(req: Request, res: Response, next: NextFunction) => {

Some files were not shown because too many files have changed in this diff Show More