Add transfer tracking and secure admin access
This commit is contained in:
@@ -15,6 +15,9 @@ class TxItem {
|
||||
final String categoryName, categoryIcon, categoryColor, type, source;
|
||||
final double amount;
|
||||
final String? note, paymentMethod, sourceText;
|
||||
final String? transferDirection, counterparty;
|
||||
final String? provider, providerTransactionId, recognitionOccurrenceId;
|
||||
final String? evidenceFingerprint, recognitionConfidence;
|
||||
final DateTime occurredAt, updatedAt;
|
||||
final bool isDeleted;
|
||||
|
||||
@@ -31,6 +34,13 @@ class TxItem {
|
||||
paymentMethod = j['paymentMethod'] as String?,
|
||||
source = j['source'] as String,
|
||||
sourceText = j['sourceText'] as String?,
|
||||
transferDirection = j['transferDirection'] as String?,
|
||||
counterparty = j['counterparty'] as String?,
|
||||
provider = j['provider'] as String?,
|
||||
providerTransactionId = j['providerTransactionId'] as String?,
|
||||
recognitionOccurrenceId = j['recognitionOccurrenceId'] as String?,
|
||||
evidenceFingerprint = j['evidenceFingerprint'] as String?,
|
||||
recognitionConfidence = j['recognitionConfidence'] as String?,
|
||||
occurredAt = ShanghaiTime.parseCivil(j['occurredAt'] as String),
|
||||
updatedAt = DateTime.parse(
|
||||
j['updatedAt'] as String? ?? j['occurredAt'] as String,
|
||||
@@ -48,7 +58,18 @@ class TxItem {
|
||||
source == 'accessibility' ||
|
||||
source == 'notification' ||
|
||||
source == 'local_ocr';
|
||||
bool get isIncome => type == 'income';
|
||||
bool get isTransfer => type == 'transfer';
|
||||
bool get isIncome =>
|
||||
type == 'income' || isTransfer && transferDirection == 'in';
|
||||
bool get isExpense =>
|
||||
type == 'expense' || isTransfer && transferDirection == 'out';
|
||||
String get typeLabel => isTransfer
|
||||
? transferDirection == 'in'
|
||||
? '转入'
|
||||
: '转出'
|
||||
: isIncome
|
||||
? '收入'
|
||||
: '支出';
|
||||
|
||||
String get sourceLabel => switch (source) {
|
||||
'ai_chat' => 'AI 聊天',
|
||||
@@ -191,6 +212,9 @@ class RecognitionBatchDraft {
|
||||
final double amount;
|
||||
final DateTime occurredAt;
|
||||
final String? note, paymentMethod, sourceText;
|
||||
final String? transferDirection, counterparty;
|
||||
final String? provider, providerTransactionId, recognitionOccurrenceId;
|
||||
final String? evidenceFingerprint, recognitionConfidence;
|
||||
|
||||
const RecognitionBatchDraft({
|
||||
required this.candidateId,
|
||||
@@ -203,6 +227,13 @@ class RecognitionBatchDraft {
|
||||
this.note,
|
||||
this.paymentMethod,
|
||||
this.sourceText,
|
||||
this.transferDirection,
|
||||
this.counterparty,
|
||||
this.provider,
|
||||
this.providerTransactionId,
|
||||
this.recognitionOccurrenceId,
|
||||
this.evidenceFingerprint,
|
||||
this.recognitionConfidence,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -336,6 +367,13 @@ class TxApi {
|
||||
String? sourceText,
|
||||
DateTime? occurredAt,
|
||||
String? clientRequestId,
|
||||
String? transferDirection,
|
||||
String? counterparty,
|
||||
String? provider,
|
||||
String? providerTransactionId,
|
||||
String? recognitionOccurrenceId,
|
||||
String? evidenceFingerprint,
|
||||
String? recognitionConfidence,
|
||||
}) async {
|
||||
final payload = <String, dynamic>{
|
||||
'ledgerId': _ledgerId,
|
||||
@@ -350,6 +388,17 @@ class TxApi {
|
||||
occurredAt ?? ShanghaiTime.now,
|
||||
).toIso8601String(),
|
||||
if (clientRequestId != null) 'clientRequestId': clientRequestId,
|
||||
if (transferDirection != null) 'transferDirection': transferDirection,
|
||||
if (counterparty != null) 'counterparty': counterparty,
|
||||
if (provider != null) 'provider': provider,
|
||||
if (providerTransactionId != null)
|
||||
'providerTransactionId': providerTransactionId,
|
||||
if (recognitionOccurrenceId != null)
|
||||
'recognitionOccurrenceId': recognitionOccurrenceId,
|
||||
if (evidenceFingerprint != null)
|
||||
'evidenceFingerprint': evidenceFingerprint,
|
||||
if (recognitionConfidence != null)
|
||||
'recognitionConfidence': recognitionConfidence,
|
||||
};
|
||||
final session = SessionStore.instance;
|
||||
if (session.shouldUseLocalOnly || categoryId < 0 || _ledgerId < 0) {
|
||||
@@ -402,6 +451,13 @@ class TxApi {
|
||||
draft.occurredAt,
|
||||
).toIso8601String(),
|
||||
'clientRequestId': draft.clientRequestId,
|
||||
'transferDirection': draft.transferDirection,
|
||||
'counterparty': draft.counterparty,
|
||||
'provider': draft.provider,
|
||||
'providerTransactionId': draft.providerTransactionId,
|
||||
'recognitionOccurrenceId': draft.recognitionOccurrenceId,
|
||||
'evidenceFingerprint': draft.evidenceFingerprint,
|
||||
'recognitionConfidence': draft.recognitionConfidence,
|
||||
},
|
||||
)
|
||||
.toList(growable: false);
|
||||
@@ -442,6 +498,14 @@ class TxApi {
|
||||
'occurredAt': localPayloads[index]['occurredAt'],
|
||||
'source': drafts[index].source,
|
||||
'sourceText': drafts[index].sourceText,
|
||||
'transferDirection': drafts[index].transferDirection,
|
||||
'counterparty': drafts[index].counterparty,
|
||||
'provider': drafts[index].provider,
|
||||
'providerTransactionId': drafts[index].providerTransactionId,
|
||||
'recognitionOccurrenceId':
|
||||
drafts[index].recognitionOccurrenceId,
|
||||
'evidenceFingerprint': drafts[index].evidenceFingerprint,
|
||||
'recognitionConfidence': drafts[index].recognitionConfidence,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -499,6 +563,8 @@ class TxApi {
|
||||
required DateTime occurredAt,
|
||||
String? note,
|
||||
String? paymentMethod,
|
||||
String? transferDirection,
|
||||
String? counterparty,
|
||||
}) async {
|
||||
final baseUpdatedAt = LocalDatabase.instance.transaction(
|
||||
id,
|
||||
@@ -511,6 +577,8 @@ class TxApi {
|
||||
'amount': amount,
|
||||
'note': note,
|
||||
'paymentMethod': paymentMethod,
|
||||
'transferDirection': transferDirection,
|
||||
'counterparty': counterparty,
|
||||
'occurredAt': ShanghaiTime.civilToUtc(occurredAt).toIso8601String(),
|
||||
if (baseUpdatedAt != null) 'baseUpdatedAt': baseUpdatedAt,
|
||||
};
|
||||
@@ -1447,7 +1515,7 @@ class ParsedDraft {
|
||||
final int categoryId;
|
||||
final String categoryName, categoryIcon, note, type;
|
||||
final double amount;
|
||||
final String? paymentMethod;
|
||||
final String? paymentMethod, transferDirection, counterparty;
|
||||
|
||||
ParsedDraft.fromJson(Map<String, dynamic> j)
|
||||
: matched = j['matched'] as bool,
|
||||
@@ -1456,12 +1524,25 @@ class ParsedDraft {
|
||||
categoryIcon = j['categoryIcon'] as String,
|
||||
amount = (j['amount'] as num).toDouble(),
|
||||
paymentMethod = j['paymentMethod'] as String?,
|
||||
transferDirection = j['transferDirection'] as String?,
|
||||
counterparty = j['counterparty'] as String?,
|
||||
note = j['note'] as String,
|
||||
type = switch (j['type']?.toString().toLowerCase()) {
|
||||
'income' => 'income',
|
||||
'expense' => 'expense',
|
||||
'transfer' => 'transfer',
|
||||
_ => 'unknown',
|
||||
};
|
||||
|
||||
bool get isIncome =>
|
||||
type == 'income' || type == 'transfer' && transferDirection == 'in';
|
||||
String get typeLabel => type == 'transfer'
|
||||
? transferDirection == 'in'
|
||||
? '转入'
|
||||
: '转出'
|
||||
: isIncome
|
||||
? '收入'
|
||||
: '支出';
|
||||
}
|
||||
|
||||
class ParseApi {
|
||||
@@ -1481,8 +1562,8 @@ class ParseApi {
|
||||
String source,
|
||||
String sourceText,
|
||||
) async {
|
||||
if (d.type != 'income' && d.type != 'expense') {
|
||||
throw StateError('请先确认收入或支出类型');
|
||||
if (d.type != 'income' && d.type != 'expense' && d.type != 'transfer') {
|
||||
throw StateError('请先确认账单类型');
|
||||
}
|
||||
return TxApi.create(
|
||||
categoryId: d.categoryId,
|
||||
@@ -1492,6 +1573,8 @@ class ParseApi {
|
||||
paymentMethod: d.paymentMethod,
|
||||
source: source,
|
||||
sourceText: sourceText,
|
||||
transferDirection: d.transferDirection,
|
||||
counterparty: d.counterparty,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user