Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
CouponCalculator
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
4 / 4
18
100.00% covered (success)
100.00%
1 / 1
 findValid
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
8
 assertApplicable
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
4
 discountCents
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 amountAfterDiscount
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5namespace ImovelZapAi\Billing\Application\Services;
6
7use ImovelZapAi\Billing\Domain\Enums\BillingCycle;
8use ImovelZapAi\Billing\Domain\Enums\CouponType;
9use ImovelZapAi\Billing\Domain\Exceptions\BillingException;
10use ImovelZapAi\Billing\Infrastructure\Persistence\Eloquent\BillingCouponModel;
11use ImovelZapAi\Organizations\Infrastructure\Persistence\Eloquent\SubscriptionPlanModel;
12
13final class CouponCalculator
14{
15    public function findValid(string $code): BillingCouponModel
16    {
17        $coupon = BillingCouponModel::query()
18            ->whereRaw('lower(code) = ?', [strtolower(trim($code))])
19            ->where('is_active', true)
20            ->first();
21
22        if ($coupon === null) {
23            throw BillingException::couponInvalid();
24        }
25
26        if ($coupon->starts_at !== null && $coupon->starts_at->isFuture()) {
27            throw BillingException::couponInvalid();
28        }
29
30        if ($coupon->expires_at !== null && $coupon->expires_at->isPast()) {
31            throw BillingException::couponInvalid();
32        }
33
34        if ($coupon->max_redemptions !== null && $coupon->redemptions_count >= $coupon->max_redemptions) {
35            throw BillingException::couponInvalid();
36        }
37
38        return $coupon;
39    }
40
41    public function assertApplicable(BillingCouponModel $coupon, SubscriptionPlanModel $plan): void
42    {
43        $codes = $coupon->applicable_plan_codes;
44        if (is_array($codes) && $codes !== [] && ! in_array($plan->code, $codes, true)) {
45            throw BillingException::couponNotApplicable();
46        }
47    }
48
49    public function discountCents(BillingCouponModel $coupon, SubscriptionPlanModel $plan, BillingCycle $cycle): int
50    {
51        $base = $cycle === BillingCycle::Yearly
52            ? (int) $plan->price_yearly_cents
53            : (int) $plan->price_monthly_cents;
54
55        if ($coupon->type === CouponType::Percent) {
56            return (int) floor($base * (min(100, max(0, (int) $coupon->amount)) / 100));
57        }
58
59        return min($base, (int) $coupon->amount);
60    }
61
62    public function amountAfterDiscount(SubscriptionPlanModel $plan, BillingCycle $cycle, ?BillingCouponModel $coupon): int
63    {
64        $base = $cycle === BillingCycle::Yearly
65            ? (int) $plan->price_yearly_cents
66            : (int) $plan->price_monthly_cents;
67
68        if ($coupon === null) {
69            return $base;
70        }
71
72        return max(0, $base - $this->discountCents($coupon, $plan, $cycle));
73    }
74}