import 'package:fintracker_app/features/imports/data/imports_api.dart'; import 'package:fintracker_app/features/pending/data/pending_api.dart'; import 'package:flutter_test/flutter_test.dart'; /// The exact payload from `docs/ai/import-contract.md`. If the backend renames a field this /// test is the first thing that notices. const _previewJson = { 'id': 12, 'broker': 'sber', 'filename': 'S930W42_11022026_17092026.html', 'sha256': '9f2c', 'size_bytes': 87333, 'parser_name': 'report_sber', 'parser_version': '1', 'parse_status': 'parsed', 'error': null, 'duplicate_of_id': null, 'account_external_id': 'S930W42', 'account_id': 57, 'account_name': 'Сбер ИИС', 'account_suggestions': [ {'id': 57, 'name': 'Сбер ИИС', 'broker': 'sber', 'source_id': 'S930W42'}, ], 'period_from': '2026-02-11', 'period_to': '2026-09-17', 'uploaded_at': '2026-09-18T12:30:00+03:00', 'committed_at': null, 'counts': { 'lines': 61, 'events_total': 47, 'events_new': 47, 'events_duplicate': 0, 'events_shadow': 0, 'events_pending': 3, 'by_kind': {'buy': 22, 'sell': 2, 'deposit': 8, 'commission': 15}, }, 'pending_instruments': [ { 'id': 4, 'source': 'report_sber', 'source_key': 'ISIN:RU000A1035S8', 'isin': 'RU000A1035S8', 'ticker': 'STME', 'board': null, 'name': 'Первая-ВечныйПортф БПИФ', 'currency': 'RUB', 'asset_class_hint': 'fund', 'occurrences': 3, 'sample_quantity': '439', 'sample_price': '4.48', 'status': 'pending', 'instrument_id': null, }, ], 'reconciliation': { 'as_of': '2026-09-17', 'positions': [ { 'instrument_id': 88, 'instrument_name': 'Аэрофлот', 'ticker': 'AFLT', 'isin': 'RU0009062285', 'qty_report': '130', 'qty_derived': '130', 'qty_delta': '0', 'matches': true, }, ], 'cash': [ { 'currency': 'RUB', 'balance_report': '3171.34', 'balance_derived': '3171.34', 'delta': '0', 'matches': true, }, ], 'matches': true, }, 'warnings': ['Раздел «Купонный доход» отсутствует в файле'], 'sample_events': [ { 'line_no': 3, 'kind': 'buy', 'trade_date': '2026-02-24', 'settle_date': '2026-02-25', 'instrument_key': 'ISIN:RU000A1035S8', 'instrument_name': 'Первая-ВечныйПортф БПИФ', 'instrument_id': null, 'quantity': '439', 'price': '4.48', 'amount': '-1966.72', 'currency': 'RUB', 'fee': '0.39', 'trade_no': '15678045077', 'dedupe_key': 'a1b2', 'is_duplicate': false, 'description': 'Покупка', }, ], }; void main() { test('parses the contract ImportPreview payload', () { final p = ImportPreview.fromJson(_previewJson); expect(p.id, 12); expect(p.broker, 'sber'); expect(p.parseStatus, 'parsed'); expect(p.accountId, 57); expect(p.periodFrom, DateTime.parse('2026-02-11')); expect(p.counts.eventsPending, 3); expect(p.counts.byKind['buy'], 22); expect(p.accountSuggestions.single.sourceId, 'S930W42'); expect(p.warnings, hasLength(1)); expect(p.canCommit, isTrue); // Money and quantities stay strings end to end — never parsed into a double here. final sample = p.sampleEvents.single; expect(sample.amount, '-1966.72'); expect(sample.price, '4.48'); expect(sample.isDuplicate, isFalse); final recon = p.reconciliation!; expect(recon.matches, isTrue); expect(recon.positions.single.qtyDelta, '0'); expect(recon.cash.single.balanceReport, '3171.34'); expect(p.pendingInstruments.single.ticker, 'STME'); }); test('an ImportSummary without the preview-only sections still parses', () { final json = Map.from(_previewJson) ..remove('sample_events') ..remove('pending_instruments') ..remove('reconciliation'); final p = ImportPreview.fromJson(json); expect(p.sampleEvents, isEmpty); expect(p.pendingInstruments, isEmpty); expect(p.reconciliation, isNull); }); test('commit is blocked until an account is known', () { final json = Map.from(_previewJson)..['account_id'] = null; expect(ImportPreview.fromJson(json).canCommit, isFalse); final failed = Map.from(_previewJson) ..['parse_status'] = 'failed'; expect(ImportPreview.fromJson(failed).canCommit, isFalse); expect(ImportPreview.fromJson(failed).canDelete, isTrue); final committed = Map.from(_previewJson) ..['parse_status'] = 'committed'; expect(ImportPreview.fromJson(committed).canDelete, isFalse); }); test( 'parses PendingResolveResult and builds the create body as plain strings', () { final r = PendingResolveResult.fromJson(const { 'id': 4, 'status': 'resolved', 'instrument_id': 88, 'events_bound': 3, 'alias_created': true, 'metrics_refreshed': true, }); expect(r.eventsBound, 3); expect(r.instrumentId, 88); final body = const NewInstrument( assetClass: 'index', name: 'Индекс МосБиржи', currency: 'RUB', isin: '', ticker: 'IMOEX', lot: 1, ).toJson(); expect(body['asset_class'], 'index'); expect( body.containsKey('isin'), isFalse, reason: 'empty optionals are omitted', ); expect(body['ticker'], 'IMOEX'); }, ); }