Files
alrahma_sunday_school_api/app/Models/DiscountVoucher.php
T
2026-04-23 00:04:35 -04:00

151 lines
4.8 KiB
PHP

<?php
namespace App\Models;
use App\Models\BaseModel;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
class DiscountVoucher extends BaseModel
{
protected $table = 'discount_vouchers';
// ✅ CI manages created_at / updated_at
public $timestamps = true;
protected $fillable = [
'code',
'discount_type',
'discount_value',
'max_uses',
'times_used',
'valid_from',
'valid_until',
'semester',
'school_year',
'is_active',
'description',
'created_at',
'updated_at',
];
protected $casts = [
'discount_value' => 'decimal:2',
'max_uses' => 'integer',
'times_used' => 'integer',
'is_active' => 'boolean',
'valid_from' => 'date',
'valid_until' => 'date',
];
/* =========================
* Model hooks (CI 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 CI 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 CI 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();
}
}