mirror of
https://github.com/discourse/discourse-subscriptions.git
synced 2025-03-06 17:49:24 +00:00
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,
|
metadata: metadata_user,
|
||||||
trial_period_days: trial_days
|
trial_period_days: trial_days
|
||||||
)
|
)
|
||||||
|
|
||||||
|
payment_intent = retrieve_payment_intent(transaction[:latest_invoice]) if transaction[:status] == 'incomplete'
|
||||||
else
|
else
|
||||||
invoice_item = ::Stripe::InvoiceItem.create(
|
invoice_item = ::Stripe::InvoiceItem.create(
|
||||||
customer: params[:customer],
|
customer: params[:customer],
|
||||||
@ -47,27 +49,14 @@ module DiscourseSubscriptions
|
|||||||
invoice = ::Stripe::Invoice.create(
|
invoice = ::Stripe::Invoice.create(
|
||||||
customer: params[:customer]
|
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
|
end
|
||||||
|
|
||||||
if transaction_ok(transaction)
|
finalize_transaction(transaction, plan) if transaction_ok(transaction)
|
||||||
group = plan_group(plan)
|
|
||||||
|
|
||||||
group.add(current_user) if group
|
transaction = transaction.to_h.merge(transaction, payment_intent: payment_intent)
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
render_json_dump transaction
|
render_json_dump transaction
|
||||||
rescue ::Stripe::InvalidRequestError => e
|
rescue ::Stripe::InvalidRequestError => e
|
||||||
@ -75,6 +64,55 @@ module DiscourseSubscriptions
|
|||||||
end
|
end
|
||||||
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
|
private
|
||||||
|
|
||||||
def metadata_user
|
def metadata_user
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import Controller from "@ember/controller";
|
import Controller from "@ember/controller";
|
||||||
import Customer from "discourse/plugins/discourse-subscriptions/discourse/models/customer";
|
import Customer from "discourse/plugins/discourse-subscriptions/discourse/models/customer";
|
||||||
import Subscription from "discourse/plugins/discourse-subscriptions/discourse/models/subscription";
|
import Subscription from "discourse/plugins/discourse-subscriptions/discourse/models/subscription";
|
||||||
|
import Transaction from "discourse/plugins/discourse-subscriptions/discourse/models/transaction";
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
|
|
||||||
export default Controller.extend({
|
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: {
|
actions: {
|
||||||
stripePaymentHandler() {
|
stripePaymentHandler() {
|
||||||
this.set("loading", true);
|
this.set("loading", true);
|
||||||
@ -59,25 +89,29 @@ export default Controller.extend({
|
|||||||
.then(result => {
|
.then(result => {
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
bootbox.alert(result.error.message || result.error);
|
bootbox.alert(result.error.message || result.error);
|
||||||
} else {
|
} else if (
|
||||||
if (result.status === "incomplete") {
|
result.status === "incomplete" ||
|
||||||
this.alert("plans.incomplete");
|
result.status === "open"
|
||||||
} else {
|
) {
|
||||||
this.alert("plans.success");
|
const transactionId = result.id;
|
||||||
}
|
const planId = this.selectedPlan;
|
||||||
|
this.handleAuthentication(plan, result).then(
|
||||||
this.transitionToRoute(
|
authenticationResult => {
|
||||||
plan.type === "recurring"
|
if (authenticationResult && !authenticationResult.error) {
|
||||||
? "user.billing.subscriptions"
|
return Transaction.finalize(transactionId, planId).then(
|
||||||
: "user.billing.payments",
|
() => {
|
||||||
Discourse.User.current().username.toLowerCase()
|
this._advanceSuccessfulTransaction(plan);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
this._advanceSuccessfulTransaction(plan);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(result => {
|
.catch(result => {
|
||||||
bootbox.alert(result.errorThrown);
|
bootbox.alert(result.errorThrown);
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.set("loading", false);
|
this.set("loading", false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
12
assets/javascripts/discourse/models/transaction.js.es6
Normal file
12
assets/javascripts/discourse/models/transaction.js.es6
Normal file
@ -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 :products, only: [:index, :show]
|
||||||
resources :subscriptions, only: [:create]
|
resources :subscriptions, only: [:create]
|
||||||
|
|
||||||
|
post '/subscriptions/finalize' => 'subscriptions#finalize'
|
||||||
|
|
||||||
get '/' => 'subscriptions#index', constraints: SubscriptionsUserConstraint.new
|
get '/' => 'subscriptions#index', constraints: SubscriptionsUserConstraint.new
|
||||||
get '/:id' => 'subscriptions#index', constraints: SubscriptionsUserConstraint.new
|
get '/:id' => 'subscriptions#index', constraints: SubscriptionsUserConstraint.new
|
||||||
end
|
end
|
||||||
|
@ -21,7 +21,7 @@ register_html_builder('server:before-head-close') do
|
|||||||
end
|
end
|
||||||
|
|
||||||
extend_content_security_policy(
|
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'
|
add_admin_route 'discourse_subscriptions.admin_navigation', 'discourse-subscriptions.products'
|
||||||
|
@ -35,7 +35,7 @@ module DiscourseSubscriptions
|
|||||||
items: [ price: 'plan_1234' ],
|
items: [ price: 'plan_1234' ],
|
||||||
metadata: { user_id: user.id, username: user.username_lower },
|
metadata: { user_id: user.id, username: user.username_lower },
|
||||||
trial_period_days: 0
|
trial_period_days: 0
|
||||||
).returns(status: 'active')
|
).returns(status: 'active', customer: 'cus_1234')
|
||||||
|
|
||||||
expect {
|
expect {
|
||||||
post "/s/subscriptions.json", params: { plan: 'plan_1234', customer: 'cus_1234' }
|
post "/s/subscriptions.json", params: { plan: 'plan_1234', customer: 'cus_1234' }
|
||||||
@ -53,9 +53,15 @@ module DiscourseSubscriptions
|
|||||||
|
|
||||||
::Stripe::InvoiceItem.expects(:create)
|
::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 {
|
expect {
|
||||||
post '/s/subscriptions.json', params: { plan: 'plan_1234', customer: 'cus_1234' }
|
post '/s/subscriptions.json', params: { plan: 'plan_1234', customer: 'cus_1234' }
|
||||||
@ -65,7 +71,7 @@ module DiscourseSubscriptions
|
|||||||
|
|
||||||
it "creates a customer model" do
|
it "creates a customer model" do
|
||||||
::Stripe::Price.expects(:retrieve).returns(type: 'recurring', metadata: {})
|
::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 {
|
expect {
|
||||||
post "/s/subscriptions.json", params: { plan: 'plan_1234', customer: 'cus_1234' }
|
post "/s/subscriptions.json", params: { plan: 'plan_1234', customer: 'cus_1234' }
|
||||||
@ -73,6 +79,30 @@ module DiscourseSubscriptions
|
|||||||
end
|
end
|
||||||
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
|
describe "user groups" do
|
||||||
let(:group_name) { 'group-123' }
|
let(:group_name) { 'group-123' }
|
||||||
let(:group) { Fabricate(:group, name: group_name) }
|
let(:group) { Fabricate(:group, name: group_name) }
|
||||||
|
Loading…
x
Reference in New Issue
Block a user