test(invoice): add money, invoice and rendering tests
This commit is contained in:
1 parent
b7f56e623d
commit
5a83c62312
3 files changed
+215
No files matched your search
@@ -0,0 +1,52 @@
|
|||||||
|
import 'package:cellar/cellar.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
bool isAscii(String text) => text.runes.every((rune) => rune < 128);
|
||||||
|
|
||||||
|
Invoice invoice() => Invoice(
|
||||||
|
number: 'FV/2026/07/001',
|
||||||
|
issueDate: DateTime(2026, 8, 28),
|
||||||
|
dueDate: DateTime(2026, 9, 11),
|
||||||
|
seller: const Party(name: 'Vulp Sp. z o.o.', address: 'Warszawa', taxId: 'PL1234567890'),
|
||||||
|
buyer: const Party(name: 'Acme <Corp>', address: 'Krakow'),
|
||||||
|
currency: Currency.pln,
|
||||||
|
paymentTerms: '14 days',
|
||||||
|
lines: [
|
||||||
|
InvoiceLine(
|
||||||
|
description: 'Consulting',
|
||||||
|
quantity: 12,
|
||||||
|
unitPrice: Money.of(150, Currency.pln),
|
||||||
|
vat: VatRate.standard,
|
||||||
|
),
|
||||||
|
InvoiceLine(
|
||||||
|
description: 'Manual',
|
||||||
|
quantity: 3,
|
||||||
|
unitPrice: Money.of(20, Currency.pln),
|
||||||
|
vat: VatRate.reduced5,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('invoice text render', () {
|
||||||
|
test('shows the totals and stays ASCII', () {
|
||||||
|
final text = renderInvoiceText(invoice());
|
||||||
|
expect(text.contains('INVOICE FV/2026/07/001'), isTrue);
|
||||||
|
expect(text.contains('Subtotal (net): 1 860.00 zl'), isTrue);
|
||||||
|
expect(text.contains('VAT total: 417.00 zl'), isTrue);
|
||||||
|
expect(text.contains('TOTAL: 2 277.00 zl'), isTrue);
|
||||||
|
expect(isAscii(text), isTrue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('invoice HTML render', () {
|
||||||
|
test('is a full ASCII document with the total', () {
|
||||||
|
final html = renderInvoiceHtml(invoice());
|
||||||
|
expect(html.contains('<!DOCTYPE html>'), isTrue);
|
||||||
|
expect(html.contains('</html>'), isTrue);
|
||||||
|
expect(html.contains('2 277.00 zl'), isTrue);
|
||||||
|
expect(html.contains('Acme <Corp>'), isTrue);
|
||||||
|
expect(isAscii(html), isTrue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import 'package:cellar/cellar.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
Invoice sampleInvoice() => Invoice(
|
||||||
|
number: 'FV/2026/001',
|
||||||
|
issueDate: DateTime(2026, 8, 28),
|
||||||
|
dueDate: DateTime(2026, 9, 11),
|
||||||
|
seller: const Party(name: 'Vulp Sp. z o.o.', taxId: 'PL1234567890'),
|
||||||
|
buyer: const Party(name: 'Acme Corp', taxId: 'PL9876543210'),
|
||||||
|
currency: Currency.pln,
|
||||||
|
lines: [
|
||||||
|
InvoiceLine(
|
||||||
|
description: 'Consulting',
|
||||||
|
quantity: 10,
|
||||||
|
unitPrice: Money.of(100, Currency.pln),
|
||||||
|
vat: VatRate.standard,
|
||||||
|
),
|
||||||
|
InvoiceLine(
|
||||||
|
description: 'License',
|
||||||
|
quantity: 2,
|
||||||
|
unitPrice: Money.of(500, Currency.pln),
|
||||||
|
vat: VatRate.standard,
|
||||||
|
discountRate: 0.1,
|
||||||
|
),
|
||||||
|
InvoiceLine(
|
||||||
|
description: 'Books',
|
||||||
|
quantity: 4,
|
||||||
|
unitPrice: Money.of(25, Currency.pln),
|
||||||
|
vat: VatRate.reduced5,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('invoice line computations', () {
|
||||||
|
test('discount reduces the net amount', () {
|
||||||
|
final line = InvoiceLine(
|
||||||
|
description: 'License',
|
||||||
|
quantity: 2,
|
||||||
|
unitPrice: Money.of(500, Currency.pln),
|
||||||
|
vat: VatRate.standard,
|
||||||
|
discountRate: 0.1,
|
||||||
|
);
|
||||||
|
expect(line.grossBeforeDiscount, Money.of(1000, Currency.pln));
|
||||||
|
expect(line.discountAmount, Money.of(100, Currency.pln));
|
||||||
|
expect(line.net, Money.of(900, Currency.pln));
|
||||||
|
expect(line.vatAmount, Money.of(207, Currency.pln));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('invoice totals', () {
|
||||||
|
test('VAT summary groups by rate', () {
|
||||||
|
final summary = sampleInvoice().vatSummary();
|
||||||
|
expect(summary.length, 2);
|
||||||
|
expect(summary[0].rate, VatRate.standard);
|
||||||
|
expect(summary[0].net, Money.of(1900, Currency.pln));
|
||||||
|
expect(summary[0].vat, Money.of(437, Currency.pln));
|
||||||
|
expect(summary[1].rate, VatRate.reduced5);
|
||||||
|
expect(summary[1].net, Money.of(100, Currency.pln));
|
||||||
|
expect(summary[1].vat, Money.of(5, Currency.pln));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('totals sum the summary', () {
|
||||||
|
final invoice = sampleInvoice();
|
||||||
|
expect(invoice.totalNet, Money.of(2000, Currency.pln));
|
||||||
|
expect(invoice.totalVat, Money.of(442, Currency.pln));
|
||||||
|
expect(invoice.totalGross, Money.of(2442, Currency.pln));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('invoice numbering', () {
|
||||||
|
test('sequence pads and advances', () {
|
||||||
|
final sequence = InvoiceSequence('FV/{seq:04}/{year}');
|
||||||
|
expect(sequence.next(DateTime(2026, 3, 1)), 'FV/0001/2026');
|
||||||
|
expect(sequence.next(DateTime(2026, 3, 1)), 'FV/0002/2026');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('month and day placeholders', () {
|
||||||
|
final sequence = InvoiceSequence('{year}/{month:02}/{seq:03}');
|
||||||
|
expect(sequence.format(DateTime(2026, 3, 5), 7), '2026/03/007');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('invoice JSON', () {
|
||||||
|
test('round trips through JSON', () {
|
||||||
|
final original = sampleInvoice();
|
||||||
|
final decoded = invoiceFromJsonString(encodeInvoiceJson(original));
|
||||||
|
expect(decoded.totalNet, original.totalNet);
|
||||||
|
expect(decoded.totalVat, original.totalVat);
|
||||||
|
expect(decoded.totalGross, original.totalGross);
|
||||||
|
expect(decoded.lines.length, 3);
|
||||||
|
expect(decoded.number, original.number);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('multicurrency reporting equivalent', () {
|
||||||
|
final invoice = Invoice(
|
||||||
|
number: 'FV/EU/1',
|
||||||
|
issueDate: DateTime(2026, 8, 28),
|
||||||
|
seller: const Party(name: 'Seller'),
|
||||||
|
buyer: const Party(name: 'Buyer'),
|
||||||
|
currency: Currency.eur,
|
||||||
|
reportingCurrency: Currency.pln,
|
||||||
|
reportingRate: 4.32,
|
||||||
|
lines: [
|
||||||
|
InvoiceLine(
|
||||||
|
description: 'Service',
|
||||||
|
quantity: 1,
|
||||||
|
unitPrice: Money.of(1000, Currency.eur),
|
||||||
|
vat: VatRate.standard,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
expect(invoice.totalGross, Money.of(1230, Currency.eur));
|
||||||
|
expect(invoice.reportingTotalGross, Money.of(5313.6, Currency.pln));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import 'package:cellar/cellar.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('money', () {
|
||||||
|
test('constructs minor units with rounding', () {
|
||||||
|
expect(Money.of(19.99, Currency.pln).minor, 1999);
|
||||||
|
expect(Money.of(19.999, Currency.pln).minor, 2000);
|
||||||
|
expect(Money.parse('19.99', Currency.pln).minor, 1999);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('scaling multiplies exactly with rounding', () {
|
||||||
|
expect(Money.of(10, Currency.pln).scaled(3), Money.of(30, Currency.pln));
|
||||||
|
expect(Money.of(0.10, Currency.pln).scaled(0.23), Money.of(0.02, Currency.pln));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('addition and subtraction', () {
|
||||||
|
final sum = Money.of(19.99, Currency.pln) + Money.of(0.01, Currency.pln);
|
||||||
|
expect(sum, Money.of(20.00, Currency.pln));
|
||||||
|
final diff = Money.of(20.00, Currency.pln) - Money.of(5.50, Currency.pln);
|
||||||
|
expect(diff, Money.of(14.50, Currency.pln));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('formatting groups and places the symbol', () {
|
||||||
|
expect(Money.of(1234.5, Currency.pln).format(), '1 234.50 zl');
|
||||||
|
expect(Money.of(5, Currency.usd).format(), r'$5.00');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('currency mismatch is rejected', () {
|
||||||
|
expect(
|
||||||
|
() => Money.of(1, Currency.pln) + Money.of(1, Currency.eur),
|
||||||
|
throwsArgumentError,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('conversion applies an exchange rate', () {
|
||||||
|
final converted = Money.of(100, Currency.pln).convertedTo(Currency.eur, 0.25);
|
||||||
|
expect(converted, Money.of(25, Currency.eur));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('known currencies resolve by code', () {
|
||||||
|
expect(Currency.byCode('usd').symbolBefore, isTrue);
|
||||||
|
expect(Currency.byCode('EUR').code, 'EUR');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in new issue
Block a user