Clientes y facturas de cliente

This commit is contained in:
David Arranz 2025-09-26 20:09:14 +02:00
parent 26442edd60
commit e93d48b930
22 changed files with 290 additions and 346 deletions

View File

@ -26,7 +26,8 @@
"noForEach": "off",
"noBannedTypes": "info",
"noUselessFragments": "off",
"useOptionalChain": "off"
"useOptionalChain": "off",
"noThisInStatic": "off"
},
"suspicious": {
"noImplicitAnyLet": "info",

View File

@ -141,11 +141,6 @@ export class Tax extends ValueObject<TaxProps> {
return `${this.toNumber().toFixed(this.scale)}%`;
}
/** Calcula el importe del impuesto sobre una base imponible */
calculateAmount(baseAmount: number): number {
return (baseAmount * this.toNumber()) / 100;
}
isZero(): boolean {
return this.toNumber() === 0;
}
@ -159,6 +154,7 @@ export class Tax extends ValueObject<TaxProps> {
greaterThan(other: Tax): boolean {
return this.toNumber() > other.toNumber();
}
lessThan(other: Tax): boolean {
return this.toNumber() < other.toNumber();
}

View File

@ -1,17 +1,8 @@
import { Collection } from "@repo/rdx-utils";
import { Tax } from "./tax";
export interface TaxesProps {
items?: Tax[];
}
export class Taxes extends Collection<Tax> {
constructor(props: TaxesProps) {
const { items } = props;
super(items);
}
public static create(props: TaxesProps): Taxes {
return new Taxes(props);
public static create<T extends Taxes>(this: new (items: Tax[]) => T, items: Tax[]): T {
return new this(items);
}
}

View File

@ -44,45 +44,4 @@ export abstract class SequelizeDomainMapper<TModel extends Model, TModelAttribut
return Result.ok(results.objects);
}
/*protected _safeMap<T>(operation: () => T, key: string): Result<T, Error> {
try {
return Result.ok(operation());
} catch (error: unknown) {
return Result.fail(error as Error);
}
}
protected _mapsValue(
row: TModel,
key: string,
customMapFn: (value: any, params: MapperParamsType) => Result<any, Error>,
params: MapperParamsType = { defaultValue: null }
): Result<any, Error> {
return customMapFn(row?.dataValues[key] ?? params.defaultValue, params);
}
protected _mapsAssociation(
row: TModel,
associationName: string,
customMapper: DomainMapperWithBulk<any, any>,
params: MapperParamsType = {}
): Result<any, Error> {
if (!customMapper) {
Result.fail(Error(`Custom mapper undefined for ${associationName}`));
}
const { filter, ...otherParams } = params;
let associationRows = row?.dataValues[associationName] ?? [];
if (filter) {
associationRows = Array.isArray(associationRows)
? associationRows.filter(filter)
: filter(associationRows);
}
return Array.isArray(associationRows)
? customMapper.mapToDomainCollection(associationRows, associationRows.length, otherParams)
: customMapper.mapToDomain(associationRows, otherParams);
}*/
}

View File

@ -149,7 +149,7 @@ export class CreateCustomerInvoicePropsMapper {
discountPercentage: discountPercentage!,
taxes: Taxes.create({ items: [] }),
taxes: Taxes.create([]),
items: items,
};
@ -219,7 +219,7 @@ export class CreateCustomerInvoicePropsMapper {
}
private mapTaxes(item: CreateCustomerInvoiceItemRequestDTO, itemIndex: number) {
const taxes = Taxes.create({ items: [] });
const taxes = Taxes.create([]);
item.taxes.split(",").every((tax_code, taxIndex) => {
const taxResult = Tax.createFromCode(tax_code, this.taxCatalog);

View File

@ -10,7 +10,6 @@ import {
} from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils";
import { CustomerInvoiceItems, InvoicePaymentMethod } from "../entities";
import { InvoiceTaxes } from "../entities/invoice-taxes";
import {
CustomerInvoiceNumber,
CustomerInvoiceSerie,
@ -23,9 +22,10 @@ export interface CustomerInvoiceProps {
companyId: UniqueID;
isProforma: boolean;
invoiceNumber: CustomerInvoiceNumber;
status: CustomerInvoiceStatus;
series: Maybe<CustomerInvoiceSerie>;
invoiceNumber: Maybe<CustomerInvoiceNumber>;
invoiceDate: UtcDate;
operationDate: Maybe<UtcDate>;
@ -33,6 +33,7 @@ export interface CustomerInvoiceProps {
customerId: UniqueID;
recipient: Maybe<InvoiceRecipient>;
reference: Maybe<string>;
notes: Maybe<TextValue>;
languageCode: LanguageCode;
@ -44,9 +45,9 @@ export interface CustomerInvoiceProps {
discountPercentage: Percentage;
verifactu_qr: string;
/*verifactu_qr: string;
verifactu_url: string;
verifactu_status: string;
verifactu_status: string;*/
}
export interface ICustomerInvoice {
@ -139,6 +140,10 @@ export class CustomerInvoice
return this.props.operationDate;
}
public get reference(): Maybe<string> {
return this.props.reference;
}
public get notes(): Maybe<TextValue> {
return this.props.notes;
}
@ -168,7 +173,7 @@ export class CustomerInvoice
return this._items;
}
public get taxes(): InvoiceTaxes {
public get taxes() {
return this.items.getTaxesAmountByTaxes();
}
@ -192,7 +197,12 @@ export class CustomerInvoice
}
private _getTaxesAmount(taxableAmount: InvoiceAmount): InvoiceAmount {
return this._taxes.getTaxesAmount(taxableAmount);
let amount = InvoiceAmount.zero(this.currencyCode.code);
for (const tax of this.taxes) {
amount = amount.add(tax.taxesAmount);
}
return amount;
}
private _getTotalAmount(taxableAmount: InvoiceAmount, taxesAmount: InvoiceAmount): InvoiceAmount {
@ -249,7 +259,7 @@ export class CustomerInvoice
...this.props,
status: CustomerInvoiceStatus.createEmitted(),
isProforma: false,
invoiceNumber: newInvoiceNumber,
invoiceNumber: Maybe.some(newInvoiceNumber),
},
this.id
);

View File

@ -2,7 +2,6 @@ import { Tax } from "@erp/core/api";
import { CurrencyCode, LanguageCode } from "@repo/rdx-ddd";
import { Collection } from "@repo/rdx-utils";
import { ItemAmount } from "../../value-objects";
import { InvoiceTax, InvoiceTaxes } from "../invoice-taxes";
import { CustomerInvoiceItem } from "./customer-invoice-item";
export interface CustomerInvoiceItemsProps {
@ -73,31 +72,33 @@ export class CustomerInvoiceItems extends Collection<CustomerInvoiceItem> {
);
}
public getTaxesAmountByTaxes(): InvoiceTaxes {
InvoiceTaxes.create({});
public getTaxesAmountByTaxes() {
const resultMap = new Map<Tax, { taxableAmount: ItemAmount; taxesAmount: ItemAmount }>();
const taxesMap = new Map<Tax, ItemAmount>();
const currencyCode = this._currencyCode.code;
for (const item of this.getAll()) {
for (const { tax, taxesAmount } of item.getTaxesAmountByTaxes()) {
const current = taxesMap.get(tax) ?? ItemAmount.zero(currencyCode);
taxesMap.set(tax, current.add(taxesAmount));
for (const { taxableAmount, tax, taxesAmount } of item.getTaxesAmountByTaxes()) {
const { taxableAmount: taxableCurrent, taxesAmount: taxesCurrent } = resultMap.get(tax) ?? {
taxableAmount: ItemAmount.zero(currencyCode),
taxesAmount: ItemAmount.zero(currencyCode),
};
resultMap.set(tax, {
taxableAmount: taxableCurrent.add(taxableAmount),
taxesAmount: taxesCurrent.add(taxesAmount),
});
}
}
const items: InvoiceTax[] = [];
for (const [tax, taxesAmount] of taxesMap) {
items.push(
InvoiceTax.create({
tax,
taxesAmount,
}).data
);
const items = [];
for (const [tax, { taxableAmount, taxesAmount }] of resultMap) {
items.push({
taxableAmount,
tax,
taxesAmount,
});
}
return InvoiceTaxes.create({
items: items,
});
return items;
}
}

View File

@ -1,36 +0,0 @@
import { Tax } from "@erp/core/api";
import { DomainEntity, UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { ItemAmount } from "../../value-objects";
export interface ItemTaxProps {
tax: Tax;
}
export class ItemTax extends DomainEntity<ItemTaxProps> {
static create(props: ItemTaxProps, id?: UniqueID): Result<ItemTax, Error> {
const itemTax = new ItemTax(props, id);
// Reglas de negocio / validaciones
// ...
// ...
return Result.ok(itemTax);
}
public get tax(): Tax {
return this.props.tax;
}
getProps(): ItemTaxProps {
return this.props;
}
toPrimitive() {
return this.getProps();
}
public getTaxAmount(taxableAmount: ItemAmount): ItemAmount {
return taxableAmount.percentage(this.tax.percentage);
}
}

View File

@ -1,24 +1,14 @@
import { Collection } from "@repo/rdx-utils";
import { Tax, Taxes } from "@erp/core/api";
import { ItemAmount } from "../../value-objects";
import { ItemTax } from "./item-tax";
export interface ItemTaxesProps {
items?: ItemTax[];
}
export class ItemTaxes extends Collection<ItemTax> {
constructor(props: ItemTaxesProps) {
const { items = [] } = props;
super(items);
}
public static create(props: ItemTaxesProps): ItemTaxes {
return new ItemTaxes(props);
export class ItemTaxes extends Taxes {
constructor(items: Tax[] = [], totalItems: number | null = null) {
super(items, totalItems);
}
public getTaxesAmount(taxableAmount: ItemAmount): ItemAmount {
return this.getAll().reduce(
(total, tax) => total.add(tax.getTaxAmount(taxableAmount)),
(total, tax) => total.add(taxableAmount.percentage(tax.percentage)),
ItemAmount.zero(taxableAmount.currencyCode)
);
}
@ -26,21 +16,22 @@ export class ItemTaxes extends Collection<ItemTax> {
public getTaxesAmountByTaxCode(taxCode: string, taxableAmount: ItemAmount): ItemAmount {
const currencyCode = taxableAmount.currencyCode;
return this.filter((itemTax) => itemTax.tax.code === taxCode).reduce((totalAmount, itemTax) => {
return itemTax.getTaxAmount(taxableAmount).add(totalAmount);
return this.filter((itemTax) => itemTax.code === taxCode).reduce((totalAmount, itemTax) => {
return taxableAmount.percentage(itemTax.percentage).add(totalAmount);
}, ItemAmount.zero(currencyCode));
}
public getTaxesAmountByTaxes(taxableAmount: ItemAmount): {
public getTaxesAmountByTaxes(taxableAmount: ItemAmount) {
return this.getAll().map((taxItem) => ({
tax: taxItem.tax,
taxesAmount: this.getTaxesAmountByTaxCode(taxItem.tax.code, taxableAmount),
taxableAmount,
tax: taxItem,
taxesAmount: this.getTaxesAmountByTaxCode(taxItem.code, taxableAmount),
}));
}
public getCodesToString(): string {
return this.getAll()
.map((taxItem) => taxItem.tax.code)
.map((taxItem) => taxItem.code)
.join(", ");
}
}

View File

@ -146,9 +146,7 @@ export class CustomerInvoiceItemDomainMapper
// 5) Construcción del elemento de dominio
const taxes = ItemTaxes.create({
items: taxesResults.data.getAll(),
});
const taxes = ItemTaxes.create(taxesResults.data.getAll());
const createResult = CustomerInvoiceItem.create(
{
@ -231,6 +229,8 @@ export class CustomerInvoiceItemDomainMapper
total_amount_value: allAmounts.totalAmount.value,
total_amount_scale: allAmounts.totalAmount.scale,
taxes: taxesResults.data,
});
}
}

View File

@ -1,9 +1,4 @@
import {
ISequelizeDomainMapper,
InfrastructureError,
MapperParamsType,
SequelizeDomainMapper,
} from "@erp/core/api";
import { ISequelizeDomainMapper, MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
import {
CurrencyCode,
LanguageCode,
@ -17,7 +12,7 @@ import {
maybeFromNullableVO,
toNullable,
} from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils";
import { Collection, Maybe, Result } from "@repo/rdx-utils";
import {
CustomerInvoice,
CustomerInvoiceItems,
@ -27,7 +22,6 @@ import {
CustomerInvoiceStatus,
InvoicePaymentMethod,
} from "../../../domain";
import { InvoiceTaxes } from "../../../domain/entities/invoice-taxes";
import { CustomerInvoiceCreationAttributes, CustomerInvoiceModel } from "../../sequelize";
import { CustomerInvoiceItemDomainMapper as CustomerInvoiceItemFullMapper } from "./customer-invoice-item.mapper";
import { InvoiceRecipientDomainMapper as InvoiceRecipientFullMapper } from "./invoice-recipient.mapper";
@ -132,7 +126,7 @@ export class CustomerInvoiceDomainMapper
);
const invoiceNumber = extractOrPushError(
CustomerInvoiceNumber.create(source.invoice_number),
maybeFromNullableVO(source.invoice_number, (value) => CustomerInvoiceNumber.create(value)),
"invoice_number",
errors
);
@ -149,6 +143,12 @@ export class CustomerInvoiceDomainMapper
errors
);
const reference = extractOrPushError(
maybeFromNullableVO(source.reference, (value) => Result.ok(String(value))),
"reference",
errors
);
const notes = extractOrPushError(
maybeFromNullableVO(source.notes, (value) => TextValue.create(value)),
"notes",
@ -186,6 +186,7 @@ export class CustomerInvoiceDomainMapper
invoiceNumber,
invoiceDate,
operationDate,
reference,
notes,
languageCode,
currencyCode,
@ -274,10 +275,6 @@ export class CustomerInvoiceDomainMapper
const recipient = recipientResult.data;
const paymentMethod = paymentMethodResult.data;
const taxes = InvoiceTaxes.create({
items: taxesResults.data.getAll(),
});
const items = CustomerInvoiceItems.create({
languageCode: attributes.languageCode!,
currencyCode: attributes.currencyCode!,
@ -297,6 +294,7 @@ export class CustomerInvoiceDomainMapper
customerId: attributes.customerId!,
recipient: recipient,
reference: attributes.reference!,
notes: attributes.notes!,
languageCode: attributes.languageCode!,
@ -306,7 +304,6 @@ export class CustomerInvoiceDomainMapper
paymentMethod: paymentMethod!,
taxes: taxes,
items,
};
@ -345,8 +342,11 @@ export class CustomerInvoiceDomainMapper
});
}
const items = itemsResult.data;
// 1) Taxes
const taxesResult = this._taxesMapper.mapToPersistenceArray(source.taxes, {
const taxesResult = this._taxesMapper.mapToPersistenceArray(new Collection(source.taxes), {
errors,
parent: source,
...params,
@ -358,18 +358,31 @@ export class CustomerInvoiceDomainMapper
});
}
const taxes = taxesResult.data;
// 3) Calcular totales
const allAmounts = source.getAllAmounts();
// 4) Construir parte
const invoiceValues: Partial<CustomerInvoiceCreationAttributes> = {
// 4) Cliente
const recipient = this._mapRecipientToPersistence(source);
// 7) Si hubo errores de mapeo, devolvemos colección de validación
if (errors.length > 0) {
return Result.fail(
new ValidationErrorCollection("Customer invoice mapping to persistence failed", errors)
);
}
const invoiceValues: CustomerInvoiceCreationAttributes = {
id: source.id.toPrimitive(),
company_id: source.companyId.toPrimitive(),
is_proforma: source.isProforma,
status: source.status.toPrimitive(),
series: toNullable(source.series, (series) => series.toPrimitive()),
invoice_number: source.invoiceNumber.toPrimitive(),
invoice_number: toNullable(source.invoiceNumber, (invoiceNumber) =>
invoiceNumber.toPrimitive()
),
invoice_date: source.invoiceDate.toPrimitive(),
operation_date: toNullable(source.operationDate, (operationDate) =>
operationDate.toPrimitive()
@ -377,6 +390,7 @@ export class CustomerInvoiceDomainMapper
language_code: source.languageCode.toPrimitive(),
currency_code: source.currencyCode.toPrimitive(),
reference: toNullable(source.reference, (reference) => reference),
notes: toNullable(source.notes, (notes) => notes.toPrimitive()),
subtotal_amount_value: allAmounts.subtotalAmount.value,
@ -397,61 +411,58 @@ export class CustomerInvoiceDomainMapper
total_amount_value: allAmounts.totalAmount.value,
total_amount_scale: allAmounts.totalAmount.scale,
customer_id: source.customerId.toPrimitive(),
payment_method_id: toNullable(source.paymentMethod, (payment) => payment.toObjectString().id),
payment_method_description: toNullable(
source.paymentMethod,
(payment) => payment.toObjectString().payment_description
),
customer_id: source.customerId.toPrimitive(),
...recipient,
taxes,
items,
};
// 5) Cliente / Recipient ??
// Si es proforma no guardamos los campos como históricos (snapshots)
if (source.isProforma) {
Object.assign(invoiceValues, {
customer_tin: null,
customer_name: null,
customer_street: null,
customer_street2: null,
customer_city: null,
customer_province: null,
customer_postal_code: null,
customer_country: null,
return Result.ok<CustomerInvoiceCreationAttributes>(invoiceValues);
}
protected _mapRecipientToPersistence(source: CustomerInvoice, params?: MapperParamsType) {
const { errors } = params as {
errors: ValidationErrorDetail[];
};
const recipient = source.recipient.getOrUndefined();
if (!source.isProforma && !recipient) {
errors.push({
path: "recipient",
message: "[CustomerInvoiceDomainMapper] Issued customer invoice w/o recipient data",
});
} else {
const recipient = source.recipient.getOrUndefined();
if (!recipient) {
return Result.fail(
new InfrastructureError(
"[CustomerInvoiceDomainMapper] Issued customer invoice w/o recipient data"
)
);
}
Object.assign(invoiceValues, {
customer_tin: recipient.tin.toPrimitive(),
customer_name: recipient.name.toPrimitive(),
customer_street: toNullable(recipient.street, (v) => v.toPrimitive()),
customer_street2: toNullable(recipient.street2, (v) => v.toPrimitive()),
customer_city: toNullable(recipient.city, (v) => v.toPrimitive()),
customer_province: toNullable(recipient.province, (v) => v.toPrimitive()),
customer_postal_code: toNullable(recipient.postalCode, (v) => v.toPrimitive()),
customer_country: toNullable(recipient.country, (v) => v.toPrimitive()),
} as Partial<CustomerInvoiceCreationAttributes>);
}
// 7) Si hubo errores de mapeo, devolvemos colección de validación
if (errors.length > 0) {
return Result.fail(
new ValidationErrorCollection("Customer invoice mapping to persistence failed", errors)
);
}
const recipientValues = {
customer_tin: !source.isProforma ? recipient!.tin.toPrimitive() : null,
customer_name: !source.isProforma ? recipient!.name.toPrimitive() : null,
customer_street: !source.isProforma
? toNullable(recipient!.street, (v) => v.toPrimitive())
: null,
customer_street2: !source.isProforma
? toNullable(recipient!.street2, (v) => v.toPrimitive())
: null,
customer_city: !source.isProforma
? toNullable(recipient!.city, (v) => v.toPrimitive())
: null,
customer_province: !source.isProforma
? toNullable(recipient!.province, (v) => v.toPrimitive())
: null,
customer_postal_code: !source.isProforma
? toNullable(recipient!.postalCode, (v) => v.toPrimitive())
: null,
customer_country: !source.isProforma
? toNullable(recipient!.country, (v) => v.toPrimitive())
: null,
};
return Result.ok<CustomerInvoiceCreationAttributes>({
...invoiceValues,
items: itemsResult.data,
taxes: taxesResult.data,
});
return recipientValues;
}
}

View File

@ -1,21 +1,15 @@
import { JsonTaxCatalogProvider } from "@erp/core";
import { MapperParamsType, SequelizeDomainMapper, Tax } from "@erp/core/api";
import {
ValidationErrorCollection,
ValidationErrorDetail,
extractOrPushError,
} from "@repo/rdx-ddd";
import { UniqueID, ValidationErrorDetail } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { InferCreationAttributes } from "sequelize";
import { CustomerInvoice, CustomerInvoiceProps } from "../../../domain";
import { InvoiceTax } from "../../../domain/entities/invoice-taxes";
import { CustomerInvoice, CustomerInvoiceItemProps, ItemAmount } from "../../../domain";
import { CustomerInvoiceTaxCreationAttributes, CustomerInvoiceTaxModel } from "../../sequelize";
export class TaxesDomainMapper extends SequelizeDomainMapper<
CustomerInvoiceTaxModel,
CustomerInvoiceTaxCreationAttributes,
InvoiceTax
{ taxableAmount: ItemAmount; tax: Tax; taxesAmount: ItemAmount }
> {
private _taxCatalog: JsonTaxCatalogProvider;
@ -35,60 +29,57 @@ export class TaxesDomainMapper extends SequelizeDomainMapper<
public mapToDomain(
source: CustomerInvoiceTaxModel,
params?: MapperParamsType
): Result<InvoiceTax, Error> {
const { errors, index, attributes } = params as {
index: number;
errors: ValidationErrorDetail[];
attributes: Partial<CustomerInvoiceProps>;
): Result<
{
taxableAmount: ItemAmount;
tax: Tax;
taxesAmount: ItemAmount;
},
Error
> {
const { attributes } = params as {
attributes: Partial<CustomerInvoiceItemProps>;
};
const tax = extractOrPushError(
Tax.createFromCode(source.tax_code, this._taxCatalog),
`taxes[${index}].tax_code`,
errors
);
// Creación del objeto de dominio
const createResult = InvoiceTax.create({
tax: tax!,
return Result.ok({
taxableAmount: ItemAmount.create({
value: source.taxable_amount_value,
currency_code: attributes.currencyCode!.code,
}).data,
tax: Tax.createFromCode(source.tax_code, this._taxCatalog).data,
taxesAmount: ItemAmount.create({
value: source.taxes_amount_value,
currency_code: attributes.currencyCode!.code,
}).data,
});
if (createResult.isFailure) {
return Result.fail(
new ValidationErrorCollection("Invoice tax creation failed", [
{ path: `taxes[${index}]`, message: createResult.error.message },
])
);
}
return createResult;
}
public mapToPersistence(
source: InvoiceTax,
source: {
taxableAmount: ItemAmount;
tax: Tax;
taxesAmount: ItemAmount;
},
params?: MapperParamsType
): Result<InferCreationAttributes<CustomerInvoiceTaxModel, {}>, Error> {
): Result<CustomerInvoiceTaxCreationAttributes, Error> {
const { errors, parent } = params as {
index: number;
parent: CustomerInvoice;
errors: ValidationErrorDetail[];
};
const taxableAmount = parent.getTaxableAmount()
const taxesAmount =
source;
return Result.ok({
tax_id: source.id.toPrimitive(),
tax_id: UniqueID.generateNewID().toPrimitive(),
invoice_id: parent.id.toPrimitive(),
tax_code: source.tax.code,
taxable_amount_value: taxableAmount.value,
taxable_amount_scale: taxableAmount.scale,
taxable_amount_value: source.taxableAmount.value,
taxable_amount_scale: source.taxableAmount.scale,
taxes_amount_value: taxesAmount.value,
taxes_amount_scale: taxesAmount.scale,
taxes_amount_value: source.taxesAmount.value,
taxes_amount_scale: source.taxesAmount.scale,
});
}
}

View File

@ -5,13 +5,15 @@ import {
SequelizeDomainMapper,
Tax,
} from "@erp/core/api";
import { CustomerInvoiceItem, ItemTax } from "@erp/customer-invoices/api/domain";
import {
UniqueID,
ValidationErrorCollection,
ValidationErrorDetail,
extractOrPushError,
} from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { CustomerInvoiceItem } from "../../../domain";
import {
CustomerInvoiceItemTaxCreationAttributes,
CustomerInvoiceItemTaxModel,
@ -21,14 +23,14 @@ export interface IItemTaxesDomainMapper
extends ISequelizeDomainMapper<
CustomerInvoiceItemTaxModel,
CustomerInvoiceItemTaxCreationAttributes,
ItemTax
Tax
> {}
export class ItemTaxesDomainMapper
extends SequelizeDomainMapper<
CustomerInvoiceItemTaxModel,
CustomerInvoiceItemTaxCreationAttributes,
ItemTax
Tax
>
implements IItemTaxesDomainMapper
{
@ -50,7 +52,7 @@ export class ItemTaxesDomainMapper
public mapToDomain(
source: CustomerInvoiceItemTaxModel,
params?: MapperParamsType
): Result<ItemTax, Error> {
): Result<Tax, Error> {
const { errors, index } = params as {
index: number;
errors: ValidationErrorDetail[];
@ -63,7 +65,7 @@ export class ItemTaxesDomainMapper
);
// Creación del objeto de dominio
const createResult = ItemTax.create({ tax: tax! });
const createResult = Tax.create(tax!);
if (createResult.isFailure) {
return Result.fail(
new ValidationErrorCollection("Invoice item tax creation failed", [
@ -76,7 +78,7 @@ export class ItemTaxesDomainMapper
}
public mapToPersistence(
source: ItemTax,
source: Tax,
params?: MapperParamsType
): Result<CustomerInvoiceItemTaxCreationAttributes, Error> {
const { errors, parent } = params as {
@ -85,13 +87,13 @@ export class ItemTaxesDomainMapper
};
const taxableAmount = parent.getTaxableAmount();
const taxAmount = source.getTaxAmount(taxableAmount);
const taxAmount = taxableAmount.percentage(source.percentage);
return Result.ok({
tax_id: source.id.toPrimitive(),
tax_id: UniqueID.generateNewID().toPrimitive(),
item_id: parent.id.toPrimitive(),
tax_code: source.tax.code,
tax_code: source.code,
taxable_amount_value: taxableAmount.value,
taxable_amount_scale: taxableAmount.scale,

View File

@ -68,26 +68,26 @@ export default (database: Sequelize) => {
taxable_amount_value: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: true,
defaultValue: null,
allowNull: false,
defaultValue: 0,
},
taxable_amount_scale: {
type: new DataTypes.SMALLINT(),
allowNull: false,
defaultValue: 2,
defaultValue: 4,
},
taxes_amount_value: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: true,
defaultValue: null,
allowNull: false,
defaultValue: 0,
},
taxes_amount_scale: {
type: new DataTypes.SMALLINT(),
allowNull: false,
defaultValue: 2,
defaultValue: 4,
},
},
{

View File

@ -1,4 +1,5 @@
import {
CreationOptional,
DataTypes,
InferAttributes,
InferCreationAttributes,
@ -28,38 +29,39 @@ export class CustomerInvoiceItemModel extends Model<
declare position: number;
declare description: string;
declare description: CreationOptional<string | null>;
declare quantity_value: number;
declare quantity_value: CreationOptional<number | null>;
declare quantity_scale: number;
declare unit_amount_value: number;
declare unit_amount_value: CreationOptional<number | null>;
declare unit_amount_scale: number;
// Subtotal
declare subtotal_amount_value: number;
declare subtotal_amount_value: CreationOptional<number | null>;
declare subtotal_amount_scale: number;
// Discount percentage
declare discount_percentage_value: number;
declare discount_percentage_value: CreationOptional<number | null>;
declare discount_percentage_scale: number;
// Discount amount
declare discount_amount_value: number;
declare discount_amount_value: CreationOptional<number | null>;
declare discount_amount_scale: number;
// Taxable amount (base imponible)
declare taxable_amount_value: number;
declare taxable_amount_value: CreationOptional<number | null>;
declare taxable_amount_scale: number;
// Total taxes amount / taxes total
declare taxes_amount_value: number;
declare taxes_amount_value: CreationOptional<number | null>;
declare taxes_amount_scale: number;
// Total
declare total_amount_value: number;
declare total_amount_value: CreationOptional<number | null>;
declare total_amount_scale: number;
// Relaciones
declare invoice: NonAttribute<CustomerInvoiceModel>;
declare taxes: NonAttribute<CustomerInvoiceItemTaxModel[]>;

View File

@ -67,8 +67,8 @@ export default (database: Sequelize) => {
taxable_amount_value: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: true,
defaultValue: null,
allowNull: false,
defaultValue: 0,
},
taxable_amount_scale: {
@ -79,8 +79,8 @@ export default (database: Sequelize) => {
taxes_amount_value: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: true,
defaultValue: null,
allowNull: false,
defaultValue: 0,
},
taxes_amount_scale: {

View File

@ -1,4 +1,5 @@
import {
CreationOptional,
DataTypes,
InferAttributes,
InferCreationAttributes,
@ -34,15 +35,20 @@ export class CustomerInvoiceModel extends Model<
declare is_proforma: boolean;
declare status: string;
declare series: string;
declare invoice_number: string;
declare invoice_date: string;
declare operation_date: string;
declare language_code: string;
declare currency_code: string;
//declare xxxxxx
declare series: CreationOptional<string | null>;
declare invoice_number: CreationOptional<string | null>;
declare invoice_date: CreationOptional<string>;
declare operation_date: CreationOptional<string | null>;
declare language_code: CreationOptional<string>;
declare currency_code: CreationOptional<string>;
declare notes: string;
declare reference: CreationOptional<string | null>;
declare notes: CreationOptional<string | null>;
// Método de pago
declare payment_method_id: CreationOptional<string | null>;
declare payment_method_description: CreationOptional<string | null>;
// Subtotal
declare subtotal_amount_value: number;
@ -70,18 +76,14 @@ export class CustomerInvoiceModel extends Model<
// Customer
declare customer_id: string;
declare customer_tin: string;
declare customer_name: string;
declare customer_street: string;
declare customer_street2: string;
declare customer_city: string;
declare customer_province: string;
declare customer_postal_code: string;
declare customer_country: string;
// Método de pago
declare payment_method_id: string;
declare payment_method_description: string;
declare customer_tin: CreationOptional<string | null>;
declare customer_name: CreationOptional<string | null>;
declare customer_street: CreationOptional<string | null>;
declare customer_street2: CreationOptional<string | null>;
declare customer_city: CreationOptional<string | null>;
declare customer_province: CreationOptional<string | null>;
declare customer_postal_code: CreationOptional<string | null>;
declare customer_country: CreationOptional<string | null>;
// Relaciones
declare items: NonAttribute<CustomerInvoiceItemModel[]>;
@ -163,8 +165,7 @@ export default (database: Sequelize) => {
invoice_date: {
type: new DataTypes.DATEONLY(),
allowNull: true,
defaultValue: null,
allowNull: false,
},
operation_date: {
@ -182,6 +183,13 @@ export default (database: Sequelize) => {
currency_code: {
type: new DataTypes.STRING(3),
allowNull: false,
defaultValue: "EUR",
},
reference: {
type: new DataTypes.STRING(),
allowNull: true,
defaultValue: null,
},
notes: {
@ -207,6 +215,7 @@ export default (database: Sequelize) => {
allowNull: false,
defaultValue: 0,
},
subtotal_amount_scale: {
type: new DataTypes.SMALLINT(),
allowNull: false,
@ -215,8 +224,8 @@ export default (database: Sequelize) => {
discount_percentage_value: {
type: new DataTypes.SMALLINT(),
allowNull: true,
defaultValue: null,
allowNull: false,
defaultValue: 0,
},
discount_percentage_scale: {
@ -227,8 +236,8 @@ export default (database: Sequelize) => {
discount_amount_value: {
type: new DataTypes.BIGINT(),
allowNull: true,
defaultValue: null,
allowNull: false,
defaultValue: 0,
},
discount_amount_scale: {
@ -239,8 +248,8 @@ export default (database: Sequelize) => {
taxable_amount_value: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: true,
defaultValue: null,
allowNull: false,
defaultValue: 0,
},
taxable_amount_scale: {
type: new DataTypes.SMALLINT(),
@ -250,8 +259,8 @@ export default (database: Sequelize) => {
taxes_amount_value: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: true,
defaultValue: null,
allowNull: false,
defaultValue: 0,
},
taxes_amount_scale: {
type: new DataTypes.SMALLINT(),
@ -261,8 +270,8 @@ export default (database: Sequelize) => {
total_amount_value: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: true,
defaultValue: null,
allowNull: false,
defaultValue: 0,
},
total_amount_scale: {

View File

@ -30,14 +30,18 @@ export interface CustomerProps {
emailPrimary: Maybe<EmailAddress>;
emailSecondary: Maybe<EmailAddress>;
phonePrimary: Maybe<PhoneNumber>;
phoneSecondary: Maybe<PhoneNumber>;
mobilePrimary: Maybe<PhoneNumber>;
mobileSecondary: Maybe<PhoneNumber>;
fax: Maybe<PhoneNumber>;
website: Maybe<URLAddress>;
legalRecord: Maybe<TextValue>;
defaultTaxes: Collection<TaxCode>;
languageCode: LanguageCode;

View File

@ -171,7 +171,7 @@ export class CustomerDomainMapper
// source.default_taxes is stored as a comma-separated string
const defaultTaxes = new Collection<TaxCode>();
if (!isNullishOrEmpty(source.default_taxes)) {
source.default_taxes.split(",").map((taxCode, index) => {
source.default_taxes!.split(",").map((taxCode, index) => {
const tax = extractOrPushError(TaxCode.create(taxCode), `default_taxes.${index}`, errors);
if (tax) {
defaultTaxes.add(tax!);

View File

@ -1,4 +1,11 @@
import { DataTypes, InferAttributes, InferCreationAttributes, Model, Sequelize } from "sequelize";
import {
CreationOptional,
DataTypes,
InferAttributes,
InferCreationAttributes,
Model,
Sequelize,
} from "sequelize";
export type CustomerCreationAttributes = InferCreationAttributes<CustomerModel, {}> & {};
@ -13,43 +20,43 @@ export class CustomerModel extends Model<
declare id: string;
declare company_id: string;
declare reference: string;
declare reference: CreationOptional<string | null>;
declare is_company: boolean;
declare name: string;
declare trade_name: string;
declare tin: string;
declare trade_name: CreationOptional<string | null>;
declare tin: CreationOptional<string | null>;
declare street: string;
declare street2: string;
declare city: string;
declare province: string;
declare postal_code: string;
declare country: string;
declare street: CreationOptional<string | null>;
declare street2: CreationOptional<string | null>;
declare city: CreationOptional<string | null>;
declare province: CreationOptional<string | null>;
declare postal_code: CreationOptional<string | null>;
declare country: CreationOptional<string | null>;
// Correos electrónicos
declare email_primary: string;
declare email_secondary: string;
declare email_primary: CreationOptional<string | null>;
declare email_secondary: CreationOptional<string | null>;
// Teléfonos fijos
declare phone_primary: string;
declare phone_secondary: string;
declare phone_primary: CreationOptional<string | null>;
declare phone_secondary: CreationOptional<string | null>;
// Móviles
declare mobile_primary: string;
declare mobile_secondary: string;
declare mobile_primary: CreationOptional<string | null>;
declare mobile_secondary: CreationOptional<string | null>;
declare fax: string;
declare website: string;
declare fax: CreationOptional<string | null>;
declare website: CreationOptional<string | null>;
declare legal_record: string;
declare legal_record: CreationOptional<string | null>;
declare default_taxes: string;
declare default_taxes: CreationOptional<string | null>;
declare status: string;
declare language_code: string;
declare currency_code: string;
declare language_code: CreationOptional<string>;
declare currency_code: CreationOptional<string>;
declare factuges_id: string;
declare factuges_id: CreationOptional<string | null>;
static associate(database: Sequelize) {}
@ -187,6 +194,12 @@ export default (database: Sequelize) => {
allowNull: true,
},
status: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: "active",
},
language_code: {
type: DataTypes.STRING(2),
allowNull: false,
@ -199,12 +212,6 @@ export default (database: Sequelize) => {
defaultValue: "EUR",
},
status: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: "active",
},
factuges_id: {
type: DataTypes.STRING,
allowNull: true,

View File

@ -21,7 +21,12 @@ export function maybeFromNullableString(input?: string | null): Maybe<string> {
}
/** Maybe<T> -> null para transporte */
export function toNullable<T>(m: Maybe<T>, map?: (t: T) => any): any | null {
export function toNullable<T, R>(m: Maybe<T>, map: (t: T) => R): R | null {
if (!m || m.isNone()) return null;
return map(m.unwrap() as T);
}
export function toNullable2<T>(m: Maybe<T>, map?: (t: T) => unknown): unknown | null {
if (!m || m.isNone()) return null;
const v = m.unwrap() as T;
return map ? String(map(v)) : String(v);

View File

@ -3,8 +3,8 @@
* Ofrece métodos básicos para manipular, consultar y recorrer los elementos.
*/
export class Collection<T> {
private items: T[];
private totalItems: number;
protected items: T[];
protected totalItems: number;
/**
* Crea una nueva colección.
@ -46,7 +46,7 @@ export class Collection<T> {
return this.removeByIndex(index);
}
public removeByIndex(index: number) {
removeByIndex(index: number) {
if (index !== -1) {
this.items.splice(index, 1);
if (this.totalItems !== null) {