This commit is contained in:
David Arranz 2025-04-01 16:26:15 +02:00
parent 7def4f7dc5
commit 41edc1bf72
195 changed files with 3317 additions and 1899 deletions

View File

@ -31,7 +31,7 @@ export function createApp(): Application {
});
app.use((req, _, next) => {
logger.info(`▶️ Incoming request ${req.method} to ${req.path}`);
logger.info(`▶️ Incoming request ${req.method} to ${req.path}`);
next();
});

View File

@ -49,7 +49,7 @@ export class EmailAddress extends ValueObject<EmailAddressProps> {
return this.props.value;
}
toString(): string {
toPrimitive() {
return this.getValue();
}
}

View File

@ -5,6 +5,7 @@ import { Quantity } from "./quantity";
import { ValueObject } from "./value-object";
const DEFAULT_SCALE = 2;
const DEFAULT_CURRENCY_CODE = "EUR";
type CurrencyData = Currency;
@ -19,8 +20,8 @@ export type RoundingMode =
interface IMoneyValueProps {
amount: number;
scale: number;
currency_code: string;
scale?: number;
currency_code?: string;
}
interface IMoneyValue {
@ -59,8 +60,8 @@ export class MoneyValue extends ValueObject<IMoneyValueProps> implements IMoneyV
this.dinero = Object.freeze(
DineroFactory({
amount,
precision: scale,
currency: currency_code as Currency,
precision: scale || DEFAULT_SCALE,
currency: (currency_code as Currency) || DEFAULT_CURRENCY_CODE,
})
); // 🔒 Garantiza inmutabilidad
}
@ -81,6 +82,28 @@ export class MoneyValue extends ValueObject<IMoneyValueProps> implements IMoneyV
return this.props;
}
/** Serializa el VO a una cadena del tipo "EUR:123400:2" */
toPersistence(): string {
return `${this.currency()}:${this.value.getAmount()}:${this.getScale()}`;
}
/** Reconstruye el VO desde la cadena persistida */
static fromPersistence(value: string): MoneyValue {
const [currencyCode, amountStr, scaleStr] = value.split(":");
const amount = parseInt(amountStr, 10);
const scale = parseInt(scaleStr, 10);
const currency = getCurrencyByCode(currencyCode);
return new MoneyValue(amount, scale, currency);
}
toPrimitive() {
return {
amount: this.amount,
scale: this.scale,
currency_code: this.currency,
};
}
convertScale(newScale: number, roundingMode: RoundingMode = "HALF_UP"): MoneyValue {
const _newDinero = this.dinero.convertPrecision(newScale, roundingMode);
return new MoneyValue({
@ -171,6 +194,12 @@ export class MoneyValue extends ValueObject<IMoneyValueProps> implements IMoneyV
return this.dinero.hasSameAmount(comparator.dinero);
}
/**
* Devuelve una cadena con el importe formateado.
* Ejemplo: 123456 -> 1,234.56
* @param locale Código de idioma y país (ej. "es-ES")
* @returns Importe formateado
*/
format(locale: string): string {
const amount = this.amount;
const currency = this.currency;

View File

@ -56,7 +56,7 @@ export class Name extends ValueObject<INameProps> {
return this.props.value;
}
toString(): string {
toPrimitive() {
return this.getValue();
}
}

View File

@ -73,6 +73,10 @@ export class Percentage extends ValueObject<IPercentageProps> implements IPercen
return this.props;
}
toPrimitive() {
return this.getValue();
}
toNumber(): number {
return this.amount / Math.pow(10, this.scale);
}

View File

@ -41,7 +41,7 @@ export class PhoneNumber extends ValueObject<PhoneNumberProps> {
return this.props.value;
}
toString(): string {
toPrimitive(): string {
return this.getValue();
}

View File

@ -99,6 +99,10 @@ export class PostalAddress extends ValueObject<IPostalAddressProps> {
return this.props;
}
toPrimitive() {
return this.getValue();
}
toString(): string {
return `${this.props.street}, ${this.props.street2}, ${this.props.city}, ${this.props.postalCode}, ${this.props.state}, ${this.props.country}`;
}

View File

@ -17,6 +17,10 @@ interface IQuantity {
toNumber(): number;
toString(): string;
isZero(): boolean;
isPositive(): boolean;
isNegative(): boolean;
increment(anotherQuantity?: Quantity): Result<Quantity, Error>;
decrement(anotherQuantity?: Quantity): Result<Quantity, Error>;
hasSameScale(otherQuantity: Quantity): boolean;
@ -58,6 +62,10 @@ export class Quantity extends ValueObject<IQuantityProps> implements IQuantity {
return this.props;
}
toPrimitive() {
return this.getValue();
}
toNumber(): number {
return this.amount / Math.pow(10, this.scale);
}
@ -66,6 +74,18 @@ export class Quantity extends ValueObject<IQuantityProps> implements IQuantity {
return this.toNumber().toFixed(this.scale);
}
isZero(): boolean {
return this.amount === 0;
}
isPositive(): boolean {
return this.amount > 0;
}
isNegative(): boolean {
return this.amount < 0;
}
increment(anotherQuantity?: Quantity): Result<Quantity, Error> {
if (!anotherQuantity) {
return Quantity.create({

View File

@ -43,7 +43,7 @@ export class Slug extends ValueObject<SlugProps> {
return this.props.value;
}
toString(): string {
toPrimitive(): string {
return this.getValue();
}
}

View File

@ -45,7 +45,7 @@ export class TINNumber extends ValueObject<TINNumberProps> {
return this.props.value;
}
toString(): string {
toPrimitive(): string {
return this.props.value;
}
}

View File

@ -36,7 +36,7 @@ export class UniqueID extends ValueObject<string> {
return this.props;
}
toString(): string {
toPrimitive(): string {
return this.props;
}
}

View File

@ -44,6 +44,13 @@ export class UtcDate extends ValueObject<IUtcDateProps> {
return this.props.value;
}
/**
* Devuelve la fecha completa en formato UTC con hora. Ejemplo: 2025-12-31T23:59:59Z.
*/
toPrimitive() {
return this.getValue();
}
/**
* Devuelve la fecha en formato UTC sin hora (YYYY-MM-DD).
*/

View File

@ -9,6 +9,8 @@ export abstract class ValueObject<T> {
abstract getValue(): any;
abstract toPrimitive(): any;
equals(other: ValueObject<T>): boolean {
if (!(other instanceof ValueObject)) {
return false;

View File

@ -0,0 +1,20 @@
export interface IErrorDTO {
detail?: string;
instance?: string;
status: number;
title: string;
type?: string;
context: IErrorContextDTO;
extra: IErrorExtraDTO;
}
export interface IErrorContextDTO {
user?: unknown;
params?: Record<string, any>;
query?: Record<string, any>;
body?: Record<string, any>;
}
export interface IErrorExtraDTO {
errors: Record<string, any>[];
}

View File

@ -0,0 +1,2 @@
export * from "./error.dto";
export * from "./types.dto";

View File

@ -0,0 +1,15 @@
export interface IMoneyDTO {
amount: number | null;
scale: number;
currency_code: string;
}
export interface IPercentageDTO {
amount: number | null;
scale: number;
}
export interface IQuantityDTO {
amount: number | null;
scale: number;
}

View File

@ -1 +1,2 @@
export * from "./dto";
export * from "./express";

View File

@ -4,7 +4,7 @@ import { ITransactionManager } from "@common/infrastructure/database";
import { logger } from "@common/infrastructure/logger";
import { Account, IAccountService } from "@contexts/accounts/domain";
export class GetAccountsUseCase {
export class GetAccountUseCase {
constructor(
private readonly accountService: IAccountService,
private readonly transactionManager: ITransactionManager

View File

@ -53,7 +53,7 @@ export class AccountStatus extends ValueObject<IAccountStatusProps> {
return AccountStatus.create(nextStatus);
}
toString(): string {
toPrimitive(): string {
return this.getValue();
}
}

View File

@ -68,11 +68,11 @@ export class AccountMapper
public mapToPersistence(source: Account, params?: MapperParamsType): AccountCreationAttributes {
return {
id: source.id.toString(),
id: source.id.toPrimitive(),
is_freelancer: source.isFreelancer,
name: source.name,
trade_name: source.tradeName.getOrUndefined(),
tin: source.tin.toString(),
tin: source.tin.toPrimitive(),
street: source.address.street,
city: source.address.city,
@ -80,9 +80,9 @@ export class AccountMapper
postal_code: source.address.postalCode,
country: source.address.country,
email: source.email.toString(),
phone: source.phone.toString(),
fax: source.fax.isSome() ? source.fax.getOrUndefined()?.toString() : undefined,
email: source.email.toPrimitive(),
phone: source.phone.toPrimitive(),
fax: source.fax.isSome() ? source.fax.getOrUndefined()?.toPrimitive() : undefined,
website: source.website.getOrUndefined(),
legal_record: source.legalRecord,

View File

@ -9,10 +9,7 @@ import {
export type AccountCreationAttributes = InferCreationAttributes<AccountModel, {}> & {};
export class AccountModel extends Model<
InferAttributes<AccountModel>,
InferCreationAttributes<AccountModel>
> {
export class AccountModel extends Model<InferAttributes<AccountModel>, AccountCreationAttributes> {
// To avoid table creation
/*static async sync(): Promise<any> {
return Promise.resolve();

View File

@ -5,7 +5,7 @@ import { accountRepository } from "@contexts/accounts/infraestructure";
import { CreateAccountController } from "./create-account.controller";
import { createAccountPresenter } from "./create-account.presenter";
export const createAccountController = () => {
export const buildCreateAccountController = () => {
const transactionManager = new SequelizeTransactionManager();
const accountService = new AccountService(accountRepository);

View File

@ -1,11 +1,11 @@
import { UniqueID } from "@common/domain";
import { ExpressController } from "@common/presentation";
import { GetAccountsUseCase } from "@contexts/accounts/application";
import { GetAccountUseCase } from "@contexts/accounts/application";
import { IGetAccountPresenter } from "./get-account.presenter";
export class GetAccountController extends ExpressController {
public constructor(
private readonly getAccount: GetAccountsUseCase,
private readonly getAccount: GetAccountUseCase,
private readonly presenter: IGetAccountPresenter
) {
super();

View File

@ -8,12 +8,12 @@ export interface IGetAccountPresenter {
export const getAccountPresenter: IGetAccountPresenter = {
toDTO: (account: Account): IGetAccountResponseDTO => ({
id: ensureString(account.id.toString()),
id: ensureString(account.id.toPrimitive()),
is_freelancer: ensureBoolean(account.isFreelancer),
name: ensureString(account.name),
trade_name: ensureString(account.tradeName.getOrUndefined()),
tin: ensureString(account.tin.toString()),
tin: ensureString(account.tin.toPrimitive()),
street: ensureString(account.address.street),
city: ensureString(account.address.city),
@ -21,9 +21,9 @@ export const getAccountPresenter: IGetAccountPresenter = {
postal_code: ensureString(account.address.postalCode),
country: ensureString(account.address.country),
email: ensureString(account.email.toString()),
phone: ensureString(account.phone.toString()),
fax: ensureString(account.fax.getOrUndefined()?.toString()),
email: ensureString(account.email.toPrimitive()),
phone: ensureString(account.phone.toPrimitive()),
fax: ensureString(account.fax.getOrUndefined()?.toPrimitive()),
website: ensureString(account.website.getOrUndefined()),
legal_record: ensureString(account.legalRecord),

View File

@ -1,15 +1,15 @@
import { SequelizeTransactionManager } from "@common/infrastructure";
import { GetAccountsUseCase } from "@contexts/accounts/application";
import { GetAccountUseCase } from "@contexts/accounts/application";
import { AccountService } from "@contexts/accounts/domain";
import { accountRepository } from "@contexts/accounts/infraestructure";
import { GetAccountController } from "./get-account.controller";
import { getAccountPresenter } from "./get-account.presenter";
export const getAccountController = () => {
export const buildGetAccountController = () => {
const transactionManager = new SequelizeTransactionManager();
const accountService = new AccountService(accountRepository);
const useCase = new GetAccountsUseCase(accountService, transactionManager);
const useCase = new GetAccountUseCase(accountService, transactionManager);
const presenter = getAccountPresenter;
return new GetAccountController(useCase, presenter);

View File

@ -5,7 +5,7 @@ import { accountRepository } from "@contexts/accounts/infraestructure";
import { ListAccountsController } from "./list-accounts.controller";
import { listAccountsPresenter } from "./list-accounts.presenter";
export const listAccountsController = () => {
export const buildListAccountsController = () => {
const transactionManager = new SequelizeTransactionManager();
const accountService = new AccountService(accountRepository);

View File

@ -5,7 +5,7 @@ import { accountRepository } from "@contexts/accounts/infraestructure";
import { UpdateAccountController } from "./update-account.controller";
import { updateAccountPresenter } from "./update-account.presenter";
export const updateAccountController = () => {
export const buildUpdateAccountController = () => {
const transactionManager = new SequelizeTransactionManager();
const accountService = new AccountService(accountRepository);

View File

@ -1,4 +1,6 @@
import { Result } from "@common/helpers";
import { ITransactionManager } from "@common/infrastructure/database";
import { logger } from "@common/infrastructure/logger";
import { RegisterData } from "@contexts/auth/domain";
import { IAuthService } from "@contexts/auth/domain/services";
@ -10,7 +12,12 @@ export class RegisterUseCase {
public async execute(registerData: RegisterData) {
return await this.transactionManager.complete(async (transaction) => {
return await this.authService.registerUser(registerData, transaction);
try {
return await this.authService.registerUser(registerData, transaction);
} catch (error: unknown) {
logger.error(error as Error);
return Result.fail(error as Error);
}
});
}
}

View File

@ -5,7 +5,7 @@ import { userRepository } from "@contexts/auth/infraestructure";
import { ListUsersController } from "./list-users.controller";
import { listUsersPresenter } from "./list-users.presenter";
export const listUsersController = () => {
export const buildListUsersController = () => {
const transactionManager = new SequelizeTransactionManager();
const userService = new UserService(userRepository);

View File

@ -5,7 +5,7 @@ import { authenticatedUserRepository, tabContextRepository } from "@contexts/aut
import { LoginController } from "./login.controller";
import { loginPresenter } from "./login.presenter";
export const loginController = () => {
export const buildLoginController = () => {
const transactionManager = new SequelizeTransactionManager();
const authService = new AuthService(authenticatedUserRepository, tabContextRepository);

View File

@ -4,7 +4,7 @@ import { AuthService } from "@contexts/auth/domain/services";
import { authenticatedUserRepository, tabContextRepository } from "@contexts/auth/infraestructure";
import { LogoutController } from "./logout.controller";
export const logoutController = () => {
export const buildLogoutController = () => {
const transactionManager = new SequelizeTransactionManager();
const authService = new AuthService(authenticatedUserRepository, tabContextRepository);

View File

@ -5,7 +5,7 @@ import { authenticatedUserRepository, tabContextRepository } from "@contexts/aut
import { RefreshTokenController } from "./refresh-token.controller";
import { refreshTokenPresenter } from "./refresh-token.presenter";
export const refreshTokenController = () => {
export const buildRefreshTokenController = () => {
const transactionManager = new SequelizeTransactionManager();
const authService = new AuthService(authenticatedUserRepository, tabContextRepository);

View File

@ -5,7 +5,7 @@ import { authenticatedUserRepository, tabContextRepository } from "@contexts/aut
import { RegisterController } from "./register.controller";
import { registerPresenter } from "./register.presenter";
export const registerController = () => {
export const buildRegisterController = () => {
const transactionManager = new SequelizeTransactionManager();
const authService = new AuthService(authenticatedUserRepository, tabContextRepository);

View File

@ -1,10 +1,17 @@
import { UniqueID } from "@common/domain";
import { UniqueID, UtcDate } 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";
import {
IInvoiceProps,
IInvoiceService,
Invoice,
InvoiceNumber,
InvoiceSerie,
InvoiceStatus,
} from "@contexts/invoices/domain";
import { ICreateInvoiceRequestDTO } from "../presentation/dto";
export class CreateInvoiceUseCase {
constructor(
@ -37,40 +44,71 @@ export class CreateInvoiceUseCase {
private validateInvoiceData(dto: ICreateInvoiceRequestDTO): Result<IInvoiceProps, Error> {
const errors: Error[] = [];
let invoice_status = InvoiceStatus.create(invoiceDTO.status).object;
const invoiceNumerOrError = InvoiceNumber.create(dto.invoice_number);
const invoiceSeriesOrError = InvoiceSerie.create(dto.invoice_series);
const issueDateOrError = UtcDate.create(dto.issue_date);
const operationDateOrError = UtcDate.create(dto.operation_date);
const result = Result.combine([
invoiceNumerOrError,
invoiceSeriesOrError,
issueDateOrError,
operationDateOrError,
]);
if (result.isFailure) {
return Result.fail(result.error);
}
const validatedData: IInvoiceProps = {
status: InvoiceStatus.createDraft(),
invoiceNumber: invoiceNumerOrError.data,
invoiceSeries: invoiceSeriesOrError.data,
issueDate: issueDateOrError.data,
operationDate: operationDateOrError.data,
//invoiceCurrency: defaultCurrency.
};
/*if (errors.length > 0) {
const message = errors.map((err) => err.message).toString();
return Result.fail(new Error(message));
}*/
return Result.ok(validatedData);
/*let invoice_status = InvoiceStatus.create(dto.status).object;
if (invoice_status.isEmpty()) {
invoice_status = InvoiceStatus.createDraft();
}
let invoice_series = InvoiceSeries.create(invoiceDTO.invoice_series).object;
let invoice_series = InvoiceSeries.create(dto.invoice_series).object;
if (invoice_series.isEmpty()) {
invoice_series = InvoiceSeries.create(invoiceDTO.invoice_series).object;
invoice_series = InvoiceSeries.create(dto.invoice_series).object;
}
let issue_date = InvoiceDate.create(invoiceDTO.issue_date).object;
let issue_date = InvoiceDate.create(dto.issue_date).object;
if (issue_date.isEmpty()) {
issue_date = InvoiceDate.createCurrentDate().object;
}
let operation_date = InvoiceDate.create(invoiceDTO.operation_date).object;
let operation_date = InvoiceDate.create(dto.operation_date).object;
if (operation_date.isEmpty()) {
operation_date = InvoiceDate.createCurrentDate().object;
}
let invoiceCurrency = Currency.createFromCode(invoiceDTO.currency).object;
let invoiceCurrency = Currency.createFromCode(dto.currency).object;
if (invoiceCurrency.isEmpty()) {
invoiceCurrency = Currency.createDefaultCode().object;
}
let invoiceLanguage = Language.createFromCode(invoiceDTO.language_code).object;
let invoiceLanguage = Language.createFromCode(dto.language_code).object;
if (invoiceLanguage.isEmpty()) {
invoiceLanguage = Language.createDefaultCode().object;
}
const items = new Collection<InvoiceItem>(
invoiceDTO.items?.map(
dto.items?.map(
(item) =>
InvoiceSimpleItem.create({
description: Description.create(item.description).object,
@ -104,6 +142,6 @@ export class CreateInvoiceUseCase {
items,
},
invoiceId
);
);*/
}
}

View File

@ -1,7 +1,7 @@
import { Collection, Result } from "@common/helpers";
import { ITransactionManager } from "@common/infrastructure/database";
import { logger } from "@common/infrastructure/logger";
import { Invoice } from "../domain";
import { IInvoiceService, Invoice } from "../domain";
export class ListInvoicesUseCase {
constructor(

View File

@ -1,13 +1,13 @@
import { IAdapter, RepositoryBuilder } from "@/contexts/common/domain";
import { UniqueID } from "@shared/contexts";
import { IInvoiceParticipantRepository } from "../../domain";
import { InvoiceParticipant } from "../../domain/InvoiceParticipant/InvoiceParticipant";
import { InvoiceCustomer } from "../../domain/entities/invoice-customer/invoice-customer";
export const participantFinder = async (
participantId: UniqueID,
adapter: IAdapter,
repository: RepositoryBuilder<IInvoiceParticipantRepository>,
): Promise<InvoiceParticipant | undefined> => {
repository: RepositoryBuilder<IInvoiceParticipantRepository>
): Promise<InvoiceCustomer | undefined> => {
if (!participantId || (participantId && participantId.isNull())) {
return Promise.resolve(undefined);
}

View File

@ -1,11 +1,13 @@
import { AggregateRoot, MoneyValue, UniqueID, UtcDate } from "@common/domain";
import { Collection, Result } from "@common/helpers";
import { Currency } from "dinero.js";
import { InvoiceStatus } from "../value-objects";
import { InvoiceCustomer, InvoiceItem, InvoiceItems } from "../entities";
import { InvoiceNumber, InvoiceSerie, InvoiceStatus } from "../value-objects";
export interface IInvoiceProps {
invoiceNumber: InvoiceNumber;
invoiceSeries: InvoiceSeries;
invoiceSeries: InvoiceSerie;
status: InvoiceStatus;
issueDate: UtcDate;
operationDate: UtcDate;
@ -13,27 +15,26 @@ export interface IInvoiceProps {
//dueDate: UtcDate; // ? --> depende de la forma de pago
//tax: Tax; // ? --> detalles?
invoiceCurrency: Currency;
invoiceCurrency: string;
language: Language;
//language: Language;
//purchareOrderNumber: string;
//notes: Note;
//senderId: UniqueID;
recipient: InvoiceParticipant;
//paymentInstructions: Note;
//paymentTerms: string;
items: Collection<InvoiceItem>;
customer?: InvoiceCustomer;
items?: Collection<InvoiceItem>;
}
export interface IInvoice {
id: UniqueID;
invoiceNumber: InvoiceNumber;
invoiceSeries: InvoiceSeries;
invoiceSeries: InvoiceSerie;
status: InvoiceStatus;
@ -42,13 +43,13 @@ export interface IInvoice {
//senderId: UniqueID;
recipient: InvoiceParticipant;
customer?: InvoiceCustomer;
//dueDate
//tax: Tax;
language: Language;
currency: Currency;
//language: Language;
invoiceCurrency: string;
//purchareOrderNumber: string;
//notes: Note;
@ -56,7 +57,7 @@ export interface IInvoice {
//paymentInstructions: Note;
//paymentTerms: string;
items: Collection<InvoiceItem>;
items: InvoiceItems;
calculateSubtotal: () => MoneyValue;
calculateTaxTotal: () => MoneyValue;
@ -64,8 +65,14 @@ export interface IInvoice {
}
export class Invoice extends AggregateRoot<IInvoiceProps> implements IInvoice {
private _items: Collection<InvoiceItem>;
protected _status: InvoiceStatus;
private _items!: Collection<InvoiceItem>;
//protected _status: InvoiceStatus;
protected constructor(props: IInvoiceProps, id?: UniqueID) {
super(props, id);
this._items = props.items || InvoiceItems.create();
}
static create(props: IInvoiceProps, id?: UniqueID): Result<Invoice, Error> {
const invoice = new Invoice(props, id);
@ -97,17 +104,17 @@ export class Invoice extends AggregateRoot<IInvoiceProps> implements IInvoice {
return this.props.senderId;
}*/
get recipient(): InvoiceParticipant {
return this.props.recipient;
get customer(): InvoiceCustomer | undefined {
return this.props.customer;
}
get operationDate() {
return this.props.operationDate;
}
get language() {
/*get language() {
return this.props.language;
}
}*/
get dueDate() {
return undefined;
@ -118,7 +125,7 @@ export class Invoice extends AggregateRoot<IInvoiceProps> implements IInvoice {
}
get status() {
return this._status;
return this.props.status;
}
get items() {
@ -145,7 +152,7 @@ export class Invoice extends AggregateRoot<IInvoiceProps> implements IInvoice {
return this.props.shipTo;
}*/
get currency() {
get invoiceCurrency() {
return this.props.invoiceCurrency;
}
@ -167,42 +174,30 @@ export class Invoice extends AggregateRoot<IInvoiceProps> implements IInvoice {
}*/
calculateSubtotal(): MoneyValue {
let subtotal: MoneyValue | null = null;
const invoiceSubtotal = MoneyValue.create({
amount: 0,
currency_code: this.props.invoiceCurrency,
scale: 2,
}).data;
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;
return this._items.getAll().reduce((subtotal, item) => {
return subtotal.add(item.calculateTotal());
}, invoiceSubtotal);
}
// Method to calculate the total tax in the invoice
calculateTaxTotal(): MoneyValue {
let taxTotal = MoneyValue.create({
const taxTotal = MoneyValue.create({
amount: 0,
currencyCode: this.props.invoiceCurrency.code,
precision: 2,
}).object;
currency_code: this.props.invoiceCurrency,
scale: 2,
}).data;
for (const item of this._items.items) {
taxTotal = taxTotal.add(item.calculateTaxAmount());
}
return taxTotal.convertPrecision(2);
return taxTotal;
}
// Method to calculate the total invoice amount, including taxes
calculateTotal(): MoneyValue {
return this.calculateSubtotal().add(this.calculateTaxTotal()).convertPrecision(2);
return this.calculateSubtotal().add(this.calculateTaxTotal());
}
}

View File

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

View File

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

View File

@ -0,0 +1,78 @@
import { EmailAddress, Name, PostalAddress, ValueObject } from "@common/domain";
import { Result } from "@common/helpers";
import { PhoneNumber } from "libphonenumber-js";
import { InvoiceAddressType } from "../../value-objects";
export interface IInvoiceAddressProps {
type: InvoiceAddressType;
title: Name;
address: PostalAddress;
email: EmailAddress;
phone: PhoneNumber;
}
export interface IInvoiceAddress {
type: InvoiceAddressType;
title: Name;
address: PostalAddress;
email: EmailAddress;
phone: PhoneNumber;
}
export class InvoiceAddress extends ValueObject<IInvoiceAddressProps> implements IInvoiceAddress {
public static create(props: IInvoiceAddressProps) {
return Result.ok(new this(props));
}
public static createShippingAddress(props: IInvoiceAddressProps) {
return Result.ok(
new this({
...props,
type: InvoiceAddressType.create("shipping").data,
})
);
}
public static createBillingAddress(props: IInvoiceAddressProps) {
return Result.ok(
new this({
...props,
type: InvoiceAddressType.create("billing").data,
})
);
}
get title(): Name {
return this.props.title;
}
get address(): PostalAddress {
return this.props.address;
}
get email(): EmailAddress {
return this.props.email;
}
get phone(): PhoneNumber {
return this.props.phone;
}
get type(): InvoiceAddressType {
return this.props.type;
}
getValue(): IInvoiceAddressProps {
return this.props;
}
toPrimitive() {
return {
type: this.type.toString(),
title: this.title.toString(),
address: this.address.toString(),
email: this.email.toString(),
phone: this.phone.toString(),
};
}
}

View File

@ -0,0 +1,61 @@
import { DomainEntity, Name, TINNumber, UniqueID } from "@common/domain";
import { Result } from "@common/helpers";
import { InvoiceAddress } from "./invoice-address";
export interface IInvoiceCustomerProps {
tin: TINNumber;
companyName: Name;
firstName: Name;
lastName: Name;
billingAddress?: InvoiceAddress;
shippingAddress?: InvoiceAddress;
}
export interface IInvoiceCustomer {
id: UniqueID;
tin: TINNumber;
companyName: Name;
firstName: Name;
lastName: Name;
billingAddress?: InvoiceAddress;
shippingAddress?: InvoiceAddress;
}
export class InvoiceCustomer
extends DomainEntity<IInvoiceCustomerProps>
implements IInvoiceCustomer
{
public static create(
props: IInvoiceCustomerProps,
id?: UniqueID
): Result<InvoiceCustomer, Error> {
const participant = new InvoiceCustomer(props, id);
return Result.ok<InvoiceCustomer>(participant);
}
get tin(): TINNumber {
return this.props.tin;
}
get companyName(): Name {
return this.props.companyName;
}
get firstName(): Name {
return this.props.firstName;
}
get lastName(): Name {
return this.props.lastName;
}
get billingAddress() {
return this.props.billingAddress;
}
get shippingAddress() {
return this.props.shippingAddress;
}
}

View File

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

View File

@ -0,0 +1,83 @@
import { MoneyValue, Percentage, Quantity } from "@common/domain";
import { InvoiceItemDescription } from "../../value-objects";
import { InvoiceItem } from "./invoice-item";
describe("InvoiceItem", () => {
it("debería calcular correctamente el subtotal (unitPrice * quantity)", () => {
const props = {
description: InvoiceItemDescription.create("Producto A"),
quantity: Quantity.create({ amount: 200, scale: 2 }),
unitPrice: MoneyValue.create(50),
discount: Percentage.create(0),
};
const result = InvoiceItem.create(props);
expect(result.isOk()).toBe(true);
const invoiceItem = result.unwrap();
expect(invoiceItem.subtotalPrice.value).toBe(100); // 50 * 2
});
it("debería calcular correctamente el total con descuento", () => {
const props = {
description: new InvoiceItemDescription("Producto B"),
quantity: new Quantity(3),
unitPrice: new MoneyValue(30),
discount: new Percentage(10), // 10%
};
const result = InvoiceItem.create(props);
expect(result.isOk()).toBe(true);
const invoiceItem = result.unwrap();
expect(invoiceItem.totalPrice.value).toBe(81); // (30 * 3) - 10% de (30 * 3)
});
it("debería devolver los valores correctos de las propiedades", () => {
const props = {
description: new InvoiceItemDescription("Producto C"),
quantity: new Quantity(1),
unitPrice: new MoneyValue(100),
discount: new Percentage(5),
};
const result = InvoiceItem.create(props);
expect(result.isOk()).toBe(true);
const invoiceItem = result.unwrap();
expect(invoiceItem.description.value).toBe("Producto C");
expect(invoiceItem.quantity.value).toBe(1);
expect(invoiceItem.unitPrice.value).toBe(100);
expect(invoiceItem.discount.value).toBe(5);
});
it("debería manejar correctamente un descuento del 0%", () => {
const props = {
description: new InvoiceItemDescription("Producto D"),
quantity: new Quantity(4),
unitPrice: new MoneyValue(25),
discount: new Percentage(0),
};
const result = InvoiceItem.create(props);
expect(result.isOk()).toBe(true);
const invoiceItem = result.unwrap();
expect(invoiceItem.totalPrice.value).toBe(100); // 25 * 4
});
it("debería manejar correctamente un descuento del 100%", () => {
const props = {
description: new InvoiceItemDescription("Producto E"),
quantity: new Quantity(2),
unitPrice: new MoneyValue(50),
discount: new Percentage(100),
};
const result = InvoiceItem.create(props);
expect(result.isOk()).toBe(true);
const invoiceItem = result.unwrap();
expect(invoiceItem.totalPrice.value).toBe(0); // (50 * 2) - 100% de (50 * 2)
});
});

View File

@ -0,0 +1,72 @@
import { MoneyValue, Percentage, Quantity, ValueObject } from "@common/domain";
import { Result } from "@common/helpers";
import { InvoiceItemDescription } from "../../value-objects";
export interface IInvoiceItemProps {
description: InvoiceItemDescription;
quantity: Quantity; // Cantidad de unidades
unitPrice: MoneyValue; // Precio unitario en la moneda de la factura
subtotalPrice?: MoneyValue; // Precio unitario * Cantidad
discount: Percentage; // % descuento
totalPrice?: MoneyValue;
}
export interface IInvoiceItem {
description: InvoiceItemDescription;
quantity: Quantity;
unitPrice: MoneyValue;
subtotalPrice: MoneyValue;
discount: Percentage;
totalPrice: MoneyValue;
}
export class InvoiceItem extends ValueObject<IInvoiceItemProps> implements IInvoiceItem {
private _subtotalPrice!: MoneyValue;
private _totalPrice!: MoneyValue;
public static create(props: IInvoiceItemProps): Result<InvoiceItem, Error> {
return Result.ok(new InvoiceItem(props));
}
get description(): InvoiceItemDescription {
return this.props.description;
}
get quantity(): Quantity {
return this.props.quantity;
}
get unitPrice(): MoneyValue {
return this.props.unitPrice;
}
get subtotalPrice(): MoneyValue {
if (!this._subtotalPrice) {
this._subtotalPrice = this.calculateSubtotal();
}
return this._subtotalPrice;
}
get discount(): Percentage {
return this.props.discount;
}
get totalPrice(): MoneyValue {
if (!this._totalPrice) {
this._totalPrice = this.calculateTotal();
}
return this._totalPrice;
}
getValue() {
return this.props;
}
calculateSubtotal(): MoneyValue {
return this.unitPrice.multiply(this.quantity.toNumber()); // Precio unitario * Cantidad
}
calculateTotal(): MoneyValue {
return this.subtotalPrice.subtract(this.subtotalPrice.percentage(this.discount.toNumber()));
}
}

View File

@ -0,0 +1,8 @@
import { Collection } from "@common/helpers";
import { InvoiceItem } from "./invoice-item";
export class InvoiceItems extends Collection<InvoiceItem> {
public static create(items?: InvoiceItem[]): InvoiceItems {
return new InvoiceItems(items);
}
}

View File

@ -0,0 +1,5 @@
export * from "./aggregates";
export * from "./entities";
export * from "./repositories";
export * from "./services";
export * from "./value-objects";

View File

@ -18,5 +18,5 @@ export interface IInvoiceService {
transaction?: any
): Promise<Result<Invoice, Error>>;
deleteInvoiceById(invoiceId: UniqueID, transaction?: any): Promise<Result<Invoice, Error>>;
deleteInvoiceById(invoiceId: UniqueID, transaction?: any): Promise<Result<boolean, Error>>;
}

View File

@ -30,7 +30,7 @@ export class InvoiceService implements IInvoiceService {
data: Partial<IInvoiceProps>,
transaction?: Transaction
): Promise<Result<Invoice, Error>> {
// Verificar si la cuenta existe
// Verificar si la factura existe
const invoiceOrError = await this.repo.findById(invoiceId, transaction);
if (invoiceOrError.isFailure) {
return Result.fail(new Error("Invoice not found"));
@ -54,7 +54,7 @@ export class InvoiceService implements IInvoiceService {
data: IInvoiceProps,
transaction?: Transaction
): Promise<Result<Invoice, Error>> {
// Verificar si la cuenta existe
// Verificar si la factura existe
const invoiceOrError = await this.repo.findById(invoiceId, transaction);
if (invoiceOrError.isSuccess) {
return Result.fail(new Error("Invoice exists"));
@ -74,21 +74,7 @@ export class InvoiceService implements IInvoiceService {
async deleteInvoiceById(
invoiceId: UniqueID,
transaction?: Transaction
): Promise<Result<Invoice, Error>> {
// Verificar si la cuenta existe
const invoiceOrError = await this.repo.findById(invoiceId, transaction);
if (invoiceOrError.isFailure) {
return Result.fail(new Error("Invoice not exists"));
}
const newInvoiceOrError = Invoice.create(data, invoiceId);
if (newInvoiceOrError.isFailure) {
return Result.fail(new Error(`Error creating invoice: ${newInvoiceOrError.error.message}`));
}
const newInvoice = newInvoiceOrError.data;
await this.repo.create(newInvoice, transaction);
return Result.ok(newInvoice);
): Promise<Result<boolean, Error>> {
return this.repo.deleteById(invoiceId, transaction);
}
}

View File

@ -0,0 +1,5 @@
export * from "./invoice-address-type";
export * from "./invoice-item-description";
export * from "./invoice-number";
export * from "./invoice-serie";
export * from "./invoice-status";

View File

@ -0,0 +1,38 @@
import { ValueObject } from "@common/domain";
import { Result } from "@common/helpers";
interface IInvoiceAddressTypeProps {
value: string;
}
export enum INVOICE_ADDRESS_TYPE {
SHIPPING = "shipping",
BILLING = "billing",
}
export class InvoiceAddressType extends ValueObject<IInvoiceAddressTypeProps> {
private static readonly ALLOWED_TYPES = ["shipping", "billing"];
static create(value: string): Result<InvoiceAddressType, Error> {
if (!this.ALLOWED_TYPES.includes(value)) {
return Result.fail(
new Error(
`Invalid address type: ${value}. Allowed types are: ${this.ALLOWED_TYPES.join(", ")}`
)
);
}
return Result.ok(new InvoiceAddressType({ value }));
}
getValue(): string {
return this.props.value;
}
toString(): string {
return this.getValue();
}
toPrimitive(): string {
return this.getValue();
}
}

View File

@ -0,0 +1,46 @@
import { ValueObject } from "@common/domain";
import { Maybe, Result } from "@common/helpers";
import { z } from "zod";
interface IInvoiceItemDescriptionProps {
value: string;
}
export class InvoiceItemDescription extends ValueObject<IInvoiceItemDescriptionProps> {
private static readonly MAX_LENGTH = 255;
protected static validate(value: string) {
const schema = z
.string()
.trim()
.max(InvoiceItemDescription.MAX_LENGTH, {
message: `Description must be at most ${InvoiceItemDescription.MAX_LENGTH} characters long`,
});
return schema.safeParse(value);
}
static create(value: string) {
const valueIsValid = InvoiceItemDescription.validate(value);
if (!valueIsValid.success) {
return Result.fail(new Error(valueIsValid.error.errors[0].message));
}
return Result.ok(new InvoiceItemDescription({ value }));
}
static createNullable(value?: string): Result<Maybe<InvoiceItemDescription>, Error> {
if (!value || value.trim() === "") {
return Result.ok(Maybe.none<InvoiceItemDescription>());
}
return InvoiceItemDescription.create(value!).map((value) => Maybe.some(value));
}
getValue(): string {
return this.props.value;
}
toString(): string {
return this.getValue();
}
}

View File

@ -0,0 +1,38 @@
import { ValueObject } from "@common/domain";
import { Result } from "@common/helpers";
import { z } from "zod";
interface IInvoiceNumberProps {
value: string;
}
export class InvoiceNumber extends ValueObject<IInvoiceNumberProps> {
private static readonly MAX_LENGTH = 255;
protected static validate(value: string) {
const schema = z
.string()
.trim()
.max(InvoiceNumber.MAX_LENGTH, {
message: `Name must be at most ${InvoiceNumber.MAX_LENGTH} characters long`,
});
return schema.safeParse(value);
}
static create(value: string) {
const valueIsValid = InvoiceNumber.validate(value);
if (!valueIsValid.success) {
return Result.fail(new Error(valueIsValid.error.errors[0].message));
}
return Result.ok(new InvoiceNumber({ value }));
}
getValue(): string {
return this.props.value;
}
toString(): string {
return this.getValue();
}
}

View File

@ -0,0 +1,46 @@
import { ValueObject } from "@common/domain";
import { Maybe, Result } from "@common/helpers";
import { z } from "zod";
interface IInvoiceSerieProps {
value: string;
}
export class InvoiceSerie extends ValueObject<IInvoiceSerieProps> {
private static readonly MAX_LENGTH = 255;
protected static validate(value: string) {
const schema = z
.string()
.trim()
.max(InvoiceSerie.MAX_LENGTH, {
message: `Name must be at most ${InvoiceSerie.MAX_LENGTH} characters long`,
});
return schema.safeParse(value);
}
static create(value: string) {
const valueIsValid = InvoiceSerie.validate(value);
if (!valueIsValid.success) {
return Result.fail(new Error(valueIsValid.error.errors[0].message));
}
return Result.ok(new InvoiceSerie({ value }));
}
static createNullable(value?: string): Result<Maybe<InvoiceSerie>, Error> {
if (!value || value.trim() === "") {
return Result.ok(Maybe.none<InvoiceSerie>());
}
return InvoiceSerie.create(value!).map((value) => Maybe.some(value));
}
getValue(): string {
return this.props.value;
}
toString(): string {
return this.getValue();
}
}

View File

@ -57,6 +57,10 @@ export class InvoiceStatus extends ValueObject<IInvoiceStatusProps> {
return this.props.value;
}
toPrimitive() {
return this.getValue();
}
canTransitionTo(nextStatus: string): boolean {
return InvoiceStatus.TRANSITIONS[this.props.value].includes(nextStatus);
}

View File

@ -1,12 +1,9 @@
import {
ISequelizeAdapter,
SequelizeRepository,
} from "@/contexts/common/infrastructure/sequelize";
import { ISequelizeAdapter, SequelizeRepository } from "@/contexts/common/infrastructure/sequelize";
import { Transaction } from "sequelize";
import { InvoiceParticipant } from "../domain";
import { InvoiceCustomer } from "../domain";
import { IInvoiceParticipantMapper } from "./mappers";
export class InvoiceParticipantRepository extends SequelizeRepository<InvoiceParticipant> {
export class InvoiceParticipantRepository extends SequelizeRepository<InvoiceCustomer> {
protected mapper: IInvoiceParticipantMapper;
public constructor(props: {

View File

@ -1,19 +1,10 @@
import {
ISequelizeMapper,
SequelizeMapper,
} from "@/contexts/common/infrastructure";
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";
import { Contact_Model, TCreationContact_Model } from "../sequelize/contact.mo.del";
import { IContactAddressMapper, createContactAddressMapper } from "./contactAddress.mapper";
export interface IContactMapper
extends ISequelizeMapper<Contact_Model, TCreationContact_Model, Contact> {}
@ -22,10 +13,7 @@ class ContactMapper
extends SequelizeMapper<Contact_Model, TCreationContact_Model, Contact>
implements IContactMapper
{
public constructor(props: {
addressMapper: IContactAddressMapper;
context: IInvoicingContext;
}) {
public constructor(props: { addressMapper: IContactAddressMapper; context: IInvoicingContext }) {
super(props);
}
@ -44,15 +32,9 @@ class ContactMapper
);
}
const billingAddress = this.props.addressMapper.mapToDomain(
source.billingAddress!,
params
);
const billingAddress = this.props.addressMapper.mapToDomain(source.billingAddress!, params);
const shippingAddress = this.props.addressMapper.mapToDomain(
source.shippingAddress!,
params
);
const shippingAddress = this.props.addressMapper.mapToDomain(source.shippingAddress!, params);
const props: IContactProps = {
tin: this.mapsValue(source, "tin", TINNumber.create),
@ -74,9 +56,7 @@ class ContactMapper
}
}
export const createContactMapper = (
context: IInvoicingContext
): IContactMapper =>
export const createContactMapper = (context: IInvoicingContext): IContactMapper =>
new ContactMapper({
addressMapper: createContactAddressMapper(context),
context,

View File

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

View File

@ -1,43 +1,23 @@
import {
ISequelizeMapper,
SequelizeMapper,
} from "@/contexts/common/infrastructure";
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 { IInvoiceSimpleItemProps, InvoiceItem, InvoiceSimpleItem } from "../../domain/entities";
import { IInvoicingContext } from "../InvoicingContext";
import {
InvoiceItem_Model,
Invoice_Model,
TCreationInvoiceItem_Model,
} from "../sequelize";
import { InvoiceItem_Model, InvoiceModel, TCreationInvoiceItem_Model } from "../sequelize";
export interface IInvoiceItemMapper
extends ISequelizeMapper<
InvoiceItem_Model,
TCreationInvoiceItem_Model,
InvoiceItem
> {}
extends ISequelizeMapper<InvoiceItem_Model, TCreationInvoiceItem_Model, InvoiceItem> {}
export const createInvoiceItemMapper = (
context: IInvoicingContext,
): IInvoiceItemMapper => new InvoiceItemMapper({ context });
export const createInvoiceItemMapper = (context: IInvoicingContext): IInvoiceItemMapper =>
new InvoiceItemMapper({ context });
class InvoiceItemMapper
extends SequelizeMapper<
InvoiceItem_Model,
TCreationInvoiceItem_Model,
InvoiceItem
>
extends SequelizeMapper<InvoiceItem_Model, TCreationInvoiceItem_Model, InvoiceItem>
implements IInvoiceItemMapper
{
protected toDomainMappingImpl(
source: InvoiceItem_Model,
params: { sourceParent: Invoice_Model },
params: { sourceParent: InvoiceModel }
): InvoiceItem {
const { sourceParent } = params;
const id = this.mapsValue(source, "item_id", UniqueID.create);
@ -50,7 +30,7 @@ class InvoiceItemMapper
amount: unit_price,
currencyCode: sourceParent.invoice_currency,
precision: 4,
}),
})
),
};
@ -65,7 +45,7 @@ class InvoiceItemMapper
protected toPersistenceMappingImpl(
source: InvoiceItem,
params: { index: number; sourceParent: Invoice },
params: { index: number; sourceParent: Invoice }
): TCreationInvoiceItem_Model {
const { index, sourceParent } = params;

View File

@ -0,0 +1,96 @@
import { UniqueID, UtcDate } from "@common/domain";
import { Result } from "@common/helpers";
import {
ISequelizeMapper,
MapperParamsType,
SequelizeMapper,
} from "@common/infrastructure/sequelize/sequelize-mapper";
import { Invoice, InvoiceNumber, InvoiceSerie, InvoiceStatus } from "@contexts/invoices/domain/";
import { InvoiceCreationAttributes, InvoiceModel } from "../sequelize";
export interface IInvoiceMapper
extends ISequelizeMapper<InvoiceModel, InvoiceCreationAttributes, Invoice> {}
export class InvoiceMapper
extends SequelizeMapper<InvoiceModel, InvoiceCreationAttributes, Invoice>
implements IInvoiceMapper
{
public mapToDomain(source: InvoiceModel, params?: MapperParamsType): Result<Invoice, Error> {
const idOrError = UniqueID.create(source.id);
const statusOrError = InvoiceStatus.create(source.invoice_status);
const invoiceSeriesOrError = InvoiceSerie.create(source.invoice_series);
const invoiceNumberOrError = InvoiceNumber.create(source.invoice_number);
const issueDateOrError = UtcDate.create(source.issue_date);
const operationDateOrError = UtcDate.create(source.operation_date);
/*const subtotalOrError = MoneyValue.create({
amount: source.subtotal,
scale: 2,
currency_code: source.invoice_currency,
});
const totalOrError = MoneyValue.create({
amount: source.total,
scale: 2,
currency_code: source.invoice_currency,
});*/
const result = Result.combine([
idOrError,
statusOrError,
invoiceSeriesOrError,
invoiceNumberOrError,
issueDateOrError,
operationDateOrError,
//subtotalOrError,
//totalOrError,
]);
if (result.isFailure) {
return Result.fail(result.error);
}
const invoiceCurrency = source.invoice_currency || "EUR";
return Invoice.create(
{
status: statusOrError.data,
invoiceSeries: invoiceSeriesOrError.data,
invoiceNumber: invoiceNumberOrError.data,
issueDate: issueDateOrError.data,
operationDate: operationDateOrError.data,
invoiceCurrency,
//currency: source.invoice_currency,
//subtotal: subtotalOrError.data,
//total: totalOrError.data,
},
idOrError.data
);
}
public mapToPersistence(source: Invoice, params?: MapperParamsType): InvoiceCreationAttributes {
const subtotal = source.calculateSubtotal();
const total = source.calculateTotal();
return {
id: source.id.toString(),
invoice_status: source.status.toPrimitive(),
invoice_series: source.invoiceSeries.toPrimitive(),
invoice_number: source.invoiceNumber.toPrimitive(),
issue_date: source.issueDate.toPrimitive(),
operation_date: source.operationDate.toPrimitive(),
invoice_language: "es",
invoice_currency: source.invoiceCurrency || "EUR",
subtotal_amount: subtotal.amount,
subtotal_scale: subtotal.scale,
total_amount: total.amount,
total_scale: total.scale,
};
}
}
const invoiceMapper: InvoiceMapper = new InvoiceMapper();
export { invoiceMapper };

View File

@ -1,20 +1,14 @@
import {
ISequelizeMapper,
SequelizeMapper,
} from "@/contexts/common/infrastructure";
import { ISequelizeMapper, SequelizeMapper } from "@/contexts/common/infrastructure";
import { Name, TINNumber, UniqueID } from "@shared/contexts";
import {
IInvoiceParticipantProps,
IInvoiceCustomerProps,
Invoice,
InvoiceParticipant,
InvoiceCustomer,
InvoiceParticipantBillingAddress,
InvoiceParticipantShippingAddress,
} from "../../domain";
import { IInvoicingContext } from "../InvoicingContext";
import {
InvoiceParticipant_Model,
TCreationInvoiceParticipant_Model,
} from "../sequelize";
import { InvoiceParticipant_Model, TCreationInvoiceParticipant_Model } from "../sequelize";
import {
IInvoiceParticipantAddressMapper,
createInvoiceParticipantAddressMapper,
@ -24,11 +18,11 @@ export interface IInvoiceParticipantMapper
extends ISequelizeMapper<
InvoiceParticipant_Model,
TCreationInvoiceParticipant_Model,
InvoiceParticipant
InvoiceCustomer
> {}
export const createInvoiceParticipantMapper = (
context: IInvoicingContext,
context: IInvoicingContext
): IInvoiceParticipantMapper =>
new InvoiceParticipantMapper({
context,
@ -39,7 +33,7 @@ class InvoiceParticipantMapper
extends SequelizeMapper<
InvoiceParticipant_Model,
TCreationInvoiceParticipant_Model,
InvoiceParticipant
InvoiceCustomer
>
implements IInvoiceParticipantMapper
{
@ -66,24 +60,20 @@ class InvoiceParticipantMapper
}
*/
const billingAddress = source.billingAddress
? ((
this.props.addressMapper as IInvoiceParticipantAddressMapper
).mapToDomain(
? ((this.props.addressMapper as IInvoiceParticipantAddressMapper).mapToDomain(
source.billingAddress,
params,
params
) as InvoiceParticipantBillingAddress)
: undefined;
const shippingAddress = source.shippingAddress
? ((
this.props.addressMapper as IInvoiceParticipantAddressMapper
).mapToDomain(
? ((this.props.addressMapper as IInvoiceParticipantAddressMapper).mapToDomain(
source.shippingAddress,
params,
params
) as InvoiceParticipantShippingAddress)
: undefined;
const props: IInvoiceParticipantProps = {
const props: IInvoiceCustomerProps = {
tin: this.mapsValue(source, "tin", TINNumber.create),
firstName: this.mapsValue(source, "first_name", Name.create),
lastName: this.mapsValue(source, "last_name", Name.create),
@ -93,7 +83,7 @@ class InvoiceParticipantMapper
};
const id = this.mapsValue(source, "participant_id", UniqueID.create);
const participantOrError = InvoiceParticipant.create(props, id);
const participantOrError = InvoiceCustomer.create(props, id);
if (participantOrError.isFailure) {
throw participantOrError.error;
@ -103,8 +93,8 @@ class InvoiceParticipantMapper
}
protected toPersistenceMappingImpl(
source: InvoiceParticipant,
params: { sourceParent: Invoice },
source: InvoiceCustomer,
params: { sourceParent: Invoice }
): TCreationInvoiceParticipant_Model {
const { sourceParent } = params;

View File

@ -1,7 +1,4 @@
import {
ISequelizeMapper,
SequelizeMapper,
} from "@/contexts/common/infrastructure";
import { ISequelizeMapper, SequelizeMapper } from "@/contexts/common/infrastructure";
import {
City,
Country,
@ -15,7 +12,7 @@ import {
} from "@shared/contexts";
import {
IInvoiceParticipantAddressProps,
InvoiceParticipant,
InvoiceCustomer,
InvoiceParticipantAddress,
} from "../../domain";
import { IInvoicingContext } from "../InvoicingContext";
@ -33,8 +30,7 @@ export interface IInvoiceParticipantAddressMapper
export const createInvoiceParticipantAddressMapper = (
context: IInvoicingContext
): IInvoiceParticipantAddressMapper =>
new InvoiceParticipantAddressMapper({ context });
): IInvoiceParticipantAddressMapper => new InvoiceParticipantAddressMapper({ context });
class InvoiceParticipantAddressMapper
extends SequelizeMapper<
@ -44,10 +40,7 @@ class InvoiceParticipantAddressMapper
>
implements IInvoiceParticipantAddressMapper
{
protected toDomainMappingImpl(
source: InvoiceParticipantAddress_Model,
params: any
) {
protected toDomainMappingImpl(source: InvoiceParticipantAddress_Model, params: any) {
const id = this.mapsValue(source, "address_id", UniqueID.create);
const props: IInvoiceParticipantAddressProps = {
@ -73,7 +66,7 @@ class InvoiceParticipantAddressMapper
protected toPersistenceMappingImpl(
source: InvoiceParticipantAddress,
params: { sourceParent: InvoiceParticipant }
params: { sourceParent: InvoiceCustomer }
) {
const { sourceParent } = params;

View File

@ -8,10 +8,7 @@ import {
Sequelize,
} from "sequelize";
import {
ContactAddress_Model,
TCreationContactAddress_Attributes,
} from "./contactAddress.model";
import { ContactAddress_Model, TCreationContactAddress_Attributes } from "./contactAddress.mo.del";
export type TCreationContact_Model = InferCreationAttributes<
Contact_Model,
@ -22,14 +19,8 @@ export type TCreationContact_Model = InferCreationAttributes<
};
export class Contact_Model extends Model<
InferAttributes<
Contact_Model,
{ omit: "shippingAddress" | "billingAddress" }
>,
InferCreationAttributes<
Contact_Model,
{ omit: "shippingAddress" | "billingAddress" }
>
InferAttributes<Contact_Model, { omit: "shippingAddress" | "billingAddress" }>,
InferCreationAttributes<Contact_Model, { omit: "shippingAddress" | "billingAddress" }>
> {
// To avoid table creation
static async sync(): Promise<any> {

View File

@ -8,7 +8,7 @@ import {
NonAttribute,
Sequelize,
} from "sequelize";
import { Contact_Model } from "./contact.model";
import { Contact_Model } from "./contact.mo.del";
export type TCreationContactAddress_Attributes = InferCreationAttributes<
ContactAddress_Model,

View File

@ -1,4 +1,4 @@
import { IInvoiceRepository } from "@contexts/invoicing/domain";
import { IInvoiceRepository } from "@contexts/invoices/domain";
import { invoiceRepository } from "./invoice.repository";
export * from "./invoice.model";

View File

@ -7,16 +7,28 @@ import {
NonAttribute,
Sequelize,
} from "sequelize";
import { Invoice_Model } from "./invoice.model";
import { InvoiceModel } from "./invoice.model";
export type TCreationInvoiceItem_Model = InferCreationAttributes<
InvoiceItem_Model,
{ omit: "invoice" }
{
/*omit: "invoice"*/
}
>;
export class InvoiceItem_Model extends Model<
InferAttributes<InvoiceItem_Model, { omit: "invoice" }>,
InferCreationAttributes<InvoiceItem_Model, { omit: "invoice" }>
InferAttributes<
InvoiceItem_Model,
{
/*omit: "invoice"*/
}
>,
InferCreationAttributes<
InvoiceItem_Model,
{
/*omit: "invoice"*/
}
>
> {
static associate(connection: Sequelize) {
const { Invoice_Model, InvoiceItem_Model } = connection.models;
@ -34,12 +46,20 @@ export class InvoiceItem_Model extends Model<
declare position: number;
declare item_type: string;
declare description: CreationOptional<string>;
declare quantity: CreationOptional<number>;
declare unit_price: CreationOptional<number>;
declare subtotal: CreationOptional<number>;
declare total: CreationOptional<number>;
declare invoice?: NonAttribute<Invoice_Model>;
declare quantity_amount: CreationOptional<number>;
declare quantity_scale: CreationOptional<number>;
declare unit_price_amount: CreationOptional<number>;
declare unit_price_scale: CreationOptional<number>;
declare subtotal_amount: CreationOptional<number>;
declare subtotal_scale: CreationOptional<number>;
declare total_amount: CreationOptional<number>;
declare total_scale: CreationOptional<number>;
declare invoice?: NonAttribute<InvoiceModel>;
}
export default (sequelize: Sequelize) => {
@ -71,14 +91,29 @@ export default (sequelize: Sequelize) => {
type: new DataTypes.TEXT(),
allowNull: true,
},
quantity: {
type: DataTypes.BIGINT(),
allowNull: true,
},
unit_price: {
quantity_amount: {
type: new DataTypes.BIGINT(),
allowNull: true,
defaultValue: null,
},
quantity_scale: {
type: new DataTypes.SMALLINT(),
allowNull: true,
defaultValue: null,
},
unit_price_amount: {
type: new DataTypes.BIGINT(),
allowNull: true,
defaultValue: null,
},
unit_price_scale: {
type: new DataTypes.SMALLINT(),
allowNull: true,
defaultValue: null,
},
/*tax_slug: {
type: new DataTypes.DECIMAL(3, 2),
allowNull: true,
@ -91,23 +126,37 @@ export default (sequelize: Sequelize) => {
type: new DataTypes.DECIMAL(3, 2),
allowNull: true,
},*/
subtotal: {
type: new DataTypes.BIGINT(),
subtotal_amount: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: true,
defaultValue: null,
},
subtotal_scale: {
type: new DataTypes.SMALLINT(),
allowNull: true,
defaultValue: null,
},
/*tax_amount: {
type: new DataTypes.BIGINT(),
allowNull: true,
},*/
total: {
type: new DataTypes.BIGINT(),
total_amount: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: true,
defaultValue: null,
},
total_scale: {
type: new DataTypes.SMALLINT(),
allowNull: true,
defaultValue: null,
},
},
{
sequelize,
tableName: "invoice_items",
},
}
);
return InvoiceItem_Model;

View File

@ -0,0 +1,141 @@
import {
CreationOptional,
DataTypes,
InferAttributes,
InferCreationAttributes,
Model,
Sequelize,
} from "sequelize";
export type InvoiceCreationAttributes = InferCreationAttributes<InvoiceModel, {}> & {};
export class InvoiceModel extends Model<InferAttributes<InvoiceModel>, InvoiceCreationAttributes> {
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: "customer",
foreignKey: "invoice_id",
onDelete: "CASCADE",
});*/
}
declare id: string;
declare invoice_status: string;
declare invoice_series: CreationOptional<string>;
declare invoice_number: CreationOptional<string>;
declare issue_date: CreationOptional<string>;
declare operation_date: CreationOptional<string>;
declare invoice_language: string;
declare invoice_currency: string;
// Subtotal
declare subtotal_amount: CreationOptional<number>;
declare subtotal_scale: CreationOptional<number>;
// Total
declare total_amount: CreationOptional<number>;
declare total_scale: CreationOptional<number>;
//declare items: NonAttribute<InvoiceItem_Model[]>;
//declare customer: NonAttribute<InvoiceParticipant_Model[]>;
}
export default (sequelize: Sequelize) => {
InvoiceModel.init(
{
id: {
type: new DataTypes.UUID(),
primaryKey: true,
},
invoice_status: {
type: new DataTypes.STRING(),
allowNull: false,
},
invoice_series: {
type: new DataTypes.STRING(),
allowNull: true,
defaultValue: null,
},
invoice_number: {
type: new DataTypes.STRING(),
allowNull: true,
defaultValue: null,
},
issue_date: {
type: new DataTypes.DATE(),
allowNull: true,
defaultValue: null,
},
operation_date: {
type: new DataTypes.DATE(),
allowNull: true,
defaultValue: null,
},
invoice_language: {
type: new DataTypes.STRING(),
allowNull: false,
},
invoice_currency: {
type: new DataTypes.STRING(3), // ISO 4217
allowNull: false,
},
subtotal_amount: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: true,
defaultValue: null,
},
subtotal_scale: {
type: new DataTypes.SMALLINT(),
allowNull: true,
defaultValue: null,
},
total_amount: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: true,
defaultValue: null,
},
total_scale: {
type: new DataTypes.SMALLINT(),
allowNull: true,
defaultValue: null,
},
},
{
sequelize,
tableName: "invoices",
paranoid: true, // softs deletes
timestamps: true,
createdAt: "created_at",
updatedAt: "updated_at",
deletedAt: "deleted_at",
indexes: [{ unique: true, fields: ["invoice_number"] }],
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope
defaultScope: {},
scopes: {},
}
);
return InvoiceModel;
};

View File

@ -7,11 +7,11 @@ import {
NonAttribute,
Sequelize,
} from "sequelize";
import { Invoice_Model } from "./invoice.model";
import { InvoiceModel } from "./invoice.model";
import {
InvoiceParticipantAddress_Model,
TCreationInvoiceParticipantAddress_Model,
} from "./invoiceParticipantAddress.model";
} from "./invoiceParticipantAddress.mo.del";
export type TCreationInvoiceParticipant_Model = InferCreationAttributes<
InvoiceParticipant_Model,
@ -32,11 +32,8 @@ export class InvoiceParticipant_Model extends Model<
>
> {
static associate(connection: Sequelize) {
const {
Invoice_Model,
InvoiceParticipantAddress_Model,
InvoiceParticipant_Model,
} = connection.models;
const { Invoice_Model, InvoiceParticipantAddress_Model, InvoiceParticipant_Model } =
connection.models;
InvoiceParticipant_Model.belongsTo(Invoice_Model, {
as: "invoice",
@ -67,7 +64,7 @@ export class InvoiceParticipant_Model extends Model<
declare shippingAddress?: NonAttribute<InvoiceParticipantAddress_Model>;
declare billingAddress?: NonAttribute<InvoiceParticipantAddress_Model>;
declare invoice?: NonAttribute<Invoice_Model>;
declare invoice?: NonAttribute<InvoiceModel>;
}
export default (sequelize: Sequelize) => {
@ -102,7 +99,7 @@ export default (sequelize: Sequelize) => {
sequelize,
tableName: "invoice_participants",
timestamps: false,
},
}
);
return InvoiceParticipant_Model;

View File

@ -7,7 +7,7 @@ import {
NonAttribute,
Sequelize,
} from "sequelize";
import { InvoiceParticipant_Model } from "./invoiceParticipant.model";
import { InvoiceParticipant_Model } from "./invoiceParticipant.mo.del";
export type TCreationInvoiceParticipantAddress_Model = InferCreationAttributes<
InvoiceParticipantAddress_Model,
@ -16,14 +16,10 @@ export type TCreationInvoiceParticipantAddress_Model = InferCreationAttributes<
export class InvoiceParticipantAddress_Model extends Model<
InferAttributes<InvoiceParticipantAddress_Model, { omit: "participant" }>,
InferCreationAttributes<
InvoiceParticipantAddress_Model,
{ omit: "participant" }
>
InferCreationAttributes<InvoiceParticipantAddress_Model, { omit: "participant" }>
> {
static associate(connection: Sequelize) {
const { InvoiceParticipantAddress_Model, InvoiceParticipant_Model } =
connection.models;
const { InvoiceParticipantAddress_Model, InvoiceParticipant_Model } = connection.models;
InvoiceParticipantAddress_Model.belongsTo(InvoiceParticipant_Model, {
as: "participant",
foreignKey: "participant_id",
@ -91,7 +87,7 @@ export default (sequelize: Sequelize) => {
{
sequelize,
tableName: "invoice_participant_addresses",
},
}
);
return InvoiceParticipantAddress_Model;

View File

@ -0,0 +1,45 @@
import { UniqueID } from "@common/domain";
import { ExpressController } from "@common/presentation";
import { CreateInvoiceUseCase } from "../../../application";
import { ICreateInvoiceRequestDTO } from "../../dto";
import { ICreateInvoicePresenter } from "./presenter";
export class CreateInvoiceController extends ExpressController {
public constructor(
private readonly createInvoice: CreateInvoiceUseCase,
private readonly presenter: ICreateInvoicePresenter
) {
super();
}
protected async executeImpl() {
const createDTO: ICreateInvoiceRequestDTO = this.req.body;
// Validar ID
const invoiceIdOrError = UniqueID.create(createDTO.id);
if (invoiceIdOrError.isFailure) return this.invalidInputError("Invoice ID not valid");
const invoiceOrError = await this.createInvoice.execute(invoiceIdOrError.data, createDTO);
if (invoiceOrError.isFailure) {
return this.handleError(invoiceOrError.error);
}
return this.ok(this.presenter.toDTO(invoiceOrError.data));
}
private handleError(error: Error) {
const message = error.message;
if (
message.includes("Database connection lost") ||
message.includes("Database request timed out")
) {
return this.unavailableError(
"Database service is currently unavailable. Please try again later."
);
}
return this.conflictError(message);
}
}

View File

@ -0,0 +1,16 @@
import { SequelizeTransactionManager } from "@common/infrastructure";
import { CreateInvoiceUseCase } from "@contexts/invoices/application/create-invoice.use-case";
import { InvoiceService } from "@contexts/invoices/domain";
import { invoiceRepository } from "@contexts/invoices/intrastructure";
import { CreateInvoiceController } from "./create-invoice.controller";
import { createInvoicePresenter } from "./presenter";
export const buildCreateInvoiceController = () => {
const transactionManager = new SequelizeTransactionManager();
const invoiceService = new InvoiceService(invoiceRepository);
const useCase = new CreateInvoiceUseCase(invoiceService, transactionManager);
const presenter = createInvoicePresenter;
return new CreateInvoiceController(useCase, presenter);
};

View File

@ -4,16 +4,16 @@ import { ICollection, IMoney_Response_DTO } from "@shared/contexts";
export const invoiceItemPresenter = (
items: ICollection<InvoiceItem>,
context: IInvoicingContext,
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,
unit_price: item.unitPrice.toPrimitive() as IMoney_Response_DTO,
subtotal: item.calculateSubtotal().toPrimitive() as IMoney_Response_DTO,
tax_amount: item.calculateTaxAmount().toPrimitive() as IMoney_Response_DTO,
total: item.calculateTotal().toPrimitive() as IMoney_Response_DTO,
}))
: [];

View File

@ -0,0 +1,28 @@
import { Invoice } from "@contexts/invoices/domain";
import { ICreateInvoiceResponseDTO } from "../../../dto";
export interface ICreateInvoicePresenter {
toDTO: (invoice: Invoice) => ICreateInvoiceResponseDTO;
}
export const createInvoicePresenter: ICreateInvoicePresenter = {
toDTO: (invoice: Invoice): ICreateInvoiceResponseDTO => ({
id: invoice.id.toString(),
invoice_status: invoice.status.toString(),
invoice_number: invoice.invoiceNumber.toString(),
invoice_series: invoice.invoiceSeries.toString(),
issue_date: invoice.issueDate.toDateString(),
operation_date: invoice.operationDate.toDateString(),
language_code: "es",
currency: invoice.currency,
subtotal: invoice.calculateSubtotal().toPrimitive(),
total: invoice.calculateTotal().toPrimitive(),
//sender: {}, //await InvoiceParticipantPresenter(invoice.senderId, context),
//customer: InvoiceParticipantPresenter(invoice.recipient, context),
//items: invoiceItemPresenter(invoice.items, context),
}),
};

View File

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

View File

@ -0,0 +1,12 @@
import { ExpressController } from "@common/presentation";
import { DeleteInvoiceUseCase } from "@contexts/invoices/application";
export class DeleteInvoiceController extends ExpressController {
public constructor(private readonly deleteInvoice: DeleteInvoiceUseCase) {
super();
}
async executeImpl(): Promise<any> {
return this.noContent();
}
}

View File

@ -0,0 +1,14 @@
import { SequelizeTransactionManager } from "@common/infrastructure";
import { DeleteInvoiceUseCase } from "@contexts/invoices/application";
import { InvoiceService } from "@contexts/invoices/domain";
import { invoiceRepository } from "@contexts/invoices/intrastructure";
import { DeleteInvoiceController } from "./delete-invoice.controller";
export const buildDeleteInvoiceController = () => {
const transactionManager = new SequelizeTransactionManager();
const invoiceService = new InvoiceService(invoiceRepository);
const useCase = new DeleteInvoiceUseCase(invoiceService, transactionManager);
return new DeleteInvoiceController(useCase);
};

View File

@ -0,0 +1,44 @@
import { UniqueID } from "@common/domain";
import { ExpressController } from "@common/presentation";
import { GetInvoiceUseCase } from "@contexts/invoices/application";
import { IGetInvoicePresenter } from "./presenter";
export class GetInvoiceController extends ExpressController {
public constructor(
private readonly getInvoice: GetInvoiceUseCase,
private readonly presenter: IGetInvoicePresenter
) {
super();
}
protected async executeImpl() {
const { invoiceId } = this.req.params;
// Validar ID
const invoiceIdOrError = UniqueID.create(invoiceId);
if (invoiceIdOrError.isFailure) return this.invalidInputError("Invoice ID not valid");
const invoiceOrError = await this.getInvoice.execute(invoiceIdOrError.data);
if (invoiceOrError.isFailure) {
return this.handleError(invoiceOrError.error);
}
return this.ok(this.presenter.toDTO(invoiceOrError.data));
}
private handleError(error: Error) {
const message = error.message;
if (
message.includes("Database connection lost") ||
message.includes("Database request timed out")
) {
return this.unavailableError(
"Database service is currently unavailable. Please try again later."
);
}
return this.conflictError(message);
}
}

View File

@ -0,0 +1,16 @@
import { SequelizeTransactionManager } from "@common/infrastructure";
import { GetInvoiceUseCase } from "@contexts/invoices/application";
import { InvoiceService } from "@contexts/invoices/domain";
import { invoiceRepository } from "@contexts/invoices/intrastructure";
import { GetInvoiceController } from "./get-invoice.controller";
import { getInvoicePresenter } from "./presenter";
export const buildGetInvoiceController = () => {
const transactionManager = new SequelizeTransactionManager();
const invoiceService = new InvoiceService(invoiceRepository);
const useCase = new GetInvoiceUseCase(invoiceService, transactionManager);
const presenter = getInvoicePresenter;
return new GetInvoiceController(useCase, presenter);
};

View File

@ -4,16 +4,16 @@ import { ICollection, IMoney_Response_DTO } from "@shared/contexts";
export const invoiceItemPresenter = (
items: ICollection<InvoiceItem>,
context: IInvoicingContext,
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,
unit_price: item.unitPrice.toPrimitive() as IMoney_Response_DTO,
subtotal: item.calculateSubtotal().toPrimitive() as IMoney_Response_DTO,
tax_amount: item.calculateTaxAmount().toPrimitive() as IMoney_Response_DTO,
total: item.calculateTotal().toPrimitive() as IMoney_Response_DTO,
}))
: [];

View File

@ -0,0 +1,46 @@
import { Invoice } from "@contexts/invoices/domain";
import { IGetInvoiceResponseDTO } from "../../../dto";
export interface IGetInvoicePresenter {
toDTO: (invoice: Invoice) => IGetInvoiceResponseDTO;
}
export const getInvoicePresenter: IGetInvoicePresenter = {
toDTO: (invoice: Invoice): IGetInvoiceResponseDTO => ({
id: invoice.id.toPrimitive(),
invoice_status: invoice.status.toString(),
invoice_number: invoice.invoiceNumber.toString(),
invoice_series: invoice.invoiceSeries.toString(),
issue_date: invoice.issueDate.toISOString(),
operation_date: invoice.operationDate.toISOString(),
language_code: "ES",
currency: invoice.currency,
subtotal: invoice.calculateSubtotal().toPrimitive(),
total: invoice.calculateTotal().toPrimitive(),
//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: "",
},*/
}),
};

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