312 lines
10 KiB
Dart
312 lines
10 KiB
Dart
// lib/services/receipt_service.dart
|
|
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'package:cashumit/models/firefly_account.dart';
|
|
import 'package:cashumit/models/receipt_item.dart';
|
|
import 'package:cashumit/services/firefly_api_service.dart';
|
|
import 'package:cashumit/services/account_mirror_service.dart';
|
|
|
|
class ReceiptService {
|
|
/// Memuat kredensial dari shared preferences.
|
|
///
|
|
/// Mengembalikan Map dengan key 'url' dan 'token' jika kredensial ada dan valid,
|
|
/// atau null jika tidak ditemukan atau tidak valid.
|
|
static Future<Map<String, String>?> loadCredentials() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final url = prefs.getString('firefly_url');
|
|
final token = prefs.getString('firefly_token');
|
|
|
|
if (url == null || token == null || url.isEmpty || token.isEmpty) {
|
|
return null;
|
|
}
|
|
|
|
return {'url': url, 'token': token};
|
|
}
|
|
|
|
/// Memuat daftar akun sumber (revenue) dan tujuan (asset) dari API.
|
|
///
|
|
/// Mengembalikan daftar akun yang berisi akun revenue dan asset.
|
|
/// Melempar exception jika terjadi kesalahan saat memuat akun.
|
|
static Future<List<Map<String, dynamic>>> loadAccounts({
|
|
required String baseUrl,
|
|
required String accessToken,
|
|
}) async {
|
|
// Mengambil akun revenue
|
|
final revenueAccounts = await FireflyApiService.fetchAccounts(
|
|
baseUrl: baseUrl,
|
|
accessToken: accessToken,
|
|
type: 'revenue',
|
|
);
|
|
|
|
// Mengambil akun asset
|
|
final assetAccounts = await FireflyApiService.fetchAccounts(
|
|
baseUrl: baseUrl,
|
|
accessToken: accessToken,
|
|
type: 'asset',
|
|
);
|
|
|
|
// Menggabungkan akun revenue dan asset untuk dropdown
|
|
final allAccounts = <Map<String, dynamic>>[];
|
|
for (var account in revenueAccounts) {
|
|
allAccounts.add({
|
|
'id': account.id,
|
|
'name': account.name,
|
|
'type': account.type,
|
|
});
|
|
}
|
|
for (var account in assetAccounts) {
|
|
allAccounts.add({
|
|
'id': account.id,
|
|
'name': account.name,
|
|
'type': account.type,
|
|
});
|
|
}
|
|
|
|
return allAccounts;
|
|
}
|
|
|
|
/// Mencari ID akun berdasarkan nama dan tipe akun.
|
|
static String? findAccountIdByName({
|
|
required String name,
|
|
required String expectedType,
|
|
required List<Map<String, dynamic>> accounts,
|
|
}) {
|
|
if (name.isEmpty) return null;
|
|
|
|
try {
|
|
// Cari akun dengan nama yang cocok dan tipe yang diharapkan
|
|
final account = accounts.firstWhere(
|
|
(account) =>
|
|
account['name'].toString().toLowerCase() == name.toLowerCase() &&
|
|
account['type'] == expectedType,
|
|
);
|
|
|
|
return account['id'] as String?;
|
|
} catch (e) {
|
|
// Jika tidak ditemukan, coba pencarian yang lebih fleksibel
|
|
for (var account in accounts) {
|
|
if (account['type'] == expectedType &&
|
|
account['name']
|
|
.toString()
|
|
.toLowerCase()
|
|
.contains(name.toLowerCase())) {
|
|
return account['id'] as String?;
|
|
}
|
|
}
|
|
|
|
// Jika masih tidak ditemukan, kembalikan null
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// Generates a transaction description based on item names
|
|
static String generateTransactionDescription(List<ReceiptItem> items) {
|
|
if (items.isEmpty) {
|
|
return 'Transaksi Struk Belanja';
|
|
}
|
|
|
|
// Take the first 5 item descriptions
|
|
final itemNames = items.take(5).map((item) => item.description).toList();
|
|
|
|
// If there are more than 5 items, append ', dll' to the last item
|
|
if (items.length > 5) {
|
|
itemNames[4] += ', dll';
|
|
}
|
|
|
|
// Join the item names with ', '
|
|
return itemNames.join(', ');
|
|
}
|
|
|
|
/// Mengirim transaksi ke Firefly III.
|
|
///
|
|
/// Sebelum mengirim transaksi, metode ini melakukan beberapa validasi:
|
|
/// 1. Memastikan ada item yang akan dikirim
|
|
/// 2. Memastikan akun sumber dan tujuan telah dipilih atau dimasukkan
|
|
/// 3. Memastikan akun sumber dan tujuan berbeda
|
|
/// 4. Memastikan akun sumber dan tujuan ada di daftar akun yang dimuat
|
|
/// 5. Memastikan tipe akun sesuai (sumber: revenue, tujuan: asset)
|
|
///
|
|
/// Mengembalikan transactionId jika berhasil, atau null jika gagal.
|
|
/// Melempar exception jika ada error validasi.
|
|
static Future<String?> submitTransaction({
|
|
required List<ReceiptItem> items,
|
|
required DateTime transactionDate,
|
|
required String sourceAccountId,
|
|
required String destinationAccountId,
|
|
required List<Map<String, dynamic>> accounts, // Daftar akun yang dimuat
|
|
required String baseUrl,
|
|
required String accessToken,
|
|
}) async {
|
|
if (items.isEmpty) {
|
|
throw Exception('Tidak ada item untuk dikirim');
|
|
}
|
|
|
|
// Validasi apakah akun benar-benar ada di Firefly III
|
|
bool sourceAccountExists = false;
|
|
bool destinationAccountExists = false;
|
|
|
|
// Cari detail akun untuk validasi tipe akun
|
|
Map<String, dynamic>? sourceAccountDetails;
|
|
Map<String, dynamic>? destinationAccountDetails;
|
|
|
|
for (var account in accounts) {
|
|
if (account['id'].toString() == sourceAccountId) {
|
|
sourceAccountExists = true;
|
|
sourceAccountDetails = account;
|
|
}
|
|
if (account['id'].toString() == destinationAccountId) {
|
|
destinationAccountExists = true;
|
|
destinationAccountDetails = account;
|
|
}
|
|
}
|
|
|
|
if (!sourceAccountExists) {
|
|
throw Exception(
|
|
'Akun sumber tidak ditemukan di daftar akun yang dimuat. Klik "Muat Ulang Akun" dan coba lagi.');
|
|
}
|
|
|
|
if (!destinationAccountExists) {
|
|
throw Exception(
|
|
'Akun tujuan tidak ditemukan di daftar akun yang dimuat. Klik "Muat Ulang Akun" dan coba lagi.');
|
|
}
|
|
|
|
// Validasi tipe akun (sumber harus revenue, tujuan harus asset)
|
|
if (sourceAccountDetails != null &&
|
|
sourceAccountDetails['type'] != 'revenue') {
|
|
// Tampilkan peringatan, tapi tidak menghentikan proses
|
|
print(
|
|
'Peringatan: Akun sumber sebaiknya bertipe revenue, tetapi akun ini bertipe ${sourceAccountDetails['type']}.');
|
|
}
|
|
|
|
if (destinationAccountDetails != null &&
|
|
destinationAccountDetails['type'] != 'asset') {
|
|
// Tampilkan peringatan, tapi tidak menghentikan proses
|
|
print(
|
|
'Peringatan: Akun tujuan sebaiknya bertipe asset, tetapi akun ini bertipe ${destinationAccountDetails['type']}.');
|
|
}
|
|
|
|
if (sourceAccountId == destinationAccountId) {
|
|
throw Exception('Akun sumber dan tujuan tidak boleh sama');
|
|
}
|
|
|
|
final total = _calculateTotal(items);
|
|
|
|
// Generate transaction description
|
|
final transactionDescription = generateTransactionDescription(items);
|
|
|
|
final transactionId = await FireflyApiService.submitDummyTransaction(
|
|
baseUrl: baseUrl,
|
|
accessToken: accessToken,
|
|
sourceId: sourceAccountId,
|
|
destinationId: destinationAccountId,
|
|
type: 'deposit',
|
|
description: transactionDescription,
|
|
date:
|
|
'${transactionDate.year}-${transactionDate.month.toString().padLeft(2, '0')}-${transactionDate.day.toString().padLeft(2, '0')}',
|
|
amount: total.toStringAsFixed(2),
|
|
);
|
|
|
|
return transactionId;
|
|
}
|
|
|
|
/// Menghitung total harga semua item
|
|
static double _calculateTotal(List<ReceiptItem> items) {
|
|
return items.fold(0.0, (sum, item) => sum + item.total);
|
|
}
|
|
|
|
/// Fungsi untuk menyimpan akun ke mirror
|
|
static Future<void> saveAccountsToMirror(
|
|
List<Map<String, dynamic>> accounts) async {
|
|
final fireflyAccounts = accounts
|
|
.map((map) => FireflyAccount(
|
|
id: map['id'].toString(),
|
|
name: map['name'].toString(),
|
|
type: map['type'].toString(),
|
|
))
|
|
.toList();
|
|
await AccountMirrorService.mirrorAccounts(fireflyAccounts);
|
|
}
|
|
|
|
/// Fungsi untuk mengambil akun dari mirror
|
|
static Future<List<Map<String, dynamic>>> getMirroredAccounts() async {
|
|
final accounts = await AccountMirrorService.getMirroredAccounts();
|
|
return accounts
|
|
.map((account) => {
|
|
'id': account.id,
|
|
'name': account.name,
|
|
'type': account.type,
|
|
})
|
|
.toList();
|
|
}
|
|
|
|
/// Fungsi untuk mendapatkan akun dengan tipe tertentu dari mirror
|
|
static Future<List<Map<String, dynamic>>> getMirroredAccountsWithType(
|
|
String type) async {
|
|
final accounts = await AccountMirrorService.getMirroredAccountsByType(type);
|
|
return accounts
|
|
.map((account) => {
|
|
'id': account.id,
|
|
'name': account.name,
|
|
'type': account.type,
|
|
})
|
|
.toList();
|
|
}
|
|
|
|
/// Fungsi untuk memperbarui mirror akun dari server
|
|
static Future<void> updateAccountMirror({
|
|
required String baseUrl,
|
|
required String accessToken,
|
|
}) async {
|
|
try {
|
|
final accounts =
|
|
await loadAccounts(baseUrl: baseUrl, accessToken: accessToken);
|
|
final fireflyAccounts = accounts
|
|
.map((map) => FireflyAccount(
|
|
id: map['id'].toString(),
|
|
name: map['name'].toString(),
|
|
type: map['type'].toString(),
|
|
))
|
|
.toList();
|
|
await AccountMirrorService.mirrorAccounts(fireflyAccounts);
|
|
} catch (e) {
|
|
print('Gagal memperbarui mirror akun: $e');
|
|
}
|
|
}
|
|
|
|
// Fungsi-fungsi untuk default account mapping telah dihapus sesuai permintaan
|
|
|
|
/// Fungsi untuk memuat akun dengan fallback mekanisme yang lebih lengkap
|
|
static Future<List<Map<String, dynamic>>> loadAccountsWithFallback({
|
|
required String baseUrl,
|
|
required String accessToken,
|
|
}) async {
|
|
try {
|
|
// Coba ambil dari server terlebih dahulu
|
|
final accounts = await loadAccounts(
|
|
baseUrl: baseUrl,
|
|
accessToken: accessToken,
|
|
);
|
|
|
|
// Simpan ke mirror jika berhasil
|
|
await updateAccountMirror(baseUrl: baseUrl, accessToken: accessToken);
|
|
|
|
return accounts;
|
|
} catch (serverError) {
|
|
print('Gagal memuat akun dari server: $serverError');
|
|
|
|
// Jika gagal dari server, coba dari mirror
|
|
try {
|
|
final mirroredAccounts = await getMirroredAccounts();
|
|
if (mirroredAccounts.isNotEmpty) {
|
|
return mirroredAccounts;
|
|
}
|
|
} catch (mirrorError) {
|
|
print('Gagal memuat dari mirror: $mirrorError');
|
|
}
|
|
|
|
// Jika semua fallback gagal, kembalikan list kosong
|
|
return [];
|
|
}
|
|
}
|
|
}
|