FEATURE: Support for coupons in checkout (#41)
This adds support for Stripe Promo Codes in the user checkout process. Also adds a discounted field to User > Billing > Subscriptions to show the amount or percent discounted. This does not currently add in support for creating promo codes in the Subscriptions interface (that will come at a later point in time). Instead a coupon can be created with a promo code right from the Stripe dashboard.
This commit is contained in:
parent
70b96cf3b8
commit
da9b58398b
|
@ -53,24 +53,31 @@ module DiscourseSubscriptions
|
||||||
begin
|
begin
|
||||||
customer = create_customer(params[:source])
|
customer = create_customer(params[:source])
|
||||||
plan = ::Stripe::Price.retrieve(params[:plan])
|
plan = ::Stripe::Price.retrieve(params[:plan])
|
||||||
|
promo_code = ::Stripe::PromotionCode.list({ code: params[:promo] }) if params[:promo].present?
|
||||||
|
promo_code = promo_code[:data][0] if promo_code && promo_code[:data] # we assume promo codes have a unique name
|
||||||
|
|
||||||
recurring_plan = plan[:type] == 'recurring'
|
recurring_plan = plan[:type] == 'recurring'
|
||||||
|
|
||||||
if recurring_plan
|
if recurring_plan
|
||||||
trial_days = plan[:metadata][:trial_period_days] if plan[:metadata] && plan[:metadata][:trial_period_days]
|
trial_days = plan[:metadata][:trial_period_days] if plan[:metadata] && plan[:metadata][:trial_period_days]
|
||||||
|
|
||||||
|
promo_code_id = promo_code[:id] if promo_code
|
||||||
|
|
||||||
transaction = ::Stripe::Subscription.create(
|
transaction = ::Stripe::Subscription.create(
|
||||||
customer: customer[:id],
|
customer: customer[:id],
|
||||||
items: [{ price: params[:plan] }],
|
items: [{ price: params[:plan] }],
|
||||||
metadata: metadata_user,
|
metadata: metadata_user,
|
||||||
trial_period_days: trial_days
|
trial_period_days: trial_days,
|
||||||
|
promotion_code: promo_code_id
|
||||||
)
|
)
|
||||||
|
|
||||||
payment_intent = retrieve_payment_intent(transaction[:latest_invoice]) if transaction[:status] == 'incomplete'
|
payment_intent = retrieve_payment_intent(transaction[:latest_invoice]) if transaction[:status] == 'incomplete'
|
||||||
else
|
else
|
||||||
|
coupon_id = promo_code[:coupon][:id] if promo_code && promo_code[:coupon] && promo_code[:coupon][:id]
|
||||||
invoice_item = ::Stripe::InvoiceItem.create(
|
invoice_item = ::Stripe::InvoiceItem.create(
|
||||||
customer: customer[:id],
|
customer: customer[:id],
|
||||||
price: params[:plan]
|
price: params[:plan],
|
||||||
|
discounts: [{ coupon: coupon_id }]
|
||||||
)
|
)
|
||||||
invoice = ::Stripe::Invoice.create(
|
invoice = ::Stripe::Invoice.create(
|
||||||
customer: customer[:id]
|
customer: customer[:id]
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { not } from "@ember/object/computed";
|
||||||
|
|
||||||
export default Controller.extend({
|
export default Controller.extend({
|
||||||
selectedPlan: null,
|
selectedPlan: null,
|
||||||
|
promoCode: null,
|
||||||
isAnonymous: not("currentUser"),
|
isAnonymous: not("currentUser"),
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
@ -32,6 +33,7 @@ export default Controller.extend({
|
||||||
const subscription = Subscription.create({
|
const subscription = Subscription.create({
|
||||||
source: result.token.id,
|
source: result.token.id,
|
||||||
plan: plan.get("id"),
|
plan: plan.get("id"),
|
||||||
|
promo: this.promoCode,
|
||||||
});
|
});
|
||||||
|
|
||||||
return subscription.save();
|
return subscription.save();
|
||||||
|
|
|
@ -12,6 +12,7 @@ const Subscription = EmberObject.extend({
|
||||||
const data = {
|
const data = {
|
||||||
source: this.source,
|
source: this.source,
|
||||||
plan: this.plan,
|
plan: this.plan,
|
||||||
|
promo: this.promo,
|
||||||
};
|
};
|
||||||
|
|
||||||
return ajax("/s/create", { method: "post", data });
|
return ajax("/s/create", { method: "post", data });
|
||||||
|
|
|
@ -19,6 +19,22 @@ const UserSubscription = EmberObject.extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@discourseComputed("discount")
|
||||||
|
discounted(discount) {
|
||||||
|
if (discount) {
|
||||||
|
const amount_off = discount.coupon.amount_off;
|
||||||
|
const percent_off = discount.coupon.percent_off;
|
||||||
|
|
||||||
|
if (amount_off) {
|
||||||
|
return `${parseFloat(amount_off * 0.01).toFixed(2)}`;
|
||||||
|
} else if (percent_off) {
|
||||||
|
return `${percent_off}%`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return I18n.t("no_value");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
return ajax(`/s/user/subscriptions/${this.id}`, {
|
return ajax(`/s/user/subscriptions/${this.id}`, {
|
||||||
method: "delete",
|
method: "delete",
|
||||||
|
|
|
@ -33,6 +33,9 @@
|
||||||
{{else if isAnonymous}}
|
{{else if isAnonymous}}
|
||||||
{{login-required}}
|
{{login-required}}
|
||||||
{{else}}
|
{{else}}
|
||||||
|
<div class='promo-code'>
|
||||||
|
{{input type="text" name="promo_code" placeholderKey="discourse_subscriptions.subscribe.promo_code" value=promoCode}}
|
||||||
|
</div>
|
||||||
|
|
||||||
{{d-button
|
{{d-button
|
||||||
disabled=loading
|
disabled=loading
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
<th>{{i18n 'discourse_subscriptions.user.subscriptions.id'}}</th>
|
<th>{{i18n 'discourse_subscriptions.user.subscriptions.id'}}</th>
|
||||||
<th>{{i18n 'discourse_subscriptions.user.plans.product'}}</th>
|
<th>{{i18n 'discourse_subscriptions.user.plans.product'}}</th>
|
||||||
<th>{{i18n 'discourse_subscriptions.user.plans.rate'}}</th>
|
<th>{{i18n 'discourse_subscriptions.user.plans.rate'}}</th>
|
||||||
|
<th>{{i18n 'discourse_subscriptions.user.subscriptions.discounted'}}</th>
|
||||||
<th>{{i18n 'discourse_subscriptions.user.subscriptions.status'}}</th>
|
<th>{{i18n 'discourse_subscriptions.user.subscriptions.status'}}</th>
|
||||||
<th>{{i18n 'discourse_subscriptions.user.subscriptions.renews'}}</th>
|
<th>{{i18n 'discourse_subscriptions.user.subscriptions.renews'}}</th>
|
||||||
<th>{{i18n 'discourse_subscriptions.user.subscriptions.created_at'}}</th>
|
<th>{{i18n 'discourse_subscriptions.user.subscriptions.created_at'}}</th>
|
||||||
|
@ -14,6 +15,7 @@
|
||||||
<td>{{subscription.id}}</td>
|
<td>{{subscription.id}}</td>
|
||||||
<td>{{subscription.product.name}}</td>
|
<td>{{subscription.product.name}}</td>
|
||||||
<td>{{subscription.plan.subscriptionRate}}</td>
|
<td>{{subscription.plan.subscriptionRate}}</td>
|
||||||
|
<td>{{subscription.discounted}}</td>
|
||||||
<td>{{subscription.status}}</td>
|
<td>{{subscription.status}}</td>
|
||||||
<td>{{subscription.endDate}}</td>
|
<td>{{subscription.endDate}}</td>
|
||||||
<td>{{format-unix-date subscription.created}}</td>
|
<td>{{format-unix-date subscription.created}}</td>
|
||||||
|
|
|
@ -53,6 +53,7 @@ en:
|
||||||
subscriptions:
|
subscriptions:
|
||||||
id: Subscription ID
|
id: Subscription ID
|
||||||
status: Status
|
status: Status
|
||||||
|
discounted: Discounted
|
||||||
renews: Renews
|
renews: Renews
|
||||||
created_at: Created
|
created_at: Created
|
||||||
cancel: cancel
|
cancel: cancel
|
||||||
|
@ -68,6 +69,7 @@ en:
|
||||||
title: Payment
|
title: Payment
|
||||||
customer:
|
customer:
|
||||||
title: Customer Details
|
title: Customer Details
|
||||||
|
promo_code: Coupon
|
||||||
buttons:
|
buttons:
|
||||||
subscribe: Subscribe
|
subscribe: Subscribe
|
||||||
purchased: Purchased
|
purchased: Purchased
|
||||||
|
|
|
@ -128,7 +128,8 @@ module DiscourseSubscriptions
|
||||||
customer: 'cus_1234',
|
customer: 'cus_1234',
|
||||||
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,
|
||||||
|
promotion_code: nil
|
||||||
).returns(status: 'active', customer: 'cus_1234')
|
).returns(status: 'active', customer: 'cus_1234')
|
||||||
|
|
||||||
expect {
|
expect {
|
||||||
|
@ -170,6 +171,63 @@ module DiscourseSubscriptions
|
||||||
post "/s/create.json", params: { plan: 'plan_1234', source: 'tok_1234' }
|
post "/s/create.json", params: { plan: 'plan_1234', source: 'tok_1234' }
|
||||||
}.to change { DiscourseSubscriptions::Customer.count }
|
}.to change { DiscourseSubscriptions::Customer.count }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "with promo code" do
|
||||||
|
before do
|
||||||
|
::Stripe::PromotionCode.expects(:list).with({ code: '123' }).returns(
|
||||||
|
data: [{
|
||||||
|
id: 'promo123',
|
||||||
|
coupon: { id: 'c123' }
|
||||||
|
}]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "applies promo code to recurring subscription" do
|
||||||
|
::Stripe::Price.expects(:retrieve).returns(
|
||||||
|
type: 'recurring',
|
||||||
|
product: 'product_12345',
|
||||||
|
metadata: {
|
||||||
|
group_name: 'awesome',
|
||||||
|
trial_period_days: 0
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
::Stripe::Subscription.expects(:create).with(
|
||||||
|
customer: 'cus_1234',
|
||||||
|
items: [ price: 'plan_1234' ],
|
||||||
|
metadata: { user_id: user.id, username: user.username_lower },
|
||||||
|
trial_period_days: 0,
|
||||||
|
promotion_code: 'promo123'
|
||||||
|
).returns(status: 'active', customer: 'cus_1234')
|
||||||
|
|
||||||
|
post "/s/create.json", params: { plan: 'plan_1234', source: 'tok_1234', promo: '123' }
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
it "applies promo code to one time purchase" do
|
||||||
|
::Stripe::Price.expects(:retrieve).returns(
|
||||||
|
type: 'one_time',
|
||||||
|
product: 'product_12345',
|
||||||
|
metadata: {
|
||||||
|
group_name: 'awesome'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
::Stripe::InvoiceItem.expects(:create).with(customer: 'cus_1234', price: 'plan_1234', discounts: [{ coupon: 'c123' }])
|
||||||
|
|
||||||
|
::Stripe::Invoice.expects(:create).returns(status: 'open', id: 'in_123')
|
||||||
|
|
||||||
|
::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')
|
||||||
|
|
||||||
|
post '/s/create.json', params: { plan: 'plan_1234', source: 'tok_1234', promo: '123' }
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "#finalize strong customer authenticated transaction" do
|
describe "#finalize strong customer authenticated transaction" do
|
||||||
|
|
Loading…
Reference in New Issue