Backend Import and Payroll Findings
Summary
This review identified class-loading failures, missing authorization checks, payroll calculation inconsistencies, unsafe null handling, import data integrity problems, and export compatibility issues.
High
1. SekolahImport cannot load because methods are declared twice (DONE)
File: app/Jobs/Imports/SekolahImport.php
SekolahImport declares data_tagihan() twice and data_pembayaran_term()
twice:
data_tagihan(): line 612 and line 970data_pembayaran_term(): line 738 and line 1096
PHP fails with:
Cannot redeclare App\Jobs\Imports\SekolahImport::data_tagihan()
This prevents the class from loading. Imports dispatched through
SekolahImport, including data_mata_pelajaran, data_guru, data_nilai,
data_tagihan, data_pembayaran_term, and data_pembayaran_siswa, will fail.
Recommendation: Remove or merge the duplicate method declarations and keep one authoritative implementation for each import method.
2. Jabatan import uses DB without importing the facade (DONE)
File: app/Jobs/Imports/MasterDataImports.php:235
The code calls:
DB::beginTransaction();
but the file does not import:
use Illuminate\Support\Facades\DB;
Because the class namespace is App\Jobs\Imports, PHP attempts to resolve
App\Jobs\Imports\DB, causing a class-not-found error whenever
data_jabatan is imported.
Recommendation: Add the missing facade import.
3. Payroll recount endpoint lacks company ownership validation (DONE)
File: app/Http/Controllers/Company/PayrollController.php:1098
The endpoint loads payroll only by ID:
$payroll = Payroll::where('payroll_id', $payrollId)->first();
The query is not constrained by the authenticated company:
->where('perusahaan_id', $data_perusahaan->perusahaan_id)
If an authenticated company user submits another company’s payroll_id, the
endpoint may recalculate and modify another tenant’s payroll. The same missing
ownership check exists before loading the employee and division.
Recommendation: Scope every payroll, employee, division, and related record lookup to the authenticated company before performing any recalculation or update.
4. Payroll totals may include job allowances inconsistently
Files: app/Http/Controllers/Company/PayrollController.php:1038-1054,
:1229-1233, and recalculatePayrollTotals()
The new helper adds tunjangan_total to payroll_total, but the stored
payroll_kompensasi value excludes it:
$payroll->payroll_kompensasi = $calc['kompensasi_total'];
$payroll->payroll_total = $calc['total'];
This creates a mismatch:
payroll_total != base salary + payroll_kompensasi - payroll_potongan
Additionally, ubah_status() calculates allowance data before calling the
helper, but only returns it in the response. The allowance is not persisted as
a separate payroll component. Later recalculations can therefore change
historical payroll totals when the employee’s current job allowances change.
Recommendation: Define one canonical payroll calculation model. Persist job allowances as payroll components or snapshot values, and ensure all stored totals reconcile with their component fields.
5. Payroll recalculation can fail when the employee has no job relation
File: app/Http/Controllers/Company/PayrollController.php
The new logic contains:
foreach ($karyawan->jabatan->tunjangan ?? [] as $t)
If Karyawan::find() returns null, or if jabatan is null, dereferencing
$karyawan->jabatan can fail before the null-coalescing fallback is applied.
The same pattern appears in recalculatePayrollTotals() and both status
methods.
Recommendation: Validate the employee before use and access the relation
through a guarded path, for example $karyawan?->jabatan?->tunjangan ?? [],
with an explicit failure response when the employee is required.
6. Manual PPH/BPJS override fields may not exist in the database
File: app/Http/Controllers/Company/PayrollController.php:1028-1035
The code indicates that the fields require a migration:
// tambahkan via migration jika belum
No migration or model/schema definition was found for:
pph_amount_overridebpjs_jht_amount_override
If these columns are absent in the deployed database, save() will fail with
an undefined-column database error.
Recommendation: Verify the deployed schema and add a migration before releasing code that reads or writes these fields. Confirm the migration has been applied in every environment.
7. Lembur export can fail when user_level is an integer (IGNORE)
File: app/Jobs/Exports/ProcessExportPengajuan.php:107 and :195
The code assumes:
$userLevelId['level_id']
However, export payloads accept user_level in multiple shapes, and the
previous implementation passed it directly as an ID. If the payload contains
an integer, PHP raises:
Cannot access offset of type int on int
The change also ignores user_level inside $filters and reads only from the
top-level $payload.
Recommendation: Normalize the payload shape before use, supporting the
documented integer and object/array forms, and define precedence between
$filters and $payload.
Medium
8. Task import can leave an orphan task if pivot insertion fails
File: app/Jobs/Imports/TugasImport.php:121-147
The task is inserted first, followed by employee pivot rows. There is no transaction. If a pivot insert fails, the catch block reports failure but the task remains in the database without its intended assignments.
Recommendation: Wrap task creation and pivot insertion in one database transaction and roll back both when any assignment fails.
9. Task import silently accepts unknown employees
File: app/Jobs/Imports/TugasImport.php:133-149
Employee names are loaded with:
->whereIn('karyawan_nama', $karyawanNames)
Missing names are ignored. The import can report success even though some requested employees were not attached, leaving an incomplete assignment set.
Recommendation: Compare requested names with matched names and reject or explicitly report every unknown employee before committing the task.
10. Task import may reject valid status values from existing templates
File: app/Jobs/Imports/TugasImport.php:54-55
The accepted statuses were reduced to:
['draft', 'inprogress', 'finished']
Existing templates or clients using published, done, approved, or
rejected are silently converted to draft and recorded as skipped. This can
alter task workflow state instead of clearly rejecting the row.
Recommendation: Preserve the established status vocabulary or provide an explicit, documented mapping. Do not silently change workflow state.
11. Payment imports use ILIKE without wildcards
File: app/Jobs/Imports/SekolahImport.php:870 and :883
These queries use:
->where('karyawan_nama', 'ilike', $namaSiswa)
->where('pb_term_name', 'ilike', $jenisPembayaran)
ILIKE without % behaves as case-insensitive equality, not a partial
match. This is inconsistent with the tagihan lookup and can fail when the
spreadsheet contains extra spacing or formatting differences.
Recommendation: Normalize spreadsheet values and define whether matching should be exact or partial. If partial matching is intended, add controlled wildcards and avoid ambiguous matches.
12. Payment import allows ambiguous partial tagihan matches
File: app/Jobs/Imports/SekolahImport.php:898-900
The query uses a partial match followed by first():
->where('tagihan_nama', 'ilike', '%' . $namaTagihan . '%')
->first()
If multiple bills contain the same text, the first arbitrary match is selected. A payment may therefore be linked to the wrong bill.
Recommendation: Match using a unique identifier where possible. Otherwise, require exactly one match and report ambiguous rows as failures.
13. Duplicate rows within one spreadsheet are not reliably prevented
The importers check the database before inserting, but do not maintain an in-memory set or consistently use a unique constraint. Duplicate rows in one import can both pass the existence check before either is inserted, depending on execution and database constraints.
This affects at least:
- tagihan imports
- payment imports
- kasbon imports
- claim/beban imports
- leave-cashout imports
Recommendation: Define a row identity for each importer, reject duplicate keys within the input batch, and enforce matching database uniqueness where appropriate.
14. Leave-cashout import does not verify leave balance or cuti_id
File: app/Jobs/Imports/PengajuanImport.php, data_pencairan_cuti
The importer creates records with:
'cuti_id' => null
It validates only employee, date, quantity, description, and status. If existing application logic expects a linked leave record or checks available leave balance, imported records can be invalid or impossible to process later.
Recommendation: Resolve and validate the related leave record, verify the available balance, and reject rows that cannot be linked safely.
15. Beban import rejects formatted currency values despite adding a parser
File: app/Jobs/Imports/PengajuanImport.php
parseNumber() was added, but data_beban still checks:
if (empty($nominalRaw) || !is_numeric($nominalRaw))
Values such as Rp 1.000.000 or 1,000.00 are rejected even though the new
parser appears intended to support formatted values.
Recommendation: Parse and validate the normalized numeric value, rather
than validating the raw formatted string with is_numeric().
16. Jabatan salary parsing misinterprets decimal values
File: app/Jobs/Imports/MasterDataImports.php:229-231
The parser uses:
str_replace(['.', ','], ['', '.'], ...)
This is not locale-safe. Examples include:
1.500.50becomes1500.50, possibly accidentally acceptable1500.50becomes1500501,500.50becomes1.50050
The logic can inflate or corrupt salary values.
Recommendation: Define the supported input locale and parse thousands and decimal separators deterministically. Reject ambiguous formats instead of guessing.
17. Allowance matching is case-insensitive in PHP but case-sensitive in SQL
File: app/Jobs/Imports/MasterDataImports.php:282-285
The code performs exact SQL matching:
->whereIn('tunjangan_nama', $names)
and lowercases results in PHP afterward. Spreadsheet values with different casing are not returned by the database query and are incorrectly reported as unmatched.
Recommendation: Normalize both sides before querying, use a case-insensitive database comparison, or resolve names through a canonical lookup.
18. Allowance import can attach duplicate pivot IDs
File: app/Jobs/Imports/MasterDataImports.php:287-300
Duplicate names in the comma-separated allowance list can add the same
tunjangan_id more than once. If the pivot table has a unique constraint, the
transaction fails. If it does not, duplicate allowance rows may be created.
Recommendation: Normalize and deduplicate allowance names and pivot IDs before inserting. Enforce uniqueness on the pivot table where appropriate.
Low / Correctness Risks
19. Payroll search endpoint has no explicit numeric validation for month/year
File: app/Http/Controllers/Company/PayrollController.php:120-125
month and year are only marked as required. Values such as abc, 0, or
invalid month values can reach the database query.
Recommendation: Validate month as an integer from 1 through 12 and year as a valid integer within the supported payroll range.
20. Export selection handling is inconsistent
Files: app/Jobs/Exports/ProcessExportNilai.php:18,85-89 and
app/Jobs/Exports/ProcessExportMapel.php:37-41
ProcessExportNilai keeps unknown selected columns and maps them to null.
ProcessExportMapel filters invalid columns. This produces inconsistent
behavior across export screens and can create blank columns in nilai exports.
Recommendation: Use one shared column-selection validator and reject or filter unknown fields consistently.
21. Jabatan export assumes every mapped item is a model
File: app/Export/TemplateJabatanExport.php:45
The default empty array is safe because map() is not called. However, callers
passing arrays or incomplete objects will fail when the code accesses
$data->tunjangan.
Recommendation: Validate the export input shape and guard optional model relations before mapping.
22. No transaction around paired entity/history inserts
Several school imports create a primary record and then its history record separately. If the history insert fails, the primary record remains while the import reports failure.
This applies to mapel, nilai, guru, and related imports.