reconstruction of the project

This commit is contained in:
root
2026-03-08 16:33:24 -04:00
parent 23b7db1107
commit c8de5f7edc
9157 changed files with 77877 additions and 1073823 deletions
+92 -75
View File
@@ -1,12 +1,18 @@
<?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';
protected $primaryKey = 'id';
// ✅ CI manages created_at / updated_at
public $timestamps = true;
protected $fillable = [
'code',
'discount_type',
@@ -23,112 +29,123 @@ class DiscountVoucher extends BaseModel
'semester',
];
// Timestamps managed by CI
public $timestamps = true;
const CREATED_AT = 'created_at';
const UPDATED_AT = 'updated_at';
// Basic validation
protected $validationRules = [
'code' => 'required|alpha_numeric_punct|max_length[50]|is_unique[discount_vouchers.code,id,{id}]',
'discount_type' => 'required|in_list[percent,fixed]',
'discount_value' => 'required|numeric|greater_than_equal_to[0]',
'max_uses' => 'permit_empty|is_natural',
'valid_from' => 'permit_empty|valid_date[Y-m-d]',
'valid_until' => 'permit_empty|valid_date[Y-m-d]',
'is_active' => 'in_list[0,1]',
'description' => 'permit_empty|string|max_length[1000]', // <-- NEW
protected $casts = [
'discount_value' => 'decimal:2',
'max_uses' => 'integer',
'times_used' => 'integer',
'is_active' => 'boolean',
'valid_from' => 'date',
'valid_until' => 'date',
];
protected $validationMessages = [
'code' => [
'is_unique' => 'The voucher code must be unique.',
],
'discount_type' => [
'in_list' => 'The discount type must be either "percent" or "fixed".',
],
];
/* =========================
* Model hooks (CI beforeInsert/beforeUpdate equivalents)
* ========================= */
// Normalize inputs before save
protected $beforeInsert = ['normalizeFields', 'validateDatesOrder'];
protected $beforeUpdate = ['normalizeFields', 'validateDatesOrder'];
protected function normalizeFields(array $data): array
protected static function booted(): void
{
if (!isset($data['data'])) return $data;
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 (array_key_exists('code', $data['data'])) {
$raw = (string) $data['data']['code'];
$normalized = strtoupper(preg_replace('/[^A-Z0-9\-]/i', '', trim($raw)));
$data['data']['code'] = $normalized;
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 (array_key_exists($f, $data['data'])) {
$v = trim((string) $data['data'][$f]);
$data['data'][$f] = ($v === '') ? null : $v;
if ($this->isDirty($f)) {
$v = $this->{$f};
if (is_string($v)) {
$v = trim($v);
$this->{$f} = ($v === '') ? null : $v;
}
}
}
if (array_key_exists('max_uses', $data['data'])) {
$v = $data['data']['max_uses'];
$data['data']['max_uses'] = ($v === '' || $v === null) ? null : max(0, (int) $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);
}
}
// Coerce booleans
if (array_key_exists('is_active', $data['data'])) {
$data['data']['is_active'] = (int) !!$data['data']['is_active'];
if ($this->isDirty('is_active')) {
$this->is_active = (bool) $this->is_active;
}
return $data;
}
protected function validateDatesOrder(array $data): array
private function validateDatesOrder(): void
{
if (!isset($data['data'])) return $data;
$from = $data['data']['valid_from'] ?? null;
$until = $data['data']['valid_until'] ?? null;
if ($from && $until && $from > $until) {
// Inject a model error; save() will fail and return errors()
$this->validator->setError('valid_until', 'Valid Until must be the same as or after Valid From.');
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.');
}
}
return $data;
}
/* =========================
* 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 function findValidVoucher(string $code, int $studentId)
public static function findValidVoucher(string $code, int $studentId): ?self
{
$code = strtoupper(preg_replace('/\s+/', '', trim($code))); // simple normalize to match stored code
$code = strtoupper(preg_replace('/\s+/', '', trim($code)));
$today = now()->toDateString();
$builder = $this->db->table('discount_vouchers v')
$q = static::query()
->from('discount_vouchers as v')
->select('v.*')
// Left join usages for this student only
->join('discount_usages u', 'u.voucher_id = v.id AND u.student_id = ' . (int) $studentId, 'left')
->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
->groupStart()
->where('v.valid_from IS NULL', null, false)
->orWhere('v.valid_from <=', date('Y-m-d'))
->groupEnd()
->where(function ($w) use ($today) {
$w->whereNull('v.valid_from')
->orWhere('v.valid_from', '<=', $today);
})
// valid_until is null or >= today
->groupStart()
->where('v.valid_until IS NULL', null, false)
->orWhere('v.valid_until >=', date('Y-m-d'))
->groupEnd()
// max_uses is null or times_used < max_uses (use raw to compare two columns)
->groupStart()
->where('v.max_uses IS NULL', null, false)
->orWhere('v.times_used < v.max_uses', null, false)
->groupEnd()
->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)
->where('u.id IS NULL', null, false);
->whereNull('u.id')
->limit(1);
return $builder->get()->getRow();
return $q->first();
}
}
}