From d63c84eca5248f9c6ea5250e1585d9edc0bc9ee5 Mon Sep 17 00:00:00 2001 From: Blake Erickson Date: Tue, 7 May 2024 08:57:37 -0600 Subject: [PATCH] FIX: One-time purchase pending invoice item (#210) This change ensures we attach the invoice item to the invoice to avoid any occurrences of an empty invoice being paid with pending invoice items. --- .../subscribe_controller.rb | 8 ++++- .../user/payments_controller.rb | 6 ++-- spec/requests/subscribe_controller_spec.rb | 34 +++++++++++++++++-- 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/app/controllers/discourse_subscriptions/subscribe_controller.rb b/app/controllers/discourse_subscriptions/subscribe_controller.rb index e1aa019..c062da6 100644 --- a/app/controllers/discourse_subscriptions/subscribe_controller.rb +++ b/app/controllers/discourse_subscriptions/subscribe_controller.rb @@ -100,16 +100,22 @@ module DiscourseSubscriptions else coupon_id = promo_code[:coupon][:id] if promo_code && promo_code[:coupon] && promo_code[:coupon][:id] + invoice = ::Stripe::Invoice.create(customer: customer[:id]) invoice_item = ::Stripe::InvoiceItem.create( customer: customer[:id], price: params[:plan], discounts: [{ coupon: coupon_id }], + invoice: invoice[:id], ) - invoice = ::Stripe::Invoice.create(customer: customer[:id]) transaction = ::Stripe::Invoice.finalize_invoice(invoice[:id]) payment_intent = retrieve_payment_intent(transaction[:id]) if transaction[:status] == "open" + if payment_intent.nil? + return( + render_json_error I18n.t("js.discourse_subscriptions.subscribe.transaction_error") + ) + end transaction = ::Stripe::Invoice.pay(invoice[:id]) if payment_intent[:status] == "successful" end diff --git a/app/controllers/discourse_subscriptions/user/payments_controller.rb b/app/controllers/discourse_subscriptions/user/payments_controller.rb index bc01287..9588d8d 100644 --- a/app/controllers/discourse_subscriptions/user/payments_controller.rb +++ b/app/controllers/discourse_subscriptions/user/payments_controller.rb @@ -45,8 +45,10 @@ module DiscourseSubscriptions invoices_with_products = all_invoices[:data].select do |invoice| invoice_lines = invoice[:lines][:data][0] if invoice[:lines] && invoice[:lines][:data] - invoice_product_id = parse_invoice_lines(invoice_lines) - product_ids.include?(invoice_product_id) + if invoice_lines + invoice_product_id = parse_invoice_lines(invoice_lines) + product_ids.include?(invoice_product_id) + end end end diff --git a/spec/requests/subscribe_controller_spec.rb b/spec/requests/subscribe_controller_spec.rb index bac3dd0..e94a7c7 100644 --- a/spec/requests/subscribe_controller_spec.rb +++ b/spec/requests/subscribe_controller_spec.rb @@ -266,6 +266,35 @@ RSpec.describe DiscourseSubscriptions::SubscribeController do }.to change { DiscourseSubscriptions::Customer.count } end + it "returns 422 on a one time payment subscription error" do + # It's possible that the invoice item doesn't get attached + # to the invoice. This means the invoice is paid, but for $0.00 with + # a pending invoice item. + ::Stripe::Price.expects(:retrieve).returns( + type: "one_time", + product: "product_12345", + metadata: { + group_name: "awesome", + }, + ) + + ::Stripe::InvoiceItem.expects(:create) + + ::Stripe::Invoice.expects(:create).returns(status: "open", id: "in_123") + + ::Stripe::Invoice.expects(:finalize_invoice).returns( + id: "in_123", + status: "paid", + payment_intent: "pi_123", + ) + + expect { + post "/s/create.json", params: { plan: "plan_1234", source: "tok_1234" } + }.not_to change { DiscourseSubscriptions::Customer.count } + + expect(response.status).to eq 422 + end + it "creates a one time payment subscription" do ::Stripe::Price.expects(:retrieve).returns( type: "one_time", @@ -414,14 +443,15 @@ RSpec.describe DiscourseSubscriptions::SubscribeController do }, ) + ::Stripe::Invoice.expects(:create).returns(status: "open", id: "in_123") + ::Stripe::InvoiceItem.expects(:create).with( customer: "cus_1234", price: "plan_1234", discounts: [{ coupon: "c123" }], + invoice: "in_123", ) - ::Stripe::Invoice.expects(:create).returns(status: "open", id: "in_123") - ::Stripe::Invoice.expects(:finalize_invoice).returns( id: "in_123", status: "open",