796 lines
26 KiB
Dart
796 lines
26 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/models/receipt_group.dart';
|
|
import 'package:cashumit/models/local_receipt.dart';
|
|
import 'package:cashumit/models/submission_task.dart';
|
|
import 'package:cashumit/models/group_submission_result.dart';
|
|
import 'package:cashumit/services/firefly_api_service.dart';
|
|
import 'package:cashumit/services/account_mirror_service.dart';
|
|
import 'package:cashumit/services/esc_pos_print_service.dart';
|
|
import 'package:cashumit/services/invoice_image_service.dart';
|
|
import 'package:cashumit/services/tag_cache_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(', ');
|
|
}
|
|
|
|
/// Format tanggal untuk API Firefly III (ISO 8601 dengan jam)
|
|
static String _formatFireflyDate(DateTime date) {
|
|
String two(int v) => v.toString().padLeft(2, '0');
|
|
return '${date.year}-${two(date.month)}-${two(date.day)}T${two(date.hour)}:${two(date.minute)}:${two(date.second)}';
|
|
}
|
|
|
|
/// Mengirim transaksi ke Firefly III.
|
|
///
|
|
/// Setiap grup dengan item menjadi satu transaksi `deposit` terpisah
|
|
/// (sumber grup -> tujuan bersama). Jika tip aktif (tipSourceAccountId
|
|
/// tidak null dan paymentAmount > total), satu transaksi tip tambahan
|
|
/// dibuat (sumber = tipSource, tujuan = tujuan bersama, amount = kembalian).
|
|
///
|
|
/// Mengembalikan list ID transaksi (per grup + tip jika ada).
|
|
/// Melempar exception jika ada error validasi.
|
|
static Future<List<String>> submitTransaction({
|
|
required List<ReceiptGroup> groups,
|
|
required DateTime transactionDate,
|
|
required String destinationAccountId,
|
|
required List<Map<String, dynamic>> accounts,
|
|
required String baseUrl,
|
|
required String accessToken,
|
|
double paymentAmount = 0.0,
|
|
String? tipSourceAccountId,
|
|
double totalAmount = 0.0,
|
|
List<String>? tags,
|
|
}) async {
|
|
final groupsWithItems = groups.where((g) => g.items.isNotEmpty).toList();
|
|
if (groupsWithItems.isEmpty) {
|
|
throw Exception('Tidak ada item untuk dikirim');
|
|
}
|
|
|
|
// Validasi akun tujuan ada di daftar
|
|
final destinationExists =
|
|
accounts.any((a) => a['id'].toString() == destinationAccountId);
|
|
if (!destinationExists) {
|
|
throw Exception(
|
|
'Akun tujuan tidak ditemukan di daftar akun yang dimuat. Klik "Muat Ulang Akun" dan coba lagi.');
|
|
}
|
|
|
|
final fireflyDate = _formatFireflyDate(transactionDate);
|
|
final transactionIds = <String>[];
|
|
|
|
// Submit satu transaksi per grup
|
|
for (final group in groupsWithItems) {
|
|
if (!group.hasSource) {
|
|
throw Exception('Setiap grup dengan item harus memiliki akun sumber');
|
|
}
|
|
if (group.sourceAccountId == destinationAccountId) {
|
|
throw Exception(
|
|
'Akun sumber grup "${group.sourceAccountName}" tidak boleh sama dengan akun tujuan');
|
|
}
|
|
|
|
final groupTotal = group.total;
|
|
final description = generateTransactionDescription(group.items);
|
|
|
|
final id = await FireflyApiService.submitDummyTransaction(
|
|
baseUrl: baseUrl,
|
|
accessToken: accessToken,
|
|
sourceId: group.sourceAccountId!,
|
|
destinationId: destinationAccountId,
|
|
type: 'deposit',
|
|
description: description,
|
|
date: fireflyDate,
|
|
amount: groupTotal.toStringAsFixed(2),
|
|
tags: group.tags.isNotEmpty ? group.tags : tags,
|
|
);
|
|
|
|
if (id == null) {
|
|
throw Exception(
|
|
'Gagal mengirim grup "${group.sourceAccountName}" ke Firefly III');
|
|
}
|
|
if (id != 'success') {
|
|
transactionIds.add(id);
|
|
}
|
|
}
|
|
|
|
// Submit transaksi tip jika aktif
|
|
final hasTip = tipSourceAccountId != null && paymentAmount > totalAmount;
|
|
if (hasTip) {
|
|
final tipAmount = paymentAmount - totalAmount;
|
|
final tipSourceExists =
|
|
accounts.any((a) => a['id'].toString() == tipSourceAccountId);
|
|
if (!tipSourceExists) {
|
|
throw Exception(
|
|
'Akun sumber tip tidak ditemukan di daftar akun yang dimuat.');
|
|
}
|
|
if (tipSourceAccountId == destinationAccountId) {
|
|
throw Exception('Akun sumber tip tidak boleh sama dengan akun tujuan');
|
|
}
|
|
|
|
final tipId = await FireflyApiService.submitDummyTransaction(
|
|
baseUrl: baseUrl,
|
|
accessToken: accessToken,
|
|
sourceId: tipSourceAccountId,
|
|
destinationId: destinationAccountId,
|
|
type: 'deposit',
|
|
description: 'Tip dari kembalian',
|
|
date: fireflyDate,
|
|
amount: tipAmount.toStringAsFixed(2),
|
|
tags: tags,
|
|
);
|
|
|
|
if (tipId == null) {
|
|
throw Exception('Gagal mengirim transaksi tip ke Firefly III');
|
|
}
|
|
if (tipId != 'success') {
|
|
transactionIds.add(tipId);
|
|
}
|
|
}
|
|
|
|
return transactionIds;
|
|
}
|
|
|
|
/// Mengirim satu grup transaksi ke Firefly III tanpa melempar exception.
|
|
/// Mengembalikan [GroupSubmissionResult] dengan status sukses/gagal.
|
|
///
|
|
/// Dipakai oleh popup daftar transaksi untuk pengiriman per-item dengan
|
|
/// retry.
|
|
static Future<GroupSubmissionResult> submitSingleGroup({
|
|
required ReceiptGroup group,
|
|
required String destinationAccountId,
|
|
required DateTime transactionDate,
|
|
required String baseUrl,
|
|
required String accessToken,
|
|
List<String>? tags,
|
|
}) async {
|
|
final result = GroupSubmissionResult(
|
|
groupId: group.id,
|
|
label: group.sourceAccountName ?? 'Grup',
|
|
amount: group.total,
|
|
isTip: false,
|
|
status: SubmissionStatus.pending,
|
|
);
|
|
|
|
if (group.items.isEmpty) {
|
|
return result.copyWith(
|
|
status: SubmissionStatus.failed,
|
|
error: 'Tidak ada item',
|
|
);
|
|
}
|
|
if (!group.hasSource) {
|
|
return result.copyWith(
|
|
status: SubmissionStatus.failed,
|
|
error: 'Akun sumber belum dipilih',
|
|
);
|
|
}
|
|
if (group.sourceAccountId == destinationAccountId) {
|
|
return result.copyWith(
|
|
status: SubmissionStatus.failed,
|
|
error: 'Akun sumber sama dengan tujuan',
|
|
);
|
|
}
|
|
|
|
try {
|
|
final description = generateTransactionDescription(group.items);
|
|
final idResult = await FireflyApiService.createTransactionDetailed(
|
|
baseUrl: baseUrl,
|
|
accessToken: accessToken,
|
|
sourceId: group.sourceAccountId!,
|
|
destinationId: destinationAccountId,
|
|
type: 'deposit',
|
|
description: description,
|
|
date: _formatFireflyDate(transactionDate),
|
|
amount: group.total.toStringAsFixed(2),
|
|
tags: group.tags.isNotEmpty ? group.tags : tags,
|
|
);
|
|
|
|
if (idResult == null) {
|
|
return result.copyWith(
|
|
status: SubmissionStatus.failed,
|
|
error: 'Server mengembalikan respons null',
|
|
);
|
|
}
|
|
final txId = idResult['transactionId'];
|
|
final jId = idResult['journalId'];
|
|
// 'success' berarti terkirim tapi ID tidak diparse; anggap sukses tanpa ID
|
|
return result.copyWith(
|
|
status: SubmissionStatus.success,
|
|
transactionId: (txId == 'success' || txId == null) ? null : txId,
|
|
journalId: (jId == null || jId.isEmpty || jId == 'success') ? null : jId,
|
|
);
|
|
} catch (e) {
|
|
return result.copyWith(
|
|
status: SubmissionStatus.failed,
|
|
error: e.toString(),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Mengirim transaksi tip ke Firefly III tanpa melempar exception.
|
|
static Future<GroupSubmissionResult> submitTip({
|
|
required String tipSourceAccountId,
|
|
required String tipSourceAccountName,
|
|
required double tipAmount,
|
|
required String destinationAccountId,
|
|
required DateTime transactionDate,
|
|
required String baseUrl,
|
|
required String accessToken,
|
|
List<String>? tags,
|
|
}) async {
|
|
final result = GroupSubmissionResult(
|
|
groupId: 'tip_${DateTime.now().millisecondsSinceEpoch}',
|
|
label: 'Tip ($tipSourceAccountName)',
|
|
amount: tipAmount,
|
|
isTip: true,
|
|
status: SubmissionStatus.pending,
|
|
);
|
|
|
|
if (tipSourceAccountId == destinationAccountId) {
|
|
return result.copyWith(
|
|
status: SubmissionStatus.failed,
|
|
error: 'Akun sumber tip sama dengan tujuan',
|
|
);
|
|
}
|
|
|
|
try {
|
|
final id = await FireflyApiService.submitDummyTransaction(
|
|
baseUrl: baseUrl,
|
|
accessToken: accessToken,
|
|
sourceId: tipSourceAccountId,
|
|
destinationId: destinationAccountId,
|
|
type: 'deposit',
|
|
description: 'Tip dari kembalian',
|
|
date: _formatFireflyDate(transactionDate),
|
|
amount: tipAmount.toStringAsFixed(2),
|
|
tags: tags,
|
|
);
|
|
|
|
if (id == null) {
|
|
return result.copyWith(
|
|
status: SubmissionStatus.failed,
|
|
error: 'Server mengembalikan respons null',
|
|
);
|
|
}
|
|
return result.copyWith(
|
|
status: SubmissionStatus.success,
|
|
transactionId: id == 'success' ? null : id,
|
|
);
|
|
} catch (e) {
|
|
return result.copyWith(
|
|
status: SubmissionStatus.failed,
|
|
error: e.toString(),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 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,
|
|
List<String>? tags,
|
|
}) 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 [];
|
|
}
|
|
}
|
|
|
|
/// Fungsi untuk memuat tag dari Firefly III dengan caching fallback
|
|
static Future<List<String>> loadTagsWithFallback({
|
|
required String baseUrl,
|
|
required String accessToken,
|
|
}) async {
|
|
return TagCacheService.getTagsWithFallback(() async {
|
|
return await FireflyApiService.fetchTags(
|
|
baseUrl: baseUrl,
|
|
accessToken: accessToken,
|
|
);
|
|
});
|
|
}
|
|
|
|
// ============ TASK BUILDER UNTUK SUBMISSION DIALOG ============
|
|
|
|
/// Bangun daftar [SubmissionTask] dari struk saat ini (sebelum kirim).
|
|
///
|
|
/// Satu task per struk: semua grup berisi item dikirim sebagai pecahan
|
|
/// (split) dari satu transaksi, ditambah satu pecahan tip bila aktif.
|
|
static List<SubmissionTask> buildTasksFromReceipt({
|
|
required List<ReceiptGroup> groups,
|
|
required String destinationAccountId,
|
|
required String destinationAccountName,
|
|
required DateTime transactionDate,
|
|
String? tipSourceAccountId,
|
|
String? tipSourceAccountName,
|
|
double? tipAmount,
|
|
List<String>? tags,
|
|
String? customerName,
|
|
String? invoiceNumber,
|
|
}) {
|
|
final groupsWithItems = groups.where((g) => g.items.isNotEmpty).toList();
|
|
if (groupsWithItems.isEmpty) return [];
|
|
|
|
return [
|
|
SubmissionTask(
|
|
taskId: 'receipt_${DateTime.now().millisecondsSinceEpoch}',
|
|
groups: groupsWithItems,
|
|
tipAmount: tipAmount,
|
|
tipSourceAccountId: tipSourceAccountId,
|
|
tipSourceAccountName: tipSourceAccountName,
|
|
destinationAccountId: destinationAccountId,
|
|
destinationAccountName: destinationAccountName,
|
|
transactionDate: transactionDate,
|
|
tags: tags,
|
|
customerName: customerName,
|
|
invoiceNumber: invoiceNumber,
|
|
),
|
|
];
|
|
}
|
|
|
|
/// Bangun daftar [SubmissionTask] dari semua local receipt yang belum
|
|
/// tersubmit. Tiap receipt menjadi satu task (satu transaksi split).
|
|
static List<SubmissionTask> buildTasksFromAllUnsubmittedReceipts(
|
|
List<LocalReceipt> receipts) {
|
|
final tasks = <SubmissionTask>[];
|
|
for (final receipt in receipts.where((r) => !r.isSubmitted)) {
|
|
if (receipt.destinationAccountId == null) continue;
|
|
final groupsWithItems =
|
|
receipt.groups.where((g) => g.items.isNotEmpty).toList();
|
|
if (groupsWithItems.isEmpty) continue;
|
|
|
|
tasks.add(SubmissionTask(
|
|
taskId: receipt.id,
|
|
groups: groupsWithItems,
|
|
tipAmount: receipt.hasTip ? receipt.tipAmount : null,
|
|
tipSourceAccountId:
|
|
receipt.hasTip ? receipt.tipSourceAccountId : null,
|
|
tipSourceAccountName:
|
|
receipt.hasTip ? receipt.tipSourceAccountName : null,
|
|
destinationAccountId: receipt.destinationAccountId!,
|
|
destinationAccountName: receipt.destinationAccountName ?? '',
|
|
transactionDate: receipt.transactionDate,
|
|
tags: receipt.tags,
|
|
customerName: receipt.customerName,
|
|
));
|
|
}
|
|
return tasks;
|
|
}
|
|
|
|
/// Kirim satu struk ke Firefly III sebagai satu transaksi split
|
|
/// (satu pecahan per grup + satu pecahan tip jika aktif).
|
|
///
|
|
/// Mengembalikan [GroupSubmissionResult] dengan status sukses/gagal.
|
|
/// Transaksi bersifat atomic: berhasil semua atau gagal semua.
|
|
static Future<GroupSubmissionResult> submitReceiptSplit({
|
|
required SubmissionTask task,
|
|
required String baseUrl,
|
|
required String accessToken,
|
|
}) async {
|
|
final result = GroupSubmissionResult(
|
|
groupId: task.taskId,
|
|
label: task.label,
|
|
amount: task.amount,
|
|
status: SubmissionStatus.pending,
|
|
);
|
|
|
|
final groups = task.groups.where((g) => g.items.isNotEmpty).toList();
|
|
if (groups.isEmpty) {
|
|
return result.copyWith(
|
|
status: SubmissionStatus.failed,
|
|
error: 'Tidak ada item',
|
|
);
|
|
}
|
|
for (final group in groups) {
|
|
if (!group.hasSource) {
|
|
return result.copyWith(
|
|
status: SubmissionStatus.failed,
|
|
error: 'Akun sumber grup "${group.sourceAccountName ?? ''}" belum dipilih',
|
|
);
|
|
}
|
|
if (group.sourceAccountId == task.destinationAccountId) {
|
|
return result.copyWith(
|
|
status: SubmissionStatus.failed,
|
|
error:
|
|
'Akun sumber grup "${group.sourceAccountName ?? ''}" sama dengan tujuan',
|
|
);
|
|
}
|
|
if (group.total <= 0) {
|
|
return result.copyWith(
|
|
status: SubmissionStatus.failed,
|
|
error:
|
|
'Total grup "${group.sourceAccountName ?? ''}" harus lebih dari 0',
|
|
);
|
|
}
|
|
}
|
|
if (task.hasTip && task.tipSourceAccountId == task.destinationAccountId) {
|
|
return result.copyWith(
|
|
status: SubmissionStatus.failed,
|
|
error: 'Akun sumber tip sama dengan tujuan',
|
|
);
|
|
}
|
|
|
|
final splits = <Map<String, dynamic>>[];
|
|
for (final group in groups) {
|
|
splits.add({
|
|
'type': 'deposit',
|
|
'source_id': group.sourceAccountId,
|
|
'destination_id': task.destinationAccountId,
|
|
'amount': group.total.toStringAsFixed(2),
|
|
'description': generateTransactionDescription(group.items),
|
|
'tags': group.tags.isNotEmpty ? group.tags : task.tags,
|
|
});
|
|
}
|
|
if (task.hasTip) {
|
|
splits.add({
|
|
'type': 'deposit',
|
|
'source_id': task.tipSourceAccountId,
|
|
'destination_id': task.destinationAccountId,
|
|
'amount': task.tipAmount!.toStringAsFixed(2),
|
|
'description': 'Tip dari kembalian',
|
|
'tags': task.tags,
|
|
});
|
|
}
|
|
|
|
// Judul transaksi (group_title): nama pembeli saja.
|
|
final customerName = task.customerName?.trim() ?? '';
|
|
final groupTitle = customerName.isNotEmpty
|
|
? customerName
|
|
: (groups.first.items.isNotEmpty
|
|
? groups.first.items.first.description
|
|
: 'Transaksi Struk Belanja');
|
|
|
|
try {
|
|
final idResult = await FireflyApiService.createSplitTransaction(
|
|
baseUrl: baseUrl,
|
|
accessToken: accessToken,
|
|
splits: splits,
|
|
date: _formatFireflyDate(task.transactionDate),
|
|
groupTitle: groupTitle,
|
|
);
|
|
|
|
if (idResult == null) {
|
|
return result.copyWith(
|
|
status: SubmissionStatus.failed,
|
|
error: 'Server mengembalikan respons null',
|
|
);
|
|
}
|
|
|
|
final txId = idResult['transactionId']?.toString();
|
|
final jId = idResult['journalId']?.toString();
|
|
return result.copyWith(
|
|
status: SubmissionStatus.success,
|
|
transactionId: (txId == null || txId == 'success') ? null : txId,
|
|
journalId: (jId == null || jId.isEmpty || jId == 'success') ? null : jId,
|
|
);
|
|
} catch (e) {
|
|
return result.copyWith(
|
|
status: SubmissionStatus.failed,
|
|
error: e.toString(),
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
/// Generate invoice gabungan (semua grup + tip) dan lampirkan sekali ke
|
|
/// transaksi Firefly III (journal pecahan pertama).
|
|
static Future<String?> attachInvoiceToReceipt({
|
|
required String baseUrl,
|
|
required String accessToken,
|
|
required String transactionId,
|
|
required String? journalId,
|
|
required List<ReceiptGroup> groups,
|
|
required DateTime transactionDate,
|
|
required String destinationAccountName,
|
|
double tipAmount = 0.0,
|
|
String? customerName,
|
|
String? invoiceNumber,
|
|
}) async {
|
|
try {
|
|
final store = await StoreConfig.fromSharedPreferences();
|
|
final items = groups.expand((g) => g.items).toList();
|
|
final resolvedCustomer = (customerName != null &&
|
|
customerName.trim().isNotEmpty)
|
|
? customerName.trim()
|
|
: (groups.isNotEmpty
|
|
? (groups.first.sourceAccountName ?? destinationAccountName)
|
|
: destinationAccountName);
|
|
|
|
final bundle = await InvoiceImageService.prepare(
|
|
storeName: store.name,
|
|
storeAddress: store.address,
|
|
adminName: store.adminName,
|
|
adminPhone: store.adminPhone,
|
|
logoPath: store.logoPath,
|
|
qrisPath: store.qrisPath,
|
|
disclaimer: store.disclaimer,
|
|
thankYouText: store.thankYouText,
|
|
pantunText: store.pantunText,
|
|
customerName: resolvedCustomer,
|
|
date: transactionDate,
|
|
items: items,
|
|
tipAmount: tipAmount,
|
|
invoiceNumber: invoiceNumber,
|
|
);
|
|
|
|
final result = await InvoiceImageService.generate(data: bundle.data);
|
|
|
|
final attachableId = (journalId != null && journalId.isNotEmpty)
|
|
? journalId
|
|
: transactionId;
|
|
final attachmentId = await FireflyApiService.createAttachment(
|
|
baseUrl: baseUrl,
|
|
accessToken: accessToken,
|
|
attachableType: 'TransactionJournal',
|
|
attachableId: attachableId,
|
|
title: 'Invoice ${bundle.invoiceNumber}',
|
|
filename: 'invoice_${bundle.invoiceNumber}.png',
|
|
);
|
|
if (attachmentId == null) return null;
|
|
|
|
final uploaded = await FireflyApiService.uploadAttachment(
|
|
baseUrl: baseUrl,
|
|
accessToken: accessToken,
|
|
attachmentId: attachmentId,
|
|
fileBytes: result.bytes,
|
|
filename: 'invoice_${bundle.invoiceNumber}.png',
|
|
);
|
|
if (!uploaded) return null;
|
|
return result.localPath;
|
|
} catch (e) {
|
|
print('attachInvoiceToReceipt error: $e');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
|
|
/// Generate invoice image dan lampirkan ke transaksi Firefly III.
|
|
static Future<String?> attachInvoiceToTransaction({
|
|
required String baseUrl,
|
|
required String accessToken,
|
|
required String transactionId,
|
|
required String? journalId,
|
|
required ReceiptGroup group,
|
|
required DateTime transactionDate,
|
|
required String destinationAccountName,
|
|
String? customerName,
|
|
}) async {
|
|
try {
|
|
final store = await StoreConfig.fromSharedPreferences();
|
|
|
|
final bundle = await InvoiceImageService.prepare(
|
|
storeName: store.name,
|
|
storeAddress: store.address,
|
|
adminName: store.adminName,
|
|
adminPhone: store.adminPhone,
|
|
logoPath: store.logoPath,
|
|
qrisPath: store.qrisPath,
|
|
disclaimer: store.disclaimer,
|
|
thankYouText: store.thankYouText,
|
|
pantunText: store.pantunText,
|
|
customerName: customerName ?? (group.sourceAccountName ?? destinationAccountName),
|
|
date: transactionDate,
|
|
items: group.items,
|
|
);
|
|
|
|
final result = await InvoiceImageService.generate(data: bundle.data);
|
|
|
|
final attachableId = (journalId != null && journalId.isNotEmpty)
|
|
? journalId
|
|
: transactionId;
|
|
final attachmentId = await FireflyApiService.createAttachment(
|
|
baseUrl: baseUrl,
|
|
accessToken: accessToken,
|
|
attachableType: 'TransactionJournal',
|
|
attachableId: attachableId,
|
|
title: 'Invoice ${bundle.invoiceNumber}',
|
|
filename: 'invoice_${bundle.invoiceNumber}.png',
|
|
);
|
|
if (attachmentId == null) return null;
|
|
|
|
final uploaded = await FireflyApiService.uploadAttachment(
|
|
baseUrl: baseUrl,
|
|
accessToken: accessToken,
|
|
attachmentId: attachmentId,
|
|
fileBytes: result.bytes,
|
|
filename: 'invoice_${bundle.invoiceNumber}.png',
|
|
);
|
|
if (!uploaded) return null;
|
|
return result.localPath;
|
|
} catch (e) {
|
|
print('attachInvoiceToTransaction error: $e');
|
|
return null;
|
|
}
|
|
}
|
|
}
|