reconstruction of the project
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user