Files
einvoice/test/test.decimal.ts

257 lines
8.4 KiB
TypeScript
Raw Normal View History

import { expect, tap } from '@git.zone/tstest/tapbundle';
import { Decimal, decimal, RoundingMode } from '../ts/formats/utils/decimal.js';
tap.test('Decimal - basic construction', async () => {
// From string
const d1 = new Decimal('123.456');
expect(d1.toString()).toEqual('123.456');
// From number
const d2 = new Decimal(123.456);
expect(d2.toString()).toEqual('123.456');
// From bigint
const d3 = new Decimal(123n);
expect(d3.toString()).toEqual('123');
// From another Decimal
const d4 = new Decimal(d1);
expect(d4.toString()).toEqual('123.456');
// Negative values
const d5 = new Decimal('-123.456');
expect(d5.toString()).toEqual('-123.456');
});
tap.test('Decimal - arithmetic operations', async () => {
const a = new Decimal('10.50');
const b = new Decimal('3.25');
// Addition
expect(a.add(b).toString()).toEqual('13.75');
// Subtraction
expect(a.subtract(b).toString()).toEqual('7.25');
// Multiplication
expect(a.multiply(b).toString()).toEqual('34.125');
// Division
expect(a.divide(b).toString()).toEqual('3.2307692307');
// Percentage
const amount = new Decimal('100');
const rate = new Decimal('19');
expect(amount.percentage(rate).toString()).toEqual('19');
});
tap.test('Decimal - rounding modes', async () => {
// HALF_UP (default)
expect(new Decimal('2.5').round(0, 'HALF_UP').toString()).toEqual('3');
expect(new Decimal('2.4').round(0, 'HALF_UP').toString()).toEqual('2');
expect(new Decimal('-2.5').round(0, 'HALF_UP').toString()).toEqual('-3');
// HALF_DOWN
expect(new Decimal('2.5').round(0, 'HALF_DOWN').toString()).toEqual('2');
expect(new Decimal('2.6').round(0, 'HALF_DOWN').toString()).toEqual('3');
expect(new Decimal('-2.5').round(0, 'HALF_DOWN').toString()).toEqual('-2');
// HALF_EVEN (Banker's rounding)
expect(new Decimal('2.5').round(0, 'HALF_EVEN').toString()).toEqual('2');
expect(new Decimal('3.5').round(0, 'HALF_EVEN').toString()).toEqual('4');
expect(new Decimal('2.4').round(0, 'HALF_EVEN').toString()).toEqual('2');
expect(new Decimal('2.6').round(0, 'HALF_EVEN').toString()).toEqual('3');
// UP (away from zero)
expect(new Decimal('2.1').round(0, 'UP').toString()).toEqual('3');
expect(new Decimal('-2.1').round(0, 'UP').toString()).toEqual('-3');
// DOWN (toward zero)
expect(new Decimal('2.9').round(0, 'DOWN').toString()).toEqual('2');
expect(new Decimal('-2.9').round(0, 'DOWN').toString()).toEqual('-2');
// CEILING (toward positive infinity)
expect(new Decimal('2.1').round(0, 'CEILING').toString()).toEqual('3');
expect(new Decimal('-2.9').round(0, 'CEILING').toString()).toEqual('-2');
// FLOOR (toward negative infinity)
expect(new Decimal('2.9').round(0, 'FLOOR').toString()).toEqual('2');
expect(new Decimal('-2.1').round(0, 'FLOOR').toString()).toEqual('-3');
});
tap.test('Decimal - EN16931 calculation scenarios', async () => {
// Line item calculation
const quantity = new Decimal('3');
const unitPrice = new Decimal('33.333333');
const lineTotal = quantity.multiply(unitPrice);
expect(lineTotal.round(2).toString()).toEqual('100');
// VAT calculation
const netAmount = new Decimal('100');
const vatRate = new Decimal('19');
const vatAmount = netAmount.percentage(vatRate);
expect(vatAmount.toString()).toEqual('19');
// Total with VAT
const grossAmount = netAmount.add(vatAmount);
expect(grossAmount.toString()).toEqual('119');
// Complex calculation with allowances
const lineExtension = new Decimal('150.00');
const allowance = new Decimal('10.00');
const charge = new Decimal('5.00');
const taxExclusive = lineExtension.subtract(allowance).add(charge);
expect(taxExclusive.toString()).toEqual('145');
const vat = taxExclusive.percentage(new Decimal('19'));
expect(vat.round(2).toString()).toEqual('27.55');
const total = taxExclusive.add(vat);
expect(total.round(2).toString()).toEqual('172.55');
});
tap.test('Decimal - comparisons', async () => {
const a = new Decimal('10.50');
const b = new Decimal('10.50');
const c = new Decimal('10.51');
// Equality
expect(a.equals(b)).toBeTrue();
expect(a.equals(c)).toBeFalse();
// With tolerance
expect(a.equals(c, '0.01')).toBeTrue();
expect(a.equals(c, '0.005')).toBeFalse();
// Comparisons
expect(a.lessThan(c)).toBeTrue();
expect(c.greaterThan(a)).toBeTrue();
expect(a.lessThanOrEqual(b)).toBeTrue();
expect(a.greaterThanOrEqual(b)).toBeTrue();
});
tap.test('Decimal - edge cases', async () => {
// Very small numbers
const tiny = new Decimal('0.0000000001');
expect(tiny.multiply(new Decimal('1000000000')).toString()).toEqual('0.1');
// Very large numbers
const huge = new Decimal('999999999999999999');
expect(huge.add(new Decimal('1')).toString()).toEqual('1000000000000000000');
// Division by zero
const zero = new Decimal('0');
const one = new Decimal('1');
let errorThrown = false;
try {
one.divide(zero);
} catch (e) {
errorThrown = true;
expect(e.message).toEqual('Division by zero');
}
expect(errorThrown).toBeTrue();
// Zero operations
expect(zero.add(one).toString()).toEqual('1');
expect(zero.multiply(one).toString()).toEqual('0');
expect(zero.isZero()).toBeTrue();
expect(one.isZero()).toBeFalse();
});
tap.test('Decimal - currency calculations with different minor units', async () => {
// EUR (2 decimal places)
const eurAmount = new Decimal('100.00');
const eurVat = eurAmount.percentage(new Decimal('19'));
expect(eurVat.round(2).toString()).toEqual('19');
// JPY (0 decimal places)
const jpyAmount = new Decimal('1000');
const jpyTax = jpyAmount.percentage(new Decimal('10'));
expect(jpyTax.round(0).toString()).toEqual('100');
// KWD (3 decimal places)
const kwdAmount = new Decimal('100.000');
const kwdTax = kwdAmount.percentage(new Decimal('5'));
expect(kwdTax.round(3).toString()).toEqual('5');
// BTC (8 decimal places for satoshis)
const btcAmount = new Decimal('0.00100000');
const btcFee = btcAmount.percentage(new Decimal('0.1'));
expect(btcFee.round(8).toString()).toEqual('0.000001');
});
tap.test('Decimal - static methods', async () => {
// Sum
const values = ['10.50', '20.25', '30.75'];
const sum = Decimal.sum(values);
expect(sum.toString()).toEqual('61.5');
// Min
const min = Decimal.min('10.50', '20.25', '5.75');
expect(min.toString()).toEqual('5.75');
// Max
const max = Decimal.max('10.50', '20.25', '5.75');
expect(max.toString()).toEqual('20.25');
// From percentage
const rate = Decimal.fromPercentage('19%');
expect(rate.toString()).toEqual('0.19');
});
tap.test('Decimal - formatting', async () => {
const value = new Decimal('1234.567890');
// Fixed decimal places
expect(value.toFixed(2)).toEqual('1234.57');
expect(value.toFixed(0)).toEqual('1235');
expect(value.toFixed(4)).toEqual('1234.5679');
// toString with decimal places
expect(value.toString(2)).toEqual('1234.56');
expect(value.toString(6)).toEqual('1234.567890');
// Automatic trailing zero removal
const rounded = new Decimal('100.00');
expect(rounded.toString()).toEqual('100');
expect(rounded.toFixed(2)).toEqual('100.00');
});
tap.test('Decimal - real-world invoice calculation', async () => {
// Invoice with multiple lines and VAT rates
const lines = [
{ quantity: '2', unitPrice: '50.00', vatRate: '19' },
{ quantity: '3', unitPrice: '33.33', vatRate: '19' },
{ quantity: '1', unitPrice: '100.00', vatRate: '7' }
];
let totalNet = Decimal.ZERO;
let totalVat19 = Decimal.ZERO;
let totalVat7 = Decimal.ZERO;
for (const line of lines) {
const quantity = new Decimal(line.quantity);
const unitPrice = new Decimal(line.unitPrice);
const lineNet = quantity.multiply(unitPrice);
totalNet = totalNet.add(lineNet);
const vatAmount = lineNet.percentage(new Decimal(line.vatRate));
if (line.vatRate === '19') {
totalVat19 = totalVat19.add(vatAmount);
} else {
totalVat7 = totalVat7.add(vatAmount);
}
}
expect(totalNet.round(2).toString()).toEqual('299.99');
expect(totalVat19.round(2).toString()).toEqual('38');
expect(totalVat7.round(2).toString()).toEqual('7');
const totalVat = totalVat19.add(totalVat7);
const totalGross = totalNet.add(totalVat);
expect(totalVat.round(2).toString()).toEqual('45');
expect(totalGross.round(2).toString()).toEqual('344.99');
});
export default tap.start();