feat(coupons): add coupon service and result value object
This commit is contained in:
1 parent
170c24ee52
commit
26d0f46729
2 files changed
+104
No files matched your search
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Bazaar\Services;
|
||||||
|
|
||||||
|
final class CouponResult
|
||||||
|
{
|
||||||
|
public const valid = 'valid';
|
||||||
|
public const notFound = 'notFound';
|
||||||
|
public const inactive = 'inactive';
|
||||||
|
public const notStarted = 'notStarted';
|
||||||
|
public const expired = 'expired';
|
||||||
|
public const exhausted = 'exhausted';
|
||||||
|
public const belowMinimum = 'belowMinimum';
|
||||||
|
|
||||||
|
private function __construct(
|
||||||
|
public readonly string $status,
|
||||||
|
public readonly int $discountCents,
|
||||||
|
public readonly ?array $coupon
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function ok(array $coupon, int $discountCents): self
|
||||||
|
{
|
||||||
|
return new self(self::valid, $discountCents, $coupon);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function fail(string $status, ?array $coupon = null): self
|
||||||
|
{
|
||||||
|
return new self($status, 0, $coupon);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isValid(): bool
|
||||||
|
{
|
||||||
|
return $this->status === self::valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function message(): string
|
||||||
|
{
|
||||||
|
return match ($this->status) {
|
||||||
|
self::valid => 'Coupon applied.',
|
||||||
|
self::notFound => 'That coupon code was not recognised.',
|
||||||
|
self::inactive => 'That coupon is no longer active.',
|
||||||
|
self::notStarted => 'That coupon is not valid yet.',
|
||||||
|
self::expired => 'That coupon has expired.',
|
||||||
|
self::exhausted => 'That coupon has reached its redemption limit.',
|
||||||
|
self::belowMinimum => 'Your cart does not meet the minimum for this coupon.',
|
||||||
|
default => 'That coupon could not be applied.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Bazaar\Services;
|
||||||
|
|
||||||
|
use Bazaar\Repositories\CouponRepository;
|
||||||
|
|
||||||
|
final class CouponService
|
||||||
|
{
|
||||||
|
public function __construct(private readonly CouponRepository $coupons)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function evaluate(string $code, int $subtotalCents, ?string $now = null): CouponResult
|
||||||
|
{
|
||||||
|
$coupon = $this->coupons->findByCode($code);
|
||||||
|
if ($coupon === null) {
|
||||||
|
return CouponResult::fail(CouponResult::notFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
$now ??= gmdate('c');
|
||||||
|
|
||||||
|
if ((int) $coupon['isActive'] !== 1) {
|
||||||
|
return CouponResult::fail(CouponResult::inactive, $coupon);
|
||||||
|
}
|
||||||
|
if ($coupon['startsAt'] !== null && $coupon['startsAt'] !== '' && $now < $coupon['startsAt']) {
|
||||||
|
return CouponResult::fail(CouponResult::notStarted, $coupon);
|
||||||
|
}
|
||||||
|
if ($coupon['expiresAt'] !== null && $coupon['expiresAt'] !== '' && $now > $coupon['expiresAt']) {
|
||||||
|
return CouponResult::fail(CouponResult::expired, $coupon);
|
||||||
|
}
|
||||||
|
if ($coupon['maxRedemptions'] !== null && (int) $coupon['redeemedCount'] >= (int) $coupon['maxRedemptions']) {
|
||||||
|
return CouponResult::fail(CouponResult::exhausted, $coupon);
|
||||||
|
}
|
||||||
|
if ($subtotalCents < (int) $coupon['minSubtotalCents']) {
|
||||||
|
return CouponResult::fail(CouponResult::belowMinimum, $coupon);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CouponResult::ok($coupon, $this->discountFor($coupon, $subtotalCents));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function discountFor(array $coupon, int $subtotalCents): int
|
||||||
|
{
|
||||||
|
$discount = $coupon['type'] === 'percent'
|
||||||
|
? (int) floor($subtotalCents * (int) $coupon['value'] / 100)
|
||||||
|
: (int) $coupon['value'];
|
||||||
|
|
||||||
|
return max(0, min($discount, $subtotalCents));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function redeem(int $couponId): void
|
||||||
|
{
|
||||||
|
$this->coupons->recordRedemption($couponId);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user