tane/apps/app_seeds/test/data/handover_test.dart
vjrj 665e3e9452 feat(handover): recordHandover — one transaction for the moment seeds change hands
Returns to the original model (data-model §2.8: a closed deal produces a
Movement and, optionally, a Plantare): the given/received Movement plus,
when they came with it, the Sale (informational payment note) and the
Plantare (return promise), all sharing the counterparty. The Movement
carries the promise via the plantare_id column the schema had reserved.

'Gave all' (one tap, the natural path for shrubs/whole plants) moves the
lot's whole quantity, leaves an empty jar (0, same unit), clears
abundance, and returns the lot to private — withdrawing its market offer
on the next publish cycle, price cleared with it.

New enum HandoverDirection; tests in test/data/handover_test.dart.
2026-07-11 08:00:08 +02:00

181 lines
6.1 KiB
Dart

import 'package:commons_core/commons_core.dart';
import 'package:drift/drift.dart' hide isNull;
import 'package:flutter_test/flutter_test.dart';
import 'package:tane/data/variety_repository.dart';
import 'package:tane/db/database.dart';
import 'package:tane/db/enums.dart';
import '../support/test_support.dart';
void main() {
late AppDatabase db;
late VarietyRepository repo;
late String varietyId;
setUp(() async {
db = newTestDatabase();
repo = newTestRepository(db);
varietyId = await repo.addQuickVariety(label: 'Tomate rosa');
});
tearDown(() => db.close());
Future<Movement> movementById(String id) =>
(db.select(db.movements)..where((m) => m.id.equals(id))).getSingle();
Future<Lot> lotById(String id) =>
(db.select(db.lots)..where((l) => l.id.equals(id))).getSingle();
test('a plain gift records just the given movement', () async {
final lotId = await repo.addLot(varietyId: varietyId);
final result = await repo.recordHandover(
lotId: lotId,
direction: HandoverDirection.iGave,
counterparty: 'María',
);
expect(result.saleId, isNull);
expect(result.plantareId, isNull);
final movement = await movementById(result.movementId);
expect(movement.type, MovementType.given);
expect(movement.lotId, lotId);
expect(movement.notes, 'María');
expect(movement.plantareId, isNull);
expect(await repo.watchSales().first, isEmpty);
expect(await repo.watchPlantares().first, isEmpty);
});
test(
'giving all depletes the lot, privatizes it and drops its price',
() async {
final lotId = await repo.addLot(
varietyId: varietyId,
quantity: const Quantity(kind: QuantityKind.grams, count: 40),
abundance: Abundance.plentyToShare,
offerStatus: OfferStatus.sell,
priceAmount: 3,
priceCurrency: '',
);
final result = await repo.recordHandover(
lotId: lotId,
direction: HandoverDirection.iGave,
gaveAll: true,
);
// The movement carries what actually moved: the lot's whole quantity.
final movement = await movementById(result.movementId);
expect(movement.quantityKind, QuantityKind.grams.name);
expect(movement.quantityPrecise, 40);
// The lot stays as an empty jar, withdrawn from the market.
final lot = await lotById(lotId);
expect(lot.quantityPrecise, 0);
expect(lot.quantityKind, QuantityKind.grams.name);
expect(lot.abundance, isNull);
expect(lot.offerStatus, OfferStatus.private);
expect(lot.priceAmount, isNull);
expect(lot.priceCurrency, isNull);
expect(
(await repo.shareableLots()).where((l) => l.lotId == lotId),
isEmpty,
);
},
);
test('giving part keeps the lot open and offered', () async {
final lotId = await repo.addLot(
varietyId: varietyId,
quantity: const Quantity(kind: QuantityKind.grams, count: 40),
offerStatus: OfferStatus.shared,
);
final result = await repo.recordHandover(
lotId: lotId,
direction: HandoverDirection.iGave,
quantity: const Quantity(kind: QuantityKind.grams, count: 10),
);
final movement = await movementById(result.movementId);
expect(movement.quantityPrecise, 10);
final lot = await lotById(lotId);
expect(lot.quantityPrecise, 40); // untouched: the log holds the history
expect(lot.offerStatus, OfferStatus.shared);
expect(
(await repo.shareableLots()).where((l) => l.lotId == lotId),
hasLength(1),
);
});
test('a payment on a give creates the matching sale record', () async {
final lotId = await repo.addLot(varietyId: varietyId);
final result = await repo.recordHandover(
lotId: lotId,
direction: HandoverDirection.iGave,
counterparty: 'María',
withPayment: true,
paymentAmount: 2.5,
paymentCurrency: 'Ğ1',
);
final sales = await repo.watchSales().first;
expect(sales, hasLength(1));
expect(sales.single.id, result.saleId);
expect(sales.single.direction, SaleDirection.iSold);
expect(sales.single.varietyId, varietyId);
expect(sales.single.counterparty, 'María');
expect(sales.single.amount, 2.5);
expect(sales.single.currency, 'Ğ1');
});
test('a promise on a give creates a plantare owed to me, linked from the '
'movement', () async {
final lotId = await repo.addLot(varietyId: varietyId);
final result = await repo.recordHandover(
lotId: lotId,
direction: HandoverDirection.iGave,
counterparty: 'María',
withPromise: true,
promiseOwedDescription: 'un puñado la próxima temporada',
);
final plantares = await repo.watchPlantares().first;
expect(plantares, hasLength(1));
expect(plantares.single.id, result.plantareId);
expect(plantares.single.direction, PlantareDirection.owedToMe);
expect(plantares.single.varietyId, varietyId);
expect(plantares.single.counterparty, 'María');
expect(plantares.single.owedDescription, 'un puñado la próxima temporada');
// The link the model reserved: the movement carries the plantare id.
final movement = await movementById(result.movementId);
expect(movement.plantareId, result.plantareId);
});
test('receiving with payment and promise mirrors the directions', () async {
final lotId = await repo.addLot(
varietyId: varietyId,
quantity: const Quantity(kind: QuantityKind.grams, count: 40),
);
final result = await repo.recordHandover(
lotId: lotId,
direction: HandoverDirection.iReceived,
// Meaningless on a receive; must not deplete anything.
gaveAll: true,
withPayment: true,
paymentAmount: 5,
paymentCurrency: '',
withPromise: true,
);
final movement = await movementById(result.movementId);
expect(movement.type, MovementType.received);
final lot = await lotById(lotId);
expect(lot.quantityPrecise, 40);
final sales = await repo.watchSales().first;
expect(sales.single.direction, SaleDirection.iBought);
final plantares = await repo.watchPlantares().first;
expect(plantares.single.direction, PlantareDirection.iReturn);
});
}