php-commerce/Data/Coupon.php
Snider cd16c7474e feat(commerce): implement CouponService with 5 methods + DTOs (#858)
- create(code, type, value, maxUses, expiresAt) → Coupon
- validate(code, order) → ValidationResult
- apply(coupon, order) → Order (mutates line-item totals)
- expire(coupon) → void
- report() → array of redemption stats

Data/Coupon.php and Data/ValidationResult.php as readonly DTOs.
Pest unit tests with _Good/_Bad/_Ugly per AX-10 for all 5 methods.
pint/pest skipped (vendor binaries missing in sandbox).
Legacy helpers in CouponService preserved.

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=858
2026-04-25 04:41:44 +01:00

82 lines
2.2 KiB
PHP

<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Data;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use Core\Mod\Commerce\Models\Coupon as CouponModel;
/**
* Persisted coupon data used by the RFC CouponService API.
*/
readonly class Coupon
{
public function __construct(
public int $id,
public string $code,
public string $type,
public float $value,
public ?int $maxUses,
public ?CarbonImmutable $expiresAt,
public bool $active,
public int $usedCount,
) {}
public static function fromModel(CouponModel $coupon): self
{
return new self(
id: (int) $coupon->id,
code: (string) $coupon->code,
type: in_array((string) $coupon->type, ['percent', 'percentage'], true) ? 'percent' : 'fixed',
value: (float) $coupon->value,
maxUses: $coupon->max_uses === null ? null : (int) $coupon->max_uses,
expiresAt: self::immutableDate($coupon->valid_until),
active: (bool) $coupon->is_active,
usedCount: (int) $coupon->used_count,
);
}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'id' => $this->id,
'code' => $this->code,
'type' => $this->type,
'value' => $this->value,
'max_uses' => $this->maxUses,
'expires_at' => $this->expiresAt?->toIso8601String(),
'active' => $this->active,
'used_count' => $this->usedCount,
];
}
public function isExpired(): bool
{
return $this->expiresAt?->isPast() ?? false;
}
public function __get(string $name): mixed
{
return match ($name) {
'max_uses' => $this->maxUses,
'expires_at' => $this->expiresAt,
'is_active' => $this->active,
'used_count' => $this->usedCount,
default => null,
};
}
private static function immutableDate(mixed $value): ?CarbonImmutable
{
if (! $value instanceof CarbonInterface) {
return null;
}
return CarbonImmutable::instance($value->toDateTime());
}
}