Files
alrahma_sunday_school_api/app/Models/DiscountVoucher.php
T
root e13df69885
API CI/CD / Validate (composer + pint) (push) Successful in 3m6s
API CI/CD / Test (PHPUnit) (push) Failing after 4m53s
API CI/CD / Build frontend assets (push) Successful in 1m2s
API CI/CD / Security audit (push) Failing after 59s
API CI/CD / Deploy to shared hosting (PHP) (push) Has been skipped
fix unittests issues
2026-07-07 20:56:32 -04:00

136 lines
4.7 KiB
PHP

<?php
namespace App\Models;
use Illuminate\Support\Carbon;
class DiscountVoucher extends BaseModel
{
protected $table = 'discount_vouchers';
// ✅ legacy manages created_at / updated_at
public $timestamps = true;
protected $fillable = ['code', 'discount_type', 'description', 'discount_value', 'max_uses', 'times_used', 'valid_from', 'valid_until', 'is_active', 'created_at', 'updated_at', 'school_year', 'semester'];
protected $casts = [
'discount_value' => 'decimal:2',
'max_uses' => 'integer',
'times_used' => 'integer',
'is_active' => 'boolean',
'valid_from' => 'date',
'valid_until' => 'date',
];
/* =========================
* Model hooks (legacy beforeInsert/beforeUpdate equivalents)
* ========================= */
protected static function booted(): void
{
static::creating(function (self $m) {
$m->normalizeFields();
$m->validateDatesOrder(); // throws if invalid
});
static::updating(function (self $m) {
$m->normalizeFields();
$m->validateDatesOrder(); // throws if invalid
});
}
private function normalizeFields(): void
{
// Normalize code: keep A-Z, 0-9 and dashes, uppercased
if ($this->isDirty('code') && $this->code !== null) {
$raw = (string) $this->code;
$this->code = strtoupper(preg_replace('/[^A-Z0-9\-]/i', '', trim($raw)));
}
// Empty strings -> NULL for dates & max_uses
foreach (['valid_from', 'valid_until'] as $f) {
if ($this->isDirty($f)) {
$v = $this->{$f};
if (is_string($v)) {
$v = trim($v);
$this->{$f} = ($v === '') ? null : $v;
}
}
}
if ($this->isDirty('max_uses')) {
$v = $this->max_uses;
if ($v === '' || $v === null) {
$this->max_uses = null;
} else {
$this->max_uses = max(0, (int) $v);
}
}
if ($this->isDirty('is_active')) {
$this->is_active = (bool) $this->is_active;
}
}
private function validateDatesOrder(): void
{
if (! empty($this->valid_from) && ! empty($this->valid_until)) {
$from = Carbon::parse($this->valid_from)->toDateString();
$until = Carbon::parse($this->valid_until)->toDateString();
if ($from > $until) {
// In Laravel, prefer validating in FormRequest.
// This keeps your legacy behavior by failing the save.
throw new \InvalidArgumentException('Valid Until must be the same as or after Valid From.');
}
}
}
/* =========================
* Query helpers
* ========================= */
/**
* Find a valid voucher for a student (not expired, active, under max uses, and not used by the student).
*
* NOTE: Your legacy joins discount_usages on u.student_id. That requires discount_usages.student_id to exist.
* If your discount_usages table does NOT have student_id (some of your other code uses parent_id),
* switch the filter to parent_id instead.
*/
public static function findValidVoucher(string $code, int $studentId): ?self
{
$code = strtoupper(preg_replace('/\s+/', '', trim($code)));
$today = now()->toDateString();
$q = static::query()
->from('discount_vouchers as v')
->select('v.*')
// Left join usages for this student only
->leftJoin('discount_usages as u', function ($join) use ($studentId) {
$join->on('u.voucher_id', '=', 'v.id')
->where('u.student_id', '=', (int) $studentId);
})
->where('v.code', $code)
->where('v.is_active', 1)
// valid_from is null or <= today
->where(function ($w) use ($today) {
$w->whereNull('v.valid_from')
->orWhere('v.valid_from', '<=', $today);
})
// valid_until is null or >= today
->where(function ($w) use ($today) {
$w->whereNull('v.valid_until')
->orWhere('v.valid_until', '>=', $today);
})
// max_uses is null or times_used < max_uses
->where(function ($w) {
$w->whereNull('v.max_uses')
->orWhereColumn('v.times_used', '<', 'v.max_uses');
})
// Not previously used by this student (no usage row)
->whereNull('u.id')
->limit(1);
return $q->first();
}
}