Add AI batch reconciliation for accessibility bills

This commit is contained in:
2026-07-25 18:41:09 +08:00
parent eb8909a192
commit 7cca34b331
25 changed files with 2771 additions and 230 deletions
@@ -138,6 +138,8 @@ class RecognitionCandidate {
final String? merchant, orderId, sourceText, note;
final String recognitionKind, amountSource;
final String? categoryHint, resultFingerprint;
final String? batchId, aiAction, aiReason;
final int? categoryId;
final int occurredAtEpochMs;
RecognitionCandidate.fromJson(Map<String, dynamic> value)
@@ -155,13 +157,68 @@ class RecognitionCandidate {
note = value['note'] as String?,
recognitionKind = value['recognitionKind']?.toString() ?? 'payment',
categoryHint = value['categoryHint']?.toString(),
categoryId = (value['categoryId'] as num?)?.toInt(),
amountSource = value['amountSource']?.toString() ?? 'result',
resultFingerprint = value['resultFingerprint']?.toString(),
batchId = value['batchId']?.toString(),
aiAction = value['aiAction']?.toString(),
aiReason = value['aiReason']?.toString(),
occurredAtEpochMs = (value['occurredAtEpochMs'] as num).toInt();
bool get canAutoImport => state == 'auto_ready' && confidence == 'auto';
}
class RecognitionBatchItem {
final String candidateId, action, reason, state, type;
final double amount;
final String? merchant;
final bool canRestore;
RecognitionBatchItem.fromJson(Map<String, dynamic> value)
: candidateId = value['candidateId']?.toString() ?? '',
action = value['action']?.toString() ?? 'keep',
reason = value['reason']?.toString() ?? '',
state = value['state']?.toString() ?? '',
type = value['type']?.toString() ?? 'expense',
amount = (value['amount'] as num?)?.toDouble() ?? 0,
merchant = value['merchant']?.toString(),
canRestore = value['canRestore'] as bool? ?? false;
}
class RecognitionBatch {
final String id, state;
final DateTime openedAt, completedAt;
final int kept, updated, created, dropped;
final bool fallback;
final String? failureReason;
final List<RecognitionBatchItem> items;
RecognitionBatch.fromJson(Map<String, dynamic> value)
: id = value['id']?.toString() ?? '',
state = value['state']?.toString() ?? '',
openedAt = DateTime.fromMillisecondsSinceEpoch(
(value['openedAt'] as num?)?.toInt() ?? 0,
isUtc: true,
),
completedAt = DateTime.fromMillisecondsSinceEpoch(
(value['completedAt'] as num?)?.toInt() ?? 0,
isUtc: true,
),
kept = ((value['summary'] as Map?)?['kept'] as num?)?.toInt() ?? 0,
updated = ((value['summary'] as Map?)?['updated'] as num?)?.toInt() ?? 0,
created = ((value['summary'] as Map?)?['created'] as num?)?.toInt() ?? 0,
dropped = ((value['summary'] as Map?)?['dropped'] as num?)?.toInt() ?? 0,
fallback = ((value['summary'] as Map?)?['fallback'] as bool?) ?? false,
failureReason = value['failureReason']?.toString(),
items = (value['items'] as List<dynamic>? ?? const [])
.map(
(item) => RecognitionBatchItem.fromJson(
Map<String, dynamic>.from(item as Map),
),
)
.toList(growable: false);
}
class SpeechEvent {
final String type;
final String? text;
@@ -398,6 +455,37 @@ class ScreenshotChannel {
}
}
static Future<List<RecognitionBatch>> listRecognitionBatches() async {
try {
final values =
await _channel.invokeMethod<List<Object?>>(
'listRecognitionBatches',
) ??
const [];
return values
.whereType<String>()
.map(
(value) => RecognitionBatch.fromJson(
jsonDecode(value) as Map<String, dynamic>,
),
)
.toList(growable: false);
} on MissingPluginException {
return const [];
}
}
static Future<bool> restoreDroppedRecognition(String candidateId) async {
try {
return await _channel.invokeMethod<bool>('restoreDroppedRecognition', {
'candidateId': candidateId,
}) ??
false;
} on MissingPluginException {
return false;
}
}
static Future<bool> requestNotificationPermission() async {
try {
return await _channel.invokeMethod<bool>(