diff --git a/test/invoice_render_test.dart b/test/invoice_render_test.dart new file mode 100644 index 0000000..0ca6df3 --- /dev/null +++ b/test/invoice_render_test.dart @@ -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 ', 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(''), isTrue); + expect(html.contains(''), isTrue); + expect(html.contains('2 277.00 zl'), isTrue); + expect(html.contains('Acme <Corp>'), isTrue); + expect(isAscii(html), isTrue); + }); + }); +} diff --git a/test/invoice_test.dart b/test/invoice_test.dart new file mode 100644 index 0000000..c9aa79f --- /dev/null +++ b/test/invoice_test.dart @@ -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)); + }); + }); +} diff --git a/test/money_test.dart b/test/money_test.dart new file mode 100644 index 0000000..c45b05d --- /dev/null +++ b/test/money_test.dart @@ -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'); + }); + }); +}