DEV: Stop deleting customers on cancel (#207)

Instead of deleting customers on cancel we will now update the
subscription status to canceled. This way we can have some visibility on
which users have canceled.
This commit is contained in:
Blake Erickson 2024-05-02 13:38:30 -06:00 committed by GitHub
parent aaa4baec8a
commit 66e8857c20
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 500 additions and 96 deletions

View File

@ -83,6 +83,7 @@ module DiscourseSubscriptions
expand: ["data.plan.product"], expand: ["data.plan.product"],
limit: PAGE_LIMIT, limit: PAGE_LIMIT,
starting_after: start, starting_after: start,
status: "all",
) )
end end

View File

@ -39,11 +39,7 @@ module DiscourseSubscriptions
user = ::User.find_by_username_or_email(email) user = ::User.find_by_username_or_email(email)
discourse_customer = Customer.find_by(user_id: user.id)
if discourse_customer.nil?
discourse_customer = Customer.create(user_id: user.id, customer_id: customer_id) discourse_customer = Customer.create(user_id: user.id, customer_id: customer_id)
end
Subscription.create( Subscription.create(
customer_id: discourse_customer.id, customer_id: discourse_customer.id,
@ -64,48 +60,60 @@ module DiscourseSubscriptions
) )
when "customer.subscription.created" when "customer.subscription.created"
when "customer.subscription.updated" when "customer.subscription.updated"
status = event[:data][:object][:status] subscription = event[:data][:object]
status = subscription[:status]
return head 200 if !%w[complete active].include?(status) return head 200 if !%w[complete active].include?(status)
customer = customer = find_active_customer(subscription[:customer], subscription[:plan][:product])
Customer.find_by(
customer_id: event[:data][:object][:customer],
product_id: event[:data][:object][:plan][:product],
)
return render_json_error "customer not found" if !customer return render_json_error "customer not found" if !customer
update_status(customer.id, subscription[:id], status)
user = ::User.find_by(id: customer.user_id) user = ::User.find_by(id: customer.user_id)
return render_json_error "user not found" if !user return render_json_error "user not found" if !user
if group = plan_group(event[:data][:object][:plan]) if group = plan_group(subscription[:plan])
group.add(user) group.add(user)
end end
when "customer.subscription.deleted" when "customer.subscription.deleted"
customer = subscription = event[:data][:object]
Customer.find_by(
customer_id: event[:data][:object][:customer], customer = find_active_customer(subscription[:customer], subscription[:plan][:product])
product_id: event[:data][:object][:plan][:product],
)
return render_json_error "customer not found" if !customer return render_json_error "customer not found" if !customer
Subscription.find_by( update_status(customer.id, subscription[:id], subscription[:status])
customer_id: customer.id,
external_id: event[:data][:object][:id],
)&.destroy!
user = ::User.find(customer.user_id) user = ::User.find(customer.user_id)
return render_json_error "user not found" if !user return render_json_error "user not found" if !user
if group = plan_group(event[:data][:object][:plan]) if group = plan_group(subscription[:plan])
group.remove(user) group.remove(user)
end end
customer.destroy!
end end
head 200 head 200
end end
private
def update_status(customer_id, subscription_id, status)
discourse_subscription =
Subscription.find_by(customer_id: customer_id, external_id: subscription_id)
discourse_subscription.update(status: status) if discourse_subscription
end
def find_active_customer(customer_id, product_id)
Customer
.joins(:subscriptions)
.where(customer_id: customer_id, product_id: product_id)
.where(
Subscription.arel_table[:status].eq(nil).or(
Subscription.arel_table[:status].not_eq("canceled"),
),
)
.first
end
end end
end end

View File

@ -150,7 +150,11 @@ module DiscourseSubscriptions
) )
if transaction[:object] == "subscription" if transaction[:object] == "subscription"
Subscription.create(customer_id: customer.id, external_id: transaction[:id]) Subscription.create(
customer_id: customer.id,
external_id: transaction[:id],
status: transaction[:status],
)
end end
end end
@ -169,7 +173,17 @@ module DiscourseSubscriptions
def current_user_products def current_user_products
return [] if current_user.nil? return [] if current_user.nil?
Customer.select(:product_id).where(user_id: current_user.id).map { |c| c.product_id }.compact Customer
.joins(:subscriptions)
.where(user_id: current_user.id)
.where(
Subscription.arel_table[:status].eq(nil).or(
Subscription.arel_table[:status].not_eq("canceled"),
),
)
.select(:product_id)
.distinct
.pluck(:product_id)
end end
def serialize_plans(plans) def serialize_plans(plans)

View File

@ -15,6 +15,7 @@ module DiscourseSubscriptions
begin begin
customer = Customer.where(user_id: current_user.id) customer = Customer.where(user_id: current_user.id)
customer_ids = customer.map { |c| c.id } if customer customer_ids = customer.map { |c| c.id } if customer
stripe_customer_ids = customer.map { |c| c.customer_id } if customer
subscription_ids = subscription_ids =
Subscription.where("customer_id in (?)", customer_ids).pluck( Subscription.where("customer_id in (?)", customer_ids).pluck(
:external_id, :external_id,
@ -24,15 +25,14 @@ module DiscourseSubscriptions
if subscription_ids if subscription_ids
plans = ::Stripe::Price.list(expand: ["data.product"], limit: 100) plans = ::Stripe::Price.list(expand: ["data.product"], limit: 100)
all_subscriptions = []
customers = stripe_customer_ids.each do |stripe_customer_id|
::Stripe::Customer.list(email: current_user.email, expand: ["data.subscriptions"]) customer_subscriptions =
::Stripe::Subscription.list(customer: stripe_customer_id, status: "all")
subscriptions = all_subscriptions.concat(customer_subscriptions[:data])
customers[:data].map { |sub_customer| sub_customer[:subscriptions][:data] }.flatten(1) end
subscriptions = all_subscriptions.select { |sub| subscription_ids.include?(sub[:id]) }
subscriptions = subscriptions.select { |sub| subscription_ids.include?(sub[:id]) }
subscriptions.map! do |subscription| subscriptions.map! do |subscription|
plan = plans[:data].find { |p| p[:id] == subscription[:items][:data][0][:price][:id] } plan = plans[:data].find { |p| p[:id] == subscription[:items][:data][0][:price][:id] }
subscription.to_h.except!(:plan) subscription.to_h.except!(:plan)

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddStatusToSubscriptions < ActiveRecord::Migration[7.0]
def change
add_column :discourse_subscriptions_subscriptions, :status, :string
end
end

View File

@ -0,0 +1,67 @@
{
"object": "list",
"data": [
{
"id": "price_1OrmlvEYXaQnncShNahrpKvA",
"object": "price",
"active": true,
"billing_scheme": "per_unit",
"created": 1709840311,
"currency": "usd",
"custom_unit_amount": null,
"livemode": false,
"lookup_key": null,
"metadata": {
"group_name": "subscribers",
"trial_period_days": "0"
},
"nickname": "EA1",
"product": {
"id": "prod_PhB6IpGhEX14Hi",
"object": "product",
"active": true,
"attributes": [
],
"created": 1709840195,
"default_price": null,
"description": null,
"images": [
],
"livemode": false,
"marketing_features": [
],
"metadata": {
"description": "Sign up and get access to an exclusive group of enthusiasts just like you!"
},
"name": "Exclusive Access",
"package_dimensions": null,
"shippable": null,
"statement_descriptor": "TESTING",
"tax_code": null,
"type": "service",
"unit_label": null,
"updated": 1709840195,
"url": null
},
"recurring": {
"aggregate_usage": null,
"interval": "month",
"interval_count": 1,
"meter": null,
"trial_period_days": null,
"usage_type": "licensed"
},
"tax_behavior": "unspecified",
"tiers_mode": null,
"transform_quantity": null,
"type": "recurring",
"unit_amount": 1000,
"unit_amount_decimal": "1000"
}
],
"has_more": false,
"url": "/v1/prices"
}

View File

@ -0,0 +1,149 @@
{
"has_more": false,
"data": [
{
"id": "sub_10z",
"object": "subscription",
"created": 1714594277,
"current_period_end": 1717272677,
"current_period_start": 1714594277,
"customer": "cus_Q1n43We0YFjnlc",
"items": {
"object": "list",
"data": [
{
"id": "si_Q1n45g1Ifcluuu",
"object": "subscription_item",
"billing_thresholds": null,
"created": 1714594277,
"discounts": [],
"metadata": {},
"plan": {
"id": "price_1OrmlvEYXaQnncShNahrpKvA",
"object": "plan",
"active": true,
"created": 1709840311,
"metadata": {
"group_name": "subscribers",
"trial_period_days": "0"
},
"nickname": "EA1",
"product": "prod_PhB6IpGhEX14Hi"
},
"price": {
"id": "price_1OrmlvEYXaQnncShNahrpKvA",
"object": "price",
"metadata": {
"group_name": "subscribers",
"trial_period_days": "0"
},
"nickname": "EA1",
"product": "prod_PhB6IpGhEX14Hi"
},
"quantity": 1,
"subscription": "sub_1PBjUnEYXaQnncShE7USquGd",
"tax_rates": []
}
],
"has_more": false,
"total_count": 1,
"url": "/v1/subscription_items?subscription=sub_1PBjUnEYXaQnncShE7USquGd"
},
"latest_invoice": "in_1PBjUnEYXaQnncSh5c7HZ2jG",
"metadata": { "user_id": "108", "username": "f79fc8fde" },
"plan": {
"id": "price_1OrmlvEYXaQnncShNahrpKvA",
"object": "plan",
"active": true,
"metadata": { "group_name": "subscribers", "trial_period_days": "0" },
"meter": null,
"nickname": "EA1",
"product": {
"id": "prod_PhB6IpGhEX14Hi",
"object": "product",
"metadata": {
"description": "Sign up and get access to an exclusive group of enthusiasts just like you!"
},
"name": "Exclusive Access"
}
},
"quantity": 1,
"schedule": null,
"start_date": 1714594277,
"status": "active"
},
{
"id": "sub_32b",
"object": "subscription",
"created": 1714594277,
"current_period_end": 1717272677,
"current_period_start": 1714594277,
"customer": "cus_Q1n43We0YFjnlc",
"items": {
"object": "list",
"data": [
{
"id": "si_Q1n45g1Ifcluuu",
"object": "subscription_item",
"billing_thresholds": null,
"created": 1714594277,
"discounts": [],
"metadata": {},
"plan": {
"id": "price_1OrmlvEYXaQnncShNahrpKvA",
"object": "plan",
"active": true,
"created": 1709840311,
"metadata": {
"group_name": "subscribers",
"trial_period_days": "0"
},
"nickname": "EA1",
"product": "prod_PhB6IpGhEX14Hi"
},
"price": {
"id": "price_1OrmlvEYXaQnncShNahrpKvA",
"object": "price",
"metadata": {
"group_name": "subscribers",
"trial_period_days": "0"
},
"nickname": "EA1",
"product": "prod_PhB6IpGhEX14Hi"
},
"quantity": 1,
"subscription": "sub_1PBjUnEYXaQnncShE7USquGd",
"tax_rates": []
}
],
"has_more": false,
"total_count": 1,
"url": "/v1/subscription_items?subscription=sub_1PBjUnEYXaQnncShE7USquGd"
},
"latest_invoice": "in_1PBjUnEYXaQnncSh5c7HZ2jG",
"metadata": { "user_id": "108", "username": "f79fc8fde" },
"plan": {
"id": "price_1OrmlvEYXaQnncShNahrpKvA",
"object": "plan",
"active": true,
"metadata": { "group_name": "subscribers", "trial_period_days": "0" },
"meter": null,
"nickname": "EA1",
"product": {
"id": "prod_PhB6IpGhEX14Hi",
"object": "product",
"metadata": {
"description": "Sign up and get access to an exclusive group of enthusiasts just like you!"
},
"name": "Exclusive Access"
}
},
"quantity": 1,
"schedule": null,
"start_date": 1714594277,
"status": "canceled"
}
],
"length": 2,
"last_record": "sub_1P9aohEYXaQnncSh4wf1wzuL"
}

View File

@ -48,7 +48,7 @@ RSpec.describe DiscourseSubscriptions::Admin::SubscriptionsController do
it "gets the subscriptions and products" do it "gets the subscriptions and products" do
::Stripe::Subscription ::Stripe::Subscription
.expects(:list) .expects(:list)
.with(expand: ["data.plan.product"], limit: 10, starting_after: nil) .with(expand: ["data.plan.product"], limit: 10, starting_after: nil, status: "all")
.returns(has_more: false, data: [{ id: "sub_12345" }, { id: "sub_nope" }]) .returns(has_more: false, data: [{ id: "sub_12345" }, { id: "sub_nope" }])
get "/s/admin/subscriptions.json" get "/s/admin/subscriptions.json"
subscriptions = response.parsed_body["data"][0]["id"] subscriptions = response.parsed_body["data"][0]["id"]
@ -60,7 +60,7 @@ RSpec.describe DiscourseSubscriptions::Admin::SubscriptionsController do
it "handles starting at a different point in the set" do it "handles starting at a different point in the set" do
::Stripe::Subscription ::Stripe::Subscription
.expects(:list) .expects(:list)
.with(expand: ["data.plan.product"], limit: 10, starting_after: "sub_nope") .with(expand: ["data.plan.product"], limit: 10, starting_after: "sub_nope", status: "all")
.returns(has_more: false, data: [{ id: "sub_77777" }, { id: "sub_yepnoep" }]) .returns(has_more: false, data: [{ id: "sub_77777" }, { id: "sub_yepnoep" }])
get "/s/admin/subscriptions.json", params: { last_record: "sub_nope" } get "/s/admin/subscriptions.json", params: { last_record: "sub_nope" }
subscriptions = response.parsed_body["data"][0]["id"] subscriptions = response.parsed_body["data"][0]["id"]

View File

@ -27,6 +27,9 @@ RSpec.describe DiscourseSubscriptions::HooksController do
let(:customer) do let(:customer) do
Fabricate(:customer, customer_id: "c_575768", product_id: "p_8654", user_id: user.id) Fabricate(:customer, customer_id: "c_575768", product_id: "p_8654", user_id: user.id)
end end
let!(:subscription) do
Fabricate(:subscription, external_id: "sub_12345", customer_id: customer.id, status: nil)
end
let(:group) { Fabricate(:group, name: "subscribers-group") } let(:group) { Fabricate(:group, name: "subscribers-group") }
let(:event_data) do let(:event_data) do
@ -43,6 +46,22 @@ RSpec.describe DiscourseSubscriptions::HooksController do
} }
end end
let(:customer_subscription_deleted_data) do
{
object: {
id: subscription.external_id,
customer: customer.customer_id,
plan: {
product: customer.product_id,
metadata: {
group_name: group.name,
},
},
status: "canceled",
},
}
end
let(:checkout_session_completed_data) do let(:checkout_session_completed_data) do
{ {
object: { object: {
@ -217,7 +236,7 @@ RSpec.describe DiscourseSubscriptions::HooksController do
describe "customer.subscription.deleted" do describe "customer.subscription.deleted" do
before do before do
event = { type: "customer.subscription.deleted", data: event_data } event = { type: "customer.subscription.deleted", data: customer_subscription_deleted_data }
::Stripe::Webhook.stubs(:construct_event).returns(event) ::Stripe::Webhook.stubs(:construct_event).returns(event)
@ -225,7 +244,9 @@ RSpec.describe DiscourseSubscriptions::HooksController do
end end
it "deletes the customer" do it "deletes the customer" do
expect { post "/s/hooks.json" }.to change { DiscourseSubscriptions::Customer.count }.by(-1) expect { post "/s/hooks.json" }.to change {
DiscourseSubscriptions::Subscription.where(status: "canceled").count
}.by(+1)
expect(response.status).to eq 200 expect(response.status).to eq 200
end end

View File

@ -69,6 +69,10 @@ RSpec.describe DiscourseSubscriptions::SubscribeController do
end end
describe "#index" do describe "#index" do
let(:customer) do
Fabricate(:customer, product_id: product[:id], user_id: user.id, customer_id: "x")
end
it "gets products" do it "gets products" do
::Stripe::Product ::Stripe::Product
.expects(:list) .expects(:list)
@ -94,7 +98,8 @@ RSpec.describe DiscourseSubscriptions::SubscribeController do
end end
it "is subscribed" do it "is subscribed" do
Fabricate(:customer, product_id: product[:id], user_id: user.id, customer_id: "x") Fabricate(:subscription, external_id: "sub_12345", customer_id: customer.id, status: nil)
::Stripe::Product ::Stripe::Product
.expects(:list) .expects(:list)
.with({ ids: product_ids, active: true }) .with({ ids: product_ids, active: true })

View File

@ -37,62 +37,48 @@ RSpec.describe DiscourseSubscriptions::User::SubscriptionsController do
before do before do
sign_in(user) sign_in(user)
Fabricate(:subscription, customer_id: customer.id, external_id: "sub_1234") Fabricate(:subscription, customer_id: customer.id, external_id: "sub_10z")
end end
describe "index" do describe "index" do
let(:plans) do plans_json =
{ File.read(
data: [ Rails.root.join(
{ id: "plan_1", product: { name: "ACME Subscriptions" } }, "plugins",
{ id: "plan_2", product: { name: "ACME Other Subscriptions" } }, "discourse-subscriptions",
], "spec",
} "fixtures",
end "json",
"stripe-price-list.json",
let(:customers) do ),
{ )
data: [
{
id: "cus_23456",
subscriptions: {
data: [
{ id: "sub_1234", items: { data: [price: { id: "plan_1" }] } },
{ id: "sub_4567", items: { data: [price: { id: "plan_2" }] } },
],
},
},
],
}
end
it "gets subscriptions" do it "gets subscriptions" do
::Stripe::Price.expects(:list).with(expand: ["data.product"], limit: 100).returns(plans) ::Stripe::Price.stubs(:list).returns(JSON.parse(plans_json, symbolize_names: true))
::Stripe::Customer subscriptions_json =
.expects(:list) File.read(
.with(email: user.email, expand: ["data.subscriptions"]) Rails.root.join(
.returns(customers) "plugins",
"discourse-subscriptions",
"spec",
"fixtures",
"json",
"stripe-subscription-list.json",
),
)
::Stripe::Subscription.stubs(:list).returns(
JSON.parse(subscriptions_json, symbolize_names: true),
)
get "/s/user/subscriptions.json" get "/s/user/subscriptions.json"
subscription = response.parsed_body.first subscription = JSON.parse(response.body, symbolize_names: true).first
expect(subscription).to eq( expect(subscription[:id]).to eq("sub_10z")
"id" => "sub_1234", expect(subscription[:items][:data][0][:plan][:id]).to eq("price_1OrmlvEYXaQnncShNahrpKvA")
"items" => { expect(subscription[:product][:name]).to eq("Exclusive Access")
"data" => [{ "price" => { "id" => "plan_1" } }],
},
"plan" => {
"id" => "plan_1",
"product" => {
"name" => "ACME Subscriptions",
},
},
"product" => {
"name" => "ACME Subscriptions",
},
)
end end
end end
@ -100,7 +86,7 @@ RSpec.describe DiscourseSubscriptions::User::SubscriptionsController do
it "updates the payment method for subscription" do it "updates the payment method for subscription" do
::Stripe::Subscription.expects(:update).once ::Stripe::Subscription.expects(:update).once
::Stripe::PaymentMethod.expects(:attach).once ::Stripe::PaymentMethod.expects(:attach).once
put "/s/user/subscriptions/sub_1234.json", params: { payment_method: "pm_abc123abc" } put "/s/user/subscriptions/sub_10z.json", params: { payment_method: "pm_abc123abc" }
end end
end end
end end

View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
module PageObjects
module Pages
class AdminSubscriptionSubscription < PageObjects::Pages::Base
SUBSCRIPTIONS_TABLE_SELECTOR = "table.discourse-patrons-table"
def visit_subscriptions
visit("/admin/plugins/discourse-subscriptions/subscriptions")
self
end
def has_subscription?(id)
has_css?("#{SUBSCRIPTIONS_TABLE_SELECTOR} tr", text: id)
self
end
def subscription_row(id)
find("#{SUBSCRIPTIONS_TABLE_SELECTOR} tr", text: id)
end
def has_number_of_subscriptions?(count)
has_css?("#{SUBSCRIPTIONS_TABLE_SELECTOR} tr", count:)
self
end
def click_cancel_nth_row(row)
find("#{SUBSCRIPTIONS_TABLE_SELECTOR} tr:nth-child(#{row}) button.btn-danger").click()
end
end
end
end

View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
module PageObjects
module Pages
class UserBillingSubscription < PageObjects::Pages::Base
SUBSCRIPTIONS_TABLE_SELECTOR = "table.discourse-subscriptions-user-table"
def visit_subscriptions
visit("/my/billing/subscriptions")
self
end
def has_subscription?(id)
has_css?("#{SUBSCRIPTIONS_TABLE_SELECTOR} tr", text: id)
self
end
def subscription_row(id)
find("#{SUBSCRIPTIONS_TABLE_SELECTOR} tr", text: id)
end
def has_number_of_subscriptions?(count)
has_css?("#{SUBSCRIPTIONS_TABLE_SELECTOR} tr", count:)
self
end
def click_cancel_nth_row(row)
find("#{SUBSCRIPTIONS_TABLE_SELECTOR} tr:nth-child(#{row}) button.btn-danger").click()
end
end
end
end

View File

@ -67,14 +67,4 @@ RSpec.describe "Pricing Table", type: :system, js: true do
text: "There are currently no products available.", text: "There are currently no products available.",
) )
end end
# Commenting out for now, not sure how to stub network reqeusts made in the browser to stripe
# it "Shows a pricing table when setup" do
# SiteSetting.discourse_subscriptions_pricing_table = '{"insert-pricing-table-embed-code"}'
# visit("/")
# find("li.nav-item_subscribe a").click
# expect(page).to have_selector('stripe-pricing-table')
# end
end end

View File

@ -0,0 +1,92 @@
# frozen_string_literal: true
describe "Subscription products", type: :system do
fab!(:admin)
fab!(:user)
fab!(:product) { Fabricate(:product, external_id: "prod_OiK") }
fab!(:customer) do
Fabricate(:customer, customer_id: "cus_Q1n", product_id: product.external_id, user_id: user.id)
end
fab!(:subscription) do
Fabricate(:subscription, customer_id: customer.id, external_id: "sub_10z", status: "active")
end
fab!(:subscription) do
Fabricate(:subscription, customer_id: customer.id, external_id: "sub_32b", status: "canceled")
end
let(:dialog) { PageObjects::Components::Dialog.new }
let(:product_subscriptions_page) { PageObjects::Pages::AdminSubscriptionProduct.new }
let(:admin_subscriptions_page) { PageObjects::Pages::AdminSubscriptionSubscription.new }
let(:user_billing_subscriptions_page) { PageObjects::Pages::UserBillingSubscription.new }
before do
SiteSetting.discourse_subscriptions_enabled = true
SiteSetting.discourse_subscriptions_secret_key = "sk_test_51xuu"
SiteSetting.discourse_subscriptions_public_key = "pk_test_51xuu"
# # this needs to be stubbed or it will try to make a request to stripe
one_product = {
id: "prod_OiK",
active: true,
name: "Tomtom",
metadata: {
description: "Photos of tomtom",
repurchaseable: true,
},
}
plans_json =
File.read(
Rails.root.join(
"plugins",
"discourse-subscriptions",
"spec",
"fixtures",
"json",
"stripe-price-list.json",
),
)
subscriptions_json =
File.read(
Rails.root.join(
"plugins",
"discourse-subscriptions",
"spec",
"fixtures",
"json",
"stripe-subscription-list.json",
),
)
::Stripe::Product.stubs(:list).returns({ data: [one_product] })
::Stripe::Product.stubs(:delete).returns({ id: "prod_OiK" })
::Stripe::Product.stubs(:retrieve).returns(one_product)
::Stripe::Price.stubs(:list).returns(JSON.parse(plans_json, symbolize_names: true))
::Stripe::Subscription.stubs(:list).returns(
JSON.parse(subscriptions_json, symbolize_names: true),
)
end
it "shows active and canceled subscriptions for admins" do
sign_in(admin)
active_subscription_row =
admin_subscriptions_page.visit_subscriptions.subscription_row("sub_10z")
expect(active_subscription_row).to have_text("active")
canceled_subscription_row =
admin_subscriptions_page.visit_subscriptions.subscription_row("sub_32b")
expect(canceled_subscription_row).to have_text("canceled")
end
it "shows active and canceled subscriptions for users" do
sign_in(user)
active_subscription_row =
user_billing_subscriptions_page.visit_subscriptions.subscription_row("sub_10z")
expect(active_subscription_row).to have_text("active")
canceled_subscription_row =
user_billing_subscriptions_page.visit_subscriptions.subscription_row("sub_32b")
expect(canceled_subscription_row).to have_text("canceled")
end
end