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", "noForEach": "off",
"noBannedTypes": "info", "noBannedTypes": "info",
"noUselessFragments": "off", "noUselessFragments": "off",
"useOptionalChain": "off" "useOptionalChain": "off",
"noThisInStatic": "off"
}, },
"suspicious": { "suspicious": {
"noImplicitAnyLet": "info", "noImplicitAnyLet": "info",

View File

@ -141,11 +141,6 @@ export class Tax extends ValueObject<TaxProps> {
return `${this.toNumber().toFixed(this.scale)}%`; 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 { isZero(): boolean {
return this.toNumber() === 0; return this.toNumber() === 0;
} }
@ -159,6 +154,7 @@ export class Tax extends ValueObject<TaxProps> {
greaterThan(other: Tax): boolean { greaterThan(other: Tax): boolean {
return this.toNumber() > other.toNumber(); return this.toNumber() > other.toNumber();
} }
lessThan(other: Tax): boolean { lessThan(other: Tax): boolean {
return this.toNumber() < other.toNumber(); return this.toNumber() < other.toNumber();
} }

View File

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

View File

@ -44,45 +44,4 @@ export abstract class SequelizeDomainMapper<TModel extends Model, TModelAttribut
return Result.ok(results.objects); 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!, discountPercentage: discountPercentage!,
taxes: Taxes.create({ items: [] }), taxes: Taxes.create([]),
items: items, items: items,
}; };
@ -219,7 +219,7 @@ export class CreateCustomerInvoicePropsMapper {
} }
private mapTaxes(item: CreateCustomerInvoiceItemRequestDTO, itemIndex: number) { private mapTaxes(item: CreateCustomerInvoiceItemRequestDTO, itemIndex: number) {
const taxes = Taxes.create({ items: [] }); const taxes = Taxes.create([]);
item.taxes.split(",").every((tax_code, taxIndex) => { item.taxes.split(",").every((tax_code, taxIndex) => {
const taxResult = Tax.createFromCode(tax_code, this.taxCatalog); const taxResult = Tax.createFromCode(tax_code, this.taxCatalog);

View File

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

View File

@ -2,7 +2,6 @@ import { Tax } from "@erp/core/api";
import { CurrencyCode, LanguageCode } from "@repo/rdx-ddd"; import { CurrencyCode, LanguageCode } from "@repo/rdx-ddd";
import { Collection } from "@repo/rdx-utils"; import { Collection } from "@repo/rdx-utils";
import { ItemAmount } from "../../value-objects"; import { ItemAmount } from "../../value-objects";
import { InvoiceTax, InvoiceTaxes } from "../invoice-taxes";
import { CustomerInvoiceItem } from "./customer-invoice-item"; import { CustomerInvoiceItem } from "./customer-invoice-item";
export interface CustomerInvoiceItemsProps { export interface CustomerInvoiceItemsProps {
@ -73,31 +72,33 @@ export class CustomerInvoiceItems extends Collection<CustomerInvoiceItem> {
); );
} }
public getTaxesAmountByTaxes(): InvoiceTaxes { public getTaxesAmountByTaxes() {
InvoiceTaxes.create({}); const resultMap = new Map<Tax, { taxableAmount: ItemAmount; taxesAmount: ItemAmount }>();
const taxesMap = new Map<Tax, ItemAmount>();
const currencyCode = this._currencyCode.code; const currencyCode = this._currencyCode.code;
for (const item of this.getAll()) { for (const item of this.getAll()) {
for (const { tax, taxesAmount } of item.getTaxesAmountByTaxes()) { for (const { taxableAmount, tax, taxesAmount } of item.getTaxesAmountByTaxes()) {
const current = taxesMap.get(tax) ?? ItemAmount.zero(currencyCode); const { taxableAmount: taxableCurrent, taxesAmount: taxesCurrent } = resultMap.get(tax) ?? {
taxesMap.set(tax, current.add(taxesAmount)); taxableAmount: ItemAmount.zero(currencyCode),
taxesAmount: ItemAmount.zero(currencyCode),
};
resultMap.set(tax, {
taxableAmount: taxableCurrent.add(taxableAmount),
taxesAmount: taxesCurrent.add(taxesAmount),
});
} }
} }
const items: InvoiceTax[] = []; const items = [];
for (const [tax, taxesAmount] of taxesMap) { for (const [tax, { taxableAmount, taxesAmount }] of resultMap) {
items.push( items.push({
InvoiceTax.create({ taxableAmount,
tax, tax,
taxesAmount, taxesAmount,
}).data });
);
} }
return InvoiceTaxes.create({ return items;
items: 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 { ItemAmount } from "../../value-objects";
import { ItemTax } from "./item-tax";
export interface ItemTaxesProps { export class ItemTaxes extends Taxes {
items?: ItemTax[]; constructor(items: Tax[] = [], totalItems: number | null = null) {
} super(items, totalItems);
export class ItemTaxes extends Collection<ItemTax> {
constructor(props: ItemTaxesProps) {
const { items = [] } = props;
super(items);
}
public static create(props: ItemTaxesProps): ItemTaxes {
return new ItemTaxes(props);
} }
public getTaxesAmount(taxableAmount: ItemAmount): ItemAmount { public getTaxesAmount(taxableAmount: ItemAmount): ItemAmount {
return this.getAll().reduce( return this.getAll().reduce(
(total, tax) => total.add(tax.getTaxAmount(taxableAmount)), (total, tax) => total.add(taxableAmount.percentage(tax.percentage)),
ItemAmount.zero(taxableAmount.currencyCode) ItemAmount.zero(taxableAmount.currencyCode)
); );
} }
@ -26,21 +16,22 @@ export class ItemTaxes extends Collection<ItemTax> {
public getTaxesAmountByTaxCode(taxCode: string, taxableAmount: ItemAmount): ItemAmount { public getTaxesAmountByTaxCode(taxCode: string, taxableAmount: ItemAmount): ItemAmount {
const currencyCode = taxableAmount.currencyCode; const currencyCode = taxableAmount.currencyCode;
return this.filter((itemTax) => itemTax.tax.code === taxCode).reduce((totalAmount, itemTax) => { return this.filter((itemTax) => itemTax.code === taxCode).reduce((totalAmount, itemTax) => {
return itemTax.getTaxAmount(taxableAmount).add(totalAmount); return taxableAmount.percentage(itemTax.percentage).add(totalAmount);
}, ItemAmount.zero(currencyCode)); }, ItemAmount.zero(currencyCode));
} }
public getTaxesAmountByTaxes(taxableAmount: ItemAmount): { public getTaxesAmountByTaxes(taxableAmount: ItemAmount) {
return this.getAll().map((taxItem) => ({ return this.getAll().map((taxItem) => ({
tax: taxItem.tax, taxableAmount,
taxesAmount: this.getTaxesAmountByTaxCode(taxItem.tax.code, taxableAmount), tax: taxItem,
taxesAmount: this.getTaxesAmountByTaxCode(taxItem.code, taxableAmount),
})); }));
} }
public getCodesToString(): string { public getCodesToString(): string {
return this.getAll() return this.getAll()
.map((taxItem) => taxItem.tax.code) .map((taxItem) => taxItem.code)
.join(", "); .join(", ");
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -68,26 +68,26 @@ export default (database: Sequelize) => {
taxable_amount_value: { taxable_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,
}, },
taxable_amount_scale: { taxable_amount_scale: {
type: new DataTypes.SMALLINT(), type: new DataTypes.SMALLINT(),
allowNull: false, allowNull: false,
defaultValue: 2, defaultValue: 4,
}, },
taxes_amount_value: { taxes_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,
}, },
taxes_amount_scale: { taxes_amount_scale: {
type: new DataTypes.SMALLINT(), type: new DataTypes.SMALLINT(),
allowNull: false, allowNull: false,
defaultValue: 2, defaultValue: 4,
}, },
}, },
{ {

View File

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

View File

@ -67,8 +67,8 @@ export default (database: Sequelize) => {
taxable_amount_value: { taxable_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,
}, },
taxable_amount_scale: { taxable_amount_scale: {
@ -79,8 +79,8 @@ export default (database: Sequelize) => {
taxes_amount_value: { taxes_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,
}, },
taxes_amount_scale: { taxes_amount_scale: {

View File

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

View File

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

View File

@ -171,7 +171,7 @@ export class CustomerDomainMapper
// source.default_taxes is stored as a comma-separated string // source.default_taxes is stored as a comma-separated string
const defaultTaxes = new Collection<TaxCode>(); const defaultTaxes = new Collection<TaxCode>();
if (!isNullishOrEmpty(source.default_taxes)) { 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); const tax = extractOrPushError(TaxCode.create(taxCode), `default_taxes.${index}`, errors);
if (tax) { if (tax) {
defaultTaxes.add(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, {}> & {}; export type CustomerCreationAttributes = InferCreationAttributes<CustomerModel, {}> & {};
@ -13,43 +20,43 @@ export class CustomerModel extends Model<
declare id: string; declare id: string;
declare company_id: string; declare company_id: string;
declare reference: string; declare reference: CreationOptional<string | null>;
declare is_company: boolean; declare is_company: boolean;
declare name: string; declare name: string;
declare trade_name: string; declare trade_name: CreationOptional<string | null>;
declare tin: string; declare tin: CreationOptional<string | null>;
declare street: string; declare street: CreationOptional<string | null>;
declare street2: string; declare street2: CreationOptional<string | null>;
declare city: string; declare city: CreationOptional<string | null>;
declare province: string; declare province: CreationOptional<string | null>;
declare postal_code: string; declare postal_code: CreationOptional<string | null>;
declare country: string; declare country: CreationOptional<string | null>;
// Correos electrónicos // Correos electrónicos
declare email_primary: string; declare email_primary: CreationOptional<string | null>;
declare email_secondary: string; declare email_secondary: CreationOptional<string | null>;
// Teléfonos fijos // Teléfonos fijos
declare phone_primary: string; declare phone_primary: CreationOptional<string | null>;
declare phone_secondary: string; declare phone_secondary: CreationOptional<string | null>;
// Móviles // Móviles
declare mobile_primary: string; declare mobile_primary: CreationOptional<string | null>;
declare mobile_secondary: string; declare mobile_secondary: CreationOptional<string | null>;
declare fax: string; declare fax: CreationOptional<string | null>;
declare website: string; 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 status: string;
declare language_code: string; declare language_code: CreationOptional<string>;
declare currency_code: string; declare currency_code: CreationOptional<string>;
declare factuges_id: string; declare factuges_id: CreationOptional<string | null>;
static associate(database: Sequelize) {} static associate(database: Sequelize) {}
@ -187,6 +194,12 @@ export default (database: Sequelize) => {
allowNull: true, allowNull: true,
}, },
status: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: "active",
},
language_code: { language_code: {
type: DataTypes.STRING(2), type: DataTypes.STRING(2),
allowNull: false, allowNull: false,
@ -199,12 +212,6 @@ export default (database: Sequelize) => {
defaultValue: "EUR", defaultValue: "EUR",
}, },
status: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: "active",
},
factuges_id: { factuges_id: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true, allowNull: true,

View File

@ -21,7 +21,12 @@ export function maybeFromNullableString(input?: string | null): Maybe<string> {
} }
/** Maybe<T> -> null para transporte */ /** 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; if (!m || m.isNone()) return null;
const v = m.unwrap() as T; const v = m.unwrap() as T;
return map ? String(map(v)) : String(v); 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. * Ofrece métodos básicos para manipular, consultar y recorrer los elementos.
*/ */
export class Collection<T> { export class Collection<T> {
private items: T[]; protected items: T[];
private totalItems: number; protected totalItems: number;
/** /**
* Crea una nueva colección. * Crea una nueva colección.
@ -46,7 +46,7 @@ export class Collection<T> {
return this.removeByIndex(index); return this.removeByIndex(index);
} }
public removeByIndex(index: number) { removeByIndex(index: number) {
if (index !== -1) { if (index !== -1) {
this.items.splice(index, 1); this.items.splice(index, 1);
if (this.totalItems !== null) { if (this.totalItems !== null) {