feat(app): scan a printed seed label back into its record

Closes the physical↔digital loop the labels opened: the QR on a printed
envelope (tane://seed, already encoded by seed_label_codec) can now be
scanned from the inventory bar. A known label opens its variety; an
unknown one asks before adding anything, then creates the variety (species
linked by exact scientific name when bundled) with a lot carrying the
label's year and origin. Scanner is pure ZXing (zxing_barcode_scanner,
MIT) — no ML Kit, no Play Services, F-Droid-safe, the stack Ğ1nkgo ships.
Mobile-only; other platforms don't show the button (OCR precedent).
The post-scan flow is a plain function (handleScannedPayload) so it's
widget-tested without a camera.
This commit is contained in:
vjrj 2026-07-18 12:26:27 +02:00
parent bb5b3d4a43
commit f1aa8f8e66
18 changed files with 1130 additions and 42 deletions

View file

@ -0,0 +1,109 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:tane/data/export_import/seed_label_codec.dart';
import 'package:tane/data/species_repository.dart';
import 'package:tane/db/database.dart';
import 'package:tane/services/seed_label_scan.dart';
import '../support/test_support.dart';
void main() {
late AppDatabase db;
late var repo = newTestRepository(db);
late SpeciesRepository species;
setUp(() {
db = newTestDatabase();
repo = newTestRepository(db);
species = newTestSpeciesRepository(db);
});
tearDown(() async => db.close());
group('resolveScannedLabel', () {
test('rejects anything that is not a seed label', () async {
expect(
await resolveScannedLabel(repo, 'https://example.org'),
const SeedScanResult.notALabel(),
);
expect(
await resolveScannedLabel(repo, 'gibberish'),
const SeedScanResult.notALabel(),
);
});
test('finds the existing variety by its label, case-insensitively',
() async {
final id = await repo.addQuickVariety(label: 'Tomate Rosa');
final raw = SeedLabelCodec.encode(
const SeedLabelData(varietyLabel: 'tomate rosa'),
);
expect(
await resolveScannedLabel(repo, raw),
SeedScanResult.match(id),
);
});
test('an unknown label comes back with its data for the add prompt',
() async {
final raw = SeedLabelCodec.encode(
const SeedLabelData(
varietyLabel: 'Acelga de Perales',
scientificName: 'Beta vulgaris',
year: 2024,
origin: 'María · Aiako',
),
);
final result = await resolveScannedLabel(repo, raw);
expect(result.data?.varietyLabel, 'Acelga de Perales');
expect(result.data?.year, 2024);
});
});
group('importScannedLabel', () {
test('creates the variety with a lot carrying year and origin', () async {
const data = SeedLabelData(
varietyLabel: 'Acelga de Perales',
year: 2024,
origin: 'María · Aiako',
);
final varietyId = await importScannedLabel(
repository: repo,
species: species,
data: data,
);
final varieties = await db.select(db.varieties).get();
expect(varieties.single.id, varietyId);
expect(varieties.single.label, 'Acelga de Perales');
final lots = await db.select(db.lots).get();
expect(lots.single.varietyId, varietyId);
expect(lots.single.harvestYear, 2024);
expect(lots.single.originName, 'María · Aiako');
});
test('links the species by exact scientific name when bundled', () async {
await species.seedBundled(const [
SpeciesSeed(
scientificName: 'Beta vulgaris',
family: 'Amaranthaceae',
commonNames: {
'es': ['Acelga'],
},
),
]);
const data = SeedLabelData(
varietyLabel: 'Una acelga cualquiera',
scientificName: 'beta vulgaris',
);
final varietyId = await importScannedLabel(
repository: repo,
species: species,
data: data,
);
final variety = await (db.select(
db.varieties,
)..where((v) => v.id.equals(varietyId))).getSingle();
final bundled = await db.select(db.species).get();
expect(variety.speciesId, bundled.single.id);
});
});
}