FEATURE: Add support for 3D Secure payments (#19)
Adds an additional checkout flow to support authentication of payment methods.
This commit is contained in:
parent
587661fafb
commit
9491f558ea
|
@ -39,6 +39,8 @@ module DiscourseSubscriptions
|
|||
metadata: metadata_user,
|
||||
trial_period_days: trial_days
|
||||
)
|
||||
|
||||
payment_intent = retrieve_payment_intent(transaction[:latest_invoice]) if transaction[:status] == 'incomplete'
|
||||
else
|
||||
invoice_item = ::Stripe::InvoiceItem.create(
|
||||
customer: params[:customer],
|
||||
|
@ -47,27 +49,14 @@ module DiscourseSubscriptions
|
|||
invoice = ::Stripe::Invoice.create(
|
||||
customer: params[:customer]
|
||||
)
|
||||
transaction = ::Stripe::Invoice.pay(invoice[:id])
|
||||
transaction = ::Stripe::Invoice.finalize_invoice(invoice[:id])
|
||||
payment_intent = retrieve_payment_intent(transaction[:id]) if transaction[:status] == 'open'
|
||||
transaction = ::Stripe::Invoice.pay(invoice[:id]) if payment_intent[:status] == 'successful'
|
||||
end
|
||||
|
||||
if transaction_ok(transaction)
|
||||
group = plan_group(plan)
|
||||
finalize_transaction(transaction, plan) if transaction_ok(transaction)
|
||||
|
||||
group.add(current_user) if group
|
||||
|
||||
customer = Customer.create(
|
||||
user_id: current_user.id,
|
||||
customer_id: params[:customer],
|
||||
product_id: plan[:product]
|
||||
)
|
||||
|
||||
if transaction[:object] == 'subscription'
|
||||
Subscription.create(
|
||||
customer_id: customer.id,
|
||||
external_id: transaction[:id]
|
||||
)
|
||||
end
|
||||
end
|
||||
transaction = transaction.to_h.merge(transaction, payment_intent: payment_intent)
|
||||
|
||||
render_json_dump transaction
|
||||
rescue ::Stripe::InvalidRequestError => e
|
||||
|
@ -75,6 +64,55 @@ module DiscourseSubscriptions
|
|||
end
|
||||
end
|
||||
|
||||
def finalize
|
||||
begin
|
||||
price = ::Stripe::Price.retrieve(params[:plan])
|
||||
transaction = retrieve_transaction(params[:transaction])
|
||||
finalize_transaction(transaction, price) if transaction_ok(transaction)
|
||||
|
||||
render_json_dump params[:transaction]
|
||||
rescue ::Stripe::InvalidRequestError => e
|
||||
render_json_error e.message
|
||||
end
|
||||
end
|
||||
|
||||
def retrieve_transaction(transaction)
|
||||
begin
|
||||
case transaction
|
||||
when /^sub_/
|
||||
::Stripe::Subscription.retrieve(transaction)
|
||||
when /^in_/
|
||||
::Stripe::Invoice.retrieve(transaction)
|
||||
end
|
||||
rescue ::Stripe::InvalidRequestError => e
|
||||
e.message
|
||||
end
|
||||
end
|
||||
|
||||
def retrieve_payment_intent(invoice_id)
|
||||
invoice = ::Stripe::Invoice.retrieve(invoice_id)
|
||||
::Stripe::PaymentIntent.retrieve(invoice[:payment_intent])
|
||||
end
|
||||
|
||||
def finalize_transaction(transaction, plan)
|
||||
group = plan_group(plan)
|
||||
|
||||
group.add(current_user) if group
|
||||
|
||||
customer = Customer.create(
|
||||
user_id: current_user.id,
|
||||
customer_id: transaction[:customer],
|
||||
product_id: plan[:product]
|
||||
)
|
||||
|
||||
if transaction[:object] == 'subscription'
|
||||
Subscription.create(
|
||||
customer_id: customer.id,
|
||||
external_id: transaction[:id]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def metadata_user
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import Controller from "@ember/controller";
|
||||
import Customer from "discourse/plugins/discourse-subscriptions/discourse/models/customer";
|
||||
import Subscription from "discourse/plugins/discourse-subscriptions/discourse/models/subscription";
|
||||
import Transaction from "discourse/plugins/discourse-subscriptions/discourse/models/transaction";
|
||||
import I18n from "I18n";
|
||||
|
||||
export default Controller.extend({
|
||||
|
@ -40,6 +41,35 @@ export default Controller.extend({
|
|||
});
|
||||
},
|
||||
|
||||
handleAuthentication(plan, transaction) {
|
||||
return this.stripe
|
||||
.confirmCardPayment(transaction.payment_intent.client_secret)
|
||||
.then(result => {
|
||||
if (
|
||||
result.paymentIntent &&
|
||||
result.paymentIntent.status === "succeeded"
|
||||
) {
|
||||
return result;
|
||||
} else {
|
||||
this.set("loading", false);
|
||||
bootbox.alert(result.error.message || result.error);
|
||||
return result;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_advanceSuccessfulTransaction(plan) {
|
||||
this.alert("plans.success");
|
||||
this.set("loading", false);
|
||||
|
||||
this.transitionToRoute(
|
||||
plan.type === "recurring"
|
||||
? "user.billing.subscriptions"
|
||||
: "user.billing.payments",
|
||||
Discourse.User.current().username.toLowerCase()
|
||||
);
|
||||
},
|
||||
|
||||
actions: {
|
||||
stripePaymentHandler() {
|
||||
this.set("loading", true);
|
||||
|
@ -59,25 +89,29 @@ export default Controller.extend({
|
|||
.then(result => {
|
||||
if (result.error) {
|
||||
bootbox.alert(result.error.message || result.error);
|
||||
} else {
|
||||
if (result.status === "incomplete") {
|
||||
this.alert("plans.incomplete");
|
||||
} else {
|
||||
this.alert("plans.success");
|
||||
}
|
||||
|
||||
this.transitionToRoute(
|
||||
plan.type === "recurring"
|
||||
? "user.billing.subscriptions"
|
||||
: "user.billing.payments",
|
||||
Discourse.User.current().username.toLowerCase()
|
||||
} else if (
|
||||
result.status === "incomplete" ||
|
||||
result.status === "open"
|
||||
) {
|
||||
const transactionId = result.id;
|
||||
const planId = this.selectedPlan;
|
||||
this.handleAuthentication(plan, result).then(
|
||||
authenticationResult => {
|
||||
if (authenticationResult && !authenticationResult.error) {
|
||||
return Transaction.finalize(transactionId, planId).then(
|
||||
() => {
|
||||
this._advanceSuccessfulTransaction(plan);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this._advanceSuccessfulTransaction(plan);
|
||||
}
|
||||
})
|
||||
.catch(result => {
|
||||
bootbox.alert(result.errorThrown);
|
||||
})
|
||||
.finally(() => {
|
||||
this.set("loading", false);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
import { ajax } from "discourse/lib/ajax";
|
||||
|
||||
export default {
|
||||
finalize(transaction, plan) {
|
||||
const data = {
|
||||
transaction: transaction,
|
||||
plan: plan
|
||||
};
|
||||
|
||||
return ajax("/s/subscriptions/finalize", { method: "post", data });
|
||||
}
|
||||
};
|
|
@ -24,6 +24,8 @@ DiscourseSubscriptions::Engine.routes.draw do
|
|||
resources :products, only: [:index, :show]
|
||||
resources :subscriptions, only: [:create]
|
||||
|
||||
post '/subscriptions/finalize' => 'subscriptions#finalize'
|
||||
|
||||
get '/' => 'subscriptions#index', constraints: SubscriptionsUserConstraint.new
|
||||
get '/:id' => 'subscriptions#index', constraints: SubscriptionsUserConstraint.new
|
||||
end
|
||||
|
|
|
@ -21,7 +21,7 @@ register_html_builder('server:before-head-close') do
|
|||
end
|
||||
|
||||
extend_content_security_policy(
|
||||
script_src: ['https://js.stripe.com/v3/']
|
||||
script_src: ['https://js.stripe.com/v3/', 'https://hooks.stripe.com']
|
||||
)
|
||||
|
||||
add_admin_route 'discourse_subscriptions.admin_navigation', 'discourse-subscriptions.products'
|
||||
|
|
|
@ -35,7 +35,7 @@ module DiscourseSubscriptions
|
|||
items: [ price: 'plan_1234' ],
|
||||
metadata: { user_id: user.id, username: user.username_lower },
|
||||
trial_period_days: 0
|
||||
).returns(status: 'active')
|
||||
).returns(status: 'active', customer: 'cus_1234')
|
||||
|
||||
expect {
|
||||
post "/s/subscriptions.json", params: { plan: 'plan_1234', customer: 'cus_1234' }
|
||||
|
@ -53,9 +53,15 @@ module DiscourseSubscriptions
|
|||
|
||||
::Stripe::InvoiceItem.expects(:create)
|
||||
|
||||
::Stripe::Invoice.expects(:create).returns(id: 'in_123')
|
||||
::Stripe::Invoice.expects(:create).returns(status: 'open', id: 'in_123')
|
||||
|
||||
::Stripe::Invoice.expects(:pay).returns(status: 'paid')
|
||||
::Stripe::Invoice.expects(:finalize_invoice).returns(id: 'in_123', status: 'open', payment_intent: 'pi_123')
|
||||
|
||||
::Stripe::Invoice.expects(:retrieve).returns(id: 'in_123', status: 'open', payment_intent: 'pi_123')
|
||||
|
||||
::Stripe::PaymentIntent.expects(:retrieve).returns(status: 'successful')
|
||||
|
||||
::Stripe::Invoice.expects(:pay).returns(status: 'paid', customer: 'cus_1234')
|
||||
|
||||
expect {
|
||||
post '/s/subscriptions.json', params: { plan: 'plan_1234', customer: 'cus_1234' }
|
||||
|
@ -65,7 +71,7 @@ module DiscourseSubscriptions
|
|||
|
||||
it "creates a customer model" do
|
||||
::Stripe::Price.expects(:retrieve).returns(type: 'recurring', metadata: {})
|
||||
::Stripe::Subscription.expects(:create).returns(status: 'active')
|
||||
::Stripe::Subscription.expects(:create).returns(status: 'active', customer: 'cus_1234')
|
||||
|
||||
expect {
|
||||
post "/s/subscriptions.json", params: { plan: 'plan_1234', customer: 'cus_1234' }
|
||||
|
@ -73,6 +79,30 @@ module DiscourseSubscriptions
|
|||
end
|
||||
end
|
||||
|
||||
describe "strong customer authenticated transaction" do
|
||||
context "with subscription" do
|
||||
it "finalizes the subscription" do
|
||||
::Stripe::Price.expects(:retrieve).returns(id: "plan_1234", product: "prod_1234", metadata: {})
|
||||
::Stripe::Subscription.expects(:retrieve).returns(id: "sub_123", customer: 'cus_1234', status: "active")
|
||||
|
||||
expect {
|
||||
post "/s/subscriptions/finalize.json", params: { plan: 'plan_1234', transaction: 'sub_1234' }
|
||||
}.to change { DiscourseSubscriptions::Customer.count }
|
||||
end
|
||||
end
|
||||
|
||||
context "with one-time payment" do
|
||||
it "finalizes the one-time payment" do
|
||||
::Stripe::Price.expects(:retrieve).returns(id: "plan_1234", product: "prod_1234", metadata: {})
|
||||
::Stripe::Invoice.expects(:retrieve).returns(id: "in_123", customer: 'cus_1234', status: "paid")
|
||||
|
||||
expect {
|
||||
post "/s/subscriptions/finalize.json", params: { plan: 'plan_1234', transaction: 'in_1234' }
|
||||
}.to change { DiscourseSubscriptions::Customer.count }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "user groups" do
|
||||
let(:group_name) { 'group-123' }
|
||||
let(:group) { Fabricate(:group, name: group_name) }
|
||||
|
|
Loading…
Reference in New Issue