cashumit/test/report_service_test.dart

451 lines
15 KiB
Dart

import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:cashumit/models/report_transaction.dart';
import 'package:cashumit/models/report_signature_config.dart';
import 'package:cashumit/models/saved_report.dart';
import 'package:cashumit/services/report_pdf_service.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('ReportTransaction Model Tests', () {
test('Pengeluaran without attachments should be incomplete and missing', () {
final tx = ReportTransaction(
id: '1',
date: DateTime(2026, 1, 1),
description: 'Beli kertas',
amount: 50000,
direction: 'pengeluaran',
counterpartyName: 'Toko Buku',
);
expect(tx.isPengeluaran, true);
expect(tx.isPemasukan, false);
expect(tx.hasFotoBarang, false);
expect(tx.hasInvoice, false);
expect(tx.isComplete, false);
expect(tx.attachmentStatus, 'missing');
expect(tx.tags, isEmpty);
});
test('Pengeluaran with only foto barang should have partial status', () {
final tx = ReportTransaction(
id: '2',
date: DateTime(2026, 1, 2),
description: 'Beli filament',
amount: 150000,
direction: 'pengeluaran',
counterpartyName: 'Toko 3D',
fotoBarangPath: '/path/to/foto.jpg',
);
expect(tx.hasFotoBarang, true);
expect(tx.hasInvoice, false);
expect(tx.isComplete, false);
expect(tx.attachmentStatus, 'partial');
});
test('Pengeluaran with both foto and invoice should be complete', () {
final tx = ReportTransaction(
id: '3',
date: DateTime(2026, 1, 3),
description: 'Beli komponen',
amount: 250000,
direction: 'pengeluaran',
counterpartyName: 'Toko Elektro',
fotoBarangPath: '/path/to/foto.jpg',
invoicePath: '/path/to/invoice.pdf',
invoiceType: 'pdf',
);
expect(tx.hasFotoBarang, true);
expect(tx.hasInvoice, true);
expect(tx.isComplete, true);
expect(tx.attachmentStatus, 'complete');
});
test('Pemasukan is always considered complete', () {
final tx = ReportTransaction(
id: '4',
date: DateTime(2026, 1, 4),
description: 'Jasa Cetak 3D',
amount: 300000,
direction: 'pemasukan',
counterpartyName: 'Pelanggan A',
);
expect(tx.isPemasukan, true);
expect(tx.isPengeluaran, false);
expect(tx.isComplete, true);
expect(tx.attachmentStatus, 'not_required');
});
test('Serialization and deserialization should preserve fields including tags and IDs', () {
final tx = ReportTransaction(
id: '5',
date: DateTime(2026, 1, 5, 14, 30),
description: 'Test serialisasi',
amount: 75000,
direction: 'pengeluaran',
counterpartyName: 'Toko Test',
tags: ['atk', 'upj', 'operasional'],
transactionGroupId: 'grp_123',
transactionJournalId: 'jrn_456',
fotoBarangPath: '/img.jpg',
fotoBarangName: 'img.jpg',
invoicePath: '/inv.pdf',
invoiceName: 'inv.pdf',
invoiceType: 'pdf',
invoiceSnapshotPath: '/inv_snapshot.png',
);
final json = tx.toJson();
final fromJson = ReportTransaction.fromJson(json);
expect(fromJson.id, tx.id);
expect(fromJson.date, tx.date);
expect(fromJson.description, tx.description);
expect(fromJson.amount, tx.amount);
expect(fromJson.direction, tx.direction);
expect(fromJson.counterpartyName, tx.counterpartyName);
expect(fromJson.tags, ['atk', 'upj', 'operasional']);
expect(fromJson.transactionGroupId, 'grp_123');
expect(fromJson.transactionJournalId, 'jrn_456');
expect(fromJson.fotoBarangPath, tx.fotoBarangPath);
expect(fromJson.invoicePath, tx.invoicePath);
expect(fromJson.invoiceType, tx.invoiceType);
expect(fromJson.invoiceSnapshotPath, tx.invoiceSnapshotPath);
expect(fromJson.hasInvoiceSnapshot, true);
});
test('copyWith creates modified copy correctly', () {
final tx = ReportTransaction(
id: '6',
date: DateTime(2026, 1, 6),
description: 'Asli',
amount: 10000,
direction: 'pengeluaran',
counterpartyName: 'Lawan',
tags: ['lama'],
);
final modified = tx.copyWith(
description: 'Diubah',
amount: 25000,
tags: ['baru', 'upj'],
invoiceSnapshotPath: '/snapshot.png',
);
expect(modified.id, '6');
expect(modified.description, 'Diubah');
expect(modified.amount, 25000);
expect(modified.tags, ['baru', 'upj']);
expect(modified.invoiceSnapshotPath, '/snapshot.png');
expect(modified.hasInvoiceSnapshot, true);
expect(tx.description, 'Asli'); // Original unchanged
expect(tx.hasInvoiceSnapshot, false);
});
});
group('ReportSignatureConfig Tests', () {
setUp(() {
SharedPreferences.setMockInitialValues({});
});
test('Default values should match 4 signatories in Laporan Keuangan 2026 - Agustus.pdf', () async {
final config = await ReportSignatureConfig.load();
expect(config.reportTitle, 'LAPORAN KEUANGAN UPJ MEKATRONIKA');
expect(config.city, 'Malang');
// 1. Pelaksana UPJ (Kiri Atas)
expect(config.signatory1Name, 'Anggoro Dwi Nur Rohman, S.Pd, M.T');
expect(config.signatory1Nip, 'NNIP. 20533816260193012106054');
// 2. Pembuat Laporan (Kanan Atas)
expect(config.signatory2Name, 'Rama Dwi Nofanto Setiawan');
// 3. Koordinator UPJ (Kiri Bawah)
expect(config.signatory3Name, 'Puguh Hartono, SPd');
expect(config.signatory3Nip, 'NIP. 197003152003121009');
// 4. KPA BLUD SMKN 4 Malang (Kanan Bawah)
expect(config.signatory4Name, 'Drs. Gunawan Dwiyono, S.ST, M.Pd');
expect(config.signatory4Nip, 'NIP. 19670930 199512 1 002');
});
test('Custom 4-signatory values can be saved and loaded', () async {
const customConfig = ReportSignatureConfig(
reportTitle: 'LAPORAN KAS TAHUNAN',
city: 'Surabaya',
signatory1Role: 'Wakil Kepala Sekolah',
signatory1Name: 'Budi Santoso, M.Pd',
signatory1Nip: '198001012005011001',
signatory2Role: 'Penyusun',
signatory2Name: 'Ahmad Dahlan',
signatory2Nip: '09876',
signatory3Role: 'Kepala Program Keahlian',
signatory3Name: 'Siti Aminah, S.T.',
signatory3Nip: '198502022010022002',
signatory4Role: 'Kepala SMKN 1',
signatory4Name: 'Dr. H. Sulaiman, M.M.',
signatory4Nip: '196503031990031003',
);
await customConfig.save();
final loaded = await ReportSignatureConfig.load();
expect(loaded.reportTitle, 'LAPORAN KAS TAHUNAN');
expect(loaded.city, 'Surabaya');
expect(loaded.signatory1Name, 'Budi Santoso, M.Pd');
expect(loaded.signatory2Name, 'Ahmad Dahlan');
expect(loaded.signatory3Name, 'Siti Aminah, S.T.');
expect(loaded.signatory4Name, 'Dr. H. Sulaiman, M.M.');
});
test('JSON serialization works correctly', () {
const config = ReportSignatureConfig();
final json = config.toJson();
final fromJson = ReportSignatureConfig.fromJson(json);
expect(fromJson.reportTitle, config.reportTitle);
expect(fromJson.signatory1Name, config.signatory1Name);
expect(fromJson.signatory4Name, config.signatory4Name);
});
});
group('SavedReport Model Tests', () {
test('Calculates financial summary and attachment counts accurately', () {
final tx1 = ReportTransaction(
id: '1',
date: DateTime(2026, 8, 1),
description: 'Pemasukan UPJ',
amount: 1000000,
direction: 'pemasukan',
counterpartyName: 'Pelanggan',
);
final tx2 = ReportTransaction(
id: '2',
date: DateTime(2026, 8, 2),
description: 'Beli Pylox',
amount: 65000,
direction: 'pengeluaran',
counterpartyName: 'Toko Seng Jaya',
fotoBarangPath: '/foto.jpg',
invoicePath: '/nota.pdf',
invoiceType: 'pdf',
);
final tx3 = ReportTransaction(
id: '3',
date: DateTime(2026, 8, 3),
description: 'Beli PCB',
amount: 8000,
direction: 'pengeluaran',
counterpartyName: 'Toko Elektronika',
);
final saved = SavedReport(
id: 'sr_001',
title: 'Laporan Kas Agustus 2026',
accountId: 'acc_1',
accountName: 'Dompet UPJ',
startDate: DateTime(2026, 8, 1),
endDate: DateTime(2026, 8, 31),
createdAt: DateTime(2026, 8, 31),
updatedAt: DateTime(2026, 8, 31),
transactions: [tx1, tx2, tx3],
);
expect(saved.totalPemasukan, 1000000);
expect(saved.totalPengeluaran, 73000);
expect(saved.saldoBersih, 927000);
expect(saved.totalPengeluaranCount, 2);
expect(saved.completedAttachmentCount, 1);
expect(saved.incompleteAttachmentCount, 1);
// Test JSON serialization
final json = saved.toJson();
final restored = SavedReport.fromJson(json);
expect(restored.id, saved.id);
expect(restored.title, saved.title);
expect(restored.accountName, 'Dompet UPJ');
expect(restored.transactions.length, 3);
expect(restored.saldoBersih, 927000);
});
});
group('ReportPdfService Tests', () {
test('paginateTransactions guarantees at least 5 rows on the last page', () {
// 1. Sedikit transaksi (10 items) -> 1 halaman
final list10 = List.generate(
10,
(i) => ReportTransaction(
id: '$i',
date: DateTime(2026, 8, 1),
description: 'Item $i',
amount: 10000,
direction: 'pengeluaran',
counterpartyName: 'Toko $i',
),
);
final paged10 = ReportPdfService.paginateTransactions(list10);
expect(paged10.length, 1);
expect(paged10[0].length, 10);
// 2. Transaksi batas 1 halaman (28 items) -> 1 halaman
final list28 = List.generate(
28,
(i) => ReportTransaction(
id: '$i',
date: DateTime(2026, 8, 1),
description: 'Item $i',
amount: 10000,
direction: 'pengeluaran',
counterpartyName: 'Toko $i',
),
);
final paged28 = ReportPdfService.paginateTransactions(list28);
expect(paged28.length, 1);
expect(paged28[0].length, 28);
// 3. Transaksi 30 items -> 2 halaman: Page 1 = 25, Page 2 = 5!
final list30 = List.generate(
30,
(i) => ReportTransaction(
id: '$i',
date: DateTime(2026, 8, 1),
description: 'Item $i',
amount: 10000,
direction: 'pengeluaran',
counterpartyName: 'Toko $i',
),
);
final paged30 = ReportPdfService.paginateTransactions(list30);
expect(paged30.length, 2);
expect(paged30[0].length, 25);
expect(paged30[1].length, 5); // Minimal 5 di lembar terakhir!
// 4. Transaksi 34 items (sesuai dokumen acuan) -> 2 halaman: Page 1 = 29, Page 2 = 5!
final list34 = List.generate(
34,
(i) => ReportTransaction(
id: '$i',
date: DateTime(2026, 8, 1),
description: 'Item $i',
amount: 10000,
direction: 'pengeluaran',
counterpartyName: 'Toko $i',
),
);
final paged34 = ReportPdfService.paginateTransactions(list34);
expect(paged34.length, 2);
expect(paged34[0].length, 29);
expect(paged34[1].length, 5); // Lembar terakhir persis 5 transaksi!
// 5. Transaksi 45 items -> 2 halaman: Page 1 = 34, Page 2 = 11 (>= 5)
final list45 = List.generate(
45,
(i) => ReportTransaction(
id: '$i',
date: DateTime(2026, 8, 1),
description: 'Item $i',
amount: 10000,
direction: 'pengeluaran',
counterpartyName: 'Toko $i',
),
);
final paged45 = ReportPdfService.paginateTransactions(list45);
expect(paged45.length, 2);
expect(paged45[0].length, 34);
expect(paged45[1].length, 11);
expect(paged45.last.length >= 5, true);
});
test('Generate PDF with 4 signatories grid 2x2 and Firefly links without throwing errors', () async {
final txList = [
ReportTransaction(
id: '1',
date: DateTime(2026, 8, 1),
description: 'Pemasukan Kas Awal',
amount: 1000000,
direction: 'pemasukan',
counterpartyName: 'Dompet UPJ',
transactionGroupId: '101',
),
ReportTransaction(
id: '2',
date: DateTime(2026, 8, 2),
description: 'Beli Pylox blackflat Dan white',
amount: 65000,
direction: 'pengeluaran',
counterpartyName: 'Toko Seng Jaya',
transactionGroupId: '102',
),
];
const config = ReportSignatureConfig();
final tempFile = File('${Directory.systemTemp.path}/test_report_4sig_${DateTime.now().millisecondsSinceEpoch}.pdf');
final pdfFile = await ReportPdfService.generateReportPdf(
transactions: txList,
startDate: DateTime(2026, 8, 1),
endDate: DateTime(2026, 8, 3),
config: config,
accountName: 'Dompet UPJ',
targetFile: tempFile,
fireflyBaseUrl: 'http://100.98.70.89:8080',
);
expect(pdfFile, isNotNull);
expect(pdfFile.existsSync(), true);
expect(pdfFile.lengthSync(), greaterThan(1000));
// Clean up test file
if (pdfFile.existsSync()) {
pdfFile.deleteSync();
}
});
test('Generate PDF with 34 transactions splits into 2 pages with 5 on last page', () async {
final txList = List.generate(
34,
(i) => ReportTransaction(
id: '$i',
date: DateTime(2026, 8, i + 1 > 31 ? 31 : i + 1),
description: 'Transaksi ke-$i',
amount: (i + 1) * 10000.0,
direction: i % 3 == 0 ? 'pemasukan' : 'pengeluaran',
counterpartyName: 'Lawan $i',
transactionGroupId: 'group_$i',
),
);
const config = ReportSignatureConfig();
final tempFile = File('${Directory.systemTemp.path}/test_report_34tx_${DateTime.now().millisecondsSinceEpoch}.pdf');
final pdfFile = await ReportPdfService.generateReportPdf(
transactions: txList,
startDate: DateTime(2026, 8, 1),
endDate: DateTime(2026, 8, 31),
config: config,
accountName: 'Dompet UPJ',
targetFile: tempFile,
fireflyBaseUrl: 'http://100.98.70.89:8080',
);
expect(pdfFile, isNotNull);
expect(pdfFile.existsSync(), true);
expect(pdfFile.lengthSync(), greaterThan(2000));
// Clean up test file
if (pdfFile.existsSync()) {
pdfFile.deleteSync();
}
});
});
}