Fix: add payslip relationship on PayrollEntry and update titleText brand setting
This commit is contained in:
@@ -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.
|
||||
*/
|
||||
|
||||
68
docs/PLAN-sidebar-payslip-fix.md
Normal file
68
docs/PLAN-sidebar-payslip-fix.md
Normal 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.
|
||||
69
tests/Feature/PayslipGenerationRelationTest.php
Normal file
69
tests/Feature/PayslipGenerationRelationTest.php
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user