Kantong Tabungan Kolaboratif & Kontrol Akses
Arsitektur multi-kantong tabungan (multi-tenant) yang memisahkan saldo, anggaran, dan transaksi per kantong serta mendukung kolaborasi antar-pengguna lewat undangan.
Membaca sekitar 2 menit
Arsitektur multi-kantong tabungan (multi-tenant) yang memisahkan saldo, anggaran, dan transaksi per kantong serta mendukung kolaborasi antar-pengguna lewat undangan.
Relasi antara pengguna dan kantong dijembatani oleh tabel akses berbasis peran ('owner' atau 'member') dan tabel undangan, lengkap dengan agregasi subquery untuk menghitung ringkasan saldo bulanan dan total.
Aksi sensitif seperti mengubah target, menghapus tabungan, atau mengeluarkan anggota dikunci ketat di level query hanya untuk pemilik ('owner') dengan proteksi agar tidak bisa menghapus dirinya sendiri.
Masalahnya dulu
Keuangan seseorang jarang hanya terkumpul di satu tempat: ada tabungan pribadi, dana darurat/pertumbuhan, hingga tabungan bersama pasangan atau keluarga. Jika seluruh transaksi dicampur di bawah satu ID pengguna, mustahil memisahkan laporan per tujuan atau mengajak orang lain ikut mencatat di satu tabungan tertentu tanpa membuka seluruh data pribadi lainnya.
Relasi Many-to-Many lewat Tabel Akses dan Undangan
Di MyPocket, entitas utama penyimpan uang adalah kantong tabungan yang tidak memiliki kolom pemilik tunggal. Sebagai gantinya, hak akses diatur melalui tabel penghubung akses dengan dua peran: 'owner' dan 'member'. Pemilik kantong dapat mengundang pengguna lain berdasarkan email melalui mekanisme undangan yang dipantau secara berkala oleh klien menggunakan SWR.
Memisahkan kantong tabungan dari tabel pengguna mengubah aplikasi yang tadinya personal menjadi ruang kolaborasi multi-pengguna.
Implementasi di ngodingan ini
Saat halaman daftar tabungan dimuat, server mengambil seluruh kantong yang boleh diakses pengguna beserta ringkasan pemasukan dan pengeluaran sepanjang waktu maupun bulan berjalan hanya dalam satu query JOIN agregasi:
const rows = await query(
`SELECT b.uuid, b.name, b.desc, b.badge, b.target,
aa.role,
COALESCE(allInc.s, 0) totalIncome,
COALESCE(allExp.s, 0) totalExpense,
COALESCE(monInc.s, 0) monthIncome,
COALESCE(monExp.s, 0) monthExpense
FROM allowedaccess aa
JOIN bank b ON b.uuid = aa.uuidbank
LEFT JOIN (SELECT uuidbank, SUM(nominal) s FROM cashflowincome GROUP BY uuidbank) allInc ON allInc.uuidbank = b.uuid
LEFT JOIN (SELECT uuidbank, SUM(total) s FROM cashflowexpense GROUP BY uuidbank) allExp ON allExp.uuidbank = b.uuid
LEFT JOIN (SELECT uuidbank, SUM(nominal) s FROM cashflowincome WHERE DATE_FORMAT(incomedate,'%Y-%m')=? GROUP BY uuidbank) monInc ON monInc.uuidbank = b.uuid
LEFT JOIN (SELECT uuidbank, SUM(total) s FROM cashflowexpense WHERE DATE_FORMAT(expensedate,'%Y-%m')=? GROUP BY uuidbank) monExp ON monExp.uuidbank = b.uuid
WHERE aa.uuiduser = ?
ORDER BY b.createdat DESC`,
[month, month, user.uuid]
);Jebakan dan detail yang harus dijaga
- Mencegah duplikasi undangan dan anggota: sebelum menyimpan undangan baru, server memvalidasi berurutan apakah email tujuan terdaftar, bukan dirinya sendiri, belum menjadi anggota di kantong tersebut, dan tidak sedang memiliki undangan berstatus pending.
- Menggunakan klausa ON DUPLICATE KEY UPDATE saat penerima menyetujui undangan agar operasi penambahan hak akses tetap idempoten jika terjadi klik ganda.