diff --git a/src/Concerns/ManagesCustomer.php b/src/Concerns/ManagesCustomer.php index f0c9f242..c7c24111 100644 --- a/src/Concerns/ManagesCustomer.php +++ b/src/Concerns/ManagesCustomer.php @@ -6,8 +6,10 @@ use Illuminate\Support\Collection; use Laravel\Cashier\Cashier; use Laravel\Cashier\CustomerBalanceTransaction; +use Laravel\Cashier\Discount; use Laravel\Cashier\Exceptions\CustomerAlreadyCreated; use Laravel\Cashier\Exceptions\InvalidCustomer; +use Laravel\Cashier\PromotionCode; use Stripe\Customer as StripeCustomer; use Stripe\Exception\InvalidRequestException as StripeInvalidRequestException; @@ -194,6 +196,20 @@ public function syncStripeCustomerDetails() ]); } + /** + * The discount that applies to the customer, if applicable. + * + * @return \Laravel\Cashier\Discount|null + */ + public function discount() + { + $customer = $this->asStripeCustomer(['discount.promotion_code']); + + return $customer->discount + ? new Discount($customer->discount) + : null; + } + /** * Apply a coupon to the customer. * @@ -209,6 +225,52 @@ public function applyCoupon($coupon) ]); } + /** + * Apply a promotion code to the customer. + * + * @param string $promotionCodeId + * @return void + */ + public function applyPromotionCode($promotionCodeId) + { + $this->assertCustomerExists(); + + $this->updateStripeCustomer([ + 'promotion_code' => $promotionCodeId, + ]); + } + + /** + * Retrieve a promotion code by its code. + * + * @param string $code + * @param array $options + * @return \Laravel\Cashier\PromotionCode|null + */ + public function findPromotionCode($code, array $options = []) + { + $codes = $this->stripe()->promotionCodes->all(array_merge([ + 'code' => $code, + 'limit' => 1, + ], $options)); + + if ($codes && $promotionCode = $codes->first()) { + return new PromotionCode($promotionCode); + } + } + + /** + * Retrieve a promotion code by its code. + * + * @param string $code + * @param array $options + * @return \Laravel\Cashier\PromotionCode|null + */ + public function findActivePromotionCode($code, array $options = []) + { + return $this->findPromotionCode($code, array_merge($options, ['active' => true])); + } + /** * Get the total balance of the customer. * diff --git a/src/Discount.php b/src/Discount.php index 1a117d25..9060491e 100644 --- a/src/Discount.php +++ b/src/Discount.php @@ -2,6 +2,7 @@ namespace Laravel\Cashier; +use Carbon\Carbon; use Illuminate\Contracts\Support\Arrayable; use Illuminate\Contracts\Support\Jsonable; use JsonSerializable; @@ -37,6 +38,40 @@ public function coupon() return new Coupon($this->discount->coupon); } + /** + * Get the promotion code applied to create this discount. + * + * @return \Laravel\Cashier\PromotionCode|null + */ + public function promotionCode() + { + if (! is_null($this->discount->promotion_code) && ! is_string($this->discount->promotion_code)) { + return new PromotionCode($this->discount->promotion_code); + } + } + + /** + * Get the date that the coupon was applied. + * + * @return \Carbon\Carbon + */ + public function start() + { + return Carbon::createFromTimestamp($this->discount->start); + } + + /** + * Get the date that this discount will end. + * + * @return \Carbon\Carbon|null + */ + public function end() + { + if (! is_null($this->discount->end)) { + return Carbon::createFromTimestamp($this->discount->end); + } + } + /** * Get the Stripe Discount instance. * diff --git a/src/PromotionCode.php b/src/PromotionCode.php new file mode 100644 index 00000000..0b783cfa --- /dev/null +++ b/src/PromotionCode.php @@ -0,0 +1,92 @@ +promotionCode = $promotionCode; + } + + /** + * Get the coupon that belongs to the promotion code. + * + * @return \Laravel\Cashier\Coupon + */ + public function coupon() + { + return new Coupon($this->promotionCode->coupon); + } + + /** + * Get the Stripe PromotionCode instance. + * + * @return \Stripe\PromotionCode + */ + public function asStripePromotionCode() + { + return $this->promotionCode; + } + + /** + * Get the instance as an array. + * + * @return array + */ + public function toArray() + { + return $this->asStripePromotionCode()->toArray(); + } + + /** + * Convert the object to its JSON representation. + * + * @param int $options + * @return string + */ + public function toJson($options = 0) + { + return json_encode($this->jsonSerialize(), $options); + } + + /** + * Convert the object into something JSON serializable. + * + * @return array + */ + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return $this->toArray(); + } + + /** + * Dynamically get values from the Stripe object. + * + * @param string $key + * @return mixed + */ + public function __get($key) + { + return $this->promotionCode->{$key}; + } +} diff --git a/src/Subscription.php b/src/Subscription.php index 4a1afec6..ec49a3f4 100644 --- a/src/Subscription.php +++ b/src/Subscription.php @@ -1312,6 +1312,46 @@ public function latestPayment() } } + /** + * The discount that applies to the subscription, if applicable. + * + * @return \Laravel\Cashier\Discount|null + */ + public function discount() + { + $subscription = $this->asStripeSubscription(['discount.promotion_code']); + + return $subscription->discount + ? new Discount($subscription->discount) + : null; + } + + /** + * Apply a coupon to the subscription. + * + * @param string $coupon + * @return void + */ + public function applyCoupon($coupon) + { + $this->updateStripeSubscription([ + 'coupon' => $coupon, + ]); + } + + /** + * Apply a promotion code to the subscription. + * + * @param string $promotionCodeId + * @return void + */ + public function applyPromotionCode($promotionCodeId) + { + $this->updateStripeSubscription([ + 'promotion_code' => $promotionCodeId, + ]); + } + /** * Make sure a subscription is not incomplete when performing changes. * diff --git a/tests/Feature/DiscountTest.php b/tests/Feature/DiscountTest.php new file mode 100644 index 00000000..5593c683 --- /dev/null +++ b/tests/Feature/DiscountTest.php @@ -0,0 +1,137 @@ +products->create([ + 'name' => 'Laravel Cashier Test Product', + 'type' => 'service', + ])->id; + + static::$priceId = self::stripe()->prices->create([ + 'product' => static::$productId, + 'nickname' => 'Monthly $10', + 'currency' => 'USD', + 'recurring' => [ + 'interval' => 'month', + ], + 'billing_scheme' => 'per_unit', + 'unit_amount' => 1000, + ])->id; + + static::$couponId = self::stripe()->coupons->create([ + 'duration' => 'repeating', + 'amount_off' => 500, + 'duration_in_months' => 3, + 'currency' => 'USD', + ])->id; + + static::$secondCouponId = self::stripe()->coupons->create([ + 'duration' => 'once', + 'percent_off' => 20, + 'currency' => 'USD', + ])->id; + + static::$promotionCodeId = self::stripe()->promotionCodes->create([ + 'coupon' => static::$secondCouponId, + 'code' => static::$promotionCodeCode = Str::random(16), + ])->id; + } + + public function test_applying_discounts_to_existing_customers() + { + $user = $this->createCustomer('applying_coupons_to_existing_customers'); + + $user->newSubscription('main', static::$priceId)->create('pm_card_visa'); + + $user->applyCoupon(static::$couponId); + + $this->assertEquals(static::$couponId, $user->discount()->coupon()->id); + + $user->applyPromotionCode(static::$promotionCodeId); + + $this->assertEquals(static::$secondCouponId, $user->discount()->coupon()->id); + $this->assertEquals(static::$promotionCodeId, $user->discount()->promotionCode()->id); + $this->assertEquals(static::$secondCouponId, $user->discount()->promotionCode()->coupon()->id); + $this->assertEquals(static::$promotionCodeCode, $user->discount()->promotionCode()->code); + } + + public function test_applying_discounts_to_existing_subscriptions() + { + $user = $this->createCustomer('applying_coupons_to_existing_subscriptions'); + + $subscription = $user->newSubscription('main', static::$priceId)->create('pm_card_visa'); + + $subscription->applyCoupon(static::$couponId); + + $this->assertEquals(static::$couponId, $subscription->discount()->coupon()->id); + + $subscription->applyPromotionCode(static::$promotionCodeId); + + $this->assertEquals(static::$secondCouponId, $subscription->discount()->coupon()->id); + $this->assertEquals(static::$promotionCodeId, $subscription->discount()->promotionCode()->id); + $this->assertEquals(static::$secondCouponId, $subscription->discount()->promotionCode()->coupon()->id); + $this->assertEquals(static::$promotionCodeCode, $subscription->discount()->promotionCode()->code); + } + + public function test_customers_can_retrieve_a_promotion_code() + { + $user = $this->createCustomer('customers_can_retrieve_a_promotion_code'); + + $promotionCode = $user->findPromotionCode(static::$promotionCodeCode); + + $this->assertEquals(static::$promotionCodeCode, $promotionCode->code); + + // Inactive promotion codes aren't retrieved with the "active only" method... + $inactivePromotionCode = $user->stripe()->promotionCodes->create([ + 'active' => false, + 'coupon' => static::$couponId, + 'code' => 'NEWYEAR', + ]); + + $promotionCode = $user->findActivePromotionCode($inactivePromotionCode->id); + + $this->assertNull($promotionCode); + } +} diff --git a/tests/Feature/SubscriptionsTest.php b/tests/Feature/SubscriptionsTest.php index 8551e2b1..15cc129e 100644 --- a/tests/Feature/SubscriptionsTest.php +++ b/tests/Feature/SubscriptionsTest.php @@ -198,7 +198,7 @@ public function test_swapping_subscription_with_coupon() 'coupon' => static::$couponId, ]); - $this->assertEquals(static::$couponId, $subscription->asStripeSubscription()->discount->coupon->id); + $this->assertEquals(static::$couponId, $subscription->discount()->coupon()->id); } public function test_swapping_subscription_and_preserving_quantity() @@ -757,19 +757,6 @@ public function test_trials_can_be_ended() $this->assertNull($subscription->trial_ends_at); } - public function test_applying_coupons_to_existing_customers() - { - $user = $this->createCustomer('applying_coupons_to_existing_customers'); - - $user->newSubscription('main', static::$priceId)->create('pm_card_visa'); - - $user->applyCoupon(static::$couponId); - - $customer = $user->asStripeCustomer(); - - $this->assertEquals(static::$couponId, $customer->discount->coupon->id); - } - public function test_subscription_state_scopes() { $user = $this->createCustomer('subscription_state_scopes');