buttons for selecting price and one time payment

This commit is contained in:
Rimian Perkins 2019-12-13 10:41:14 +11:00
parent e27b55ea6f
commit 57fb508514
13 changed files with 264 additions and 90 deletions

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
module DiscourseSubscriptions
class PaymentsController < ::ApplicationController
include DiscourseSubscriptions::Stripe
skip_before_action :verify_authenticity_token, only: [:create]
before_action :set_api_key
requires_login
def create
begin
payment = ::Stripe::PaymentIntent.create(
payment_method_types: ['card'],
payment_method: params[:payment_method],
amount: params[:amount],
currency: params[:currency],
confirm: true
)
render_json_dump payment
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
rescue ::Stripe::CardError => e
render_json_error 'Card Declined'
end
end
end
end

View File

@ -1,5 +1,18 @@
import { equal } from "@ember/object/computed";
export default Ember.Component.extend({
planButtonSelected: equal("planTypeIsSelected", true),
paymentButtonSelected: equal("planTypeIsSelected", false),
actions: {
selectPlans() {
this.set("planTypeIsSelected", true);
},
selectPayments() {
this.set("planTypeIsSelected", false);
},
clickPlan(plan) {
this.plans.map(p => p.set("selected", false));
plan.set("selected", true);

View File

@ -1,4 +1,4 @@
import computed from "ember-addons/ember-computed-decorators";
import computed from "discourse-common/utils/decorators";
import User from "discourse/models/user";
export default Ember.Component.extend({

View File

@ -1,7 +1,22 @@
import Customer from "discourse/plugins/discourse-subscriptions/discourse/models/customer";
import Payment from "discourse/plugins/discourse-subscriptions/discourse/models/payment";
import Subscription from "discourse/plugins/discourse-subscriptions/discourse/models/subscription";
import computed from "discourse-common/utils/decorators";
import { i18n } from "discourse/lib/computed";
export default Ember.Controller.extend({
planTypeIsSelected: true,
@computed("planTypeIsSelected")
type(planTypeIsSelected) {
return planTypeIsSelected ? "plans" : "payment";
},
@computed("type")
buttonText(type) {
return I18n.t(`discourse_subscriptions.${this.get("type")}.payment_button`);
},
init() {
this._super(...arguments);
this.set(
@ -13,54 +28,83 @@ export default Ember.Controller.extend({
this.set("cardElement", elements.create("card", { hidePostalCode: true }));
},
alert(path) {
bootbox.alert(I18n.t(`discourse_subscriptions.${path}`));
},
createPayment(plan) {
return this.stripe
.createPaymentMethod("card", this.get("cardElement"))
.then(result => {
const payment = Payment.create({
payment_method: result.paymentMethod.id,
amount: plan.get("amount"),
currency: plan.get("currency")
});
return payment.save();
});
},
createSubsciption(plan) {
return this.stripe.createToken(this.get("cardElement")).then(result => {
if (result.error) {
return result;
} else {
const customer = Customer.create({ source: result.token.id });
return customer.save().then(c => {
const subscription = Subscription.create({
customer: c.id,
plan: plan.get("id")
});
return subscription.save();
});
}
});
},
actions: {
stripePaymentHandler() {
this.set("loading", true);
const type = this.get("type");
const plan = this.get("model.plans")
.filterBy("selected")
.get("firstObject");
if (!plan) {
bootbox.alert(
I18n.t(
"discourse_subscriptions.transactions.payment.validate.plan.required"
)
);
this.alert(`${type}.validate.payment_options.required`);
this.set("loading", false);
return;
}
this.stripe.createToken(this.get("cardElement")).then(result => {
if (result.error) {
bootbox.alert(result.error.message);
let transaction;
if (this.planTypeIsSelected) {
transaction = this.createSubsciption(plan);
} else {
transaction = this.createPayment(plan);
}
transaction
.then(result => {
if (result.error) {
bootbox.alert(result.error.message || result.error);
} else {
this.alert(`${type}.success`);
this.transitionToRoute(
"user.subscriptions",
Discourse.User.current().username.toLowerCase()
);
}
})
.catch(result => {
bootbox.alert(result.errorThrown);
})
.finally(() => {
this.set("loading", false);
} else {
const customer = Customer.create({ source: result.token.id });
customer.save().then(c => {
const subscription = Subscription.create({
customer: c.id,
plan: plan.get("id")
});
subscription
.save()
.then(() => {
bootbox.alert(
I18n.t("discourse_subscriptions.transactions.payment.success")
);
this.transitionToRoute(
"user.subscriptions",
Discourse.User.current().username.toLowerCase()
);
})
.finally(() => {
this.set("loading", false);
});
});
}
});
});
}
}
});

View File

@ -0,0 +1,15 @@
import { ajax } from "discourse/lib/ajax";
const Payment = Discourse.Model.extend({
save() {
const data = {
payment_method: this.payment_method,
amount: this.amount,
currency: this.currency
};
return ajax("/s/payments", { method: "post", data });
}
});
export default Payment;

View File

@ -1,27 +1,35 @@
{{#ds-button
id="discourse-subscriptions-payment-type-plan"
selected=true
class="btn-discourse-subscriptions-payment-type"
}}
{{i18n "discourse_subscriptions.plans.purchase"}}
{{/ds-button}}
<div class="subscribe-buttons">
{{#ds-button
id="discourse-subscriptions-payment-type-plan"
selected=planButtonSelected
action="selectPlans"
class="btn-discourse-subscriptions-payment-type"
}}
{{i18n "discourse_subscriptions.plans.purchase"}}
{{/ds-button}}
{{#ds-button
id="discourse-subscriptions-payment-type-payment"
selected=false
class="btn-discourse-subscriptions-payment-type"
}}
{{i18n "discourse_subscriptions.payment.purchase"}}
{{/ds-button}}
{{#ds-button
id="discourse-subscriptions-payment-type-payment"
selected=paymentButtonSelected
action="selectPayments"
class="btn-discourse-subscriptions-payment-type"
}}
{{i18n "discourse_subscriptions.payment.purchase"}}
{{/ds-button}}
</div>
<hr>
<p>
{{i18n "discourse_subscriptions.plans.select"}}
{{#if planTypeIsSelected}}
{{i18n "discourse_subscriptions.plans.select"}}
{{else}}
{{i18n "discourse_subscriptions.payment.select"}}
{{/if}}
</p>
<div id="subscribe-buttons">
<div class="subscribe-buttons">
{{#each plans as |plan|}}
{{#ds-button
action="clickPlan"
@ -30,7 +38,11 @@
class="btn-discourse-subscriptions-subscribe"
}}
<div class="interval">
{{i18n (concat "discourse_subscriptions.plans.interval.adverb." plan.interval)}}
{{#if planTypeIsSelected}}
{{i18n (concat "discourse_subscriptions.plans.interval.adverb." plan.interval)}}
{{else}}
{{i18n "discourse_subscriptions.payment.interval"}}
{{/if}}
</div>
<span class="amount">
{{format-currency plan.currency plan.amountDollars}}

View File

@ -14,7 +14,10 @@
{{i18n 'discourse_subscriptions.subscribe.card.title'}}
</h2>
{{payment-options plans=model.plans}}
{{payment-options
plans=model.plans
planTypeIsSelected=planTypeIsSelected
}}
<hr>
@ -27,7 +30,7 @@
disabled=loading
action="stripePaymentHandler"
class="btn btn-primary btn-payment"}}
{{i18n 'discourse_subscriptions.subscribe.buttons.subscribe'}}
{{buttonText}}
{{/d-button}}
{{/if}}

View File

@ -1,4 +1,4 @@
#subscribe-buttons {
.subscribe-buttons {
display: flex;
justify-content: space-around;

View File

@ -18,26 +18,35 @@ en:
title: Discourse Subscriptions
admin_navigation: Subscriptions
optional: Optional
transactions:
payment:
success: Your payment was successful
validate:
plan:
required: Please select a subscription plan.
navigation:
subscriptions: Subscriptions
subscribe: Subscribe
billing: Billing
plans:
select: Select subscription plan
purchase: Purchase a subscription
select: Select subscription plan
interval:
adverb:
week: Weekly
month: Monthly
year: Yearly
payment_button:
Subscribe
success: Thank you! Your subscription has been created.
validate:
payment_options:
required: Please select a subscription plan.
payment:
purchase: Make just one payment
select: Select a payment option
interval:
One payment
payment_button:
Pay Once
success: Thank you!
validate:
payment_options:
required: Please select a payment option.
one_time:
heading:
payment: Make a Payment

View File

@ -18,6 +18,7 @@ DiscourseSubscriptions::Engine.routes.draw do
resources :customers, only: [:create]
resources :invoices, only: [:index]
resources :payments, only: [:create]
resources :patrons, only: [:index, :create]
resources :plans, only: [:index]
resources :products, only: [:index, :show]

View File

@ -62,6 +62,7 @@ after_initialize do
"../app/controllers/invoices_controller",
"../app/controllers/patrons_controller",
"../app/controllers/plans_controller",
"../app/controllers/payments_controller",
"../app/controllers/products_controller",
"../app/controllers/subscriptions_controller",
"../app/models/customer",

View File

@ -0,0 +1,40 @@
# frozen_string_literal: true
require 'rails_helper'
module DiscourseSubscriptions
RSpec.describe PaymentsController do
context "not authenticated" do
it "does not create a payment intent" do
::Stripe::PaymentIntent.expects(:create).never
post "/s/payments.json", params: { }
end
end
context "authenticated" do
let(:user) { Fabricate(:user) }
before do
sign_in(user)
end
describe "create" do
it "creates a payment intent" do
::Stripe::PaymentIntent.expects(:create).with(
payment_method_types: ['card'],
payment_method: 'pm_123',
amount: '999',
currency: 'gdp',
confirm: true
)
post "/s/payments.json", params: {
payment_method: 'pm_123',
amount: 999,
currency: 'gdp'
}
end
end
end
end
end

View File

@ -18,7 +18,7 @@ componentTest("Discourse Subscriptions payment options have no plans", {
});
componentTest("Discourse Subscriptions payment options has content", {
template: `{{payment-options plans=plans}}`,
template: `{{payment-options plans=plans planTypeIsSelected=planTypeIsSelected}}`,
async test(assert) {
this.set("plans", [
@ -26,21 +26,8 @@ componentTest("Discourse Subscriptions payment options has content", {
{ currency: "gdp", interval: "month", amountDollars: "9.99" }
]);
assert.equal(
find(".btn-discourse-subscriptions-payment-type").length,
2,
"The payment type buttons are shown"
);
assert.equal(
find("#discourse-subscriptions-payment-type-plan.btn-primary").length,
1,
"The plan payment type button is selected"
);
assert.equal(
find("#discourse-subscriptions-payment-type-payment.btn-primary").length,
0,
"The single payment type button is not selected"
);
this.set("planTypeIsSelected", true);
assert.equal(
find(".btn-discourse-subscriptions-payment-type").length,
2,
@ -73,22 +60,40 @@ componentTest("Discourse Subscriptions payment options has content", {
}
});
componentTest("Discourse Subscriptions payment options plan is selected", {
template: `{{payment-options plans=plans}}`,
beforeEach() {},
componentTest("Discourse Subscriptions payment type plan", {
template: `{{payment-options plans=plans planTypeIsSelected=planTypeIsSelected}}`,
async test(assert) {
this.set("plans", [
EmberObject.create({
currency: "aud",
interval: "year",
amountDollars: "44.99"
})
{ currency: "aud", interval: "year", amountDollars: "44.99" }
]);
await click(".btn-discourse-subscriptions-subscribe:first-child");
this.set("planTypeIsSelected", true);
assert.ok(this.get("plans.firstObject.selected"), "it selected the plan");
assert.equal(
find("#discourse-subscriptions-payment-type-plan.btn-primary").length,
1,
"The plan type button is selected"
);
assert.equal(
find("#discourse-subscriptions-payment-type-payment.btn-primary").length,
0,
"The payment type button is not selected"
);
await click("#discourse-subscriptions-payment-type-payment");
assert.equal(
find("#discourse-subscriptions-payment-type-plan.btn-primary").length,
0,
"The plan type button is selected"
);
assert.equal(
find("#discourse-subscriptions-payment-type-payment.btn-primary").length,
1,
"The payment type button is not selected"
);
}
});