451 lines
15 KiB
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();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|