diff --git a/CHANGELOG.md b/CHANGELOG.md index ab5b886f..2ecc8b91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ - Multiple discounts on receipts ([#1147](https://github.com/laravel/cashier-stripe/pull/1147)) - Preview upcoming invoice ([#1146](https://github.com/laravel/cashier-stripe/pull/1146)) - Add new metered price methods ([#1177](https://github.com/laravel/cashier-stripe/pull/1177)) +- Allow customers to be synced with Stripe ([#1178](https://github.com/laravel/cashier-stripe/pull/1178), [#1183](https://github.com/laravel/cashier-stripe/pull/1183)) +- Add `stripe_product` column to `subscriptions_items` table ([#1185](https://github.com/laravel/cashier-stripe/pull/1185)) ### Changed - Rename plans to prices ([#1166](https://github.com/laravel/cashier-stripe/pull/1166)) diff --git a/UPGRADE.md b/UPGRADE.md index 041848f8..bfcd31e6 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -198,6 +198,20 @@ PR: https://github.com/laravel/cashier-stripe/pull/1120 The hosted payment page for handling payment method failures has been improved to provide support for additional payment methods. No changes to your application are required if you have not published the `payment.blade.php` template. However, all translation support has been removed. If you were relying on this functionality you should publish the view and re-add the appropriate calls to Laravel's translation services. +### Stripe Product Support + +PR: https://github.com/laravel/cashier-stripe/pull/1185 + +Cashier Stripe v13 comes with support for checking Stripe Product identifiers. To provide support for this feature, a new `stripe_product` column should be added to the `stripe_subscriptions` table: + +```php +Schema::table('subscription_items', function (Blueprint $table) { + $table->string('stripe_product')->nullable()->after('stripe_id'); +}); +``` + +If you'd like to make use of the new `onProduct` & `subscribedToProduct` methods on your billable model, you should ensure the records in the `subscription_items` have their `stripe_product` column filled with the correct Product ID from Stripe. + ## Upgrading To 12.8 From 12.7 ### Metered Billing diff --git a/database/factories/SubscriptionItemFactory.php b/database/factories/SubscriptionItemFactory.php index afb233ab..9f827a49 100644 --- a/database/factories/SubscriptionItemFactory.php +++ b/database/factories/SubscriptionItemFactory.php @@ -26,6 +26,7 @@ public function definition() return [ 'subscription_id' => Subscription::factory(), 'stripe_id' => 'si_'.Str::random(40), + 'stripe_product' => 'prod_'.Str::random(40), 'stripe_price' => 'price_'.Str::random(40), 'quantity' => null, ]; diff --git a/database/migrations/2019_05_03_000003_create_subscription_items_table.php b/database/migrations/2019_05_03_000003_create_subscription_items_table.php index fad18497..40e41712 100644 --- a/database/migrations/2019_05_03_000003_create_subscription_items_table.php +++ b/database/migrations/2019_05_03_000003_create_subscription_items_table.php @@ -17,6 +17,7 @@ public function up() $table->bigIncrements('id'); $table->unsignedBigInteger('subscription_id'); $table->string('stripe_id')->index(); + $table->string('stripe_product'); $table->string('stripe_price'); $table->integer('quantity')->nullable(); $table->timestamps(); diff --git a/src/Concerns/ManagesSubscriptions.php b/src/Concerns/ManagesSubscriptions.php index 1a986aab..f9ce90c4 100644 --- a/src/Concerns/ManagesSubscriptions.php +++ b/src/Concerns/ManagesSubscriptions.php @@ -125,6 +125,30 @@ public function hasIncompletePayment($name = 'default') return false; } + /** + * Determine if the Stripe model is actively subscribed to one of the given products. + * + * @param string|string[] $products + * @param string $name + * @return bool + */ + public function subscribedToProduct($products, $name = 'default') + { + $subscription = $this->subscription($name); + + if (! $subscription || ! $subscription->valid()) { + return false; + } + + foreach ((array) $products as $product) { + if ($subscription->hasProduct($product)) { + return true; + } + } + + return false; + } + /** * Determine if the Stripe model is actively subscribed to one of the given prices. * @@ -149,6 +173,19 @@ public function subscribedToPrice($prices, $name = 'default') return false; } + /** + * Determine if the customer has a valid subscription on the given product. + * + * @param string $price + * @return bool + */ + public function onProduct($price) + { + return ! is_null($this->subscriptions->first(function (Subscription $subscription) use ($price) { + return $subscription->valid() && $subscription->hasProduct($price); + })); + } + /** * Determine if the customer has a valid subscription on the given price. * diff --git a/src/Http/Controllers/WebhookController.php b/src/Http/Controllers/WebhookController.php index 7ac4fd46..c9542f08 100644 --- a/src/Http/Controllers/WebhookController.php +++ b/src/Http/Controllers/WebhookController.php @@ -90,6 +90,7 @@ protected function handleCustomerSubscriptionCreated(array $payload) foreach ($data['items']['data'] as $item) { $subscription->items()->create([ 'stripe_id' => $item['id'], + 'stripe_product' => $item['price']['product'], 'stripe_price' => $item['price']['id'], 'quantity' => $item['quantity'] ?? null, ]); @@ -183,6 +184,7 @@ protected function handleCustomerSubscriptionUpdated(array $payload) $subscription->items()->updateOrCreate([ 'stripe_id' => $item['id'], ], [ + 'stripe_product' => $item['price']['product'], 'stripe_price' => $item['price']['id'], 'quantity' => $item['quantity'] ?? null, ]); diff --git a/src/Subscription.php b/src/Subscription.php index d977758f..989e60f4 100644 --- a/src/Subscription.php +++ b/src/Subscription.php @@ -120,6 +120,19 @@ public function hasSinglePrice() return ! $this->hasMultiplePrices(); } + /** + * Determine if the subscription has a specific product. + * + * @param string $product + * @return bool + */ + public function hasProduct($product) + { + return $this->items->contains(function (SubscriptionItem $item) use ($product) { + return $item->stripe_product === $product; + }); + } + /** * Determine if the subscription has a specific price. * @@ -675,6 +688,7 @@ public function swap($prices, array $options = []) $this->items()->updateOrCreate([ 'stripe_id' => $item->id, ], [ + 'stripe_product' => $item->price->product, 'stripe_price' => $item->price->id, 'quantity' => $item->quantity, ]); @@ -816,7 +830,7 @@ public function addPrice($price, $quantity = 1, array $options = []) throw SubscriptionUpdateFailure::duplicatePrice($this, $price); } - $item = $this->owner->stripe()->subscriptionItems->create(array_merge([ + $stripeSubscriptionItem = $this->owner->stripe()->subscriptionItems->create(array_merge([ 'subscription' => $this->stripe_id, 'price' => $price, 'quantity' => $quantity, @@ -826,8 +840,9 @@ public function addPrice($price, $quantity = 1, array $options = []) ], $options)); $this->items()->create([ - 'stripe_id' => $item->id, - 'stripe_price' => $price, + 'stripe_id' => $stripeSubscriptionItem->id, + 'stripe_product' => $stripeSubscriptionItem->price->product, + 'stripe_price' => $stripeSubscriptionItem->price->id, 'quantity' => $quantity, ]); diff --git a/src/SubscriptionBuilder.php b/src/SubscriptionBuilder.php index 6f2a713d..a7913bdd 100644 --- a/src/SubscriptionBuilder.php +++ b/src/SubscriptionBuilder.php @@ -286,6 +286,7 @@ protected function createSubscription(StripeSubscription $stripeSubscription) foreach ($stripeSubscription->items as $item) { $subscription->items()->create([ 'stripe_id' => $item->id, + 'stripe_product' => $item->price->product, 'stripe_price' => $item->price->id, 'quantity' => $item->quantity, ]); diff --git a/src/SubscriptionItem.php b/src/SubscriptionItem.php index 098405e5..89ba4ea8 100644 --- a/src/SubscriptionItem.php +++ b/src/SubscriptionItem.php @@ -151,7 +151,8 @@ public function swap($price, array $options = []) ], $options)); $this->fill([ - 'stripe_price' => $price, + 'stripe_product' => $stripeSubscriptionItem->price->product, + 'stripe_price' => $stripeSubscriptionItem->price->id, 'quantity' => $stripeSubscriptionItem->quantity, ])->save(); diff --git a/tests/Feature/MultipriceSubscriptionsTest.php b/tests/Feature/MultipriceSubscriptionsTest.php index a29a3d4e..1446b202 100644 --- a/tests/Feature/MultipriceSubscriptionsTest.php +++ b/tests/Feature/MultipriceSubscriptionsTest.php @@ -98,6 +98,7 @@ public function test_customers_can_have_multiprice_subscriptions() ->create('pm_card_visa'); $this->assertTrue($user->subscribed('main', self::$priceId)); + $this->assertTrue($user->onProduct(self::$productId)); $this->assertTrue($user->onPrice(self::$priceId)); $item = $subscription->findItemOrFail(self::$priceId); @@ -122,6 +123,7 @@ public function test_customers_can_add_prices() $subscription->addPrice(self::$otherPriceId, 5); + $this->assertTrue($user->onProduct(self::$productId)); $this->assertTrue($user->onPrice(self::$priceId)); $this->assertFalse($user->onPrice(self::$premiumPriceId)); @@ -331,6 +333,7 @@ protected function createSubscriptionWithSinglePrice(User $user) $subscription->items()->create([ 'stripe_id' => 'it_foo', + 'stripe_product' => self::$productId, 'stripe_price' => self::$priceId, 'quantity' => 1, ]); @@ -354,6 +357,7 @@ protected function createSubscriptionWithMultiplePrices(User $user) $subscription->items()->create([ 'stripe_id' => 'it_foo', + 'stripe_product' => self::$productId, 'stripe_price' => self::$otherPriceId, 'quantity' => 1, ]); diff --git a/tests/Feature/SubscriptionsTest.php b/tests/Feature/SubscriptionsTest.php index d6e37e13..b74acf52 100644 --- a/tests/Feature/SubscriptionsTest.php +++ b/tests/Feature/SubscriptionsTest.php @@ -116,6 +116,7 @@ public function test_subscriptions_can_be_created() $this->assertSame($metadata, $subscription->asStripeSubscription()->metadata->toArray()); $this->assertTrue($user->subscribed('main')); + $this->assertTrue($user->subscribedToProduct(static::$productId, 'main')); $this->assertTrue($user->subscribedToPrice(static::$priceId, 'main')); $this->assertFalse($user->subscribedToPrice(static::$priceId, 'something')); $this->assertFalse($user->subscribedToPrice(static::$otherPriceId, 'main')); diff --git a/tests/Feature/WebhooksTest.php b/tests/Feature/WebhooksTest.php index 2bbd746c..d3035b94 100644 --- a/tests/Feature/WebhooksTest.php +++ b/tests/Feature/WebhooksTest.php @@ -57,7 +57,7 @@ public function test_subscriptions_are_created() 'items' => [ 'data' => [[ 'id' => 'bar', - 'price' => ['id' => 'price_foo'], + 'price' => ['id' => 'price_foo', 'product' => 'prod_bar'], 'quantity' => 10, ]], ], @@ -76,6 +76,7 @@ public function test_subscriptions_are_created() $this->assertDatabaseHas('subscription_items', [ 'stripe_id' => 'bar', + 'stripe_product' => 'prod_bar', 'stripe_price' => 'price_foo', 'quantity' => 10, ]); @@ -94,6 +95,7 @@ public function test_subscriptions_are_updated() $item = $subscription->items()->create([ 'stripe_id' => 'it_foo', + 'stripe_product' => 'prod_bar', 'stripe_price' => 'price_bar', 'quantity' => 1, ]); @@ -109,7 +111,7 @@ public function test_subscriptions_are_updated() 'items' => [ 'data' => [[ 'id' => 'bar', - 'price' => ['id' => 'price_foo'], + 'price' => ['id' => 'price_foo', 'product' => 'prod_bar'], 'quantity' => 5, ]], ], @@ -127,6 +129,7 @@ public function test_subscriptions_are_updated() $this->assertDatabaseHas('subscription_items', [ 'subscription_id' => $subscription->id, 'stripe_id' => 'bar', + 'stripe_product' => 'prod_bar', 'stripe_price' => 'price_foo', 'quantity' => 5, ]); @@ -150,6 +153,7 @@ public function test_subscriptions_on_update_cancel_at_date_is_correct() $item = $subscription->items()->create([ 'stripe_id' => 'it_foo', + 'stripe_product' => 'prod_bar', 'stripe_price' => 'price_bar', 'quantity' => 1, ]); @@ -166,7 +170,7 @@ public function test_subscriptions_on_update_cancel_at_date_is_correct() 'items' => [ 'data' => [[ 'id' => 'bar', - 'price' => ['id' => 'price_foo'], + 'price' => ['id' => 'price_foo', 'product' => 'prod_bar'], 'quantity' => 5, ]], ], @@ -185,6 +189,7 @@ public function test_subscriptions_on_update_cancel_at_date_is_correct() $this->assertDatabaseHas('subscription_items', [ 'subscription_id' => $subscription->id, 'stripe_id' => 'bar', + 'stripe_product' => 'prod_bar', 'stripe_price' => 'price_foo', 'quantity' => 5, ]); @@ -212,7 +217,7 @@ public function test_cancelled_subscription_is_properly_reactivated() 'items' => [ 'data' => [[ 'id' => $subscription->items()->first()->stripe_id, - 'price' => ['id' => static::$priceId], + 'price' => ['id' => static::$priceId, 'product' => static::$productId], 'quantity' => 1, ]], ],