# ERP Payroll Amendment Closed Period: Koreksi Tanpa Edit

> Cara mengoreksi payroll yang sudah paid tanpa mengubahnya: amendment record menghubungkan run lama ke revision draft, lengkap dengan alasan, pembuat, dan audit trail.

URL: https://www.matthewswong.com/id/blog/erp-payroll-amendment-correct-closed-period/

- Author: Matthews Wong (https://www.matthewswong.com/id/about/)
- Published: 2026-10-17
- Updated: 2026-10-17
- Category: ERP

> **Key takeaway:** Untuk mengoreksi payroll run yang sudah closed, jangan edit run-nya. Buat amendment record yang menghubungkan run paid ke revision run draft baru, dengan alasan wajib dan nama pembuat, lalu proses revision lewat alur approval biasa. Run asli tetap immutable, dan audit trail menunjukkan apa yang berubah serta alasannya.

Sebuah payroll run sudah berstatus paid, file bank sudah terkirim, lalu ada yang menemukan satu baris yang salah. Solusi yang menggoda adalah satu statement UPDATE pada run yang sudah closed. Cukup sepuluh detik, tetapi diam-diam payslip, file bank, dan database jadi tidak sepakat soal apa yang terjadi di bulan itu.

Tulisan ini menjelaskan cara saya memodelkan koreksi di modul payroll sebuah ERP yang saya bangun, yang pada case study ANCoraPRO di site ini dibahas di level AP dan AR. Semuanya soal desain: tabel, state, dan invariant. Saya memakai nama generik dan contoh kecil yang ditulis ulang, dan sengaja tidak ada angka pajak, tarif, atau gaji.

## Run yang sudah paid itu bukti, bukan dokumen yang bisa diedit

Begitu uang keluar, payroll run bukan lagi dokumen kerja, melainkan catatan dari sesuatu yang sudah terjadi. Payslip diterbitkan darinya, disbursement dibuat darinya, dan finance mungkin sudah membukukannya. Kalau barisnya diedit sesudahnya, semua artefak itu menunjuk ke angka yang sudah tidak ada.

Koreksi harus berupa fakta baru, bukan fakta lama yang diubah. Itu aturan yang sama dengan jurnal pembalik di akuntansi, dan alasan yang sama mengapa post audit trail di site ini menekankan history yang append-only. Pertanyaannya tinggal bagaimana membuat fakta baru bisa ditemukan dari fakta lama.

## State machine menentukan di mana temboknya

Payroll period saya punya lima state: draft, submitted, approved, paid, dan cancelled. Temboknya dijaga oleh code di status, bukan oleh komentar. Setiap transisi memeriksa state yang diharapkan, dan edit item ditolak kecuali period-nya draft. Aturan yang saya andalkan:

- Hanya period draft yang itemnya boleh diedit, jadi run submitted, approved, atau paid otomatis read-only.
- Hanya draft yang bisa di-submit, hanya run submitted yang bisa di-approve, dan hanya run approved yang bisa ditandai paid.
- Menandai paid mengubah period dan semua item-nya menjadi paid dalam satu database transaction, sehingga tidak ada run yang setengah paid.
- Amend hanya boleh dari paid. State lain ditolak, sehingga jalur amendment tidak pernah bisa dipakai untuk menghindari alur normal.

Akibatnya, fitur amendment tidak butuh pengecualian dari aturan immutability. Ia tidak mengedit run paid, ia membuat draft, dan draft adalah satu-satunya state tempat edit itu sah.

## Data model: dua run dan satu baris penghubung

Amendment adalah payroll period kedua ditambah satu baris kecil yang mengikatnya ke yang pertama. Baris penghubung itu membawa tiga hal yang ditanyakan auditor: run mana yang dikoreksi, kenapa, dan siapa yang melakukannya. Berikut bentuknya, ditulis ulang dengan nama generik.

Ada tiga detail penting. Foreign key revision bersifat unique, jadi satu revision run hanya milik satu amendment, sementara satu original bisa punya banyak amendment dari waktu ke waktu. Relasi ke original tidak memakai cascade, dan Prisma memberi default Restrict pada onDelete untuk relasi wajib, sehingga original yang punya amendment tidak bisa terhapus tanpa sengaja. Lalu revision memakai year dan month yang sama, yang bentrok dengan composite unique key, jadi ia membawa label yang berbeda.

Poin terakhir ada kerumitan di sisi database. Dokumentasi PostgreSQL menyebut bahwa secara default dua nilai NULL tidak dianggap sama dalam perbandingan unique. Run reguler dengan label NULL karena itu tidak terlindungi oleh composite key tersebut saja, sebab itu revision mengisi label non-null dan saya juga menyimpan run number unique terpisah.

Beri revision run number sendiri yang dibentuk dari original, misalnya nomor original ditambah suffix AMEND dan counter. Orang yang membaca mutasi bank atau tiket jadi bisa melihat garis keturunannya tanpa membuka database.

## Membuat amendment sebagai clone dari run paid

Langkah pembuatan adalah satu service method di balik permission-nya sendiri. Di controller saya, ia berada di belakang approve guard, bukan prepare guard, jadi orang yang boleh meng-approve payroll adalah orang yang boleh membukanya kembali sebagai revision. Ia memvalidasi tiga hal sebelum menulis apa pun: original ada, statusnya paid, dan alasan tidak kosong.

Setelah itu ia meng-clone setiap baris original ke run draft baru. Tiap baris hasil clone diberi warning marker, dalam kasus saya bernama AMENDMENT_CLONE, supaya reviewer yang membuka draft langsung tahu baris itu warisan dan masih perlu dicek. Total run dihitung ulang dari baris hasil clone, lalu satu baris history dan satu audit entry tier critical mencatat alasannya.

Berikut alurnya, disederhanakan. Penomoran membaca jumlah amendment yang ada lalu menambah satu. Dua staf yang berebut akan menghitung nomor yang sama, dan run number yang unique mengubah tabrakan itu menjadi respons conflict, bukan duplikat.

## Kenapa full clone, dan biaya alternatifnya

Saya memilih full clone dari run sebagai revision. Itu keputusan desain, bukan hasil pengukuran: saya tidak mem-benchmark alternatifnya, dan tabel di bawah adalah penalaran saya tentang apa yang dilakukan tiap opsi pada run yang closed dan pada pertanyaan audit.

Pendekatan

Nasib run yang closed

Bisa menjawab apa yang berubah dan kenapa

Biaya utama

|  |  |  |  |
| --- | --- | --- | --- |
| Edit run paid langsung | Tertimpa, payslip dan file bank jadi tidak cocok dengannya | Hanya jika ada log terpisah dan tidak ada yang melewatinya | Menghancurkan bukti yang nanti dibutuhkan |
| Reverse dan rebook (run negatif) | Tidak disentuh, run kedua membatalkannya | Ya, tapi harus membaca dua run untuk melihat efek netto | Dua approval untuk satu koreksi, melelahkan bagi approver |
| Delta run (hanya baris yang berubah) | Tidak disentuh | Ya, delta persis sama dengan perubahannya | Setiap report harus tahu menambahkan delta ke original |
| Full clone sebagai revision draft (yang saya bangun) | Tidak disentuh, dihubungkan lewat baris amendment | Ya: alasan, pembuat, dan perbandingan baris per baris | Report yang menjumlahkan run bisa menghitung satu bulan dua kali kecuali mengikuti link |

Saya memilih clone karena reviewer melihat payroll run yang lengkap dan biasa, dan bisa memakai layar, rantai approval, dan export yang sama dengan run lain. Harganya ada di baris terakhir tabel, dan itu harga yang nyata.

## Kegagalan yang dicegah desain ini, dan yang masih tersisa

Tanpa baris penghubung, kegagalan yang paling mungkin bukan niat jahat, melainkan kesunyian. Seseorang memperbaiki satu baris, angka barunya benar, dan enam bulan kemudian tidak ada yang bisa menjelaskan kenapa run Maret berbeda dari yang dibayarkan ke karyawan. Ini penalaran saya tentang failure mode, bukan insiden yang saya laporkan. Baris amendment mengubah pertanyaan itu menjadi sekadar lookup.

Kegagalan yang masih tersisa ada di implementasi saya sendiri. Method create menjalankan insert period, insert tiap baris, update total, baris penghubung, dan baris history sebagai statement terpisah. Sementara mark-paid di service yang sama memakai database transaction. Kalau proses mati setelah period terbentuk tetapi sebelum baris penghubung, hasilnya draft run yang tidak ditunjuk oleh apa pun.

Buat revision run, baris hasil clone, dan baris penghubung dalam satu transaction. Draft yatim yang tampak seperti run biasa tapi tanpa baris amendment lebih buruk daripada tanpa amendment, karena ia tidak membawa alasan maupun original.

## Keterbatasan dan apa yang akan saya ubah

Unit spec untuk jalur amendment mencakup dua perilaku: original yang tidak paid ditolak, dan yang paid di-clone dengan marker di tiap baris. Spec itu tidak mencakup alasan kosong, jalur conflict, maupun report. Ini celah yang akan saya tutup, plus pertanyaan yang tidak dijawab oleh code:

- Bungkus pembuatan dalam satu transaction seperti di atas, dan tambah test yang mematikan proses di antara statement.
- Clone menyalin nominal yang tersimpan di tiap baris tetapi tidak menyalin link ke record cicilan pinjaman, jadi state cicilan tetap di baris original. Putuskan dengan sengaja apa yang harus dilakukan revision pada potongan yang sudah lunas.
- Clone dimulai tanpa disbursement request. Apakah revision dibayar penuh atau hanya selisihnya terhadap original adalah pertanyaan kebijakan yang tidak dijawab oleh schema, dan membayarnya penuh karena salah paham berarti membayar dua kali.
- Setiap report yang menjumlahkan run per bulan butuh aturan eksplisit: pakai revision terbaru, atau original ditambah delta-nya, tetapi jangan keduanya.

Saya belum mengukur apa pun di bawah beban, dan jalur amendment secara alami jarang dipakai. Anggap desain ini kuat di invariant dan belum terbukti di volume.

## Checklist Senin untuk modul payroll Anda

Daftar semua state run Anda dan pastikan edit item ditolak di setiap state kecuali draft.

Tambah tabel amendment berisi run original, revision run yang unique, alasan wajib, pembuat, dan timestamp.

Batasi amend hanya untuk run paid, dan taruh di belakang permission approve, bukan permission prepare.

Clone ke draft dalam satu transaction, tandai tiap baris hasil clone, lalu hitung ulang total.

Tulis ke history run sekaligus audit log, dan sertakan alasan di keduanya.

Putuskan secara tertulis bagaimana amendment dibayar dan bagaimana report menghindari penghitungan bulan yang sama dua kali.

Payroll run yang sudah closed adalah catatan, jadi koreksilah dengan menambah revision yang terhubung, bukan dengan mengubahnya. Baris amendment hanya butuh satu tabel kecil dan satu transaction, dan ia mengubah siapa yang mengubah ini dan kenapa dari investigasi menjadi query.

## Sumber dan bacaan lanjutan

- Dokumentasi PostgreSQL: constraints, termasuk unique dan foreign key
- Dokumentasi PostgreSQL: transactions
- Dokumentasi Prisma: referential actions
- Dokumentasi Prisma: transactions dan nested writes
- Dokumentasi Prisma: relations

## Source links

- https://www.postgresql.org/docs/current/ddl-constraints.html
- https://www.postgresql.org/docs/current/tutorial-transactions.html
- https://www.prisma.io/docs/orm/v7/prisma-schema/data-model/relations/referential-actions
- https://www.prisma.io/docs/orm/v7/prisma-client/queries/transactions
- https://www.prisma.io/docs/orm/v7/prisma-schema/data-model/relations

## FAQ

### Bagaimana cara mengoreksi payroll run yang sudah paid?

Jangan edit run yang sudah paid. Buat amendment: payroll run draft baru hasil clone dari run paid, ditambah baris penghubung yang menyimpan run asli, alasan, dan pembuatnya. Perbaiki baris di draft lalu proses lewat langkah submit dan approve biasa.

### Kenapa payroll period yang sudah closed harus immutable?

Payslip, disbursement bank, dan jurnal akuntansi semuanya dibuat dari run itu. Kalau diedit sesudahnya, artefak tersebut menunjuk ke angka yang sudah tidak ada. Run yang immutable menjaga satu catatan tepercaya tentang apa yang benar-benar dibayarkan.

### Apa bedanya payroll amendment dengan reversal?

Reversal membukukan run kedua yang negatif untuk membatalkan yang pertama, dan angka yang benar datang dari run ketiga. Amendment menghubungkan satu revision draft ke original dan membandingkannya baris per baris. Keduanya tidak menyentuh run yang closed, tetapi amendment hanya butuh satu approval untuk satu koreksi.

### Field apa saja yang harus ada di tabel payroll amendment?

Minimal: id run original, id revision run yang unique, alasan wajib, pembuat, dan timestamp pembuatan. Jadikan id revision unique agar satu revision hanya milik satu amendment, dan hindari cascade delete pada relasi ke original.

### Apakah report bisa menghitung satu bulan dua kali jika ada amendment?

Bisa, jika report hanya menjumlahkan semua run dalam sebulan dan amendment adalah full clone dari original. Tentukan aturan sejak awal: pakai hanya revision terbaru, atau original ditambah delta, dan pastikan semua report mengikuti link amendment.

---

- HTML version: https://www.matthewswong.com/id/blog/erp-payroll-amendment-correct-closed-period/
- English: https://www.matthewswong.com/en/blog/erp-payroll-amendment-correct-closed-period/
- Site index for agents: https://www.matthewswong.com/llms.txt
- Sitemap: https://www.matthewswong.com/sitemap.xml
- API description (OpenAPI): https://www.matthewswong.com/openapi.json
