A sale is a distinct model from a gift or a Plantare (reproduction
commitment): a recorded seed sale or purchase with an optional price in
ANY currency — €, Ğ1, time, or none yet. Mirrors the Plantare feature.
- schema v10: Sales table (SyncColumns), guarded createTable migration,
schema dump + generated schema_v10 for the migration round-trip test
- enum SaleDirection { iSold, iBought }
- VarietyRepository: create/watch/watchForVariety/delete + backup
export/exportForSync/import (LWW-by-HLC, tombstones)
- InventorySnapshot.sales + JSON codec encode/decode (round-trips amount,
currency, counterparty)
- UI: SalesScreen (/sales), sale sheet, drawer entry, variety-detail
action beside 'add Plantare'; money hides a trailing .0
- i18n sale block + menu.sales in en/es/pt/ast
- tests: sales repo test (5) incl. Ğ1/price-less/backup round-trip;
Sales screen added to the small-screen overflow guard
Also harden the overflow guard: swallow the headless engine's image
resource service PNG-decode errors (unrelated to layout) while still
failing on real RenderFlex overflows, so home/about stop reporting
false failures.
71 lines
2.5 KiB
Dart
71 lines
2.5 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:tane/db/database.dart';
|
|
import 'package:tane/db/enums.dart';
|
|
|
|
import '../support/test_support.dart';
|
|
|
|
/// A Sale is a recorded seed sale/purchase — a model separate from a gift or a
|
|
/// Plantare, with a price in any currency.
|
|
void main() {
|
|
late AppDatabase db;
|
|
setUp(() => db = newTestDatabase());
|
|
tearDown(() => db.close());
|
|
|
|
test('records a sale with amount + currency and lists it', () async {
|
|
final repo = newTestRepository(db);
|
|
final vid = await repo.addQuickVariety(label: 'Tomate rosa');
|
|
await repo.createSale(
|
|
direction: SaleDirection.iSold,
|
|
varietyId: vid,
|
|
counterparty: 'Ana',
|
|
amount: 5,
|
|
currency: '€',
|
|
);
|
|
|
|
final all = await repo.watchSales().first;
|
|
expect(all.single.direction, SaleDirection.iSold);
|
|
expect(all.single.amount, 5);
|
|
expect(all.single.currency, '€');
|
|
expect(all.single.counterparty, 'Ana');
|
|
expect(await repo.watchSalesForVariety(vid).first, hasLength(1));
|
|
});
|
|
|
|
test('supports any currency (Ğ1, time…) and a price-less record', () async {
|
|
final repo = newTestRepository(db);
|
|
await repo.createSale(direction: SaleDirection.iBought, amount: 12, currency: 'Ğ1');
|
|
await repo.createSale(direction: SaleDirection.iSold); // no figure yet
|
|
final all = await repo.watchSales().first;
|
|
expect(all, hasLength(2));
|
|
expect(all.any((s) => s.currency == 'Ğ1' && s.amount == 12), isTrue);
|
|
expect(all.any((s) => s.amount == null), isTrue);
|
|
});
|
|
|
|
test('deleting a sale tombstones it', () async {
|
|
final repo = newTestRepository(db);
|
|
final id = await repo.createSale(direction: SaleDirection.iSold, amount: 3);
|
|
expect(await repo.watchSales().first, hasLength(1));
|
|
await repo.deleteSale(id);
|
|
expect(await repo.watchSales().first, isEmpty);
|
|
});
|
|
|
|
test('sales survive a backup round-trip', () async {
|
|
final repoA = newTestRepository(db);
|
|
await repoA.createSale(
|
|
direction: SaleDirection.iSold,
|
|
counterparty: 'Feria de la comarca',
|
|
amount: 2.5,
|
|
currency: '€',
|
|
);
|
|
final snapshot = await repoA.exportInventory();
|
|
expect(snapshot.sales, hasLength(1));
|
|
|
|
final dbB = newTestDatabase();
|
|
addTearDown(dbB.close);
|
|
final repoB = newTestRepository(dbB);
|
|
await repoB.importInventory(snapshot);
|
|
final onB = await repoB.watchSales().first;
|
|
expect(onB.single.amount, 2.5);
|
|
expect(onB.single.currency, '€');
|
|
expect(onB.single.counterparty, 'Feria de la comarca');
|
|
});
|
|
}
|