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
+101
View File
@@ -185,6 +185,27 @@ class PeriodStats {
analysis = json['analysis'] as String?;
}
class RecognitionBatchDraft {
final String candidateId, clientRequestId, type, source;
final int categoryId;
final double amount;
final DateTime occurredAt;
final String? note, paymentMethod, sourceText;
const RecognitionBatchDraft({
required this.candidateId,
required this.clientRequestId,
required this.categoryId,
required this.type,
required this.amount,
required this.occurredAt,
required this.source,
this.note,
this.paymentMethod,
this.sourceText,
});
}
class TxApi {
static final _dio = ApiClient.instance.dio;
@@ -361,6 +382,86 @@ class TxApi {
}
}
static Future<Map<String, TxItem>> createRecognitionBatch(
String batchId,
List<RecognitionBatchDraft> drafts,
) async {
if (drafts.isEmpty) return const {};
final localPayloads = drafts
.map(
(draft) => <String, dynamic>{
'ledgerId': _ledgerId,
'categoryId': draft.categoryId,
'type': draft.type,
'amount': draft.amount,
'note': draft.note,
'paymentMethod': draft.paymentMethod,
'source': draft.source,
'sourceText': draft.sourceText,
'occurredAt': ShanghaiTime.civilToUtc(
draft.occurredAt,
).toIso8601String(),
'clientRequestId': draft.clientRequestId,
},
)
.toList(growable: false);
Map<String, TxItem> createLocal() {
final values = LocalDatabase.instance.createTransactionsBatch(
localPayloads,
enqueueSyncChanges: _queueOfflineChanges,
);
return {
for (var index = 0; index < drafts.length; index++)
drafts[index].candidateId: TxItem.fromJson(values[index]),
};
}
final session = SessionStore.instance;
if (session.shouldUseLocalOnly ||
_ledgerId < 0 ||
drafts.any((draft) => draft.categoryId < 0)) {
return createLocal();
}
try {
final response = await _dio.post(
'/api/transactions/recognition-batch',
data: {
'batchId': batchId,
'ledgerId': _ledgerId,
'items': [
for (var index = 0; index < drafts.length; index++)
{
'candidateId': drafts[index].candidateId,
'clientRequestId': drafts[index].clientRequestId,
'categoryId': drafts[index].categoryId,
'type': drafts[index].type,
'amount': drafts[index].amount,
'note': drafts[index].note,
'paymentMethod': drafts[index].paymentMethod,
'occurredAt': localPayloads[index]['occurredAt'],
'source': drafts[index].source,
'sourceText': drafts[index].sourceText,
},
],
},
);
final result = <String, TxItem>{};
for (final raw in response.data as List<dynamic>) {
final item = Map<String, dynamic>.from(raw as Map);
final transaction = Map<String, dynamic>.from(
item['transaction'] as Map,
);
LocalDatabase.instance.cacheTransaction(transaction);
result[item['candidateId'].toString()] = TxItem.fromJson(transaction);
}
return result;
} catch (error) {
if (!isConnectivityError(error) || !session.isAccount) rethrow;
return createLocal();
}
}
static Future<void> delete(int id) async {
final baseUpdatedAt = LocalDatabase.instance.transaction(
id,