Commit 8a66d739 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat(rentals): escalation tiers, dual utilities modes, bank-rate late fee, grace period

## Schema (Phase_96_001)
- escalation_type / escalation_rate / escalation_tiers_json — flat or tiered annual rent increases
- utilities_mode / utilities_rent_pct / utilities_facility_pct / facility_monthly_cost — support rent%, facility-cost%, or both
- payment_due_day — configurable per-contract (default day 5)
- late_fee_bank_rate — annual bank rate for daily penalty calculation
- grace_period_months / early_termination_months — additional contract terms
- Data migration: backfills utilities_rent_pct from utilities_percentage

## Service layer
- RentalContractService: computes escalated total_amount across flat/tiered modes; handles all utilities modes; recalculates VAT and grand_total
- RentalInvoiceService: calcBase() now escalation-aware (by period); bulkGenerate skips grace months and uses payment_due_day; calcLateFee supports bank-rate daily formula
- Seeds RENTAL_LATE_FEE_BANK_RATE business rule (27.25% annual)

## UI
- contract_form: new sections for escalation (dynamic tiers table), utilities mode, payment terms, extra contract conditions; year-by-year preview
- contract_show: mode-aware utilities display, escalation card, grace/termination info
Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
parent 8ae05b0b
......@@ -191,13 +191,17 @@ class RentalController extends Controller
$entities = RentalEntity::search(['status' => 'active'], 1000, 1)['data'] ?? [];
$facilities = Facility::allActive();
$defaultVat = (float) (RuleEngine::getValue('RENTAL_VAT_PCT') ?? 1.00);
$bankRate = (float) (RuleEngine::getValue('RENTAL_LATE_FEE_BANK_RATE', 'annual_rate') ?? 27.25);
return $this->view('Rentals.Views.contract_form', [
'contract' => null,
'entities' => $entities,
'facilities' => $facilities,
'statuses' => RentalContract::getStatuses(),
'defaultVat' => $defaultVat,
'contract' => null,
'entities' => $entities,
'facilities' => $facilities,
'statuses' => RentalContract::getStatuses(),
'defaultVat' => $defaultVat,
'bankRate' => $bankRate,
'escalationTypes' => RentalContract::getEscalationTypes(),
'utilitiesModes' => RentalContract::getUtilitiesModes(),
]);
}
......@@ -221,6 +225,19 @@ class RentalController extends Controller
$lateFeeRate = (float) $request->post('late_fee_rate', 0.00);
$notes = trim((string) $request->post('notes', ''));
// New escalation / utilities / terms fields
$escalationType = trim((string) $request->post('escalation_type', 'none'));
$escalationRate = (float) $request->post('escalation_rate', 0.00);
$escalationTiersJson = trim((string) $request->post('escalation_tiers_json', ''));
$utilitiesMode = trim((string) $request->post('utilities_mode', 'rent_pct'));
$utilitiesRentPct = (float) $request->post('utilities_rent_pct', 0.00);
$utilitiesFacilityPct = (float) $request->post('utilities_facility_pct', 0.00);
$facilityMonthlyCost = $request->post('facility_monthly_cost', '') !== '' ? (float) $request->post('facility_monthly_cost', 0) : null;
$paymentDueDay = (int) $request->post('payment_due_day', 5);
$lateFeeBankRate = (float) $request->post('late_fee_bank_rate', 0.00);
$gracePeriodMonths = (int) $request->post('grace_period_months', 0);
$earlyTerminationMonths = (int) $request->post('early_termination_months', 0);
// Validation
$errors = [];
......@@ -256,20 +273,31 @@ class RentalController extends Controller
}
$contract = RentalContractService::createContract([
'entity_id' => $entityId,
'facility_id' => $facilityId,
'activity_type' => $activityType ?: null,
'start_date' => $startDate,
'end_date' => $endDate,
'total_units' => $totalUnits,
'unit_rate' => $unitRate,
'time_tier' => $timeTier,
'vat_percentage' => $vatPercentage,
'utilities_percentage' => $utilitiesPercentage,
'late_fee_type' => $lateFeeType,
'late_fee_rate' => $lateFeeRate,
'deposit_percentage' => $depositPercentage,
'notes' => $notes ?: null,
'entity_id' => $entityId,
'facility_id' => $facilityId,
'activity_type' => $activityType ?: null,
'start_date' => $startDate,
'end_date' => $endDate,
'total_units' => $totalUnits,
'unit_rate' => $unitRate,
'time_tier' => $timeTier,
'vat_percentage' => $vatPercentage,
'utilities_percentage' => $utilitiesPercentage,
'late_fee_type' => $lateFeeType,
'late_fee_rate' => $lateFeeRate,
'escalation_type' => $escalationType,
'escalation_rate' => $escalationRate,
'escalation_tiers_json' => $escalationTiersJson ?: null,
'utilities_mode' => $utilitiesMode,
'utilities_rent_pct' => $utilitiesRentPct,
'utilities_facility_pct' => $utilitiesFacilityPct,
'facility_monthly_cost' => $facilityMonthlyCost,
'payment_due_day' => $paymentDueDay,
'late_fee_bank_rate' => $lateFeeBankRate,
'grace_period_months' => $gracePeriodMonths,
'early_termination_months'=> $earlyTerminationMonths,
'deposit_percentage' => $depositPercentage,
'notes' => $notes ?: null,
]);
return $this->redirect('/rentals/contracts/' . $contract->id)->withSuccess('تم إنشاء العقد بنجاح');
......
......@@ -35,6 +35,17 @@ class RentalContract extends Model
'grand_total',
'late_fee_type',
'late_fee_rate',
'escalation_type',
'escalation_rate',
'escalation_tiers_json',
'utilities_mode',
'utilities_rent_pct',
'utilities_facility_pct',
'facility_monthly_cost',
'payment_due_day',
'late_fee_bank_rate',
'grace_period_months',
'early_termination_months',
'deposit_percentage',
'deposit_amount',
'deposit_status',
......@@ -60,6 +71,31 @@ class RentalContract extends Model
];
}
/**
* Escalation types with Arabic labels.
*/
public static function getEscalationTypes(): array
{
return [
'none' => 'لا يوجد',
'flat' => 'موحد',
'tiered' => 'متدرج',
];
}
/**
* Utilities modes with Arabic labels.
*/
public static function getUtilitiesModes(): array
{
return [
'none' => 'لا يوجد',
'rent_pct' => 'نسبة من الإيجار',
'facility_cost_pct' => 'نسبة من تكلفة المرفق',
'both' => 'الاثنان معاً',
];
}
/**
* Calculate monthly invoice amounts from a contract row.
* Returns [base, utilities, vat, total].
......
This diff is collapsed.
This diff is collapsed.
<?php
declare(strict_types=1);
return [
'up' => function (\App\Core\Database $db): void {
$table = 'rental_contracts';
$columns = [
'escalation_type' => "ALTER TABLE `{$table}` ADD COLUMN `escalation_type` VARCHAR(10) NOT NULL DEFAULT 'none' AFTER `late_fee_rate`",
'escalation_rate' => "ALTER TABLE `{$table}` ADD COLUMN `escalation_rate` DECIMAL(5,2) NOT NULL DEFAULT 0.00 AFTER `escalation_type`",
'escalation_tiers_json' => "ALTER TABLE `{$table}` ADD COLUMN `escalation_tiers_json` JSON NULL AFTER `escalation_rate`",
'utilities_mode' => "ALTER TABLE `{$table}` ADD COLUMN `utilities_mode` VARCHAR(20) NOT NULL DEFAULT 'rent_pct' AFTER `escalation_tiers_json`",
'utilities_rent_pct' => "ALTER TABLE `{$table}` ADD COLUMN `utilities_rent_pct` DECIMAL(5,2) NOT NULL DEFAULT 0.00 AFTER `utilities_mode`",
'utilities_facility_pct' => "ALTER TABLE `{$table}` ADD COLUMN `utilities_facility_pct` DECIMAL(5,2) NOT NULL DEFAULT 0.00 AFTER `utilities_rent_pct`",
'facility_monthly_cost' => "ALTER TABLE `{$table}` ADD COLUMN `facility_monthly_cost` DECIMAL(15,2) NULL AFTER `utilities_facility_pct`",
'payment_due_day' => "ALTER TABLE `{$table}` ADD COLUMN `payment_due_day` TINYINT UNSIGNED NOT NULL DEFAULT 5 AFTER `facility_monthly_cost`",
'late_fee_bank_rate' => "ALTER TABLE `{$table}` ADD COLUMN `late_fee_bank_rate` DECIMAL(7,4) NOT NULL DEFAULT 0.00 AFTER `payment_due_day`",
'grace_period_months' => "ALTER TABLE `{$table}` ADD COLUMN `grace_period_months` TINYINT UNSIGNED NOT NULL DEFAULT 0 AFTER `late_fee_bank_rate`",
'early_termination_months' => "ALTER TABLE `{$table}` ADD COLUMN `early_termination_months` TINYINT UNSIGNED NOT NULL DEFAULT 0 AFTER `grace_period_months`",
];
foreach ($columns as $column => $sql) {
$exists = $db->selectOne(
"SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = ? AND column_name = ?",
[$table, $column]
);
if (!$exists) {
$db->raw($sql);
}
}
// Data migration: backfill utilities_rent_pct from legacy utilities_percentage
$db->raw(
"UPDATE `{$table}` SET `utilities_rent_pct` = `utilities_percentage` WHERE `utilities_rent_pct` = 0.00 AND `utilities_percentage` > 0"
);
},
'down' => "
ALTER TABLE `rental_contracts`
DROP COLUMN IF EXISTS `escalation_type`,
DROP COLUMN IF EXISTS `escalation_rate`,
DROP COLUMN IF EXISTS `escalation_tiers_json`,
DROP COLUMN IF EXISTS `utilities_mode`,
DROP COLUMN IF EXISTS `utilities_rent_pct`,
DROP COLUMN IF EXISTS `utilities_facility_pct`,
DROP COLUMN IF EXISTS `facility_monthly_cost`,
DROP COLUMN IF EXISTS `payment_due_day`,
DROP COLUMN IF EXISTS `late_fee_bank_rate`,
DROP COLUMN IF EXISTS `grace_period_months`,
DROP COLUMN IF EXISTS `early_termination_months`;
",
];
<?php
declare(strict_types=1);
return function (\App\Core\Database $db): void {
$existing = $db->selectOne(
"SELECT id FROM business_rules WHERE rule_code = ? AND branch_id IS NULL",
['RENTAL_LATE_FEE_BANK_RATE']
);
if ($existing) {
return;
}
$db->insert('business_rules', [
'rule_code' => 'RENTAL_LATE_FEE_BANK_RATE',
'category' => 'rentals',
'name_ar' => 'النسبة البنكية للغرامات',
'name_en' => 'Bank Late Fee Rate',
'description_ar' => 'النسبة السنوية المعتمدة لاحتساب غرامات التأخير اليومية على عقود الإيجار',
'data_type' => 'percentage',
'current_value_json' => '{"annual_rate":"27.25"}',
'parameters_json' => '{"annual_rate":"decimal"}',
'effective_from' => date('Y-m-d'),
'is_active' => 1,
'version' => 1,
]);
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment