From 7def4f7dc58a640cc5f93d7c121fbe0e04e995c5 Mon Sep 17 00:00:00 2001 From: david Date: Tue, 18 Mar 2025 09:05:00 +0100 Subject: [PATCH] . --- .../domain/services/account-service.test.ts | 3 + ....validation.dto.ts => accounts.schemas.ts} | 36 +- .../accounts/presentation/dto/index.ts | 2 +- .../application/create-invoice.use-case.ts | 109 +++++ .../application/delete-invoice.use-case.ts | 23 + .../application/get-invoice.use-case.ts | 23 + .../contexts/invoicing/application/index.ts | 5 + .../application/list-invoices.use-case.ts | 22 + .../invoicing/application/services/index.ts | 2 + .../services/participantAddressFinder.ts | 70 +++ .../application/services/participantFinder.ts | 20 + .../application/update-invoice.use-case.ts | 398 ++++++++++++++++++ .../invoicing/domain/Contact/Contact.ts | 64 +++ .../Contact/IContactRepository.interface.ts | 16 + .../invoicing/domain/Contact/index.ts | 2 + .../ContactAddress.ts/ContactAddress.ts | 22 + .../domain/ContactAddress.ts/index.ts | 1 + .../domain/InvoiceItems/InvoiceBaseItem.ts | 64 +++ .../domain/InvoiceItems/InvoiceBudgetItem.ts | 15 + .../domain/InvoiceItems/InvoiceItems.ts | 21 + .../domain/InvoiceItems/InvoiceSimpleItem.ts | 23 + .../invoicing/domain/InvoiceItems/index.ts | 7 + ...IInvoiceParticipantRepository.interface.ts | 10 + .../InvoiceParticipant/InvoiceParticipant.ts | 64 +++ .../domain/InvoiceParticipant/Recipient.ts | 3 + .../domain/InvoiceParticipant/Supplier.ts | 3 + .../domain/InvoiceParticipant/index.ts | 4 + ...ParticipantAddress.repository.interface.ts | 10 + .../InvoiceParticipantAddress.ts | 22 + .../InvoiceParticipantBillingAddress.ts | 32 ++ .../InvoiceParticipantShippingAddress.ts | 32 ++ .../domain/InvoiceParticipantAddress/index.ts | 4 + .../invoicing/domain/aggregates/index.ts | 1 + .../invoicing/domain/aggregates/invoice.ts | 208 +++++++++ .../src/contexts/invoicing/domain/index.ts | 6 + .../invoicing/domain/repositories/index.ts | 1 + .../invoice-repository.interface.ts | 12 + .../invoicing/domain/services/index.ts | 2 + .../services/invoice-service.interface.ts | 22 + .../domain/services/invoice.service.ts | 94 +++++ .../invoicing/domain/value-objects/index.ts | 1 + .../domain/value-objects/invoice-number.ts | 41 ++ .../domain/value-objects/invoice-serie.ts | 41 ++ .../domain/value-objects/invoice-status.ts | 76 ++++ .../intrastructure/Contact.repository.ts | 77 ++++ .../intrastructure/Invoice.repository.ts | 101 +++++ .../InvoiceParticipant.repository.ts | 57 +++ .../InvoiceParticipantAddress.repository.ts | 44 ++ .../intrastructure/InvoicingContext.ts | 43 ++ .../invoicing/intrastructure/index.ts | 2 + .../intrastructure/mappers/contact.mapper.ts | 83 ++++ .../mappers/contactAddress.mapper.ts | 65 +++ .../invoicing/intrastructure/mappers/index.ts | 6 + .../intrastructure/mappers/invoice.mapper.ts | 115 +++++ .../mappers/invoiceItem.mapper.ts | 87 ++++ .../mappers/invoiceParticipant.mapper.ts | 129 ++++++ .../invoiceParticipantAddress.mapper.ts | 94 +++++ .../intrastructure/sequelize/contact.model.ts | 93 ++++ .../sequelize/contactAddress.model.ts | 75 ++++ .../intrastructure/sequelize/index.ts | 10 + .../intrastructure/sequelize/invoice.model.ts | 146 +++++++ .../sequelize/invoice.repository.ts | 88 ++++ .../sequelize/invoiceItem.model.ts | 114 +++++ .../sequelize/invoiceParticipant.model.ts | 109 +++++ .../invoiceParticipantAddress.model.ts | 98 +++++ .../create-invoice/CreateInvoiceController.ts | 89 ++++ .../controllers/create-invoice/index.ts | 84 ++++ .../presenter/CreateInvoice.presenter.ts | 39 ++ .../presenter/InvoiceItem.presenter.ts | 19 + .../presenter/InvoiceParticipant.presenter.ts | 26 ++ .../InvoiceParticipantAddress.presenter.ts | 19 + .../create-invoice/presenter/index.ts | 1 + .../delete-invoice/DeleteInvoiceController.ts | 65 +++ .../controllers/delete-invoice/index.ts | 35 ++ .../get-invoice/GetInvoiceController.ts | 86 ++++ .../controllers/get-invoice/index.ts | 34 ++ .../presenter/GetInvoice.presenter.ts | 57 +++ .../presenter/InvoiceItem.presenter.ts | 19 + .../presenter/InvoiceParticipant.presenter.ts | 26 ++ .../InvoiceParticipantAddress.presenter.ts | 19 + .../get-invoice/presenter/index.ts | 1 + .../presentation/controllers/index.ts | 5 + .../controllers/list-invoices/index.ts | 13 + .../list-invoices/list-invoices.controller.ts | 47 +++ .../presenter/InvoiceParticipant.presenter.ts | 22 + .../InvoiceParticipantAddress.presenter.ts | 17 + .../list-invoices/presenter/index.ts | 1 + .../presenter/list-invoices.presenter.ts | 33 ++ .../update-invoice/UpdateInvoiceController.ts | 91 ++++ .../controllers/update-invoice/index.ts | 83 ++++ .../presenter/InvoiceItem.presenter.ts | 19 + .../presenter/InvoiceParticipant.presenter.ts | 26 ++ .../InvoiceParticipantAddress.presenter.ts | 19 + .../presenter/UpdateInvoice.presenter.ts | 39 ++ .../update-invoice/presenter/index.ts | 1 + .../invoicing/presentation/dto/index.ts | 3 + .../presentation/dto/invoices.request.dto.ts | 52 +++ .../presentation/dto/invoices.response.dto.ts | 114 +++++ .../presentation/dto/invoices.schemas.ts | 11 + apps/server/src/routes/accounts.routes.ts | 20 +- apps/server/src/routes/invoicingRoutes.ts | 66 +++ 101 files changed, 4533 insertions(+), 41 deletions(-) rename apps/server/src/contexts/accounts/presentation/dto/{accounts.validation.dto.ts => accounts.schemas.ts} (59%) create mode 100644 apps/server/src/contexts/invoicing/application/create-invoice.use-case.ts create mode 100644 apps/server/src/contexts/invoicing/application/delete-invoice.use-case.ts create mode 100644 apps/server/src/contexts/invoicing/application/get-invoice.use-case.ts create mode 100644 apps/server/src/contexts/invoicing/application/index.ts create mode 100644 apps/server/src/contexts/invoicing/application/list-invoices.use-case.ts create mode 100644 apps/server/src/contexts/invoicing/application/services/index.ts create mode 100644 apps/server/src/contexts/invoicing/application/services/participantAddressFinder.ts create mode 100644 apps/server/src/contexts/invoicing/application/services/participantFinder.ts create mode 100644 apps/server/src/contexts/invoicing/application/update-invoice.use-case.ts create mode 100644 apps/server/src/contexts/invoicing/domain/Contact/Contact.ts create mode 100644 apps/server/src/contexts/invoicing/domain/Contact/IContactRepository.interface.ts create mode 100644 apps/server/src/contexts/invoicing/domain/Contact/index.ts create mode 100644 apps/server/src/contexts/invoicing/domain/ContactAddress.ts/ContactAddress.ts create mode 100644 apps/server/src/contexts/invoicing/domain/ContactAddress.ts/index.ts create mode 100644 apps/server/src/contexts/invoicing/domain/InvoiceItems/InvoiceBaseItem.ts create mode 100644 apps/server/src/contexts/invoicing/domain/InvoiceItems/InvoiceBudgetItem.ts create mode 100644 apps/server/src/contexts/invoicing/domain/InvoiceItems/InvoiceItems.ts create mode 100644 apps/server/src/contexts/invoicing/domain/InvoiceItems/InvoiceSimpleItem.ts create mode 100644 apps/server/src/contexts/invoicing/domain/InvoiceItems/index.ts create mode 100644 apps/server/src/contexts/invoicing/domain/InvoiceParticipant/IInvoiceParticipantRepository.interface.ts create mode 100644 apps/server/src/contexts/invoicing/domain/InvoiceParticipant/InvoiceParticipant.ts create mode 100644 apps/server/src/contexts/invoicing/domain/InvoiceParticipant/Recipient.ts create mode 100644 apps/server/src/contexts/invoicing/domain/InvoiceParticipant/Supplier.ts create mode 100644 apps/server/src/contexts/invoicing/domain/InvoiceParticipant/index.ts create mode 100644 apps/server/src/contexts/invoicing/domain/InvoiceParticipantAddress/InvoiceParticipantAddress.repository.interface.ts create mode 100644 apps/server/src/contexts/invoicing/domain/InvoiceParticipantAddress/InvoiceParticipantAddress.ts create mode 100644 apps/server/src/contexts/invoicing/domain/InvoiceParticipantAddress/InvoiceParticipantBillingAddress.ts create mode 100644 apps/server/src/contexts/invoicing/domain/InvoiceParticipantAddress/InvoiceParticipantShippingAddress.ts create mode 100644 apps/server/src/contexts/invoicing/domain/InvoiceParticipantAddress/index.ts create mode 100644 apps/server/src/contexts/invoicing/domain/aggregates/index.ts create mode 100644 apps/server/src/contexts/invoicing/domain/aggregates/invoice.ts create mode 100644 apps/server/src/contexts/invoicing/domain/index.ts create mode 100644 apps/server/src/contexts/invoicing/domain/repositories/index.ts create mode 100644 apps/server/src/contexts/invoicing/domain/repositories/invoice-repository.interface.ts create mode 100644 apps/server/src/contexts/invoicing/domain/services/index.ts create mode 100644 apps/server/src/contexts/invoicing/domain/services/invoice-service.interface.ts create mode 100644 apps/server/src/contexts/invoicing/domain/services/invoice.service.ts create mode 100644 apps/server/src/contexts/invoicing/domain/value-objects/index.ts create mode 100644 apps/server/src/contexts/invoicing/domain/value-objects/invoice-number.ts create mode 100644 apps/server/src/contexts/invoicing/domain/value-objects/invoice-serie.ts create mode 100644 apps/server/src/contexts/invoicing/domain/value-objects/invoice-status.ts create mode 100644 apps/server/src/contexts/invoicing/intrastructure/Contact.repository.ts create mode 100644 apps/server/src/contexts/invoicing/intrastructure/Invoice.repository.ts create mode 100644 apps/server/src/contexts/invoicing/intrastructure/InvoiceParticipant.repository.ts create mode 100644 apps/server/src/contexts/invoicing/intrastructure/InvoiceParticipantAddress.repository.ts create mode 100644 apps/server/src/contexts/invoicing/intrastructure/InvoicingContext.ts create mode 100644 apps/server/src/contexts/invoicing/intrastructure/index.ts create mode 100644 apps/server/src/contexts/invoicing/intrastructure/mappers/contact.mapper.ts create mode 100644 apps/server/src/contexts/invoicing/intrastructure/mappers/contactAddress.mapper.ts create mode 100644 apps/server/src/contexts/invoicing/intrastructure/mappers/index.ts create mode 100644 apps/server/src/contexts/invoicing/intrastructure/mappers/invoice.mapper.ts create mode 100644 apps/server/src/contexts/invoicing/intrastructure/mappers/invoiceItem.mapper.ts create mode 100644 apps/server/src/contexts/invoicing/intrastructure/mappers/invoiceParticipant.mapper.ts create mode 100644 apps/server/src/contexts/invoicing/intrastructure/mappers/invoiceParticipantAddress.mapper.ts create mode 100644 apps/server/src/contexts/invoicing/intrastructure/sequelize/contact.model.ts create mode 100644 apps/server/src/contexts/invoicing/intrastructure/sequelize/contactAddress.model.ts create mode 100644 apps/server/src/contexts/invoicing/intrastructure/sequelize/index.ts create mode 100644 apps/server/src/contexts/invoicing/intrastructure/sequelize/invoice.model.ts create mode 100644 apps/server/src/contexts/invoicing/intrastructure/sequelize/invoice.repository.ts create mode 100644 apps/server/src/contexts/invoicing/intrastructure/sequelize/invoiceItem.model.ts create mode 100644 apps/server/src/contexts/invoicing/intrastructure/sequelize/invoiceParticipant.model.ts create mode 100644 apps/server/src/contexts/invoicing/intrastructure/sequelize/invoiceParticipantAddress.model.ts create mode 100644 apps/server/src/contexts/invoicing/presentation/controllers/create-invoice/CreateInvoiceController.ts create mode 100644 apps/server/src/contexts/invoicing/presentation/controllers/create-invoice/index.ts create mode 100644 apps/server/src/contexts/invoicing/presentation/controllers/create-invoice/presenter/CreateInvoice.presenter.ts create mode 100644 apps/server/src/contexts/invoicing/presentation/controllers/create-invoice/presenter/InvoiceItem.presenter.ts create mode 100644 apps/server/src/contexts/invoicing/presentation/controllers/create-invoice/presenter/InvoiceParticipant.presenter.ts create mode 100644 apps/server/src/contexts/invoicing/presentation/controllers/create-invoice/presenter/InvoiceParticipantAddress.presenter.ts create mode 100644 apps/server/src/contexts/invoicing/presentation/controllers/create-invoice/presenter/index.ts create mode 100644 apps/server/src/contexts/invoicing/presentation/controllers/delete-invoice/DeleteInvoiceController.ts create mode 100644 apps/server/src/contexts/invoicing/presentation/controllers/delete-invoice/index.ts create mode 100644 apps/server/src/contexts/invoicing/presentation/controllers/get-invoice/GetInvoiceController.ts create mode 100644 apps/server/src/contexts/invoicing/presentation/controllers/get-invoice/index.ts create mode 100644 apps/server/src/contexts/invoicing/presentation/controllers/get-invoice/presenter/GetInvoice.presenter.ts create mode 100644 apps/server/src/contexts/invoicing/presentation/controllers/get-invoice/presenter/InvoiceItem.presenter.ts create mode 100644 apps/server/src/contexts/invoicing/presentation/controllers/get-invoice/presenter/InvoiceParticipant.presenter.ts create mode 100644 apps/server/src/contexts/invoicing/presentation/controllers/get-invoice/presenter/InvoiceParticipantAddress.presenter.ts create mode 100644 apps/server/src/contexts/invoicing/presentation/controllers/get-invoice/presenter/index.ts create mode 100644 apps/server/src/contexts/invoicing/presentation/controllers/index.ts create mode 100644 apps/server/src/contexts/invoicing/presentation/controllers/list-invoices/index.ts create mode 100644 apps/server/src/contexts/invoicing/presentation/controllers/list-invoices/list-invoices.controller.ts create mode 100644 apps/server/src/contexts/invoicing/presentation/controllers/list-invoices/presenter/InvoiceParticipant.presenter.ts create mode 100644 apps/server/src/contexts/invoicing/presentation/controllers/list-invoices/presenter/InvoiceParticipantAddress.presenter.ts create mode 100644 apps/server/src/contexts/invoicing/presentation/controllers/list-invoices/presenter/index.ts create mode 100644 apps/server/src/contexts/invoicing/presentation/controllers/list-invoices/presenter/list-invoices.presenter.ts create mode 100644 apps/server/src/contexts/invoicing/presentation/controllers/update-invoice/UpdateInvoiceController.ts create mode 100644 apps/server/src/contexts/invoicing/presentation/controllers/update-invoice/index.ts create mode 100644 apps/server/src/contexts/invoicing/presentation/controllers/update-invoice/presenter/InvoiceItem.presenter.ts create mode 100644 apps/server/src/contexts/invoicing/presentation/controllers/update-invoice/presenter/InvoiceParticipant.presenter.ts create mode 100644 apps/server/src/contexts/invoicing/presentation/controllers/update-invoice/presenter/InvoiceParticipantAddress.presenter.ts create mode 100644 apps/server/src/contexts/invoicing/presentation/controllers/update-invoice/presenter/UpdateInvoice.presenter.ts create mode 100644 apps/server/src/contexts/invoicing/presentation/controllers/update-invoice/presenter/index.ts create mode 100644 apps/server/src/contexts/invoicing/presentation/dto/index.ts create mode 100644 apps/server/src/contexts/invoicing/presentation/dto/invoices.request.dto.ts create mode 100644 apps/server/src/contexts/invoicing/presentation/dto/invoices.response.dto.ts create mode 100644 apps/server/src/contexts/invoicing/presentation/dto/invoices.schemas.ts create mode 100644 apps/server/src/routes/invoicingRoutes.ts diff --git a/apps/server/src/contexts/accounts/domain/services/account-service.test.ts b/apps/server/src/contexts/accounts/domain/services/account-service.test.ts index 45881e70..6264fb50 100644 --- a/apps/server/src/contexts/accounts/domain/services/account-service.test.ts +++ b/apps/server/src/contexts/accounts/domain/services/account-service.test.ts @@ -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(), diff --git a/apps/server/src/contexts/accounts/presentation/dto/accounts.validation.dto.ts b/apps/server/src/contexts/accounts/presentation/dto/accounts.schemas.ts similarity index 59% rename from apps/server/src/contexts/accounts/presentation/dto/accounts.validation.dto.ts rename to apps/server/src/contexts/accounts/presentation/dto/accounts.schemas.ts index 7612fe8c..fc0aa9ad 100644 --- a/apps/server/src/contexts/accounts/presentation/dto/accounts.validation.dto.ts +++ b/apps/server/src/contexts/accounts/presentation/dto/accounts.schemas.ts @@ -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({}); diff --git a/apps/server/src/contexts/accounts/presentation/dto/index.ts b/apps/server/src/contexts/accounts/presentation/dto/index.ts index 1086720f..c2224ec7 100644 --- a/apps/server/src/contexts/accounts/presentation/dto/index.ts +++ b/apps/server/src/contexts/accounts/presentation/dto/index.ts @@ -1,3 +1,3 @@ export * from "./accounts.request.dto"; export * from "./accounts.response.dto"; -export * from "./accounts.validation.dto"; +export * from "./accounts.schemas"; diff --git a/apps/server/src/contexts/invoicing/application/create-invoice.use-case.ts b/apps/server/src/contexts/invoicing/application/create-invoice.use-case.ts new file mode 100644 index 00000000..539c3681 --- /dev/null +++ b/apps/server/src/contexts/invoicing/application/create-invoice.use-case.ts @@ -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> { + 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 { + 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( + 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 + ); + } +} diff --git a/apps/server/src/contexts/invoicing/application/delete-invoice.use-case.ts b/apps/server/src/contexts/invoicing/application/delete-invoice.use-case.ts new file mode 100644 index 00000000..ae4519de --- /dev/null +++ b/apps/server/src/contexts/invoicing/application/delete-invoice.use-case.ts @@ -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> { + 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); + } + }); + } +} diff --git a/apps/server/src/contexts/invoicing/application/get-invoice.use-case.ts b/apps/server/src/contexts/invoicing/application/get-invoice.use-case.ts new file mode 100644 index 00000000..52e61300 --- /dev/null +++ b/apps/server/src/contexts/invoicing/application/get-invoice.use-case.ts @@ -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> { + 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); + } + }); + } +} diff --git a/apps/server/src/contexts/invoicing/application/index.ts b/apps/server/src/contexts/invoicing/application/index.ts new file mode 100644 index 00000000..83e308e7 --- /dev/null +++ b/apps/server/src/contexts/invoicing/application/index.ts @@ -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"; diff --git a/apps/server/src/contexts/invoicing/application/list-invoices.use-case.ts b/apps/server/src/contexts/invoicing/application/list-invoices.use-case.ts new file mode 100644 index 00000000..e70a8fac --- /dev/null +++ b/apps/server/src/contexts/invoicing/application/list-invoices.use-case.ts @@ -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, 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); + } + }); + } +} diff --git a/apps/server/src/contexts/invoicing/application/services/index.ts b/apps/server/src/contexts/invoicing/application/services/index.ts new file mode 100644 index 00000000..b79a7a12 --- /dev/null +++ b/apps/server/src/contexts/invoicing/application/services/index.ts @@ -0,0 +1,2 @@ +export * from "./participantAddressFinder"; +export * from "./participantFinder"; diff --git a/apps/server/src/contexts/invoicing/application/services/participantAddressFinder.ts b/apps/server/src/contexts/invoicing/application/services/participantAddressFinder.ts new file mode 100644 index 00000000..7a85cfaf --- /dev/null +++ b/apps/server/src/contexts/invoicing/application/services/participantAddressFinder.ts @@ -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, +) => { + if (addressId.isNull()) { + return Result.fail( + ApplicationServiceError.create( + ApplicationServiceError.INVALID_REQUEST_PARAM, + `Participant address ID required`, + ), + ); + } + + const transaction = adapter.startTransaction(); + let address: NullOr = null; + + try { + await transaction.complete(async (t) => { + address = await repository({ transaction: t }).getById(addressId); + }); + + if (address === null) { + return Result.fail( + ApplicationServiceError.create( + ApplicationServiceError.NOT_FOUND_ERROR, + "", + { + id: addressId.toString(), + entity: "participant address", + }, + ), + ); + } + + return Result.ok(address); + } catch (error: unknown) { + const _error = error as Error; + + if (repository().isRepositoryError(_error)) { + return Result.fail( + ApplicationServiceError.create( + ApplicationServiceError.REPOSITORY_ERROR, + _error.message, + _error, + ), + ); + } + + return Result.fail( + ApplicationServiceError.create( + ApplicationServiceError.UNEXCEPTED_ERROR, + _error.message, + _error, + ), + ); + } +}; diff --git a/apps/server/src/contexts/invoicing/application/services/participantFinder.ts b/apps/server/src/contexts/invoicing/application/services/participantFinder.ts new file mode 100644 index 00000000..2fa96b00 --- /dev/null +++ b/apps/server/src/contexts/invoicing/application/services/participantFinder.ts @@ -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, +): Promise => { + 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); +}; diff --git a/apps/server/src/contexts/invoicing/application/update-invoice.use-case.ts b/apps/server/src/contexts/invoicing/application/update-invoice.use-case.ts new file mode 100644 index 00000000..310dcc1c --- /dev/null +++ b/apps/server/src/contexts/invoicing/application/update-invoice.use-case.ts @@ -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 + ): Promise> { + 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 + ): Result, Error> { + const errors: Error[] = []; + const validatedData: Partial = {}; + + // 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( + 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 // Misc errors (value objects) + | Result; // Success! + +export class UpdateInvoiceUseCase2 + implements + IUseCase<{ id: UniqueID; data: IUpdateInvoice_DTO }, Promise> +{ + 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(name: string) { + return this._repositoryManager.getRepository(name); + } + + private handleValidationFailure( + validationError: Error, + message?: string + ): Result { + return Result.fail( + UseCaseError.create( + UseCaseError.INVALID_INPUT_DATA, + message ? message : validationError.message, + validationError + ) + ); + } + + async execute(request: { + id: UniqueID; + data: IUpdateInvoice_DTO; + }): Promise { + 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("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( + UseCaseError.create(UseCaseError.NOT_FOUND_ERROR, `Invoice not found`, { + id: request.id.toString(), + entity: "invoice", + }) + ); + } + + return Result.ok(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( + 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( + 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("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("Invoice"); + + try { + await transaction.complete(async (t) => { + const invoiceRepo = invoiceRepoBuilder({ transaction: t }); + await invoiceRepo.save(invoice); + }); + + return Result.ok(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 { + return Result.fail( + UseCaseError.create(UseCaseError.UNEXCEPTED_ERROR, error.message, error) + ); + } + + private handleRepositoryError( + error: BaseError, + repository: IInvoiceRepository + ): Result { + const { message, details } = repository.handleRepositoryError(error); + return Result.fail( + UseCaseError.create(UseCaseError.REPOSITORY_ERROR, message, details) + ); + } +} diff --git a/apps/server/src/contexts/invoicing/domain/Contact/Contact.ts b/apps/server/src/contexts/invoicing/domain/Contact/Contact.ts new file mode 100644 index 00000000..8962c536 --- /dev/null +++ b/apps/server/src/contexts/invoicing/domain/Contact/Contact.ts @@ -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; + shippingAddress: GenericAddress; +} + +export interface IContact { + id: UniqueID; + tin: TINNumber; + companyName: Name; + firstName: Name; + lastName: Name; + + billingAddress: GenericAddress; + shippingAddress: GenericAddress; +} + +export class Contact extends Entity implements IContact { + public static create( + props: IContactProps, + id?: UniqueID, + ): Result { + const participant = new Contact(props, id); + return Result.ok(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; + } +} diff --git a/apps/server/src/contexts/invoicing/domain/Contact/IContactRepository.interface.ts b/apps/server/src/contexts/invoicing/domain/Contact/IContactRepository.interface.ts new file mode 100644 index 00000000..42dc1aac --- /dev/null +++ b/apps/server/src/contexts/invoicing/domain/Contact/IContactRepository.interface.ts @@ -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 { + getById(id: UniqueID): Promise; + + getById2( + id: UniqueID, + billingAddressId: UniqueID, + shippingAddressId: UniqueID, + ): Promise; + + exists(id: UniqueID): Promise; +} diff --git a/apps/server/src/contexts/invoicing/domain/Contact/index.ts b/apps/server/src/contexts/invoicing/domain/Contact/index.ts new file mode 100644 index 00000000..5eba5e9c --- /dev/null +++ b/apps/server/src/contexts/invoicing/domain/Contact/index.ts @@ -0,0 +1,2 @@ +export * from "./Contact"; +export * from "./IContactRepository.interface"; diff --git a/apps/server/src/contexts/invoicing/domain/ContactAddress.ts/ContactAddress.ts b/apps/server/src/contexts/invoicing/domain/ContactAddress.ts/ContactAddress.ts new file mode 100644 index 00000000..48f4fe4a --- /dev/null +++ b/apps/server/src/contexts/invoicing/domain/ContactAddress.ts/ContactAddress.ts @@ -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 + implements IContactAddress +{ + public static create(props: IContactAddressProps, id?: UniqueID) { + return Result.ok(new this(props, id)); + } +} diff --git a/apps/server/src/contexts/invoicing/domain/ContactAddress.ts/index.ts b/apps/server/src/contexts/invoicing/domain/ContactAddress.ts/index.ts new file mode 100644 index 00000000..d07727c4 --- /dev/null +++ b/apps/server/src/contexts/invoicing/domain/ContactAddress.ts/index.ts @@ -0,0 +1 @@ +export * from "./ContactAddress"; diff --git a/apps/server/src/contexts/invoicing/domain/InvoiceItems/InvoiceBaseItem.ts b/apps/server/src/contexts/invoicing/domain/InvoiceItems/InvoiceBaseItem.ts new file mode 100644 index 00000000..d72c9487 --- /dev/null +++ b/apps/server/src/contexts/invoicing/domain/InvoiceItems/InvoiceBaseItem.ts @@ -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

+ extends Entity

+ 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; + } +} diff --git a/apps/server/src/contexts/invoicing/domain/InvoiceItems/InvoiceBudgetItem.ts b/apps/server/src/contexts/invoicing/domain/InvoiceItems/InvoiceBudgetItem.ts new file mode 100644 index 00000000..6847b0c5 --- /dev/null +++ b/apps/server/src/contexts/invoicing/domain/InvoiceItems/InvoiceBudgetItem.ts @@ -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 + implements IInvoiceBudgetItem { + //private contents: (InvoiceLineItem | InvoiceChapter)[] = []; +} diff --git a/apps/server/src/contexts/invoicing/domain/InvoiceItems/InvoiceItems.ts b/apps/server/src/contexts/invoicing/domain/InvoiceItems/InvoiceItems.ts new file mode 100644 index 00000000..0c915547 --- /dev/null +++ b/apps/server/src/contexts/invoicing/domain/InvoiceItems/InvoiceItems.ts @@ -0,0 +1,21 @@ +export interface WithInvoiceItems { + items: InvoiceItems; +} + +export class InvoiceIems { + 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); + } + } +} diff --git a/apps/server/src/contexts/invoicing/domain/InvoiceItems/InvoiceSimpleItem.ts b/apps/server/src/contexts/invoicing/domain/InvoiceItems/InvoiceSimpleItem.ts new file mode 100644 index 00000000..a57e0a9b --- /dev/null +++ b/apps/server/src/contexts/invoicing/domain/InvoiceItems/InvoiceSimpleItem.ts @@ -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 + implements IInvoiceSimpleItem +{ + public static create( + props: IInvoiceSimpleItemProps, + id?: UniqueID, + ): Result { + return Result.ok(new InvoiceSimpleItem(props, id)); + } +} diff --git a/apps/server/src/contexts/invoicing/domain/InvoiceItems/index.ts b/apps/server/src/contexts/invoicing/domain/InvoiceItems/index.ts new file mode 100644 index 00000000..b3f5ff76 --- /dev/null +++ b/apps/server/src/contexts/invoicing/domain/InvoiceItems/index.ts @@ -0,0 +1,7 @@ +import { InvoiceBudgetItem } from "./InvoiceBudgetItem"; +import { InvoiceSimpleItem } from "./InvoiceSimpleItem"; + +export * from "./InvoiceBudgetItem"; +export * from "./InvoiceSimpleItem"; + +export type InvoiceItem = InvoiceSimpleItem | InvoiceBudgetItem; diff --git a/apps/server/src/contexts/invoicing/domain/InvoiceParticipant/IInvoiceParticipantRepository.interface.ts b/apps/server/src/contexts/invoicing/domain/InvoiceParticipant/IInvoiceParticipantRepository.interface.ts new file mode 100644 index 00000000..35ae51bd --- /dev/null +++ b/apps/server/src/contexts/invoicing/domain/InvoiceParticipant/IInvoiceParticipantRepository.interface.ts @@ -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 { + getById(id: UniqueID): Promise; + exists(id: UniqueID): Promise; +} diff --git a/apps/server/src/contexts/invoicing/domain/InvoiceParticipant/InvoiceParticipant.ts b/apps/server/src/contexts/invoicing/domain/InvoiceParticipant/InvoiceParticipant.ts new file mode 100644 index 00000000..8d7b3cad --- /dev/null +++ b/apps/server/src/contexts/invoicing/domain/InvoiceParticipant/InvoiceParticipant.ts @@ -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 + implements IInvoiceParticipant +{ + public static create( + props: IInvoiceParticipantProps, + id?: UniqueID, + ): Result { + const participant = new InvoiceParticipant(props, id); + return Result.ok(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; + } +} diff --git a/apps/server/src/contexts/invoicing/domain/InvoiceParticipant/Recipient.ts b/apps/server/src/contexts/invoicing/domain/InvoiceParticipant/Recipient.ts new file mode 100644 index 00000000..8e1f49a1 --- /dev/null +++ b/apps/server/src/contexts/invoicing/domain/InvoiceParticipant/Recipient.ts @@ -0,0 +1,3 @@ +import { InvoiceParticipant } from "./InvoiceParticipant"; + +export class Recipient extends InvoiceParticipant {} diff --git a/apps/server/src/contexts/invoicing/domain/InvoiceParticipant/Supplier.ts b/apps/server/src/contexts/invoicing/domain/InvoiceParticipant/Supplier.ts new file mode 100644 index 00000000..62cf7932 --- /dev/null +++ b/apps/server/src/contexts/invoicing/domain/InvoiceParticipant/Supplier.ts @@ -0,0 +1,3 @@ +import { InvoiceParticipant } from "./InvoiceParticipant"; + +export class Supplier extends InvoiceParticipant {} diff --git a/apps/server/src/contexts/invoicing/domain/InvoiceParticipant/index.ts b/apps/server/src/contexts/invoicing/domain/InvoiceParticipant/index.ts new file mode 100644 index 00000000..51c6717b --- /dev/null +++ b/apps/server/src/contexts/invoicing/domain/InvoiceParticipant/index.ts @@ -0,0 +1,4 @@ +export * from "./IInvoiceParticipantRepository.interface"; +export * from "./InvoiceParticipant"; +export * from "./Recipient"; +export * from "./Supplier"; diff --git a/apps/server/src/contexts/invoicing/domain/InvoiceParticipantAddress/InvoiceParticipantAddress.repository.interface.ts b/apps/server/src/contexts/invoicing/domain/InvoiceParticipantAddress/InvoiceParticipantAddress.repository.interface.ts new file mode 100644 index 00000000..d36b052c --- /dev/null +++ b/apps/server/src/contexts/invoicing/domain/InvoiceParticipantAddress/InvoiceParticipantAddress.repository.interface.ts @@ -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 { + getById(id: UniqueID): Promise; + exists(id: UniqueID): Promise; +} diff --git a/apps/server/src/contexts/invoicing/domain/InvoiceParticipantAddress/InvoiceParticipantAddress.ts b/apps/server/src/contexts/invoicing/domain/InvoiceParticipantAddress/InvoiceParticipantAddress.ts new file mode 100644 index 00000000..8005bae9 --- /dev/null +++ b/apps/server/src/contexts/invoicing/domain/InvoiceParticipantAddress/InvoiceParticipantAddress.ts @@ -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 + implements IInvoiceParticipantAddress +{ + public static create(props: IInvoiceParticipantAddressProps, id?: UniqueID) { + return Result.ok(new this(props, id)); + } +} diff --git a/apps/server/src/contexts/invoicing/domain/InvoiceParticipantAddress/InvoiceParticipantBillingAddress.ts b/apps/server/src/contexts/invoicing/domain/InvoiceParticipantAddress/InvoiceParticipantBillingAddress.ts new file mode 100644 index 00000000..d3743146 --- /dev/null +++ b/apps/server/src/contexts/invoicing/domain/InvoiceParticipantAddress/InvoiceParticipantBillingAddress.ts @@ -0,0 +1,32 @@ +import { Result, UniqueID } from "@shared/contexts"; +import { + IInvoiceParticipantAddress, + IInvoiceParticipantAddressProps, + InvoiceParticipantAddress, +} from "./InvoiceParticipantAddress"; + +export interface IInvoiceParticipantBillingAddressProps + extends Omit {} + +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(address); + } +} diff --git a/apps/server/src/contexts/invoicing/domain/InvoiceParticipantAddress/InvoiceParticipantShippingAddress.ts b/apps/server/src/contexts/invoicing/domain/InvoiceParticipantAddress/InvoiceParticipantShippingAddress.ts new file mode 100644 index 00000000..96d489b4 --- /dev/null +++ b/apps/server/src/contexts/invoicing/domain/InvoiceParticipantAddress/InvoiceParticipantShippingAddress.ts @@ -0,0 +1,32 @@ +import { Result, UniqueID } from "@shared/contexts"; +import { + IInvoiceParticipantAddress, + IInvoiceParticipantAddressProps, + InvoiceParticipantAddress, +} from "./InvoiceParticipantAddress"; + +export interface IInvoiceParticipantShippingAddressProps + extends Omit {} + +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(address); + } +} diff --git a/apps/server/src/contexts/invoicing/domain/InvoiceParticipantAddress/index.ts b/apps/server/src/contexts/invoicing/domain/InvoiceParticipantAddress/index.ts new file mode 100644 index 00000000..4591ab09 --- /dev/null +++ b/apps/server/src/contexts/invoicing/domain/InvoiceParticipantAddress/index.ts @@ -0,0 +1,4 @@ +export * from "./InvoiceParticipantAddress"; +export * from "./InvoiceParticipantAddress.repository.interface"; +export * from "./InvoiceParticipantBillingAddress"; +export * from "./InvoiceParticipantShippingAddress"; diff --git a/apps/server/src/contexts/invoicing/domain/aggregates/index.ts b/apps/server/src/contexts/invoicing/domain/aggregates/index.ts new file mode 100644 index 00000000..c759eb69 --- /dev/null +++ b/apps/server/src/contexts/invoicing/domain/aggregates/index.ts @@ -0,0 +1 @@ +export * from "./invoice"; diff --git a/apps/server/src/contexts/invoicing/domain/aggregates/invoice.ts b/apps/server/src/contexts/invoicing/domain/aggregates/invoice.ts new file mode 100644 index 00000000..6be556ca --- /dev/null +++ b/apps/server/src/contexts/invoicing/domain/aggregates/invoice.ts @@ -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; +} + +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; + + calculateSubtotal: () => MoneyValue; + calculateTaxTotal: () => MoneyValue; + calculateTotal: () => MoneyValue; +} + +export class Invoice extends AggregateRoot implements IInvoice { + private _items: Collection; + protected _status: InvoiceStatus; + + static create(props: IInvoiceProps, id?: UniqueID): Result { + 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); + } +} diff --git a/apps/server/src/contexts/invoicing/domain/index.ts b/apps/server/src/contexts/invoicing/domain/index.ts new file mode 100644 index 00000000..624f976d --- /dev/null +++ b/apps/server/src/contexts/invoicing/domain/index.ts @@ -0,0 +1,6 @@ +export * from "./aggregates"; +export * from "./Contact"; +export * from "./ContactAddress.ts"; +export * from "./InvoiceItems"; +export * from "./InvoiceParticipant"; +export * from "./InvoiceParticipantAddress"; diff --git a/apps/server/src/contexts/invoicing/domain/repositories/index.ts b/apps/server/src/contexts/invoicing/domain/repositories/index.ts new file mode 100644 index 00000000..7a8b94a0 --- /dev/null +++ b/apps/server/src/contexts/invoicing/domain/repositories/index.ts @@ -0,0 +1 @@ +export * from "./invoice-repository.interface"; diff --git a/apps/server/src/contexts/invoicing/domain/repositories/invoice-repository.interface.ts b/apps/server/src/contexts/invoicing/domain/repositories/invoice-repository.interface.ts new file mode 100644 index 00000000..12abee3c --- /dev/null +++ b/apps/server/src/contexts/invoicing/domain/repositories/invoice-repository.interface.ts @@ -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, Error>>; + findById(id: UniqueID, transaction?: any): Promise>; + deleteById(id: UniqueID, transaction?: any): Promise>; + + create(invoice: Invoice, transaction?: any): Promise; + update(invoice: Invoice, transaction?: any): Promise; +} diff --git a/apps/server/src/contexts/invoicing/domain/services/index.ts b/apps/server/src/contexts/invoicing/domain/services/index.ts new file mode 100644 index 00000000..73a5d9ba --- /dev/null +++ b/apps/server/src/contexts/invoicing/domain/services/index.ts @@ -0,0 +1,2 @@ +export * from "./invoice-service.interface"; +export * from "./invoice.service"; diff --git a/apps/server/src/contexts/invoicing/domain/services/invoice-service.interface.ts b/apps/server/src/contexts/invoicing/domain/services/invoice-service.interface.ts new file mode 100644 index 00000000..03ea031e --- /dev/null +++ b/apps/server/src/contexts/invoicing/domain/services/invoice-service.interface.ts @@ -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, Error>>; + findInvoiceById(invoiceId: UniqueID, transaction?: any): Promise>; + + updateInvoiceById( + invoiceId: UniqueID, + data: Partial, + transaction?: any + ): Promise>; + + createInvoice( + invoiceId: UniqueID, + data: IInvoiceProps, + transaction?: any + ): Promise>; + + deleteInvoiceById(invoiceId: UniqueID, transaction?: any): Promise>; +} diff --git a/apps/server/src/contexts/invoicing/domain/services/invoice.service.ts b/apps/server/src/contexts/invoicing/domain/services/invoice.service.ts new file mode 100644 index 00000000..59b44c64 --- /dev/null +++ b/apps/server/src/contexts/invoicing/domain/services/invoice.service.ts @@ -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, 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> { + return await this.repo.findById(invoiceId, transaction); + } + + async updateInvoiceById( + invoiceId: UniqueID, + data: Partial, + transaction?: Transaction + ): Promise> { + // 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> { + // 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> { + // 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); + } +} diff --git a/apps/server/src/contexts/invoicing/domain/value-objects/index.ts b/apps/server/src/contexts/invoicing/domain/value-objects/index.ts new file mode 100644 index 00000000..992c7ed2 --- /dev/null +++ b/apps/server/src/contexts/invoicing/domain/value-objects/index.ts @@ -0,0 +1 @@ +export * from "./invoice-status"; diff --git a/apps/server/src/contexts/invoicing/domain/value-objects/invoice-number.ts b/apps/server/src/contexts/invoicing/domain/value-objects/invoice-number.ts new file mode 100644 index 00000000..a3b6a72a --- /dev/null +++ b/apps/server/src/contexts/invoicing/domain/value-objects/invoice-number.ts @@ -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, + options: IStringValueObjectOptions, + ) { + const rule = Joi.string() + .allow(null, "") + .default("") + .trim() + .label(options.label ? options.label : "value"); + + return RuleValidator.validate(rule, value); + } + + public static create( + value: UndefinedOr, + 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)); + } +} diff --git a/apps/server/src/contexts/invoicing/domain/value-objects/invoice-serie.ts b/apps/server/src/contexts/invoicing/domain/value-objects/invoice-serie.ts new file mode 100644 index 00000000..f168a795 --- /dev/null +++ b/apps/server/src/contexts/invoicing/domain/value-objects/invoice-serie.ts @@ -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, + options: IStringValueObjectOptions, + ) { + const rule = Joi.string() + .allow(null, "") + .default("") + .trim() + .label(options.label ? options.label : "value"); + + return RuleValidator.validate(rule, value); + } + + public static create( + value: UndefinedOr, + 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)); + } +} diff --git a/apps/server/src/contexts/invoicing/domain/value-objects/invoice-status.ts b/apps/server/src/contexts/invoicing/domain/value-objects/invoice-status.ts new file mode 100644 index 00000000..19f8fb7f --- /dev/null +++ b/apps/server/src/contexts/invoicing/domain/value-objects/invoice-status.ts @@ -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 { + private static readonly ALLOWED_STATUSES = ["draft", "emitted", "sent", "rejected"]; + + private static readonly TRANSITIONS: Record = { + draft: [INVOICE_STATUS.EMITTED], + emitted: [INVOICE_STATUS.SENT, INVOICE_STATUS.REJECTED, INVOICE_STATUS.DRAFT], + sent: [INVOICE_STATUS.REJECTED], + rejected: [], + }; + + static create(value: string): Result { + 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 { + 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(); + } +} diff --git a/apps/server/src/contexts/invoicing/intrastructure/Contact.repository.ts b/apps/server/src/contexts/invoicing/intrastructure/Contact.repository.ts new file mode 100644 index 00000000..9ccd432a --- /dev/null +++ b/apps/server/src/contexts/invoicing/intrastructure/Contact.repository.ts @@ -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 + 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 { + 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 { + return this._exists("Customer", "id", id.toString()); + } +} diff --git a/apps/server/src/contexts/invoicing/intrastructure/Invoice.repository.ts b/apps/server/src/contexts/invoicing/intrastructure/Invoice.repository.ts new file mode 100644 index 00000000..c890826a --- /dev/null +++ b/apps/server/src/contexts/invoicing/intrastructure/Invoice.repository.ts @@ -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; + filters: Record; +}; + +export class InvoiceRepository + extends SequelizeRepository + 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 { + 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> { + 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 { + 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 { + return this._removeById("Invoice_Model", id); + } + + public async exists(id: UniqueID): Promise { + return this._exists("Invoice_Model", "id", id.toString()); + } +} diff --git a/apps/server/src/contexts/invoicing/intrastructure/InvoiceParticipant.repository.ts b/apps/server/src/contexts/invoicing/intrastructure/InvoiceParticipant.repository.ts new file mode 100644 index 00000000..de77b304 --- /dev/null +++ b/apps/server/src/contexts/invoicing/intrastructure/InvoiceParticipant.repository.ts @@ -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 { + 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 { + 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 { + 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 { + return this._exists("Customer", "id", id.toString()); + }*/ +} diff --git a/apps/server/src/contexts/invoicing/intrastructure/InvoiceParticipantAddress.repository.ts b/apps/server/src/contexts/invoicing/intrastructure/InvoiceParticipantAddress.repository.ts new file mode 100644 index 00000000..a010fcbf --- /dev/null +++ b/apps/server/src/contexts/invoicing/intrastructure/InvoiceParticipantAddress.repository.ts @@ -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 { + 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 { + 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 { + return this._exists("CustomerAddress", "id", id.toString()); + } +} diff --git a/apps/server/src/contexts/invoicing/intrastructure/InvoicingContext.ts b/apps/server/src/contexts/invoicing/intrastructure/InvoicingContext.ts new file mode 100644 index 00000000..75ca5396 --- /dev/null +++ b/apps/server/src/contexts/invoicing/intrastructure/InvoicingContext.ts @@ -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 }; diff --git a/apps/server/src/contexts/invoicing/intrastructure/index.ts b/apps/server/src/contexts/invoicing/intrastructure/index.ts new file mode 100644 index 00000000..7ccefa3a --- /dev/null +++ b/apps/server/src/contexts/invoicing/intrastructure/index.ts @@ -0,0 +1,2 @@ +export * from "./mappers"; +export * from "./sequelize"; diff --git a/apps/server/src/contexts/invoicing/intrastructure/mappers/contact.mapper.ts b/apps/server/src/contexts/invoicing/intrastructure/mappers/contact.mapper.ts new file mode 100644 index 00000000..8c10523d --- /dev/null +++ b/apps/server/src/contexts/invoicing/intrastructure/mappers/contact.mapper.ts @@ -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 {} + +class ContactMapper + extends SequelizeMapper + 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, + }); diff --git a/apps/server/src/contexts/invoicing/intrastructure/mappers/contactAddress.mapper.ts b/apps/server/src/contexts/invoicing/intrastructure/mappers/contactAddress.mapper.ts new file mode 100644 index 00000000..4e05d8f8 --- /dev/null +++ b/apps/server/src/contexts/invoicing/intrastructure/mappers/contactAddress.mapper.ts @@ -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; + } +} diff --git a/apps/server/src/contexts/invoicing/intrastructure/mappers/index.ts b/apps/server/src/contexts/invoicing/intrastructure/mappers/index.ts new file mode 100644 index 00000000..2e676120 --- /dev/null +++ b/apps/server/src/contexts/invoicing/intrastructure/mappers/index.ts @@ -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"; diff --git a/apps/server/src/contexts/invoicing/intrastructure/mappers/invoice.mapper.ts b/apps/server/src/contexts/invoicing/intrastructure/mappers/invoice.mapper.ts new file mode 100644 index 00000000..56a6cec4 --- /dev/null +++ b/apps/server/src/contexts/invoicing/intrastructure/mappers/invoice.mapper.ts @@ -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 {} + +export const createInvoiceMapper = (context: IInvoicingContext): IInvoiceMapper => + new InvoiceMapper({ + context, + invoiceItemMapper: createInvoiceItemMapper(context), + participantMapper: createInvoiceParticipantMapper(context), + }); + +class InvoiceMapper + extends SequelizeMapper + 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; + } +} diff --git a/apps/server/src/contexts/invoicing/intrastructure/mappers/invoiceItem.mapper.ts b/apps/server/src/contexts/invoicing/intrastructure/mappers/invoiceItem.mapper.ts new file mode 100644 index 00000000..ee275418 --- /dev/null +++ b/apps/server/src/contexts/invoicing/intrastructure/mappers/invoiceItem.mapper.ts @@ -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; + } +} diff --git a/apps/server/src/contexts/invoicing/intrastructure/mappers/invoiceParticipant.mapper.ts b/apps/server/src/contexts/invoicing/intrastructure/mappers/invoiceParticipant.mapper.ts new file mode 100644 index 00000000..fdb319ad --- /dev/null +++ b/apps/server/src/contexts/invoicing/intrastructure/mappers/invoiceParticipant.mapper.ts @@ -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 }), + }; + } +} diff --git a/apps/server/src/contexts/invoicing/intrastructure/mappers/invoiceParticipantAddress.mapper.ts b/apps/server/src/contexts/invoicing/intrastructure/mappers/invoiceParticipantAddress.mapper.ts new file mode 100644 index 00000000..33686df0 --- /dev/null +++ b/apps/server/src/contexts/invoicing/intrastructure/mappers/invoiceParticipantAddress.mapper.ts @@ -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(), + }; + } +} diff --git a/apps/server/src/contexts/invoicing/intrastructure/sequelize/contact.model.ts b/apps/server/src/contexts/invoicing/intrastructure/sequelize/contact.model.ts new file mode 100644 index 00000000..32da79c0 --- /dev/null +++ b/apps/server/src/contexts/invoicing/intrastructure/sequelize/contact.model.ts @@ -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 { + 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; + declare company_name: CreationOptional; + declare first_name: CreationOptional; + declare last_name: CreationOptional; + + declare shippingAddress?: NonAttribute; + declare billingAddress?: NonAttribute; +} + +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; +}; diff --git a/apps/server/src/contexts/invoicing/intrastructure/sequelize/contactAddress.model.ts b/apps/server/src/contexts/invoicing/intrastructure/sequelize/contactAddress.model.ts new file mode 100644 index 00000000..9f101dc0 --- /dev/null +++ b/apps/server/src/contexts/invoicing/intrastructure/sequelize/contactAddress.model.ts @@ -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, + TCreationContactAddress_Attributes +> { + // To avoid table creation + static async sync(): Promise { + 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; + declare type: string; + declare street: CreationOptional; + declare postal_code: CreationOptional; + declare city: CreationOptional; + declare province: CreationOptional; + declare country: CreationOptional; + declare phone: CreationOptional; + declare email: CreationOptional; + + declare customer?: NonAttribute; +} + +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; +}; diff --git a/apps/server/src/contexts/invoicing/intrastructure/sequelize/index.ts b/apps/server/src/contexts/invoicing/intrastructure/sequelize/index.ts new file mode 100644 index 00000000..2db96ef4 --- /dev/null +++ b/apps/server/src/contexts/invoicing/intrastructure/sequelize/index.ts @@ -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; +}; diff --git a/apps/server/src/contexts/invoicing/intrastructure/sequelize/invoice.model.ts b/apps/server/src/contexts/invoicing/intrastructure/sequelize/invoice.model.ts new file mode 100644 index 00000000..38fa5975 --- /dev/null +++ b/apps/server/src/contexts/invoicing/intrastructure/sequelize/invoice.model.ts @@ -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, + InferCreationAttributes +> { + static async trackRevision( + connection: Sequelize, + sequelizeRevision: SequelizeRevision + ) { + 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; + declare invoice_number: CreationOptional; + declare issue_date: CreationOptional; + declare operation_date: CreationOptional; + declare invoice_language: string; + declare invoice_currency: string; + declare subtotal: number; + declare total: number; + + declare items: NonAttribute; + declare participants: NonAttribute; +} + +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; +}; diff --git a/apps/server/src/contexts/invoicing/intrastructure/sequelize/invoice.repository.ts b/apps/server/src/contexts/invoicing/intrastructure/sequelize/invoice.repository.ts new file mode 100644 index 00000000..f93f194f --- /dev/null +++ b/apps/server/src/contexts/invoicing/intrastructure/sequelize/invoice.repository.ts @@ -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 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> { + 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, 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> { + 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> { + try { + this._deleteById(InvoiceModel, id); + return Result.ok(true); + } catch (error: any) { + return this._handleDatabaseError(error, this._customErrorMapper); + } + } + + async create(invoice: Invoice, transaction?: Transaction): Promise { + const invoiceData = this._mapper.mapToPersistence(invoice); + await this._save(InvoiceModel, invoice.id, invoiceData, {}, transaction); + } + + async update(invoice: Invoice, transaction?: Transaction): Promise { + const invoiceData = this._mapper.mapToPersistence(invoice); + await this._save(InvoiceModel, invoice.id, invoiceData, {}, transaction); + } +} + +const invoiceRepository = new InvoiceRepository(invoiceMapper); +export { invoiceRepository }; diff --git a/apps/server/src/contexts/invoicing/intrastructure/sequelize/invoiceItem.model.ts b/apps/server/src/contexts/invoicing/intrastructure/sequelize/invoiceItem.model.ts new file mode 100644 index 00000000..f2ba4801 --- /dev/null +++ b/apps/server/src/contexts/invoicing/intrastructure/sequelize/invoiceItem.model.ts @@ -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, + InferCreationAttributes +> { + 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; + declare position: number; + declare item_type: string; + declare description: CreationOptional; + declare quantity: CreationOptional; + declare unit_price: CreationOptional; + declare subtotal: CreationOptional; + declare total: CreationOptional; + + declare invoice?: NonAttribute; +} + +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; +}; diff --git a/apps/server/src/contexts/invoicing/intrastructure/sequelize/invoiceParticipant.model.ts b/apps/server/src/contexts/invoicing/intrastructure/sequelize/invoiceParticipant.model.ts new file mode 100644 index 00000000..78df602a --- /dev/null +++ b/apps/server/src/contexts/invoicing/intrastructure/sequelize/invoiceParticipant.model.ts @@ -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; + declare company_name: CreationOptional; + declare first_name: CreationOptional; + declare last_name: CreationOptional; + + declare shippingAddress?: NonAttribute; + declare billingAddress?: NonAttribute; + + declare invoice?: NonAttribute; +} + +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; +}; diff --git a/apps/server/src/contexts/invoicing/intrastructure/sequelize/invoiceParticipantAddress.model.ts b/apps/server/src/contexts/invoicing/intrastructure/sequelize/invoiceParticipantAddress.model.ts new file mode 100644 index 00000000..def690c6 --- /dev/null +++ b/apps/server/src/contexts/invoicing/intrastructure/sequelize/invoiceParticipantAddress.model.ts @@ -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, + 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; + declare postal_code: CreationOptional; + declare city: CreationOptional; + declare province: CreationOptional; + declare country: CreationOptional; + declare phone: CreationOptional; + declare email: CreationOptional; + + declare participant?: NonAttribute; +} + +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; +}; diff --git a/apps/server/src/contexts/invoicing/presentation/controllers/create-invoice/CreateInvoiceController.ts b/apps/server/src/contexts/invoicing/presentation/controllers/create-invoice/CreateInvoiceController.ts new file mode 100644 index 00000000..c04f9f19 --- /dev/null +++ b/apps/server/src/contexts/invoicing/presentation/controllers/create-invoice/CreateInvoiceController.ts @@ -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 { + 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 = result.object; + + return this.created( + this.presenter.map(invoice, this.context) + ); + } catch (error: unknown) { + return this.fail(error as IServerError); + } + } +} diff --git a/apps/server/src/contexts/invoicing/presentation/controllers/create-invoice/index.ts b/apps/server/src/contexts/invoicing/presentation/controllers/create-invoice/index.ts new file mode 100644 index 00000000..6f1b8cb4 --- /dev/null +++ b/apps/server/src/contexts/invoicing/presentation/controllers/create-invoice/index.ts @@ -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, + ); +}; diff --git a/apps/server/src/contexts/invoicing/presentation/controllers/create-invoice/presenter/CreateInvoice.presenter.ts b/apps/server/src/contexts/invoicing/presentation/controllers/create-invoice/presenter/CreateInvoice.presenter.ts new file mode 100644 index 00000000..0feae5c7 --- /dev/null +++ b/apps/server/src/contexts/invoicing/presentation/controllers/create-invoice/presenter/CreateInvoice.presenter.ts @@ -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), + }; + }, +}; diff --git a/apps/server/src/contexts/invoicing/presentation/controllers/create-invoice/presenter/InvoiceItem.presenter.ts b/apps/server/src/contexts/invoicing/presentation/controllers/create-invoice/presenter/InvoiceItem.presenter.ts new file mode 100644 index 00000000..6024c716 --- /dev/null +++ b/apps/server/src/contexts/invoicing/presentation/controllers/create-invoice/presenter/InvoiceItem.presenter.ts @@ -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, + 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, + })) + : []; diff --git a/apps/server/src/contexts/invoicing/presentation/controllers/create-invoice/presenter/InvoiceParticipant.presenter.ts b/apps/server/src/contexts/invoicing/presentation/controllers/create-invoice/presenter/InvoiceParticipant.presenter.ts new file mode 100644 index 00000000..ea628008 --- /dev/null +++ b/apps/server/src/contexts/invoicing/presentation/controllers/create-invoice/presenter/InvoiceParticipant.presenter.ts @@ -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, + ), + }; +}; diff --git a/apps/server/src/contexts/invoicing/presentation/controllers/create-invoice/presenter/InvoiceParticipantAddress.presenter.ts b/apps/server/src/contexts/invoicing/presentation/controllers/create-invoice/presenter/InvoiceParticipantAddress.presenter.ts new file mode 100644 index 00000000..4e637287 --- /dev/null +++ b/apps/server/src/contexts/invoicing/presentation/controllers/create-invoice/presenter/InvoiceParticipantAddress.presenter.ts @@ -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(), + }; +}; diff --git a/apps/server/src/contexts/invoicing/presentation/controllers/create-invoice/presenter/index.ts b/apps/server/src/contexts/invoicing/presentation/controllers/create-invoice/presenter/index.ts new file mode 100644 index 00000000..a9312352 --- /dev/null +++ b/apps/server/src/contexts/invoicing/presentation/controllers/create-invoice/presenter/index.ts @@ -0,0 +1 @@ +export * from "./CreateInvoice.presenter"; diff --git a/apps/server/src/contexts/invoicing/presentation/controllers/delete-invoice/DeleteInvoiceController.ts b/apps/server/src/contexts/invoicing/presentation/controllers/delete-invoice/DeleteInvoiceController.ts new file mode 100644 index 00000000..c7075473 --- /dev/null +++ b/apps/server/src/contexts/invoicing/presentation/controllers/delete-invoice/DeleteInvoiceController.ts @@ -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 { + 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); + } + } +} diff --git a/apps/server/src/contexts/invoicing/presentation/controllers/delete-invoice/index.ts b/apps/server/src/contexts/invoicing/presentation/controllers/delete-invoice/index.ts new file mode 100644 index 00000000..fb9c949e --- /dev/null +++ b/apps/server/src/contexts/invoicing/presentation/controllers/delete-invoice/index.ts @@ -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, + }); +}; diff --git a/apps/server/src/contexts/invoicing/presentation/controllers/get-invoice/GetInvoiceController.ts b/apps/server/src/contexts/invoicing/presentation/controllers/get-invoice/GetInvoiceController.ts new file mode 100644 index 00000000..af74cb81 --- /dev/null +++ b/apps/server/src/contexts/invoicing/presentation/controllers/get-invoice/GetInvoiceController.ts @@ -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 { + 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 = result.object; + + return this.ok( + await this.presenter.map(invoice, this.context) + ); + } catch (e: unknown) { + return this.fail(e as IServerError); + } + } +} diff --git a/apps/server/src/contexts/invoicing/presentation/controllers/get-invoice/index.ts b/apps/server/src/contexts/invoicing/presentation/controllers/get-invoice/index.ts new file mode 100644 index 00000000..ed0a5c55 --- /dev/null +++ b/apps/server/src/contexts/invoicing/presentation/controllers/get-invoice/index.ts @@ -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, + ); +}; diff --git a/apps/server/src/contexts/invoicing/presentation/controllers/get-invoice/presenter/GetInvoice.presenter.ts b/apps/server/src/contexts/invoicing/presentation/controllers/get-invoice/presenter/GetInvoice.presenter.ts new file mode 100644 index 00000000..eb766427 --- /dev/null +++ b/apps/server/src/contexts/invoicing/presentation/controllers/get-invoice/presenter/GetInvoice.presenter.ts @@ -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; +} + +export const getInvoicePresenter: IGetInvoicePresenter = { + map: async ( + invoice: Invoice, + context: IInvoicingContext, + ): Promise => { + 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: "", + }, + }; + }, +}; diff --git a/apps/server/src/contexts/invoicing/presentation/controllers/get-invoice/presenter/InvoiceItem.presenter.ts b/apps/server/src/contexts/invoicing/presentation/controllers/get-invoice/presenter/InvoiceItem.presenter.ts new file mode 100644 index 00000000..6024c716 --- /dev/null +++ b/apps/server/src/contexts/invoicing/presentation/controllers/get-invoice/presenter/InvoiceItem.presenter.ts @@ -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, + 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, + })) + : []; diff --git a/apps/server/src/contexts/invoicing/presentation/controllers/get-invoice/presenter/InvoiceParticipant.presenter.ts b/apps/server/src/contexts/invoicing/presentation/controllers/get-invoice/presenter/InvoiceParticipant.presenter.ts new file mode 100644 index 00000000..635aa696 --- /dev/null +++ b/apps/server/src/contexts/invoicing/presentation/controllers/get-invoice/presenter/InvoiceParticipant.presenter.ts @@ -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 => { + 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, + ), + }; +}; diff --git a/apps/server/src/contexts/invoicing/presentation/controllers/get-invoice/presenter/InvoiceParticipantAddress.presenter.ts b/apps/server/src/contexts/invoicing/presentation/controllers/get-invoice/presenter/InvoiceParticipantAddress.presenter.ts new file mode 100644 index 00000000..15478c1b --- /dev/null +++ b/apps/server/src/contexts/invoicing/presentation/controllers/get-invoice/presenter/InvoiceParticipantAddress.presenter.ts @@ -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 => { + 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(), + }; +}; diff --git a/apps/server/src/contexts/invoicing/presentation/controllers/get-invoice/presenter/index.ts b/apps/server/src/contexts/invoicing/presentation/controllers/get-invoice/presenter/index.ts new file mode 100644 index 00000000..f91273b1 --- /dev/null +++ b/apps/server/src/contexts/invoicing/presentation/controllers/get-invoice/presenter/index.ts @@ -0,0 +1 @@ +export * from "./GetInvoice.presenter"; diff --git a/apps/server/src/contexts/invoicing/presentation/controllers/index.ts b/apps/server/src/contexts/invoicing/presentation/controllers/index.ts new file mode 100644 index 00000000..beae3e39 --- /dev/null +++ b/apps/server/src/contexts/invoicing/presentation/controllers/index.ts @@ -0,0 +1,5 @@ +export * from "./create-invoice"; +export * from "./delete-invoice"; +export * from "./get-invoice"; +export * from "./list-invoices"; +export * from "./update-invoice"; diff --git a/apps/server/src/contexts/invoicing/presentation/controllers/list-invoices/index.ts b/apps/server/src/contexts/invoicing/presentation/controllers/list-invoices/index.ts new file mode 100644 index 00000000..804a27b5 --- /dev/null +++ b/apps/server/src/contexts/invoicing/presentation/controllers/list-invoices/index.ts @@ -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); +}; diff --git a/apps/server/src/contexts/invoicing/presentation/controllers/list-invoices/list-invoices.controller.ts b/apps/server/src/contexts/invoicing/presentation/controllers/list-invoices/list-invoices.controller.ts new file mode 100644 index 00000000..50a17d7c --- /dev/null +++ b/apps/server/src/contexts/invoicing/presentation/controllers/list-invoices/list-invoices.controller.ts @@ -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); + } +} diff --git a/apps/server/src/contexts/invoicing/presentation/controllers/list-invoices/presenter/InvoiceParticipant.presenter.ts b/apps/server/src/contexts/invoicing/presentation/controllers/list-invoices/presenter/InvoiceParticipant.presenter.ts new file mode 100644 index 00000000..a6030b13 --- /dev/null +++ b/apps/server/src/contexts/invoicing/presentation/controllers/list-invoices/presenter/InvoiceParticipant.presenter.ts @@ -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!, + ), + }; +}; diff --git a/apps/server/src/contexts/invoicing/presentation/controllers/list-invoices/presenter/InvoiceParticipantAddress.presenter.ts b/apps/server/src/contexts/invoicing/presentation/controllers/list-invoices/presenter/InvoiceParticipantAddress.presenter.ts new file mode 100644 index 00000000..0dda05df --- /dev/null +++ b/apps/server/src/contexts/invoicing/presentation/controllers/list-invoices/presenter/InvoiceParticipantAddress.presenter.ts @@ -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(), + }; +}; diff --git a/apps/server/src/contexts/invoicing/presentation/controllers/list-invoices/presenter/index.ts b/apps/server/src/contexts/invoicing/presentation/controllers/list-invoices/presenter/index.ts new file mode 100644 index 00000000..9ecb5c89 --- /dev/null +++ b/apps/server/src/contexts/invoicing/presentation/controllers/list-invoices/presenter/index.ts @@ -0,0 +1 @@ +export * from "./list-invoices.presenter"; diff --git a/apps/server/src/contexts/invoicing/presentation/controllers/list-invoices/presenter/list-invoices.presenter.ts b/apps/server/src/contexts/invoicing/presentation/controllers/list-invoices/presenter/list-invoices.presenter.ts new file mode 100644 index 00000000..1802522b --- /dev/null +++ b/apps/server/src/contexts/invoicing/presentation/controllers/list-invoices/presenter/list-invoices.presenter.ts @@ -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) => IListInvoicesResponseDTO[]; +} + +export const listInvoicesPresenter: IListInvoicesPresenter = { + toDTO: (invoices: Collection): 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; + }); + }, +}; diff --git a/apps/server/src/contexts/invoicing/presentation/controllers/update-invoice/UpdateInvoiceController.ts b/apps/server/src/contexts/invoicing/presentation/controllers/update-invoice/UpdateInvoiceController.ts new file mode 100644 index 00000000..381496a7 --- /dev/null +++ b/apps/server/src/contexts/invoicing/presentation/controllers/update-invoice/UpdateInvoiceController.ts @@ -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 { + 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 = result.object; + + return this.ok( + this.presenter.map(invoice, this.context), + ); + } catch (e: unknown) { + return this.fail(e as IServerError); + } + } +} diff --git a/apps/server/src/contexts/invoicing/presentation/controllers/update-invoice/index.ts b/apps/server/src/contexts/invoicing/presentation/controllers/update-invoice/index.ts new file mode 100644 index 00000000..1fd59f14 --- /dev/null +++ b/apps/server/src/contexts/invoicing/presentation/controllers/update-invoice/index.ts @@ -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, + ); +}; diff --git a/apps/server/src/contexts/invoicing/presentation/controllers/update-invoice/presenter/InvoiceItem.presenter.ts b/apps/server/src/contexts/invoicing/presentation/controllers/update-invoice/presenter/InvoiceItem.presenter.ts new file mode 100644 index 00000000..6024c716 --- /dev/null +++ b/apps/server/src/contexts/invoicing/presentation/controllers/update-invoice/presenter/InvoiceItem.presenter.ts @@ -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, + 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, + })) + : []; diff --git a/apps/server/src/contexts/invoicing/presentation/controllers/update-invoice/presenter/InvoiceParticipant.presenter.ts b/apps/server/src/contexts/invoicing/presentation/controllers/update-invoice/presenter/InvoiceParticipant.presenter.ts new file mode 100644 index 00000000..b416c7ba --- /dev/null +++ b/apps/server/src/contexts/invoicing/presentation/controllers/update-invoice/presenter/InvoiceParticipant.presenter.ts @@ -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, + ), + }; +}; diff --git a/apps/server/src/contexts/invoicing/presentation/controllers/update-invoice/presenter/InvoiceParticipantAddress.presenter.ts b/apps/server/src/contexts/invoicing/presentation/controllers/update-invoice/presenter/InvoiceParticipantAddress.presenter.ts new file mode 100644 index 00000000..376d491f --- /dev/null +++ b/apps/server/src/contexts/invoicing/presentation/controllers/update-invoice/presenter/InvoiceParticipantAddress.presenter.ts @@ -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(), + }; +}; diff --git a/apps/server/src/contexts/invoicing/presentation/controllers/update-invoice/presenter/UpdateInvoice.presenter.ts b/apps/server/src/contexts/invoicing/presentation/controllers/update-invoice/presenter/UpdateInvoice.presenter.ts new file mode 100644 index 00000000..90ed2125 --- /dev/null +++ b/apps/server/src/contexts/invoicing/presentation/controllers/update-invoice/presenter/UpdateInvoice.presenter.ts @@ -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), + }; + }, +}; diff --git a/apps/server/src/contexts/invoicing/presentation/controllers/update-invoice/presenter/index.ts b/apps/server/src/contexts/invoicing/presentation/controllers/update-invoice/presenter/index.ts new file mode 100644 index 00000000..88e907ef --- /dev/null +++ b/apps/server/src/contexts/invoicing/presentation/controllers/update-invoice/presenter/index.ts @@ -0,0 +1 @@ +export * from "./UpdateInvoice.presenter"; diff --git a/apps/server/src/contexts/invoicing/presentation/dto/index.ts b/apps/server/src/contexts/invoicing/presentation/dto/index.ts new file mode 100644 index 00000000..804ee569 --- /dev/null +++ b/apps/server/src/contexts/invoicing/presentation/dto/index.ts @@ -0,0 +1,3 @@ +export * from "./invoices.request.dto"; +export * from "./invoices.response.dto"; +export * from "./invoices.schemas"; diff --git a/apps/server/src/contexts/invoicing/presentation/dto/invoices.request.dto.ts b/apps/server/src/contexts/invoicing/presentation/dto/invoices.request.dto.ts new file mode 100644 index 00000000..6df3cb63 --- /dev/null +++ b/apps/server/src/contexts/invoicing/presentation/dto/invoices.request.dto.ts @@ -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; +} diff --git a/apps/server/src/contexts/invoicing/presentation/dto/invoices.response.dto.ts b/apps/server/src/contexts/invoicing/presentation/dto/invoices.response.dto.ts new file mode 100644 index 00000000..b0b6dc2c --- /dev/null +++ b/apps/server/src/contexts/invoicing/presentation/dto/invoices.response.dto.ts @@ -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; + +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; +} diff --git a/apps/server/src/contexts/invoicing/presentation/dto/invoices.schemas.ts b/apps/server/src/contexts/invoicing/presentation/dto/invoices.schemas.ts new file mode 100644 index 00000000..1f38009e --- /dev/null +++ b/apps/server/src/contexts/invoicing/presentation/dto/invoices.schemas.ts @@ -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({}); diff --git a/apps/server/src/routes/accounts.routes.ts b/apps/server/src/routes/accounts.routes.ts index 35fbd1b3..da094817 100644 --- a/apps/server/src/routes/accounts.routes.ts +++ b/apps/server/src/routes/accounts.routes.ts @@ -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) => { diff --git a/apps/server/src/routes/invoicingRoutes.ts b/apps/server/src/routes/invoicingRoutes.ts new file mode 100644 index 00000000..db549683 --- /dev/null +++ b/apps/server/src/routes/invoicingRoutes.ts @@ -0,0 +1,66 @@ +import { validateRequestDTO } from "@common/presentation"; +import { checkTabContext } from "@contexts/auth/infraestructure"; +import { + ICreateInvoiceRequestSchema, + IDeleteInvoiceRequestSchema, + IGetInvoiceRequestSchema, + IUpdateInvoiceRequestSchema, + ListInvoicesRequestSchema, +} from "@contexts/invoicing/presentation/dto"; +import { NextFunction, Request, Response, Router } from "express"; + +export const invoicingRouter = (appRouter: Router) => { + const routes: Router = Router({ mergeParams: true }); + + routes.get( + "/", + validateRequestDTO(ListInvoicesRequestSchema), + checkTabContext, + //checkUser, + (req: Request, res: Response, next: NextFunction) => { + listInvoicesController().execute(req, res, next); + } + ); + + routes.get( + "/:invoiceId", + validateRequestDTO(IGetInvoiceRequestSchema), + checkTabContext, + //checkUser, + (req: Request, res: Response, next: NextFunction) => { + getInvoiceController().execute(req, res, next); + } + ); + + routes.post( + "/", + validateRequestDTO(ICreateInvoiceRequestSchema), + checkTabContext, + //checkUser, + (req: Request, res: Response, next: NextFunction) => { + createInvoiceController().execute(req, res, next); + } + ); + + routes.put( + "/:invoiceId", + validateRequestDTO(IUpdateInvoiceRequestSchema), + checkTabContext, + //checkUser, + (req: Request, res: Response, next: NextFunction) => { + updateInvoiceController().execute(req, res, next); + } + ); + + routes.delete( + "/:invoiceId", + validateRequestDTO(IDeleteInvoiceRequestSchema), + checkTabContext, + //checkUser, + (req: Request, res: Response, next: NextFunction) => { + updateInvoiceController().execute(req, res, next); + } + ); + + appRouter.use("/invoices", routes); +};