phpvms/app/Traits/JournalTrait.php
Nabeel Shahzad 290544cdd8
fix(finance): default the journal currency when settings are absent
A fresh install dies creating the first airline:

    App\Models\Airline::initJournal(): Argument #1 ($currency_code)
    must be of type string, null given

setting() returns its $default for a key it cannot read -- retrieve()
throws SettingNotFound and the helper swallows it -- and the created
hook passed no default, so it handed null to a string parameter. The
'USD' default on initJournal() cannot help: an explicit argument always
beats a declared default, even when that argument is null.

The installer creates the airline and user in the same step, and the
settings seed is gated on a separate wizard step, so units.currency is
not guaranteed to exist by then. Every other reader of this setting
already names the fallback -- Money.php:59,74 and
PirepFinanceService.php:57,61 -- so this is the odd one out.

Airline and User are the only models using the trait; both are covered.
The suite could not have caught this because tests/Pest.php seeds
SettingsSeeder before every test, making an empty settings table
unreachable, so the new tests clear it explicitly.
2026-07-26 00:46:42 -05:00

65 lines
1.9 KiB
PHP

<?php
namespace App\Traits;
use App\Models\Journal;
use Exception;
use Illuminate\Database\Eloquent\Relations\MorphOne;
trait JournalTrait
{
/**
* Initialize a new journal when a new record is created
*/
public static function bootJournalTrait(): void
{
static::created(function ($model): void {
// The default on initJournal() cannot cover this: an explicit
// argument always beats a declared default, and setting() returns
// null for a key it cannot read. During install the first airline
// is created before units.currency necessarily exists, so passing
// the bare lookup hands a null to a string parameter. Every other
// caller of this setting already names the fallback.
$model->initJournal(setting('units.currency', 'USD'));
});
}
/**
* Morph to Journal.
*/
public function journal(): MorphOne
{
return $this->morphOne(Journal::class, 'morphed');
}
/**
* Initialize a journal for a given model object
*
*
*
* @throws Exception
*/
public function initJournal(string $currency_code = 'USD'): ?Journal
{
if (!$this->journal) {
$journal = new Journal();
$journal->type = $this->journal_type;
$journal->currency = $currency_code;
$journal->balance = 0;
$this->journal()->save($journal);
$journal->refresh();
// The `!$this->journal` guard above lazy-loaded the relation and cached
// it as null. Saving through the relation does not update that cache, so
// without this the model keeps returning null for ->journal until it is
// reloaded from the database. Reflect the freshly-created journal here.
$this->setRelation('journal', $journal);
return $journal;
}
return null;
}
}