diff --git a/src/Promotion/Contracts/Promotion.php b/src/Promotion/Contracts/Promotion.php index 633251413..84f9a43eb 100644 --- a/src/Promotion/Contracts/Promotion.php +++ b/src/Promotion/Contracts/Promotion.php @@ -22,6 +22,12 @@ public static function findByCouponCode(string $couponCode): ?Promotion; public function isValid(?\DateTimeInterface $at = null): bool; + public function hasStarted(?\DateTimeInterface $at = null): bool; + + public function isExpired(?\DateTimeInterface $at = null): bool; + + public function isDepleted(): bool; + public function isEligible(object $subject): bool; public function isCouponBased(): bool; diff --git a/src/Promotion/Models/Promotion.php b/src/Promotion/Models/Promotion.php index 7371a1420..f029b469c 100644 --- a/src/Promotion/Models/Promotion.php +++ b/src/Promotion/Models/Promotion.php @@ -92,11 +92,44 @@ public function getActions(): Collection public function isValid(?\DateTimeInterface $at = null): bool { - if (null !== $this->usage_limit && $this->usage_count >= $this->usage_limit) { + return !$this->isDepleted() && $this->hasStarted() && !$this->isExpired($at); + } + + public function hasStarted(?\DateTimeInterface $at = null): bool + { + if (null === $this->starts_at) { + return true; + } + + $baseDate = $at ?? Carbon::now($this->starts_at->getTimezone()); + if (!$baseDate instanceof Carbon) { + $baseDate = Carbon::instance($baseDate); + } + + return $baseDate->isAfter($this->starts_at); + } + + public function isExpired(?\DateTimeInterface $at = null): bool + { + if (null === $this->ends_at) { + return false; + } + + $baseDate = $at ?? Carbon::now($this->ends_at->getTimezone()); + if (!$baseDate instanceof Carbon) { + $baseDate = Carbon::instance($baseDate); + } + + return $baseDate->isAfter($this->ends_at); + } + + public function isDepleted(): bool + { + if (null === $this->usage_limit) { return false; } - return null === $this->ends_at || $this->ends_at->isAfter($at ?? Carbon::now($this->ends_at->getTimezone())); + return $this->usage_count >= $this->usage_limit; } public function isEligible(object $subject): bool diff --git a/src/Promotion/Tests/PromotionDepletionTest.php b/src/Promotion/Tests/PromotionDepletionTest.php new file mode 100644 index 000000000..c27ad736d --- /dev/null +++ b/src/Promotion/Tests/PromotionDepletionTest.php @@ -0,0 +1,40 @@ +assertFalse((new Promotion())->isDepleted()); + } + + /** @test */ + public function it_is_depleted_if_the_usage_limit_is_zero() + { + $this->assertTrue((new Promotion(['usage_limit' => 0]))->isDepleted()); + } + + /** @test */ + public function it_is_depleted_if_the_usage_limit_equals_the_usage_count() + { + $this->assertTrue((new Promotion(['usage_limit' => 1, 'usage_count' => 1]))->isDepleted()); + } + + /** @test */ + public function it_is_depleted_if_the_usage_limit_is_smaller_than_the_usage_count() + { + $this->assertTrue((new Promotion(['usage_limit' => 1, 'usage_count' => 2]))->isDepleted()); + } + + /** @test */ + public function it_is_not_depleted_if_the_usage_limit_is_greater_than_the_usage_count() + { + $this->assertFalse((new Promotion(['usage_limit' => 3, 'usage_count' => 2]))->isDepleted()); + } +} diff --git a/src/Promotion/Tests/PromotionExpiryTest.php b/src/Promotion/Tests/PromotionExpiryTest.php new file mode 100644 index 000000000..00fa839fc --- /dev/null +++ b/src/Promotion/Tests/PromotionExpiryTest.php @@ -0,0 +1,53 @@ +assertFalse((new Promotion(['ends_at' => Carbon::now()->addDay()]))->isExpired()); + } + + /** @test */ + public function it_is_not_expired_if_the_end_date_is_null() + { + $this->assertFalse((new Promotion())->isExpired()); + } + + /** @test */ + public function it_is_expired_if_the_end_date_is_now() + { + $this->assertTrue((new Promotion(['ends_at' => new \DateTime()]))->isExpired()); + } + + /** @test */ + public function it_is_expired_if_the_end_date_is_in_the_past() + { + $this->assertTrue((new Promotion(['ends_at' => Carbon::yesterday()]))->isExpired()); + } + + /** @test */ + public function it_can_tell_if_it_will_be_expired_at_a_future_date() + { + $tomorrow = new \DateTime('+1day'); + $this->assertTrue((new Promotion(['ends_at' => new \DateTime()]))->isExpired($tomorrow)); + $this->assertFalse((new Promotion(['ends_at' => new \DateTime('+2day')]))->isExpired($tomorrow)); + $this->assertFalse((new Promotion())->isExpired($tomorrow)); + } + + /** @test */ + public function it_can_tell_if_it_was_expired_at_a_past_date() + { + $yesterday = new \DateTime('-1day'); + $this->assertFalse((new Promotion(['ends_at' => new \DateTime()]))->isExpired($yesterday)); + $this->assertTrue((new Promotion(['ends_at' => new \DateTime('-2day')]))->isExpired($yesterday)); + $this->assertFalse((new Promotion())->isExpired($yesterday)); + } +} diff --git a/src/Promotion/Tests/PromotionStartTest.php b/src/Promotion/Tests/PromotionStartTest.php new file mode 100644 index 000000000..bfb0f2dd4 --- /dev/null +++ b/src/Promotion/Tests/PromotionStartTest.php @@ -0,0 +1,53 @@ +assertFalse((new Promotion(['starts_at' => Carbon::now()->addDay()]))->hasStarted()); + } + + /** @test */ + public function it_has_started_if_the_start_date_is_null() + { + $this->assertTrue((new Promotion())->hasStarted()); + } + + /** @test */ + public function it_has_started_if_the_start_date_is_now() + { + $this->assertTrue((new Promotion(['starts_at' => new \DateTime()]))->hasStarted()); + } + + /** @test */ + public function it_has_started_if_the_start_date_is_in_the_past() + { + $this->assertTrue((new Promotion(['starts_at' => Carbon::yesterday()]))->hasStarted()); + } + + /** @test */ + public function it_can_tell_if_it_will_be_started_at_a_future_date() + { + $tomorrow = new \DateTime('+1day'); + $this->assertTrue((new Promotion(['starts_at' => new \DateTime()]))->hasStarted($tomorrow)); + $this->assertFalse((new Promotion(['starts_at' => new \DateTime('+2day')]))->hasStarted($tomorrow)); + $this->assertTrue((new Promotion())->hasStarted($tomorrow)); + } + + /** @test */ + public function it_can_tell_if_it_was_started_at_a_past_date() + { + $yesterday = new \DateTime('-1day'); + $this->assertFalse((new Promotion(['starts_at' => new \DateTime()]))->hasStarted($yesterday)); + $this->assertTrue((new Promotion(['starts_at' => new \DateTime('-2day')]))->hasStarted($yesterday)); + $this->assertTrue((new Promotion())->hasStarted($yesterday)); + } +} diff --git a/src/Promotion/Tests/PromotionValidityTest.php b/src/Promotion/Tests/PromotionValidityTest.php new file mode 100644 index 000000000..7511488d2 --- /dev/null +++ b/src/Promotion/Tests/PromotionValidityTest.php @@ -0,0 +1,113 @@ +assertFalse((new Promotion(['starts_at' => Carbon::tomorrow()]))->isValid()); + } + + /** @test */ + public function it_is_not_valid_if_it_has_already_expired() + { + $this->assertFalse((new Promotion(['ends_at' => Carbon::tomorrow()]))->isValid()); + } + + /** @test */ + public function it_is_not_valid_if_it_is_depleted() + { + $this->assertFalse((new Promotion(['usage_limit' => 1, 'usage_count' => 1]))->isValid()); + } + + /** @test */ + public function it_is_not_valid_if_it_has_started_not_expired_but_is_depleted() + { + $promo = new Promotion([ + 'usage_limit' => 1, + 'usage_count' => 1, + 'starts_at' => Carbon::yesterday(), + 'ends_at' => Carbon::tomorrow(), + ]); + + $this->assertFalse($promo->isValid()); + } + + /** @test */ + public function it_is_not_valid_if_it_has_not_started_yet_has_no_expiry_and_is_not_depleted() + { + $promo = new Promotion([ + 'usage_limit' => 10, + 'usage_count' => 1, + 'starts_at' => Carbon::tomorrow(), + ]); + + $this->assertFalse($promo->isValid()); + } + + /** @test */ + public function it_is_not_valid_if_it_has_no_start_date_has_expired_and_is_not_depleted() + { + $promo = new Promotion([ + 'usage_limit' => 10, + 'usage_count' => 1, + 'ends_at' => Carbon::yesterday(), + ]); + + $this->assertFalse($promo->isValid()); + } + + /** @test */ + public function it_is_valid_if_it_has_no_start_date_has_not_expired_and_is_not_depleted() + { + $promo = new Promotion([ + 'usage_limit' => 10, + 'usage_count' => 1, + 'ends_at' => Carbon::tomorrow(), + ]); + + $this->assertTrue($promo->isValid()); + } + + /** @test */ + public function it_is_valid_if_it_has_started_has_not_expired_and_is_not_depleted() + { + $promo = new Promotion([ + 'usage_limit' => 2, + 'usage_count' => 1, + 'starts_at' => Carbon::yesterday(), + 'ends_at' => Carbon::tomorrow(), + ]); + + $this->assertTrue($promo->isValid()); + } + + /** @test */ + public function it_is_valid_if_it_has_started_has_not_expired_and_is_unlimited() + { + $promo = new Promotion([ + 'usage_limit' => null, + 'usage_count' => 10000, + 'starts_at' => Carbon::yesterday(), + 'ends_at' => Carbon::tomorrow(), + ]); + + $this->assertTrue($promo->isValid()); + } + + /** @test */ + public function it_is_valid_if_it_has_neither_start_nor_end_date_and_is_unlimited() + { + $promo = new Promotion(); + + $this->assertTrue($promo->isValid()); + } + +}