Fix: add payslip relationship on PayrollEntry and update titleText brand setting

This commit is contained in:
2026-09-14 17:24:30 +08:00
parent 031eb2ddd7
commit fa6cc08830
3 changed files with 145 additions and 0 deletions

View File

@@ -66,6 +66,14 @@ class PayrollEntry extends BaseModel
return $this->belongsTo(PayrollRun::class);
}
/**
* Get the payslip for this payroll entry.
*/
public function payslip()
{
return $this->hasOne(Payslip::class, 'payroll_entry_id');
}
/**
* Get the employee.
*/

View File

@@ -0,0 +1,68 @@
# Implementation Plan - Sidebar Brand Title & Payslip Generation Fix
**Task Name:** Change Sidebar Brand Name & Fix Payslip Generation Error
**Target Plan File:** `docs/PLAN-sidebar-payslip-fix.md`
**Status:** Ready for Review
---
## 1. Problem Analysis & Root Cause
### Item A: Sidebar Brand Name ("SEB CONNEXION INC." → "SCX Software")
- **Mechanism:** In [app-sidebar.tsx](file:///Users/dvapp/Documents/HRM/resources/js/components/app-sidebar.tsx#L1100), when no custom brand logo is uploaded, the sidebar header renders `{titleText || 'WorkDo'}`.
- **Source of Data:** The `titleText` property is stored in the `settings` database table (`key = 'titleText'`) and exposed to frontend Inertia props via `settings()` / `useBrand()`.
- **Current DB Value:** Currently stores `"SEB CONNEXION INC."`.
- **Resolution Options:**
1. **From Admin UI:** Navigate to **Settings** (`/settings`) → **Brand Settings** tab → Change **"Title Text"** to `SCX Software` → Click **Save Changes**.
2. **Automated/Backend:** Update the `settings` table record directly for `titleText` to `SCX Software`.
---
### Item B: Payslip Generation Error
- **Error:** `Failed to generate payslips: Call to undefined method App\Models\PayrollEntry::payslip()`
- **Mechanism:** In [PayslipController.php](file:///Users/dvapp/Documents/HRM/app/Http/Controllers/PayslipController.php#L314-L316), inside `bulkGenerate()`:
```php
$pendingEntries = PayrollEntry::where('payroll_run_id', $payrollRun->id)
->whereDoesntHave('payslip')
->exists();
```
Calling `whereDoesntHave('payslip')` requires an Eloquent relation method named `payslip()` on [PayrollEntry](file:///Users/dvapp/Documents/HRM/app/Models/PayrollEntry.php).
- **Missing Code:** [PayrollEntry.php](file:///Users/dvapp/Documents/HRM/app/Models/PayrollEntry.php) defines `payrollRun()`, `employee()`, `adjustments()`, and `creator()`, but **does not define** the inverse `payslip()` relation:
```php
/**
* Get the payslip associated with this payroll entry.
*/
public function payslip()
{
return $this->hasOne(Payslip::class, 'payroll_entry_id');
}
```
---
## 2. Proposed Task Breakdown
### Phase 1: Fix Eloquent Relation on `PayrollEntry`
- **File:** [app/Models/PayrollEntry.php](file:///Users/dvapp/Documents/HRM/app/Models/PayrollEntry.php)
- **Changes:**
- Add `public function payslip()` relationship method returning `$this->hasOne(Payslip::class, 'payroll_entry_id')`.
- Add typehints and docblock.
### Phase 2: Update Sidebar Title Setting
- **Database / Command:**
- Update `Setting::where('key', 'titleText')->update(['value' => 'SCX Software'])` so current and future sessions immediately see **SCX Software**.
- Provide clear instructions for updating it anytime via **Settings > Brand Settings > Title Text**.
### Phase 3: Verification & Automated Tests
- **Verification Commands:**
- Run unit/feature tests: `php artisan test --filter=PayslipTest` (or equivalent).
- Test Eloquent query resolution: verify `PayrollEntry::whereDoesntHave('payslip')->toSql()` runs without exception.
- Verify `bulkGenerate` payslip generation workflow succeeds and marks run as completed when all entries have payslips.
---
## 3. Risk Assessment & Mitigations
- **Risk:** Does `Payslip` table have entries without `payroll_entry_id`?
- **Mitigation:** The `payslips` migration and model already define `payroll_entry_id` as foreign key. The `hasOne` relationship maps 1:1 with existing schema.
- **Risk:** Cache persistence of `settings()` helper.
- **Mitigation:** Clear configuration and app cache (`php artisan cache:clear`) after updating the `titleText` setting.

View File

@@ -0,0 +1,69 @@
<?php
namespace Tests\Feature;
use App\Models\PayrollEntry;
use App\Models\PayrollRun;
use App\Models\Payslip;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class PayslipGenerationRelationTest extends TestCase
{
use RefreshDatabase;
public function test_payroll_entry_defines_payslip_relationship(): void
{
$company = User::factory()->create(['type' => 'company']);
$employee = User::factory()->create(['type' => 'employee', 'created_by' => $company->id]);
$payrollRun = PayrollRun::create([
'title' => 'Test Payroll Run',
'payroll_frequency' => 'monthly',
'pay_period_start' => '2026-09-01',
'pay_period_end' => '2026-09-15',
'pay_date' => '2026-09-15',
'status' => 'processing',
'created_by' => $company->id,
]);
$payrollEntry = PayrollEntry::create([
'payroll_run_id' => $payrollRun->id,
'employee_id' => $employee->id,
'basic_salary' => 15000,
'net_pay' => 15000,
'gross_pay' => 15000,
'created_by' => $company->id,
]);
// Verify relationship method exists and query works
$pending = PayrollEntry::where('payroll_run_id', $payrollRun->id)
->whereDoesntHave('payslip')
->exists();
$this->assertTrue($pending);
// Create payslip for entry
$payslip = Payslip::create([
'payroll_entry_id' => $payrollEntry->id,
'employee_id' => $employee->id,
'payslip_number' => 'PS-TEST-001',
'pay_period_start' => '2026-09-01',
'pay_period_end' => '2026-09-15',
'pay_date' => '2026-09-15',
'status' => 'generated',
'created_by' => $company->id,
]);
$this->assertInstanceOf(Payslip::class, $payrollEntry->fresh()->payslip);
$this->assertEquals($payslip->id, $payrollEntry->fresh()->payslip->id);
// Now whereDoesntHave should return false
$pendingAfter = PayrollEntry::where('payroll_run_id', $payrollRun->id)
->whereDoesntHave('payslip')
->exists();
$this->assertFalse($pendingAfter);
}
}