/// Domain models for the personal ingtrak bank-data API. library; import 'transaction_meta.dart' show merchantKeyFromDescription; /// Result of `POST /api/connect`. class ConnectStart { const ConnectStart({required this.authUrl, required this.state}); final String authUrl; final String state; factory ConnectStart.fromJson(Map json) { return ConnectStart( authUrl: json['authUrl'] as String? ?? '', state: json['state'] as String? ?? '', ); } } /// Link status from `/api/connect/status` or `/api/connect/complete`. class ConnectStatus { const ConnectStatus({ required this.status, required this.linked, this.accountId, this.sessionId, this.state, this.validUntil, }); final String status; final bool linked; final String? accountId; final String? sessionId; final String? state; final String? validUntil; factory ConnectStatus.fromJson(Map json) { return ConnectStatus( status: json['status'] as String? ?? 'unknown', linked: json['linked'] as bool? ?? false, accountId: json['accountId'] as String?, sessionId: json['sessionId'] as String?, state: json['state'] as String?, validUntil: json['validUntil'] as String?, ); } static const disconnected = ConnectStatus( status: 'disconnected', linked: false, ); } /// `GET /api/balance` payload. class BankBalance { const BankBalance({ required this.amount, required this.currency, this.balanceType, }); final double amount; final String currency; final String? balanceType; factory BankBalance.fromJson(Map json) { final rawAmount = json['amount']; final amount = switch (rawAmount) { num n => n.toDouble(), String s => double.tryParse(s) ?? 0, _ => 0.0, }; return BankBalance( amount: amount, currency: json['currency'] as String? ?? 'EUR', balanceType: json['balanceType'] as String?, ); } } /// One row from `GET /api/transactions`. class BankTransaction { const BankTransaction({ required this.date, required this.description, required this.amount, required this.currency, required this.pending, this.isManual = false, }); final String date; final String description; final double amount; final String currency; final bool pending; /// Admin-created test row (editable / deletable); not from bank feed or CSV. final bool isManual; /// Stable local key used for per-occurrence annotations. String get id => '$date|$currency|$amount|$description'; /// Match key for auto-category (same merchant / description). String get descriptionKey => description.trim().toLowerCase().replaceAll(RegExp(r'\s+'), ' '); /// Fuzzy merchant key — strips dates / terminal / SEPA noise. String get merchantKey => merchantKeyFromDescription(description); /// Exact match for copying notes (same description + amount + currency). String get exactKey => '$descriptionKey|$amount|$currency'; static String stripManualStamp(String description) => description.replaceFirst(RegExp(r' ·#\d+$'), ''); /// True when [other] is the same row for delete/tombstone matching. /// /// Uses id first, then date + cents + currency + description (stamp-insensitive), /// so PocketBase float/amount rehydration cannot bypass a local delete. bool matchesForDelete(BankTransaction other) { if (id == other.id) return true; if (date != other.date) return false; if (currency.toUpperCase() != other.currency.toUpperCase()) return false; if ((amount * 100).round() != (other.amount * 100).round()) return false; if (descriptionKey == other.descriptionKey) return true; return stripManualStamp(description).trim().toLowerCase() == stripManualStamp(other.description).trim().toLowerCase(); } BankTransaction copyWith({ String? date, String? description, double? amount, String? currency, bool? pending, bool? isManual, }) { return BankTransaction( date: date ?? this.date, description: description ?? this.description, amount: amount ?? this.amount, currency: currency ?? this.currency, pending: pending ?? this.pending, isManual: isManual ?? this.isManual, ); } Map toJson() => { 'date': date, 'description': description, 'amount': amount, 'currency': currency, 'pending': pending, if (isManual) 'isManual': true, }; factory BankTransaction.fromJson(Map json) { final rawAmount = json['amount']; final amount = switch (rawAmount) { num n => n.toDouble(), String s => double.tryParse(s) ?? 0, _ => 0.0, }; return BankTransaction( date: json['date'] as String? ?? '', description: json['description'] as String? ?? 'Unknown', amount: amount, currency: json['currency'] as String? ?? 'EUR', pending: json['pending'] as bool? ?? false, isManual: json['isManual'] as bool? ?? json['is_manual'] as bool? ?? false, ); } }