Facturas de cliente

This commit is contained in:
David Arranz 2025-09-04 19:57:04 +02:00
parent 206d5bf9d3
commit b7a58ebad5
39 changed files with 1056 additions and 445 deletions

View File

@ -0,0 +1,45 @@
import { toEmptyString } from "@repo/rdx-ddd";
import { CreateCustomerInvoiceResponseDTO } from "../../../../common/dto";
import { CustomerInvoice } from "../../../domain";
type CreateCustomerInvoiceItemsByInvoiceIdResponseDTO = CreateCustomerInvoiceResponseDTO["items"];
export class CreateCustomerInvoiceItemsAssembler {
toDTO(invoice: CustomerInvoice): CreateCustomerInvoiceItemsByInvoiceIdResponseDTO {
const { items } = invoice;
return items.map((item, index) => ({
id: item.id.toString(),
position: String(index),
description: toEmptyString(item.description, (value) => value.toPrimitive()),
quantity: item.quantity.match(
(quantity) => {
const { value, scale } = quantity.toPrimitive();
return { value: value.toString(), scale: scale.toString() };
},
() => ({ value: "", scale: "" })
),
unit_amount: item.unitAmount.match(
(unitAmount) => {
const { value, scale } = unitAmount.toPrimitive();
return { value: value.toString(), scale: scale.toString() };
},
() => ({ value: "", scale: "" })
),
discount_percentage: item.discountPercentage.match(
(discountPercentage) => {
const { value, scale } = discountPercentage.toPrimitive();
return { value: value.toString(), scale: scale.toString() };
},
() => ({ value: "", scale: "" })
),
total_amount: {
value: item.totalAmount.toPrimitive().value.toString(),
scale: item.totalAmount.toPrimitive().scale.toString(),
},
}));
}
}

View File

@ -1,26 +1,68 @@
import { CustomerInvoice } from "@erp/customer-invoices/api/domain"; import { UpdateCustomerInvoiceByIdResponseDTO } from "@erp/customer-invoices/common/dto";
import { CustomerInvoicesCreationResponseDTO } from "@erp/customer-invoices/common/dto"; import { toEmptyString } from "@repo/rdx-ddd";
import { CustomerInvoice } from "../../../domain";
import { CreateCustomerInvoiceItemsAssembler } from "./create-customer-invoice-items.assembler";
export class CreateCustomerInvoiceAssembler {
private _itemsAssembler!: CreateCustomerInvoiceItemsAssembler;
constructor() {
this._itemsAssembler = new CreateCustomerInvoiceItemsAssembler();
}
public toDTO(invoice: CustomerInvoice): UpdateCustomerInvoiceByIdResponseDTO {
const items = this._itemsAssembler.toDTO(invoice);
export class CreateCustomerInvoicesAssembler {
public toDTO(invoice: CustomerInvoice): CustomerInvoicesCreationResponseDTO {
return { return {
id: invoice.id.toPrimitive(), id: invoice.id.toPrimitive(),
company_id: invoice.companyId.toPrimitive(),
invoice_status: invoice.status.toString(),
invoice_number: invoice.invoiceNumber.toString(), invoice_number: invoice.invoiceNumber.toString(),
invoice_series: invoice.invoiceSeries.toString(), status: invoice.status.toPrimitive(),
issue_date: invoice.issueDate.toISOString(), series: invoice.series.toString(),
operation_date: invoice.operationDate.toISOString(),
language_code: "ES",
currency: "EUR",
//subtotal_price: invoice.calculateSubtotal().toPrimitive(), invoice_date: invoice.invoiceDate.toDateString(),
//total_price: invoice.calculateTotal().toPrimitive(), operation_date: toEmptyString(invoice.operationDate, (value) => value.toDateString()),
//recipient: CustomerInvoiceParticipantAssembler(customerInvoice.recipient), notes: toEmptyString(invoice.notes, (value) => value.toPrimitive()),
language_code: invoice.languageCode.toPrimitive(),
currency_code: invoice.currencyCode.toPrimitive(),
subtotal_amount: {
value: invoice.subtotalAmount.value.toString(),
scale: invoice.subtotalAmount.scale.toString(),
},
discount_percentage: {
value: invoice.discountPercentage.value.toString(),
scale: invoice.discountPercentage.scale.toString(),
},
discount_amount: {
value: invoice.discountAmount.value.toString(),
scale: invoice.discountAmount.scale.toString(),
},
taxable_amount: {
value: invoice.taxableAmount.value.toString(),
scale: invoice.taxableAmount.scale.toString(),
},
tax_amount: {
value: invoice.taxAmount.value.toString(),
scale: invoice.taxAmount.scale.toString(),
},
total_amount: {
value: invoice.totalAmount.value.toString(),
scale: invoice.totalAmount.scale.toString(),
},
items,
metadata: { metadata: {
entity: "customer-invoice", entity: "customer-invoices",
}, },
}; };
} }

View File

@ -1,62 +1,78 @@
import { DuplicateEntityError, ITransactionManager } from "@erp/core/api"; import { DuplicateEntityError, ITransactionManager } from "@erp/core/api";
import { CreateCustomerInvoiceRequestDTO } from "@erp/customer-invoices/common/dto"; import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { Transaction } from "sequelize"; import { Transaction } from "sequelize";
import { ICustomerInvoiceService } from "../../domain"; import { CreateCustomerInvoiceRequestDTO } from "../../../common/dto";
import { mapDTOToCustomerInvoiceProps } from "../helpers"; import { CustomerInvoiceService } from "../../domain";
import { CreateCustomerInvoicesAssembler } from "./assembler"; import { CreateCustomerInvoiceAssembler } from "./assembler";
import { mapDTOToCreateCustomerInvoiceProps } from "./map-dto-to-create-customer-invoice-props";
type CreateCustomerInvoiceUseCaseInput = { type CreateCustomerInvoiceUseCaseInput = {
tenantId: string; companyId: UniqueID;
dto: CreateCustomerInvoiceRequestDTO; dto: CreateCustomerInvoiceRequestDTO;
}; };
export class CreateCustomerInvoiceUseCase { export class CreateCustomerInvoiceUseCase {
constructor( constructor(
private readonly service: ICustomerInvoiceService, private readonly service: CustomerInvoiceService,
private readonly transactionManager: ITransactionManager, private readonly transactionManager: ITransactionManager,
private readonly assembler: CreateCustomerInvoicesAssembler private readonly assembler: CreateCustomerInvoiceAssembler
) {} ) {}
public execute(params: CreateCustomerInvoiceUseCaseInput) { public execute(params: CreateCustomerInvoiceUseCaseInput) {
const { dto, tenantId } = params; const { dto, companyId } = params;
const invoicePropsOrError = mapDTOToCustomerInvoiceProps(dto);
if (invoicePropsOrError.isFailure) { // 1) Mapear DTO → props de dominio
return Result.fail(invoicePropsOrError.error); const dtoResult = mapDTOToCreateCustomerInvoiceProps(dto);
if (dtoResult.isFailure) {
return Result.fail(dtoResult.error);
} }
const { props, id } = invoicePropsOrError.data; const { props, id } = dtoResult.data;
const invoiceOrError = this.service.build(props, id);
if (invoiceOrError.isFailure) { // 2) Construir entidad de dominio
return Result.fail(invoiceOrError.error); const buildResult = this.service.buildInvoiceInCompany(companyId, props, id);
if (buildResult.isFailure) {
return Result.fail(buildResult.error);
} }
const newInvoice = invoiceOrError.data; const newInvoice = buildResult.data;
// 3) Ejecutar bajo transacción: verificar duplicado → persistir → ensamblar vista
return this.transactionManager.complete(async (transaction: Transaction) => { return this.transactionManager.complete(async (transaction: Transaction) => {
try { const existsGuard = await this.ensureNotExists(companyId, id, transaction);
const duplicateCheck = await this.service.existsById(id, transaction); if (existsGuard.isFailure) {
return Result.fail(existsGuard.error);
if (duplicateCheck.isFailure) {
return Result.fail(duplicateCheck.error);
}
if (duplicateCheck.data) {
return Result.fail(new DuplicateEntityError("CustomerInvoice", id.toString()));
}
const result = await this.service.save(newInvoice, transaction);
if (result.isFailure) {
return Result.fail(result.error);
}
const viewDTO = this.assembler.toDTO(newInvoice);
return Result.ok(viewDTO);
} catch (error: unknown) {
return Result.fail(error as Error);
} }
const saveResult = await this.service.saveInvoice(newInvoice, transaction);
if (saveResult.isFailure) {
return Result.fail(saveResult.error);
}
const viewDTO = this.assembler.toDTO(saveResult.data);
return Result.ok(viewDTO);
}); });
} }
/**
Verifica que no exista uana factura con el mismo id en la companyId.
*/
private async ensureNotExists(
companyId: UniqueID,
id: UniqueID,
transaction: Transaction
): Promise<Result<void, Error>> {
const existsResult = await this.service.existsByIdInCompany(companyId, id, transaction);
if (existsResult.isFailure) {
return Result.fail(existsResult.error);
}
if (existsResult.data) {
return Result.fail(new DuplicateEntityError("Customer invoice", "id", String(id)));
}
return Result.ok<void>(undefined);
}
} }

View File

@ -0,0 +1,190 @@
import {
DomainError,
ValidationErrorCollection,
ValidationErrorDetail,
extractOrPushError,
} from "@erp/core/api";
import {
CurrencyCode,
LanguageCode,
Percentage,
TextValue,
UniqueID,
UtcDate,
maybeFromNullableVO,
} from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { CreateCustomerInvoiceRequestDTO } from "../../../common/dto";
import {
CustomerInvoiceItemDescription,
CustomerInvoiceItemDiscount,
CustomerInvoiceItemProps,
CustomerInvoiceItemQuantity,
CustomerInvoiceItemUnitAmount,
CustomerInvoiceNumber,
CustomerInvoiceProps,
CustomerInvoiceSerie,
CustomerInvoiceStatus,
} from "../../domain";
/**
* Convierte el DTO a las props validadas (CustomerProps).
* No construye directamente el agregado.
*
* @param dto - DTO con los datos de la factura de cliente
* @returns
*
*/
export function mapDTOToCreateCustomerInvoiceProps(dto: CreateCustomerInvoiceRequestDTO) {
try {
const errors: ValidationErrorDetail[] = [];
const customerId = extractOrPushError(UniqueID.create(dto.id), "id", errors);
const companyId = extractOrPushError(UniqueID.create(dto.company_id), "company_id", errors);
const invoiceNumber = extractOrPushError(
CustomerInvoiceNumber.create(dto.invoice_number),
"invoice_number",
errors
);
const status = extractOrPushError(CustomerInvoiceStatus.create(dto.status), "status", errors);
const series = extractOrPushError(
maybeFromNullableVO(dto.series, (value) => CustomerInvoiceSerie.create(value)),
"series",
errors
);
const invoiceDate = extractOrPushError(
UtcDate.createFromISO(dto.invoice_date),
"invoice_date",
errors
);
const operationDate = extractOrPushError(
maybeFromNullableVO(dto.operation_date, (value) => UtcDate.createFromISO(value)),
"operation_date",
errors
);
const notes = extractOrPushError(
maybeFromNullableVO(dto.notes, (value) => TextValue.create(value)),
"notes",
errors
);
const languageCode = extractOrPushError(
LanguageCode.create(dto.language_code),
"language_code",
errors
);
const currencyCode = extractOrPushError(
CurrencyCode.create(dto.currency_code),
"currency_code",
errors
);
const discountPercentage = extractOrPushError(
Percentage.create({
value: Number(dto.discount_percentage.value),
scale: Number(dto.discount_percentage.scale),
}),
"discount_percentage",
errors
);
const items = mapDTOToCreateCustomerInvoiceItemsProps(dto, errors);
if (errors.length > 0) {
return Result.fail(
new ValidationErrorCollection("Customer invoice props mapping failed", errors)
);
}
const invoiceProps: CustomerInvoiceProps = {
companyId: companyId!,
status: status!,
invoiceNumber: invoiceNumber!,
invoiceDate: invoiceDate!,
operationDate: operationDate!,
series: series!,
notes: notes!,
languageCode: languageCode!,
currencyCode: currencyCode!,
discountPercentage: discountPercentage!,
};
return Result.ok({ id: customerId!, props: invoiceProps });
} catch (err: unknown) {
return Result.fail(new DomainError("Customer invoice props mapping failed", { cause: err }));
}
}
function mapDTOToCreateCustomerInvoiceItemsProps(
dto: CreateCustomerInvoiceRequestDTO,
errors: ValidationErrorDetail[]
): CustomerInvoiceItemProps[] | undefined {
const items: CustomerInvoiceItemProps[] = [];
const languageCode = extractOrPushError(
LanguageCode.create(dto.language_code),
"language_code",
errors
);
const currencyCode = extractOrPushError(
CurrencyCode.create(dto.currency_code),
"currency_code",
errors
);
dto.items.forEach((item, index) => {
const description = extractOrPushError(
maybeFromNullableVO(item.description, (value) =>
CustomerInvoiceItemDescription.create(value)
),
"description",
errors
);
const quantity = extractOrPushError(
maybeFromNullableVO(item.quantity, (value) => CustomerInvoiceItemQuantity.create(value)),
"quantity",
errors
);
const unitAmount = extractOrPushError(
maybeFromNullableVO(item.unit_amount, (value) => CustomerInvoiceItemUnitAmount.create(value)),
"unit_amount",
errors
);
const discountPercentage = extractOrPushError(
maybeFromNullableVO(item.discount_percentage, (value) =>
CustomerInvoiceItemDiscount.create(value)
),
"discount_percentage",
errors
);
items.push({
currencyCode: currencyCode!,
languageCode: languageCode!,
description: description!,
quantity: quantity!,
unitAmount: unitAmount!,
discountPercentage: discountPercentage!,
});
});
return items;
}

View File

@ -1,8 +1,8 @@
import { UpdateCustomerInvoiceByIdResponseDTO } from "@erp/customer-invoices/common/dto";
import { toEmptyString } from "@repo/rdx-ddd"; import { toEmptyString } from "@repo/rdx-ddd";
import { GetCustomerInvoiceByIdResponseDTO } from "../../../../common/dto";
import { CustomerInvoice } from "../../../domain"; import { CustomerInvoice } from "../../../domain";
type GetCustomerInvoiceItemsByInvoiceIdResponseDTO = UpdateCustomerInvoiceByIdResponseDTO["items"]; type GetCustomerInvoiceItemsByInvoiceIdResponseDTO = GetCustomerInvoiceByIdResponseDTO["items"];
export class GetCustomerInvoiceItemsAssembler { export class GetCustomerInvoiceItemsAssembler {
toDTO(invoice: CustomerInvoice): GetCustomerInvoiceItemsByInvoiceIdResponseDTO { toDTO(invoice: CustomerInvoice): GetCustomerInvoiceItemsByInvoiceIdResponseDTO {

View File

@ -21,7 +21,7 @@ export class GetCustomerInvoiceAssembler {
status: invoice.status.toPrimitive(), status: invoice.status.toPrimitive(),
series: invoice.series.toString(), series: invoice.series.toString(),
issue_date: invoice.issueDate.toDateString(), invoice_date: invoice.invoiceDate.toDateString(),
operation_date: toEmptyString(invoice.operationDate, (value) => value.toDateString()), operation_date: toEmptyString(invoice.operationDate, (value) => value.toDateString()),
notes: toEmptyString(invoice.notes, (value) => value.toPrimitive()), notes: toEmptyString(invoice.notes, (value) => value.toPrimitive()),
@ -65,46 +65,6 @@ export class GetCustomerInvoiceAssembler {
entity: "customer-invoices", entity: "customer-invoices",
}, },
//subtotal: customerInvoice.calculateSubtotal().toPrimitive(),
//total: customerInvoice.calculateTotal().toPrimitive(),
/*items:
customerInvoice.items.size() > 0
? customerInvoice.items.map((item: CustomerInvoiceItem) => ({
description: item.description.toString(),
quantity: item.quantity.toPrimitive(),
unit_measure: "",
unit_price: item.unitPrice.toPrimitive(),
subtotal: item.calculateSubtotal().toPrimitive(),
//tax_amount: item.calculateTaxAmount().toPrimitive(),
total: item.calculateTotal().toPrimitive(),
}))
: [],*/
//sender: {}, //await CustomerInvoiceParticipantAssembler(customerInvoice.senderId, context),
/*recipient: await CustomerInvoiceParticipantAssembler(customerInvoice.recipient, context),
items: customerInvoiceItemAssembler(customerInvoice.items, context),
payment_term: {
payment_type: "",
due_date: "",
},
due_amount: {
currency: customerInvoice.currency.toString(),
precision: 2,
amount: 0,
},
custom_fields: [],
metadata: {
create_time: "",
last_updated_time: "",
delete_time: "",
},*/
}; };
} }
} }

View File

@ -36,7 +36,11 @@ export function mapDTOToCustomerInvoiceProps(dto: CreateCustomerInvoiceCommandDT
"invoice_series", "invoice_series",
errors errors
); );
const issueDate = extractOrPushError(UtcDate.createFromISO(dto.issue_date), "issue_date", errors); const invoiceDate = extractOrPushError(
UtcDate.createFromISO(dto.invoice_date),
"invoice_date",
errors
);
const operationDate = extractOrPushError( const operationDate = extractOrPushError(
UtcDate.createFromISO(dto.operation_date), UtcDate.createFromISO(dto.operation_date),
"operation_date", "operation_date",
@ -59,7 +63,7 @@ export function mapDTOToCustomerInvoiceProps(dto: CreateCustomerInvoiceCommandDT
const invoiceProps: CustomerInvoiceProps = { const invoiceProps: CustomerInvoiceProps = {
invoiceNumber: invoiceNumber!, invoiceNumber: invoiceNumber!,
invoiceSeries: invoiceSeries!, invoiceSeries: invoiceSeries!,
issueDate: issueDate!, invoiceDate: invoiceDate!,
operationDate: operationDate!, operationDate: operationDate!,
status: CustomerInvoiceStatus.createDraft(), status: CustomerInvoiceStatus.createDraft(),
currency, currency,

View File

@ -15,7 +15,7 @@ export class ListCustomerInvoicesAssembler {
invoice_status: invoice.status.toString(), invoice_status: invoice.status.toString(),
invoice_number: invoice.invoiceNumber.toString(), invoice_number: invoice.invoiceNumber.toString(),
invoice_series: invoice.invoiceSeries.toString(), invoice_series: invoice.invoiceSeries.toString(),
issue_date: invoice.issueDate.toISOString(), invoice_date: invoice.invoiceDate.toISOString(),
operation_date: invoice.operationDate.toISOString(), operation_date: invoice.operationDate.toISOString(),
language_code: "ES", language_code: "ES",
currency: "EUR", currency: "EUR",

View File

@ -21,7 +21,7 @@ export class UpdateCustomerInvoiceAssembler {
status: invoice.status.toPrimitive(), status: invoice.status.toPrimitive(),
series: invoice.series.toString(), series: invoice.series.toString(),
issue_date: invoice.issueDate.toDateString(), invoice_date: invoice.invoiceDate.toDateString(),
operation_date: toEmptyString(invoice.operationDate, (value) => value.toDateString()), operation_date: toEmptyString(invoice.operationDate, (value) => value.toDateString()),
notes: toEmptyString(invoice.notes, (value) => value.toPrimitive()), notes: toEmptyString(invoice.notes, (value) => value.toPrimitive()),

View File

@ -52,9 +52,9 @@ export class CreateCustomerInvoiceUseCase {
customerInvoice_series = CustomerInvoiceSeries.create(customerInvoiceDTO.customerInvoice_series).object; customerInvoice_series = CustomerInvoiceSeries.create(customerInvoiceDTO.customerInvoice_series).object;
} }
let issue_date = CustomerInvoiceDate.create(customerInvoiceDTO.issue_date).object; let invoice_date = CustomerInvoiceDate.create(customerInvoiceDTO.invoice_date).object;
if (issue_date.isEmpty()) { if (invoice_date.isEmpty()) {
issue_date = CustomerInvoiceDate.createCurrentDate().object; invoice_date = CustomerInvoiceDate.createCurrentDate().object;
} }
let operation_date = CustomerInvoiceDate.create(customerInvoiceDTO.operation_date).object; let operation_date = CustomerInvoiceDate.create(customerInvoiceDTO.operation_date).object;
@ -96,7 +96,7 @@ export class CreateCustomerInvoiceUseCase {
return DraftCustomerInvoice.create( return DraftCustomerInvoice.create(
{ {
customerInvoiceSeries: customerInvoice_series, customerInvoiceSeries: customerInvoice_series,
issueDate: issue_date, invoiceDate: invoice_date,
operationDate: operation_date, operationDate: operation_date,
customerInvoiceCurrency, customerInvoiceCurrency,
language: customerInvoiceLanguage, language: customerInvoiceLanguage,
@ -285,9 +285,9 @@ export class UpdateCustomerInvoiceUseCase2
customerInvoice_series = CustomerInvoiceSeries.create(customerInvoiceDTO.customerInvoice_series).object; customerInvoice_series = CustomerInvoiceSeries.create(customerInvoiceDTO.customerInvoice_series).object;
} }
let issue_date = CustomerInvoiceDate.create(customerInvoiceDTO.issue_date).object; let invoice_date = CustomerInvoiceDate.create(customerInvoiceDTO.invoice_date).object;
if (issue_date.isEmpty()) { if (invoice_date.isEmpty()) {
issue_date = CustomerInvoiceDate.createCurrentDate().object; invoice_date = CustomerInvoiceDate.createCurrentDate().object;
} }
let operation_date = CustomerInvoiceDate.create(customerInvoiceDTO.operation_date).object; let operation_date = CustomerInvoiceDate.create(customerInvoiceDTO.operation_date).object;
@ -329,7 +329,7 @@ export class UpdateCustomerInvoiceUseCase2
return DraftCustomerInvoice.create( return DraftCustomerInvoice.create(
{ {
customerInvoiceSeries: customerInvoice_series, customerInvoiceSeries: customerInvoice_series,
issueDate: issue_date, invoiceDate: invoice_date,
operationDate: operation_date, operationDate: operation_date,
customerInvoiceCurrency, customerInvoiceCurrency,
language: customerInvoiceLanguage, language: customerInvoiceLanguage,

View File

@ -23,7 +23,7 @@ export interface CustomerInvoiceProps {
status: CustomerInvoiceStatus; status: CustomerInvoiceStatus;
series: Maybe<CustomerInvoiceSerie>; series: Maybe<CustomerInvoiceSerie>;
issueDate: UtcDate; invoiceDate: UtcDate;
operationDate: Maybe<UtcDate>; operationDate: Maybe<UtcDate>;
notes: Maybe<TextValue>; notes: Maybe<TextValue>;
@ -33,7 +33,6 @@ export interface CustomerInvoiceProps {
//tax: Tax; // ? --> detalles? //tax: Tax; // ? --> detalles?
//purchareOrderNumber: string; //purchareOrderNumber: string;
//notes: Note;
//senderId: UniqueID; //senderId: UniqueID;
@ -43,18 +42,18 @@ export interface CustomerInvoiceProps {
languageCode: LanguageCode; languageCode: LanguageCode;
currencyCode: CurrencyCode; currencyCode: CurrencyCode;
subtotalAmount: MoneyValue; //subtotalAmount: MoneyValue;
discountPercentage: Percentage; discountPercentage: Percentage;
//discountAmount: MoneyValue; //discountAmount: MoneyValue;
//taxableAmount: MoneyValue; //taxableAmount: MoneyValue;
taxAmount: MoneyValue; //taxAmount: MoneyValue;
totalAmount: MoneyValue; //totalAmount: MoneyValue;
//customer?: CustomerInvoiceCustomer; //customer?: CustomerInvoiceCustomer;
items?: CustomerInvoiceItems; items: CustomerInvoiceItems;
} }
export type CustomerInvoicePatchProps = Partial<Omit<CustomerInvoiceProps, "companyId">>; export type CustomerInvoicePatchProps = Partial<Omit<CustomerInvoiceProps, "companyId">>;
@ -107,8 +106,8 @@ export class CustomerInvoice extends AggregateRoot<CustomerInvoiceProps> {
return this.props.invoiceNumber; return this.props.invoiceNumber;
} }
public get issueDate(): UtcDate { public get invoiceDate(): UtcDate {
return this.props.issueDate; return this.props.invoiceDate;
} }
public get operationDate(): Maybe<UtcDate> { public get operationDate(): Maybe<UtcDate> {
@ -128,7 +127,7 @@ export class CustomerInvoice extends AggregateRoot<CustomerInvoiceProps> {
} }
public get subtotalAmount(): MoneyValue { public get subtotalAmount(): MoneyValue {
return this.props.subtotalAmount; throw new Error("discountAmount not implemented");
} }
public get discountPercentage(): Percentage { public get discountPercentage(): Percentage {
@ -144,7 +143,7 @@ export class CustomerInvoice extends AggregateRoot<CustomerInvoiceProps> {
} }
public get taxAmount(): MoneyValue { public get taxAmount(): MoneyValue {
return this.props.taxAmount; throw new Error("discountAmount not implemented");
} }
public get totalAmount(): MoneyValue { public get totalAmount(): MoneyValue {

View File

@ -1,4 +1,11 @@
import { CurrencyCode, DomainEntity, LanguageCode, UniqueID } from "@repo/rdx-ddd"; import {
CurrencyCode,
DomainEntity,
LanguageCode,
MoneyValue,
Percentage,
UniqueID,
} from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils"; import { Maybe, Result } from "@repo/rdx-utils";
import { import {
CustomerInvoiceItemDescription, CustomerInvoiceItemDescription,
@ -12,8 +19,8 @@ import {
export interface CustomerInvoiceItemProps { export interface CustomerInvoiceItemProps {
description: Maybe<CustomerInvoiceItemDescription>; description: Maybe<CustomerInvoiceItemDescription>;
quantity: Maybe<CustomerInvoiceItemQuantity>; // Cantidad de unidades quantity: Maybe<CustomerInvoiceItemQuantity>; // Cantidad de unidades
unitPrice: Maybe<CustomerInvoiceItemUnitAmount>; // Precio unitario en la moneda de la factura unitAmount: Maybe<CustomerInvoiceItemUnitAmount>; // Precio unitario en la moneda de la factura
discount: Maybe<CustomerInvoiceItemDiscount>; // % descuento discountPercentage: Maybe<CustomerInvoiceItemDiscount>; // % descuento
languageCode: LanguageCode; languageCode: LanguageCode;
currencyCode: CurrencyCode; currencyCode: CurrencyCode;
@ -49,7 +56,7 @@ export class CustomerInvoiceItem extends DomainEntity<CustomerInvoiceItemProps>
} }
get unitAmount(): Maybe<CustomerInvoiceItemUnitAmount> { get unitAmount(): Maybe<CustomerInvoiceItemUnitAmount> {
return this.props.unitPrice; return this.props.unitAmount;
} }
get subtotalAmount(): CustomerInvoiceItemSubtotalAmount { get subtotalAmount(): CustomerInvoiceItemSubtotalAmount {
@ -59,8 +66,12 @@ export class CustomerInvoiceItem extends DomainEntity<CustomerInvoiceItemProps>
return this._subtotalAmount; return this._subtotalAmount;
} }
get discountPercentage(): Maybe<CustomerInvoiceItemDiscount> { get discountPercentage(): Maybe<Percentage> {
return this.props.discount; return this.props.discountPercentage;
}
get discountAmount(): Maybe<MoneyValue> {
throw new Error("Not implemented");
} }
get totalAmount(): CustomerInvoiceItemTotalAmount { get totalAmount(): CustomerInvoiceItemTotalAmount {

View File

@ -1,3 +0,0 @@
import { Quantity } from "@repo/rdx-ddd";
export class CustomerInvoiceItemQuantity extends Quantity {}

View File

@ -3,10 +3,10 @@ import { MoneyValue, MoneyValueProps } from "@repo/rdx-ddd";
export class CustomerInvoiceItemSubtotalAmount extends MoneyValue { export class CustomerInvoiceItemSubtotalAmount extends MoneyValue {
public static DEFAULT_SCALE = 4; public static DEFAULT_SCALE = 4;
static create({ value: amount, currency_code, scale }: MoneyValueProps) { static create({ value, currency_code }: MoneyValueProps) {
const props = { const props = {
amount: Number(amount), value: Number(value),
scale: scale ?? MoneyValue.DEFAULT_SCALE, scale: CustomerInvoiceItemSubtotalAmount.DEFAULT_SCALE,
currency_code, currency_code,
}; };
return MoneyValue.create(props); return MoneyValue.create(props);

View File

@ -1,6 +1,6 @@
import { MoneyValue, MoneyValueProps } from "@repo/rdx-ddd"; import { MoneyValue, MoneyValueProps } from "@repo/rdx-ddd";
export class CustomerInvoiceItemTotalAmount extends MoneyValue { export class ItemTotalAmount extends MoneyValue {
public static DEFAULT_SCALE = 4; public static DEFAULT_SCALE = 4;
static create({ value, currency_code, scale }: MoneyValueProps) { static create({ value, currency_code, scale }: MoneyValueProps) {

View File

@ -1,10 +1,10 @@
export * from "./customer-invoice-address-type"; export * from "./customer-invoice-address-type";
export * from "./customer-invoice-item-description"; export * from "./customer-invoice-item-description";
export * from "./customer-invoice-item-discount"; export * from "./customer-invoice-item-discount";
export * from "./customer-invoice-item-quantity";
export * from "./customer-invoice-item-subtotal-amount"; export * from "./customer-invoice-item-subtotal-amount";
export * from "./customer-invoice-item-total-amount"; export * from "./customer-invoice-item-total-amount";
export * from "./customer-invoice-item-unit-amount"; export * from "./customer-invoice-item-unit-amount";
export * from "./customer-invoice-number"; export * from "./customer-invoice-number";
export * from "./customer-invoice-serie"; export * from "./customer-invoice-serie";
export * from "./customer-invoice-status"; export * from "./customer-invoice-status";
export * from "./item-quantity";

View File

@ -0,0 +1,13 @@
import { Quantity, QuantityProps } from "@repo/rdx-ddd";
export class ItemQuantity extends Quantity {
public static DEFAULT_SCALE = 2;
static create({ value }: QuantityProps) {
const props = {
value: Number(value),
scale: ItemQuantity.DEFAULT_SCALE,
};
return Quantity.create(props);
}
}

View File

@ -1,18 +1,16 @@
import type { ModuleParams } from "@erp/core/api"; import type { ModuleParams } from "@erp/core/api";
import { SequelizeTransactionManager } from "@erp/core/api"; import { SequelizeTransactionManager } from "@erp/core/api";
import { import {
CreateCustomerInvoiceAssembler,
CreateCustomerInvoiceUseCase, CreateCustomerInvoiceUseCase,
CreateCustomerInvoicesAssembler,
DeleteCustomerInvoiceUseCase, DeleteCustomerInvoiceUseCase,
GetCustomerInvoiceAssembler, GetCustomerInvoiceAssembler,
GetCustomerInvoiceItemsAssembler,
GetCustomerInvoiceUseCase, GetCustomerInvoiceUseCase,
ListCustomerInvoicesAssembler, ListCustomerInvoicesAssembler,
ListCustomerInvoicesUseCase, ListCustomerInvoicesUseCase,
UpdateCustomerInvoiceAssembler, UpdateCustomerInvoiceAssembler,
UpdateCustomerInvoiceUseCase,
} from "../application"; } from "../application";
import { CustomerInvoiceService, ICustomerInvoiceService } from "../domain"; import { CustomerInvoiceService } from "../domain";
import { CustomerInvoiceMapper } from "./mappers"; import { CustomerInvoiceMapper } from "./mappers";
import { CustomerInvoiceRepository } from "./sequelize"; import { CustomerInvoiceRepository } from "./sequelize";
@ -20,11 +18,11 @@ type InvoiceDeps = {
transactionManager: SequelizeTransactionManager; transactionManager: SequelizeTransactionManager;
repo: CustomerInvoiceRepository; repo: CustomerInvoiceRepository;
mapper: CustomerInvoiceMapper; mapper: CustomerInvoiceMapper;
service: ICustomerInvoiceService; service: CustomerInvoiceService;
assemblers: { assemblers: {
list: ListCustomerInvoicesAssembler; list: ListCustomerInvoicesAssembler;
get: GetCustomerInvoiceItemsAssembler; get: GetCustomerInvoiceAssembler;
create: CreateCustomerInvoicesAssembler; create: CreateCustomerInvoiceAssembler;
update: UpdateCustomerInvoiceAssembler; update: UpdateCustomerInvoiceAssembler;
}; };
build: { build: {
@ -41,7 +39,7 @@ type InvoiceDeps = {
let _repo: CustomerInvoiceRepository | null = null; let _repo: CustomerInvoiceRepository | null = null;
let _mapper: CustomerInvoiceMapper | null = null; let _mapper: CustomerInvoiceMapper | null = null;
let _service: ICustomerInvoiceService | null = null; let _service: CustomerInvoiceService | null = null;
let _assemblers: InvoiceDeps["assemblers"] | null = null; let _assemblers: InvoiceDeps["assemblers"] | null = null;
export function getInvoiceDependencies(params: ModuleParams): InvoiceDeps { export function getInvoiceDependencies(params: ModuleParams): InvoiceDeps {
@ -56,7 +54,7 @@ export function getInvoiceDependencies(params: ModuleParams): InvoiceDeps {
_assemblers = { _assemblers = {
list: new ListCustomerInvoicesAssembler(), // transforma domain → ListDTO list: new ListCustomerInvoicesAssembler(), // transforma domain → ListDTO
get: new GetCustomerInvoiceAssembler(), // transforma domain → DetailDTO get: new GetCustomerInvoiceAssembler(), // transforma domain → DetailDTO
create: new CreateCustomerInvoicesAssembler(), // transforma domain → CreatedDTO create: new CreateCustomerInvoiceAssembler(), // transforma domain → CreatedDTO
update: new UpdateCustomerInvoiceAssembler(), // transforma domain -> UpdateDTO update: new UpdateCustomerInvoiceAssembler(), // transforma domain -> UpdateDTO
}; };
} }

View File

@ -4,25 +4,17 @@ import { CreateCustomerInvoiceRequestDTO } from "../../../../common/dto";
import { CreateCustomerInvoiceUseCase } from "../../../application"; import { CreateCustomerInvoiceUseCase } from "../../../application";
export class CreateCustomerInvoiceController extends ExpressController { export class CreateCustomerInvoiceController extends ExpressController {
public constructor( public constructor(private readonly useCase: CreateCustomerInvoiceUseCase) {
private readonly useCase: CreateCustomerInvoiceUseCase
/* private readonly presenter: any */
) {
super(); super();
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId")); this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
} }
protected async executeImpl() { protected async executeImpl() {
const tenantId = this.getTenantId()!; // garantizado por tenantGuard const companyId = this.getTenantId()!; // garantizado por tenantGuard
const dto = this.req.body as CreateCustomerInvoiceRequestDTO; const dto = this.req.body as CreateCustomerInvoiceRequestDTO;
/* const result = await this.useCase.execute({ dto, companyId });
// Inyectar empresa del usuario autenticado (ownership)
dto.customerCompanyId = user.companyId;
*/
const result = await this.useCase.execute({ tenantId, dto });
return result.match( return result.match(
(data) => this.created(data), (data) => this.created(data),

View File

@ -1,5 +1,11 @@
import { ISequelizeMapper, MapperParamsType, SequelizeMapper } from "@erp/core/api"; import {
import { UniqueID } from "@repo/rdx-ddd"; ISequelizeMapper,
MapperParamsType,
SequelizeMapper,
ValidationErrorDetail,
extractOrPushError,
} from "@erp/core/api";
import { CurrencyCode, LanguageCode, Quantity, UniqueID, maybeFromNullableVO, toNullable } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { InferCreationAttributes } from "sequelize"; import { InferCreationAttributes } from "sequelize";
import { import {
@ -35,70 +41,75 @@ export class CustomerInvoiceItemMapper
source: CustomerInvoiceItemModel, source: CustomerInvoiceItemModel,
params?: MapperParamsType params?: MapperParamsType
): Result<CustomerInvoiceItem, Error> { ): Result<CustomerInvoiceItem, Error> {
const { sourceParent } = params as { sourceParent: CustomerInvoiceModel }; const { sourceParent, errors } = params as {
sourceParent: CustomerInvoiceModel;
errors: ValidationErrorDetail[];
};
// Validación y creación de ID único const itemId = extractOrPushError(UniqueID.create(source.item_id), "item_id", errors);
const idOrError = UniqueID.create(source.item_id);
if (idOrError.isFailure) {
return Result.fail(idOrError.error);
}
// Validación y creación de descripción const languageCode = extractOrPushError(
const descriptionOrError = CustomerInvoiceItemDescription.create(source.description || ""); LanguageCode.create(sourceParent.language_code),
if (descriptionOrError.isFailure) { "language_code",
return Result.fail(descriptionOrError.error); errors
} );
// Validación y creación de cantidad const currencyCode = extractOrPushError(
const quantityOrError = CustomerInvoiceItemQuantity.create({ CurrencyCode.create(sourceParent.currency_code),
amount: source.quantity_amount, "currency_code",
scale: source.quantity_scale, errors
}); );
if (quantityOrError.isFailure) {
return Result.fail(quantityOrError.error);
}
// Validación y creación de precio unitario const description = extractOrPushError(
const unitPriceOrError = CustomerInvoiceItemUnitAmount.create({ maybeFromNullableVO(source.description, (value) =>
value: source.unit_price_amount, CustomerInvoiceItemDescription.create(value)
scale: source.unit_price_scale, ),
currency_code: sourceParent.invoice_currency, "description",
}); errors
if (unitPriceOrError.isFailure) { );
return Result.fail(unitPriceOrError.error);
}
// Validación y creación de descuento const quantity = extractOrPushError(
const discountOrError = CustomerInvoiceItemDiscount.create({ maybeFromNullableVO(source.quantity_value, (value) =>
value: source.discount_amount || 0, Quantity.create({ value, })
scale: source.discount_scale || 0,
});
if (discountOrError.isFailure) {
return Result.fail(discountOrError.error);
}
// Combinación de resultados CustomerInvoiceItemQuantity.create({
const result = Result.combine([ value: source.quantity_amouwnt,
idOrError, scale: source.quantity_scale,
descriptionOrError, }),
quantityOrError, "discount_percentage",
unitPriceOrError, errors
discountOrError, );
]);
const unitAmount = extractOrPushError(
CustomerInvoiceItemUnitAmount.create({
value: source.unit_price_amount,
scale: source.unit_price_scale,
}),
"discount_percentage",
errors
);
const discountPercentage = extractOrPushError(
CustomerInvoiceItemDiscount.create({
value: source.discount_amount,
scale: source.discount_scale,
}),
"discount_percentage",
errors
);
if (result.isFailure) {
return Result.fail(result.error);
}
// Creación del objeto de dominio // Creación del objeto de dominio
return CustomerInvoiceItem.create( return CustomerInvoiceItem.create(
{ {
description: descriptionOrError.data, languageCode: languageCode!,
quantity: quantityOrError.data, currencyCode: currencyCode!,
unitPrice: unitPriceOrError.data, description: description!,
discount: discountOrError.data, quantity: quantity!,
}, unitAmount: unitAmount!,
idOrError.data discountPercentage: discountPercentage!,
},º
id
); );
} }
@ -106,35 +117,44 @@ export class CustomerInvoiceItemMapper
source: CustomerInvoiceItem, source: CustomerInvoiceItem,
params?: MapperParamsType params?: MapperParamsType
): InferCreationAttributes<CustomerInvoiceItemModel, {}> { ): InferCreationAttributes<CustomerInvoiceItemModel, {}> {
1
const { index, sourceParent } = params as { const { index, sourceParent } = params as {
index: number; index: number;
sourceParent: CustomerInvoice; sourceParent: CustomerInvoice;
}; };
const lineData = {
parent_id: undefined,
return {
item_id: source.id.toPrimitive(),
invoice_id: sourceParent.id.toPrimitive(), invoice_id: sourceParent.id.toPrimitive(),
item_type: "simple",
position: index, position: index,
item_id: source.id.toPrimitive(), description: toNullable(source.description, (description) => description.toPrimitive()),
description: source.description.toPrimitive(),
quantity_amount: source.quantity.toPrimitive().amount, quantity_value: toNullable(source.quantity, (value) => value.toPrimitive().value),
quantity_scale: source.quantity.toPrimitive().scale, quantity_scale: source.quantity.match(
(value) => value.toPrimitive().scale,
() => CustomerInvoiceItemQuantity.DEFAULT_SCALE),
unit_price_amount: source.unitAmount.toPrimitive().amount, unit_amount_value: toNullable(source.unitAmount, (value) => value.toPrimitive().value),
unit_price_scale: source.unitAmount.toPrimitive().scale, unit_amount_scale: source.unitAmount.match(
(value) => value.toPrimitive().scale,
() => CustomerInvoiceItemUnitAmount.DEFAULT_SCALE),
subtotal_amount: source.subtotalAmount.toPrimitive().amount, subtotal_amount_value: source.subtotalAmount.toPrimitive().value,
subtotal_scale: source.subtotalAmount.toPrimitive().scale, subtotal_amount_scale: source.subtotalAmount.toPrimitive().scale,
discount_amount: source.discount.toPrimitive().amount, discount_percentage_value: toNullable(source.discountPercentage, (value) => value.toPrimitive().value),
discount_scale: source.discount.toPrimitive().scale, discount_percentage_scale: source.discountPercentage.match(
(value) => value.toPrimitive().scale,
() => CustomerInvoiceItemUnitAmount.DEFAULT_SCALE),
total_amount: source.totalAmount.toPrimitive().amount, discount_amount_value: source.subtotalAmount.toPrimitive().value,
total_scale: source.totalAmount.toPrimitive().scale, discount_amount_scale: source.subtotalAmount.toPrimitive().scale,
total_amount_value: source.totalAmount.toPrimitive().value,
total_amount_scale: source.totalAmount.toPrimitive().scale,
}; };
return lineData;
} }
} }

View File

@ -6,7 +6,15 @@ import {
ValidationErrorDetail, ValidationErrorDetail,
extractOrPushError, extractOrPushError,
} from "@erp/core/api"; } from "@erp/core/api";
import { CurrencyCode, LanguageCode, UniqueID, UtcDate, maybeFromNullableVO } from "@repo/rdx-ddd"; import {
CurrencyCode,
LanguageCode,
Percentage,
TextValue,
UniqueID,
UtcDate,
maybeFromNullableVO,
} from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { import {
CustomerInvoice, CustomerInvoice,
@ -29,86 +37,125 @@ export class CustomerInvoiceMapper
extends SequelizeMapper<CustomerInvoiceModel, CustomerInvoiceCreationAttributes, CustomerInvoice> extends SequelizeMapper<CustomerInvoiceModel, CustomerInvoiceCreationAttributes, CustomerInvoice>
implements ICustomerInvoiceMapper implements ICustomerInvoiceMapper
{ {
private customerInvoiceItemMapper: CustomerInvoiceItemMapper; private _itemsMapper: CustomerInvoiceItemMapper;
constructor() { constructor() {
super(); super();
this.customerInvoiceItemMapper = new CustomerInvoiceItemMapper(); // Instanciar el mapper de items this._itemsMapper = new CustomerInvoiceItemMapper(); // Instanciar el mapper de items
} }
public mapToDomain( public mapToDomain(
source: CustomerInvoiceModel, source: CustomerInvoiceModel,
params?: MapperParamsType params?: MapperParamsType
): Result<CustomerInvoice, Error> { ): Result<CustomerInvoice, Error> {
const errors: ValidationErrorDetail[] = []; try {
const errors: ValidationErrorDetail[] = [];
const invoiceId = extractOrPushError(UniqueID.create(source.id), "id", errors); const invoiceId = extractOrPushError(UniqueID.create(source.id), "id", errors);
const companyId = extractOrPushError(UniqueID.create(source.company_id), "company_id", errors); const companyId = extractOrPushError(
UniqueID.create(source.company_id),
const status = extractOrPushError( "company_id",
CustomerInvoiceStatus.create(source.status), errors
"status",
errors
);
const series = extractOrPushError(CustomerInvoiceSerie.create(source.series), "series", errors);
const invoiceNumber = extractOrPushError(
CustomerInvoiceNumber.create(source.invoice_number),
"invoice_number",
errors
);
const issueDate = extractOrPushError(
UtcDate.createFromISO(source.issue_date),
"issue_date",
errors
);
const operationDate = extractOrPushError(
maybeFromNullableVO(source.operation_date, (value) => UtcDate.createFromISO(value)),
"operation_date",
errors
);
const languageCode = extractOrPushError(
LanguageCode.create(source.language_code),
"language_code",
errors
);
const currencyCode = extractOrPushError(
CurrencyCode.create(source.currency_code),
"currency_code",
errors
);
if (errors.length > 0) {
return Result.fail(
new ValidationErrorCollection("Customer invoice props mapping failed", errors)
); );
const status = extractOrPushError(
CustomerInvoiceStatus.create(source.status),
"status",
errors
);
const series = extractOrPushError(
maybeFromNullableVO(source.series, (value) => CustomerInvoiceSerie.create(value)),
"serie",
errors
);
const invoiceNumber = extractOrPushError(
CustomerInvoiceNumber.create(source.invoice_number),
"invoice_number",
errors
);
const invoiceDate = extractOrPushError(
UtcDate.createFromISO(source.invoice_date),
"invoice_date",
errors
);
const operationDate = extractOrPushError(
maybeFromNullableVO(source.operation_date, (value) => UtcDate.createFromISO(value)),
"operation_date",
errors
);
const notes = extractOrPushError(
maybeFromNullableVO(source.notes, (value) => TextValue.create(value)),
"notes",
errors
);
const languageCode = extractOrPushError(
LanguageCode.create(source.language_code),
"language_code",
errors
);
const currencyCode = extractOrPushError(
CurrencyCode.create(source.currency_code),
"currency_code",
errors
);
const discountPercentage = extractOrPushError(
Percentage.create({
value: source.discount_percentage_value,
scale: source.discount_percentage_scale,
}),
"discount_percentage",
errors
);
if (errors.length > 0) {
return Result.fail(
new ValidationErrorCollection("Customer invoice props mapping failed", errors)
);
}
// Mapear los items de la factura
const items = this._itemsMapper.mapArrayToDomain(source.items, {
sourceParent: source,
errors,
...params,
});
if (errors.length > 0) {
return Result.fail(
new ValidationErrorCollection("Customer invoice item props mapping failed", errors)
);
}
const invoiceProps: CustomerInvoiceProps = {
companyId: companyId!,
status: status!,
series: series!,
invoiceNumber: invoiceNumber!,
invoiceDate: invoiceDate!,
operationDate: operationDate!,
notes: notes!,
languageCode: languageCode!,
currencyCode: currencyCode!,
discountPercentage: discountPercentage!,
items: items!,
};
return CustomerInvoice.create(invoiceProps, invoiceId);
} catch (err: unknown) {
return Result.fail(err as Error);
} }
// Mapear los items de la factura
/*const itemsOrErrors = this.customerInvoiceItemMapper.mapArrayToDomain(source.items, {
sourceParent: source,
...params,
});
if (itemsOrErrors.isFailure) {
return Result.fail(itemsOrErrors.error);
}*/
const invoiceProps: CustomerInvoiceProps = {
status: status!,
series: series!,
invoiceNumber: invoiceNumber!,
issueDate: issueDate!,
operationDate: operationDate!,
languageCode: languageCode!,
currencyCode: currencyCode!,
//items: itemsOrErrors.data,
};
return CustomerInvoice.create(invoiceProps, invoiceId);
} }
public mapToPersistence( public mapToPersistence(
@ -118,14 +165,14 @@ export class CustomerInvoiceMapper
const subtotal = source.calculateSubtotal(); const subtotal = source.calculateSubtotal();
const total = source.calculateTotal(); const total = source.calculateTotal();
const items = this.customerInvoiceItemMapper.mapCollectionToPersistence(source.items, params); const items = this._itemsMapper.mapCollectionToPersistence(source.items, params);
return { return {
id: source.id.toString(), id: source.id.toString(),
invoice_status: source.status.toPrimitive(), invoice_status: source.status.toPrimitive(),
invoice_series: source.invoiceSeries.toPrimitive(), invoice_series: source.invoiceSeries.toPrimitive(),
invoice_number: source.invoiceNumber.toPrimitive(), invoice_number: source.invoiceNumber.toPrimitive(),
issue_date: source.issueDate.toPrimitive(), invoice_date: source.invoiceDate.toPrimitive(),
operation_date: source.operationDate.toPrimitive(), operation_date: source.operationDate.toPrimitive(),
invoice_language: "es", invoice_language: "es",
invoice_currency: source.currency || "EUR", invoice_currency: source.currency || "EUR",

View File

@ -7,7 +7,7 @@ const ALLOWED_FILTERS = {
customerId: "customer_id", customerId: "customer_id",
invoiceSeries: "invoice_series", invoiceSeries: "invoice_series",
invoiceNumber: "invoice_number", invoiceNumber: "invoice_number",
issueDate: "issue_date", invoiceDate: "invoice_date",
status: "status", status: "status",
currencyCode: "currency_code", currencyCode: "currency_code",
// Rango por total (en unidades menores) // Rango por total (en unidades menores)
@ -15,8 +15,8 @@ const ALLOWED_FILTERS = {
} as const; } as const;
const ALLOWED_SORT: Record<string, string | string[]> = { const ALLOWED_SORT: Record<string, string | string[]> = {
// Sort "issueDate" realmente ordena por (issue_date DESC, invoice_series ASC, invoice_number DESC, id DESC) // Sort "invoiceDate" realmente ordena por (invoice_date DESC, invoice_series ASC, invoice_number DESC, id DESC)
issueDate: ["issue_date"], invoiceDate: ["invoice_date"],
invoiceNumber: ["invoice_number"], invoiceNumber: ["invoice_number"],
invoiceSeries: ["invoice_series"], invoiceSeries: ["invoice_series"],
status: ["status"], status: ["status"],
@ -31,7 +31,7 @@ export const DEFAULT_LIST_ATTRIBUTES = [
"customer_id", "customer_id",
"invoice_series", "invoice_series",
"invoice_number", "invoice_number",
"issue_date", "invoice_date",
"status", "status",
"total_amount_value", "total_amount_value",
"total_amount_scale", "total_amount_scale",
@ -47,7 +47,7 @@ type Sanitized = {
attributes: (string | any)[]; attributes: (string | any)[];
// keyset opcional // keyset opcional
keyset?: { keyset?: {
after?: { issueDate: string; invoiceSeries: string; invoiceNumber: number; id: string }; after?: { invoiceDate: string; invoiceSeries: string; invoiceNumber: number; id: string };
}; };
}; };
@ -114,9 +114,9 @@ export function sanitizeListCriteria(criteria: Criteria): Sanitized {
.split(",") .split(",")
.filter(Boolean); .filter(Boolean);
if (sortArray.length === 0) { if (sortArray.length === 0) {
// orden por defecto: issue_date desc, invoice_series asc, invoice_number desc, id desc // orden por defecto: invoice_date desc, invoice_series asc, invoice_number desc, id desc
order.push( order.push(
["issue_date", "DESC"], ["invoice_date", "DESC"],
["invoice_series", "ASC"], ["invoice_series", "ASC"],
["invoice_number", "DESC"], ["invoice_number", "DESC"],
["id", "DESC"] ["id", "DESC"]

View File

@ -0,0 +1,107 @@
import {
DataTypes,
InferAttributes,
InferCreationAttributes,
Model,
NonAttribute,
Sequelize,
} from "sequelize";
import { CustomerInvoiceItem } from "../../domain";
export type CustomerInvoiceItemTaxesCreationAttributes = InferCreationAttributes<
CustomerInvoiceItemTaxesModel,
{}
>;
export class CustomerInvoiceItemTaxesModel extends Model<
InferAttributes<CustomerInvoiceItemTaxesModel>,
InferCreationAttributes<CustomerInvoiceItemTaxesModel>
> {
declare id: string;
declare item_id: string;
declare tax_code: string; //"iva_21"
// Taxable amount (base imponible) // 100,00 €
declare taxable_amount_value: number;
declare taxable_amount_scale: number;
// Total tax amount / taxes total // 21,00 €
declare tax_amount_value: number;
declare tax_amount_scale: number;
// Relaciones
declare item: NonAttribute<CustomerInvoiceItem>;
static associate(database: Sequelize) {
const { CustomerInvoiceItemModel } = database.models;
CustomerInvoiceItemTaxesModel.belongsTo(CustomerInvoiceItemModel, {
as: "item",
targetKey: "id",
foreignKey: "item_id",
onDelete: "CASCADE",
});
}
static hooks(database: Sequelize) {}
}
export default (database: Sequelize) => {
CustomerInvoiceItemTaxesModel.init(
{
id: {
type: new DataTypes.UUID(),
primaryKey: true,
},
item_id: {
type: DataTypes.UUID,
allowNull: false,
},
tax_code: {
type: new DataTypes.STRING(),
allowNull: false,
},
taxable_amount_value: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: true,
defaultValue: null,
},
taxable_amount_scale: {
type: new DataTypes.SMALLINT(),
allowNull: false,
defaultValue: 2,
},
tax_amount_value: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: true,
defaultValue: null,
},
tax_amount_scale: {
type: new DataTypes.SMALLINT(),
allowNull: false,
defaultValue: 2,
},
},
{
sequelize: database,
tableName: "customer_invoices_item_taxes",
underscored: true,
indexes: [{ name: "tax_code_idx", fields: ["tax_code"], unique: false }],
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope
defaultScope: {},
scopes: {},
}
);
return CustomerInvoiceItemTaxesModel;
};

View File

@ -20,26 +20,39 @@ export class CustomerInvoiceItemModel extends Model<
declare item_id: string; declare item_id: string;
declare invoice_id: string; declare invoice_id: string;
declare parent_id: string;
declare position: number; declare position: number;
declare item_type: string;
declare description: string; declare description: string;
declare quantity_amount: number; declare quantity_value: number;
declare quantity_scale: number; declare quantity_scale: number;
declare unit_price_amount: number; declare unit_amount_value: number;
declare unit_price_scale: number; declare unit_amount_scale: number;
declare subtotal_amount: number; // Subtotal
declare subtotal_scale: number; declare subtotal_amount_value: number;
declare subtotal_amount_scale: number;
declare discount_amount: number; // Discount percentage
declare discount_scale: number; declare discount_percentage_value: number;
declare discount_percentage_scale: number;
declare total_amount: number; // Discount amount
declare total_scale: number; declare discount_amount_value: number;
declare discount_amount_scale: number;
// Taxable amount (base imponible)
declare taxable_amount_value: number;
declare taxable_amount_scale: number;
// Total taxes amount / taxes total
declare taxes_amount_value: number;
declare taxes_amount_scale: number;
// Total
declare total_amount_value: number;
declare total_amount_scale: number;
declare invoice: NonAttribute<CustomerInvoiceModel>; declare invoice: NonAttribute<CustomerInvoiceModel>;
@ -62,101 +75,119 @@ export default (database: Sequelize) => {
type: new DataTypes.UUID(), type: new DataTypes.UUID(),
primaryKey: true, primaryKey: true,
}, },
invoice_id: { invoice_id: {
type: new DataTypes.UUID(), type: new DataTypes.UUID(),
allowNull: false, allowNull: false,
}, },
parent_id: {
type: new DataTypes.UUID(),
allowNull: true, // Puede ser nulo para elementos de nivel superior
},
position: { position: {
type: new DataTypes.MEDIUMINT().UNSIGNED, type: new DataTypes.MEDIUMINT().UNSIGNED,
autoIncrement: false, autoIncrement: false,
allowNull: false, allowNull: false,
}, },
item_type: {
type: new DataTypes.STRING(),
allowNull: false,
defaultValue: "simple",
},
description: { description: {
type: new DataTypes.TEXT(), type: new DataTypes.TEXT(),
allowNull: true, allowNull: true,
defaultValue: null, defaultValue: null,
}, },
quantity_amount: { quantity_value: {
type: new DataTypes.BIGINT(), type: new DataTypes.BIGINT(),
allowNull: true, allowNull: true,
defaultValue: null, defaultValue: null,
}, },
quantity_scale: { quantity_scale: {
type: new DataTypes.SMALLINT(), type: new DataTypes.SMALLINT(),
allowNull: true, allowNull: false,
defaultValue: null, defaultValue: 2,
}, },
unit_price_amount: { unit_amount_value: {
type: new DataTypes.BIGINT(), type: new DataTypes.BIGINT(),
allowNull: true, allowNull: true,
defaultValue: null, defaultValue: null,
}, },
unit_price_scale: {
unit_amount_scale: {
type: new DataTypes.SMALLINT(), type: new DataTypes.SMALLINT(),
allowNull: true, allowNull: false,
defaultValue: null, defaultValue: 4,
}, },
/*tax_slug: { subtotal_amount_value: {
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_amount: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: true, allowNull: true,
defaultValue: null, defaultValue: null,
}, },
subtotal_scale: {
subtotal_amount_scale: {
type: new DataTypes.SMALLINT(),
allowNull: false,
defaultValue: 4,
},
discount_percentage_value: {
type: new DataTypes.SMALLINT(), type: new DataTypes.SMALLINT(),
allowNull: true, allowNull: true,
defaultValue: null, defaultValue: null,
}, },
discount_amount: { discount_percentage_scale: {
type: new DataTypes.SMALLINT(), type: new DataTypes.SMALLINT(),
allowNull: true, allowNull: false,
defaultValue: null, defaultValue: 2,
},
discount_scale: {
type: new DataTypes.SMALLINT(),
allowNull: true,
defaultValue: null,
}, },
/*tax_amount: { discount_amount_value: {
type: new DataTypes.BIGINT(),
allowNull: true,
},*/
total_amount: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: true, allowNull: true,
defaultValue: null, defaultValue: null,
}, },
total_scale: {
discount_amount_scale: {
type: new DataTypes.SMALLINT(), type: new DataTypes.SMALLINT(),
allowNull: false,
defaultValue: 4,
},
taxable_amount_value: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: true, allowNull: true,
defaultValue: null, defaultValue: null,
}, },
taxable_amount_scale: {
type: new DataTypes.SMALLINT(),
allowNull: false,
defaultValue: 4,
},
taxes_amount_value: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: true,
defaultValue: null,
},
taxes_amount_scale: {
type: new DataTypes.SMALLINT(),
allowNull: false,
defaultValue: 4,
},
total_amount_value: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: true,
defaultValue: null,
},
total_amount_scale: {
type: new DataTypes.SMALLINT(),
allowNull: false,
defaultValue: 4,
},
}, },
{ {
sequelize: database, sequelize: database,

View File

@ -0,0 +1,107 @@
import {
DataTypes,
InferAttributes,
InferCreationAttributes,
Model,
NonAttribute,
Sequelize,
} from "sequelize";
import { CustomerInvoice } from "../../domain";
export type CustomerInvoiceTaxesCreationAttributes = InferCreationAttributes<
CustomerInvoiceTaxesModel,
{}
>;
export class CustomerInvoiceTaxesModel extends Model<
InferAttributes<CustomerInvoiceTaxesModel>,
InferCreationAttributes<CustomerInvoiceTaxesModel>
> {
declare id: string;
declare invoice_id: string;
declare tax_code: string; //"iva_21"
// Taxable amount (base imponible) // 100,00 €
declare taxable_amount_value: number;
declare taxable_amount_scale: number;
// Total tax amount / taxes total // 21,00 €
declare tax_amount_value: number;
declare tax_amount_scale: number;
// Relaciones
declare invoice: NonAttribute<CustomerInvoice>;
static associate(database: Sequelize) {
const { CustomerInvoiceModel } = database.models;
CustomerInvoiceTaxesModel.belongsTo(CustomerInvoiceModel, {
as: "invoice",
targetKey: "id",
foreignKey: "invoice_id",
onDelete: "CASCADE",
});
}
static hooks(database: Sequelize) {}
}
export default (database: Sequelize) => {
CustomerInvoiceTaxesModel.init(
{
id: {
type: new DataTypes.UUID(),
primaryKey: true,
},
invoice_id: {
type: DataTypes.UUID,
allowNull: false,
},
tax_code: {
type: new DataTypes.STRING(),
allowNull: false,
},
taxable_amount_value: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: true,
defaultValue: null,
},
taxable_amount_scale: {
type: new DataTypes.SMALLINT(),
allowNull: false,
defaultValue: 2,
},
tax_amount_value: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: true,
defaultValue: null,
},
tax_amount_scale: {
type: new DataTypes.SMALLINT(),
allowNull: false,
defaultValue: 2,
},
},
{
sequelize: database,
tableName: "customer_invoices_taxes",
underscored: true,
indexes: [{ name: "tax_code_idx", fields: ["tax_code"], unique: false }],
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope
defaultScope: {},
scopes: {},
}
);
return CustomerInvoiceTaxesModel;
};

View File

@ -28,11 +28,13 @@ export class CustomerInvoiceModel extends Model<
declare status: string; declare status: string;
declare series: string; declare series: string;
declare invoice_number: string; declare invoice_number: string;
declare issue_date: string; declare invoice_date: string;
declare operation_date: string; declare operation_date: string;
declare language_code: string; declare language_code: string;
declare currency_code: string; declare currency_code: string;
declare notes: string;
// Subtotal // Subtotal
declare subtotal_amount_value: number; declare subtotal_amount_value: number;
declare subtotal_amount_scale: number; declare subtotal_amount_scale: number;
@ -117,7 +119,7 @@ export default (database: Sequelize) => {
defaultValue: null, defaultValue: null,
}, },
issue_date: { invoice_date: {
type: new DataTypes.DATEONLY(), type: new DataTypes.DATEONLY(),
allowNull: true, allowNull: true,
defaultValue: null, defaultValue: null,
@ -138,18 +140,23 @@ export default (database: Sequelize) => {
currency_code: { currency_code: {
type: new DataTypes.STRING(3), type: new DataTypes.STRING(3),
allowNull: false, allowNull: false,
defaultValue: "EUR", },
notes: {
type: new DataTypes.TEXT(),
allowNull: true,
defaultValue: null,
}, },
subtotal_amount_value: { subtotal_amount_value: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: true, allowNull: false,
defaultValue: null, defaultValue: 0,
}, },
subtotal_amount_scale: { subtotal_amount_scale: {
type: new DataTypes.SMALLINT(), type: new DataTypes.SMALLINT(),
allowNull: true, allowNull: false,
defaultValue: null, defaultValue: 2,
}, },
discount_percentage_value: { discount_percentage_value: {
@ -160,8 +167,8 @@ export default (database: Sequelize) => {
discount_percentage_scale: { discount_percentage_scale: {
type: new DataTypes.SMALLINT(), type: new DataTypes.SMALLINT(),
allowNull: true, allowNull: false,
defaultValue: null, defaultValue: 2,
}, },
discount_amount_value: { discount_amount_value: {
@ -172,8 +179,8 @@ export default (database: Sequelize) => {
discount_amount_scale: { discount_amount_scale: {
type: new DataTypes.SMALLINT(), type: new DataTypes.SMALLINT(),
allowNull: true, allowNull: false,
defaultValue: null, defaultValue: 2,
}, },
taxable_amount_value: { taxable_amount_value: {
@ -183,8 +190,8 @@ export default (database: Sequelize) => {
}, },
taxable_amount_scale: { taxable_amount_scale: {
type: new DataTypes.SMALLINT(), type: new DataTypes.SMALLINT(),
allowNull: true, allowNull: false,
defaultValue: null, defaultValue: 2,
}, },
tax_amount_value: { tax_amount_value: {
@ -194,8 +201,8 @@ export default (database: Sequelize) => {
}, },
tax_amount_scale: { tax_amount_scale: {
type: new DataTypes.SMALLINT(), type: new DataTypes.SMALLINT(),
allowNull: true, allowNull: false,
defaultValue: null, defaultValue: 2,
}, },
total_amount_value: { total_amount_value: {
@ -206,8 +213,8 @@ export default (database: Sequelize) => {
total_amount_scale: { total_amount_scale: {
type: new DataTypes.SMALLINT(), type: new DataTypes.SMALLINT(),
allowNull: true, allowNull: false,
defaultValue: null, defaultValue: 2,
}, },
}, },
{ {

View File

@ -1,36 +1,39 @@
import { NumericStringSchema, PercentageSchema } from "@erp/core";
import * as z from "zod/v4"; import * as z from "zod/v4";
export const CreateCustomerInvoiceRequestSchema = z.object({ export const CreateCustomerInvoiceRequestSchema = z.object({
id: z.uuid(), id: z.uuid(),
invoice_status: z.string(), company_id: z.uuid(),
invoice_number: z.string().min(1, "Customer invoice number is required"),
invoice_series: z.string().min(1, "Customer invoice series is required"), invoice_number: z.string(),
issue_date: z.string().datetime({ offset: true, message: "Invalid issue date format" }), status: z.string().default("draft"),
operation_date: z.string().datetime({ offset: true, message: "Invalid operation date format" }), series: z.string().default(""),
description: z.string(),
language_code: z.string().min(2, "Language code must be at least 2 characters long"), invoice_date: z.string(),
currency_code: z.string().min(3, "Currency code must be at least 3 characters long"), operation_date: z.string().default(""),
notes: z.string().optional(),
items: z.array( notes: z.string().default(""),
z.object({
description: z.string().min(1, "Item description is required"), language_code: z.string().toLowerCase().default("es"),
quantity: z.object({ currency_code: z.string().toUpperCase().default("EUR"),
value: z.number().positive("Quantity amount must be positive"),
scale: z.number().int().nonnegative("Quantity scale must be a non-negative integer"), discount_percentage: PercentageSchema.default({
}), value: "0",
unit_amount: z.object({ scale: "2",
value: z.number().positive("Unit price amount must be positive"), }),
scale: z.number().int().nonnegative("Unit price scale must be a non-negative integer"),
currency_code: z items: z
.string() .array(
.min(3, "Unit price currency code must be at least 3 characters long"), z.object({
}), id: z.uuid(),
discount_percentage: z.object({ position: z.string(),
value: z.number().nonnegative("Discount amount cannot be negative"), description: z.string().default(""),
scale: z.number().int().nonnegative("Discount scale must be a non-negative integer"), quantity: NumericStringSchema.default(""),
}), unit_amount: NumericStringSchema.default(""),
}) discount_percentage: NumericStringSchema.default(""),
), })
)
.default([]),
}); });
export type CreateCustomerInvoiceRequestDTO = z.infer<typeof CreateCustomerInvoiceRequestSchema>; export type CreateCustomerInvoiceRequestDTO = z.infer<typeof CreateCustomerInvoiceRequestSchema>;

View File

@ -0,0 +1,42 @@
import { AmountSchema, MetadataSchema, PercentageSchema, QuantitySchema } from "@erp/core";
import * as z from "zod/v4";
export const CreateCustomerInvoiceResponseSchema = z.object({
id: z.uuid(),
company_id: z.uuid(),
invoice_number: z.string(),
status: z.string(),
series: z.string(),
invoice_date: z.string(),
operation_date: z.string(),
notes: z.string(),
language_code: z.string(),
currency_code: z.string(),
subtotal_amount: AmountSchema,
discount_percentage: PercentageSchema,
discount_amount: AmountSchema,
taxable_amount: AmountSchema,
tax_amount: AmountSchema,
total_amount: AmountSchema,
items: z.array(
z.object({
id: z.uuid(),
position: z.string(),
description: z.string(),
quantity: QuantitySchema,
unit_amount: AmountSchema,
discount_percentage: PercentageSchema,
total_amount: AmountSchema,
})
),
metadata: MetadataSchema.optional(),
});
export type CreateCustomerInvoiceResponseDTO = z.infer<typeof CreateCustomerInvoiceResponseSchema>;

View File

@ -1,19 +0,0 @@
import { MetadataSchema } from "@erp/core";
import * as z from "zod/v4";
export const CustomerInvoicesCreationResponseSchema = z.object({
id: z.uuid(),
invoice_status: z.string(),
invoice_number: z.string(),
invoice_series: z.string(),
issue_date: z.iso.datetime({ offset: true }),
operation_date: z.iso.datetime({ offset: true }),
language_code: z.string(),
currency: z.string(),
metadata: MetadataSchema.optional(),
});
export type CustomerInvoicesCreationResponseDTO = z.infer<
typeof CustomerInvoicesCreationResponseSchema
>;

View File

@ -7,7 +7,7 @@ export const CustomerInvoiceListResponseSchema = createListViewResponseSchema(
invoice_status: z.string(), invoice_status: z.string(),
invoice_number: z.string(), invoice_number: z.string(),
invoice_series: z.string(), invoice_series: z.string(),
issue_date: z.iso.datetime({ offset: true }), invoice_date: z.iso.datetime({ offset: true }),
operation_date: z.iso.datetime({ offset: true }), operation_date: z.iso.datetime({ offset: true }),
language_code: z.string(), language_code: z.string(),
currency: z.string(), currency: z.string(),

View File

@ -9,7 +9,7 @@ export const GetCustomerInvoiceByIdResponseSchema = z.object({
status: z.string(), status: z.string(),
series: z.string(), series: z.string(),
issue_date: z.string(), invoice_date: z.string(),
operation_date: z.string(), operation_date: z.string(),
notes: z.string(), notes: z.string(),

View File

@ -1,4 +1,4 @@
export * from "./customer-invoice-creation.response.dto"; export * from "./create-customer-invoice.response.dto";
export * from "./customer-invoices-list.response.dto"; export * from "./customer-invoices-list.response.dto";
export * from "./get-customer-invoice-by-id.response.dto"; export * from "./get-customer-invoice-by-id.response.dto";
export * from "./update-customer-invoice-by-id.response.dto"; export * from "./update-customer-invoice-by-id.response.dto";

View File

@ -9,7 +9,7 @@ export const UpdateCustomerInvoiceByIdResponseSchema = z.object({
status: z.string(), status: z.string(),
series: z.string(), series: z.string(),
issue_date: z.string(), invoice_date: z.string(),
operation_date: z.string(), operation_date: z.string(),
notes: z.string(), notes: z.string(),

View File

@ -17,7 +17,7 @@
"invoice_number": "Inv. number", "invoice_number": "Inv. number",
"invoice_series": "Serie", "invoice_series": "Serie",
"invoice_status": "Status", "invoice_status": "Status",
"issue_date": "Date", "invoice_date": "Date",
"total_price": "Total price" "total_price": "Total price"
} }
}, },
@ -52,7 +52,7 @@
"placeholder": "", "placeholder": "",
"description": "" "description": ""
}, },
"issue_date": { "invoice_date": {
"label": "Date", "label": "Date",
"placeholder": "Select a date", "placeholder": "Select a date",
"description": "Invoice issue date" "description": "Invoice issue date"

View File

@ -35,8 +35,8 @@ export const CustomerInvoicesListGrid = () => {
{ field: "invoice_series", headerName: t("pages.list.grid_columns.invoice_series") }, { field: "invoice_series", headerName: t("pages.list.grid_columns.invoice_series") },
{ {
field: "issue_date", field: "invoice_date",
headerName: t("pages.list.grid_columns.issue_date"), headerName: t("pages.list.grid_columns.invoice_date"),
valueFormatter: (params: ValueFormatterParams) => { valueFormatter: (params: ValueFormatterParams) => {
return formatDate(params.value); return formatDate(params.value);
}, },

View File

@ -49,7 +49,7 @@ const invoiceFormSchema = z.object({
invoice_status: z.string(), invoice_status: z.string(),
invoice_number: z.string().min(1, "Número de factura requerido"), invoice_number: z.string().min(1, "Número de factura requerido"),
invoice_series: z.string().min(1, "Serie requerida"), invoice_series: z.string().min(1, "Serie requerida"),
issue_date: z.string(), invoice_date: z.string(),
operation_date: z.string(), operation_date: z.string(),
language_code: z.string(), language_code: z.string(),
currency: z.string(), currency: z.string(),
@ -144,7 +144,7 @@ const defaultInvoiceData = {
invoice_status: "draft", invoice_status: "draft",
invoice_number: "1", invoice_number: "1",
invoice_series: "A", invoice_series: "A",
issue_date: "2025-04-30T00:00:00.000Z", invoice_date: "2025-04-30T00:00:00.000Z",
operation_date: "2025-04-30T00:00:00.000Z", operation_date: "2025-04-30T00:00:00.000Z",
description: "", description: "",
language_code: "ES", language_code: "ES",
@ -332,11 +332,11 @@ export const CustomerInvoiceEditForm = ({
<DatePickerInputField <DatePickerInputField
control={form.control} control={form.control}
name='issue_date' name='invoice_date'
required required
label={t("form_fields.issue_date.label")} label={t("form_fields.invoice_date.label")}
placeholder={t("form_fields.issue_date.placeholder")} placeholder={t("form_fields.invoice_date.placeholder")}
description={t("form_fields.issue_date.description")} description={t("form_fields.invoice_date.description")}
/> />
<TextField <TextField
@ -429,11 +429,11 @@ export const CustomerInvoiceEditForm = ({
<DatePickerInputField <DatePickerInputField
control={form.control} control={form.control}
name='issue_date' name='invoice_date'
required required
label={t("form_fields.issue_date.label")} label={t("form_fields.invoice_date.label")}
placeholder={t("form_fields.issue_date.placeholder")} placeholder={t("form_fields.invoice_date.placeholder")}
description={t("form_fields.issue_date.description")} description={t("form_fields.invoice_date.description")}
/> />
<TextField <TextField
@ -815,14 +815,14 @@ export const CustomerInvoiceEditForm = ({
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button variant='outline' className='w-full justify-start text-left font-normal'> <Button variant='outline' className='w-full justify-start text-left font-normal'>
<CalendarIcon className='mr-2 h-4 w-4' /> <CalendarIcon className='mr-2 h-4 w-4' />
{format(issueDate, "PPP", { locale: es })} {format(invoiceDate, "PPP", { locale: es })}
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className='w-auto p-0' align='start'> <PopoverContent className='w-auto p-0' align='start'>
<Calendar <Calendar
mode='single' mode='single'
selected={issueDate} selected={invoiceDate}
onSelect={(date) => handleDateChange("issue_date", date)} onSelect={(date) => handleDateChange("invoice_date", date)}
initialFocus initialFocus
/> />
</PopoverContent> </PopoverContent>

View File

@ -8,8 +8,8 @@ import { CreateCustomersAssembler } from "./assembler";
import { mapDTOToCreateCustomerProps } from "./map-dto-to-create-customer-props"; import { mapDTOToCreateCustomerProps } from "./map-dto-to-create-customer-props";
type CreateCustomerUseCaseInput = { type CreateCustomerUseCaseInput = {
dto: CreateCustomerRequestDTO;
companyId: UniqueID; companyId: UniqueID;
dto: CreateCustomerRequestDTO;
}; };
export class CreateCustomerUseCase { export class CreateCustomerUseCase {
@ -30,7 +30,7 @@ export class CreateCustomerUseCase {
const { props, id } = dtoResult.data; const { props, id } = dtoResult.data;
// 3) Construir entidad de dominio // 2) Construir entidad de dominio
const buildResult = this.service.buildCustomerInCompany(companyId, props, id); const buildResult = this.service.buildCustomerInCompany(companyId, props, id);
if (buildResult.isFailure) { if (buildResult.isFailure) {
return Result.fail(buildResult.error); return Result.fail(buildResult.error);
@ -38,7 +38,7 @@ export class CreateCustomerUseCase {
const newCustomer = buildResult.data; const newCustomer = buildResult.data;
// 4) Ejecutar bajo transacción: verificar duplicado → persistir → ensamblar vista // 3) Ejecutar bajo transacción: verificar duplicado → persistir → ensamblar vista
return this.transactionManager.complete(async (transaction: Transaction) => { return this.transactionManager.complete(async (transaction: Transaction) => {
const existsGuard = await this.ensureNotExists(companyId, id, transaction); const existsGuard = await this.ensureNotExists(companyId, id, transaction);
if (existsGuard.isFailure) { if (existsGuard.isFailure) {

View File

@ -16,7 +16,7 @@ export const updateCustomerPresenter: IUpdateCustomerPresenter = {
customer_status: customer.status.toString(), customer_status: customer.status.toString(),
customer_number: customer.customerNumber.toString(), customer_number: customer.customerNumber.toString(),
customer_series: customer.customerSeries.toString(), customer_series: customer.customerSeries.toString(),
issue_date: customer.issueDate.toISO8601(), invoice_date: customer.invoiceDate.toISO8601(),
operation_date: customer.operationDate.toISO8601(), operation_date: customer.operationDate.toISO8601(),
language_code: customer.language.toString(), language_code: customer.language.toString(),
currency: customer.currency.toString(), currency: customer.currency.toString(),

View File

@ -182,7 +182,6 @@ export default (database: Sequelize) => {
indexes: [ indexes: [
{ name: "company_idx", fields: ["company_id"], unique: false }, { name: "company_idx", fields: ["company_id"], unique: false },
{ name: "idx_company_idx", fields: ["id", "company_id"], unique: true }, { name: "idx_company_idx", fields: ["id", "company_id"], unique: true },
{ name: "email_idx", fields: ["email"], unique: true },
], ],
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope whereMergeStrategy: "and", // <- cómo tratar el merge de un scope