look ahead

This commit is contained in:
davmar
2026-09-10 00:35:54 +02:00
parent 91d6dc9e54
commit 5b22fc8694
20 changed files with 2942 additions and 799 deletions
+120
View File
@@ -325,6 +325,126 @@ class TxExactTemplate {
}
}
/// How a [RecurringTemplate] resolves its projected amount.
enum RecurringAmountRule {
lastValue,
avgLastTxs,
labelAvgMonths,
}
/// What a recurring template is anchored to.
enum RecurringTemplateSource {
transaction,
label,
}
/// First-class look-ahead recurring template (manual or auto from marked txs).
class RecurringTemplate {
const RecurringTemplate({
required this.id,
required this.name,
required this.dayOfMonth,
required this.currency,
required this.source,
required this.amountRule,
this.merchantKey = '',
this.sourceTxId,
this.labelId,
this.avgCount = 5,
this.autoFromRecurring = false,
});
final String id;
final String name;
final int dayOfMonth;
final String currency;
final RecurringTemplateSource source;
final RecurringAmountRule amountRule;
final String merchantKey;
final String? sourceTxId;
final String? labelId;
final int avgCount;
final bool autoFromRecurring;
RecurringTemplate copyWith({
String? id,
String? name,
int? dayOfMonth,
String? currency,
RecurringTemplateSource? source,
RecurringAmountRule? amountRule,
String? merchantKey,
String? sourceTxId,
bool clearSourceTxId = false,
String? labelId,
bool clearLabelId = false,
int? avgCount,
bool? autoFromRecurring,
}) {
return RecurringTemplate(
id: id ?? this.id,
name: name ?? this.name,
dayOfMonth: dayOfMonth ?? this.dayOfMonth,
currency: currency ?? this.currency,
source: source ?? this.source,
amountRule: amountRule ?? this.amountRule,
merchantKey: merchantKey ?? this.merchantKey,
sourceTxId: clearSourceTxId ? null : (sourceTxId ?? this.sourceTxId),
labelId: clearLabelId ? null : (labelId ?? this.labelId),
avgCount: avgCount ?? this.avgCount,
autoFromRecurring: autoFromRecurring ?? this.autoFromRecurring,
);
}
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'dayOfMonth': dayOfMonth,
'currency': currency,
'source': source.name,
'amountRule': amountRule.name,
'merchantKey': merchantKey,
'sourceTxId': sourceTxId,
'labelId': labelId,
'avgCount': avgCount,
'autoFromRecurring': autoFromRecurring,
};
factory RecurringTemplate.fromJson(Map<String, dynamic> json) {
return RecurringTemplate(
id: json['id'] as String? ?? '',
name: json['name'] as String? ?? 'Recurring',
dayOfMonth: (json['dayOfMonth'] as num?)?.toInt().clamp(1, 28) ?? 1,
currency: json['currency'] as String? ?? 'EUR',
source: _parseTemplateSource(json['source'] as String?),
amountRule: _parseAmountRule(json['amountRule'] as String?),
merchantKey: json['merchantKey'] as String? ?? '',
sourceTxId: json['sourceTxId'] as String?,
labelId: json['labelId'] as String?,
avgCount: (json['avgCount'] as num?)?.toInt().clamp(1, 36) ?? 5,
autoFromRecurring: json['autoFromRecurring'] as bool? ?? false,
);
}
/// Stable id for auto templates linked to a bank transaction.
static String autoIdForTx(String txId) => 'auto_${txId.hashCode.toRadixString(16)}';
}
RecurringTemplateSource _parseTemplateSource(String? raw) {
return switch (raw) {
'label' => RecurringTemplateSource.label,
_ => RecurringTemplateSource.transaction,
};
}
RecurringAmountRule _parseAmountRule(String? raw) {
return switch (raw) {
'avgLastTxs' => RecurringAmountRule.avgLastTxs,
'labelAvgMonths' => RecurringAmountRule.labelAvgMonths,
_ => RecurringAmountRule.lastValue,
};
}
/// Strip bank noise so supermarket / POS lines group by merchant stem.
///
/// Example: `BCK*AH CAMPUS DIEMEN ZUID NLD Pasvolgnr:…` → `ah campus`