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:
Justin DiRose 2021-01-07 15:25:44 -06:00 committed by GitHub
parent 70b96cf3b8
commit da9b58398b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 94 additions and 3 deletions

View File

@ -53,24 +53,31 @@ module DiscourseSubscriptions
begin
customer = create_customer(params[:source])
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'
if recurring_plan
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(
customer: customer[:id],
items: [{ price: params[:plan] }],
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'
else
coupon_id = promo_code[:coupon][:id] if promo_code && promo_code[:coupon] && promo_code[:coupon][:id]
invoice_item = ::Stripe::InvoiceItem.create(
customer: customer[:id],
price: params[:plan]
price: params[:plan],
discounts: [{ coupon: coupon_id }]
)
invoice = ::Stripe::Invoice.create(
customer: customer[:id]

View File

@ -6,6 +6,7 @@ import { not } from "@ember/object/computed";
export default Controller.extend({
selectedPlan: null,
promoCode: null,
isAnonymous: not("currentUser"),
init() {
@ -32,6 +33,7 @@ export default Controller.extend({
const subscription = Subscription.create({
source: result.token.id,
plan: plan.get("id"),
promo: this.promoCode,
});
return subscription.save();

View File

@ -12,6 +12,7 @@ const Subscription = EmberObject.extend({
const data = {
source: this.source,
plan: this.plan,
promo: this.promo,
};
return ajax("/s/create", { method: "post", data });

View File

@ -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() {
return ajax(`/s/user/subscriptions/${this.id}`, {
method: "delete",

View File

@ -33,6 +33,9 @@
{{else if isAnonymous}}
{{login-required}}
{{else}}
<div class='promo-code'>
{{input type="text" name="promo_code" placeholderKey="discourse_subscriptions.subscribe.promo_code" value=promoCode}}
</div>
{{d-button
disabled=loading

View File

@ -4,6 +4,7 @@
<th>{{i18n 'discourse_subscriptions.user.subscriptions.id'}}</th>
<th>{{i18n 'discourse_subscriptions.user.plans.product'}}</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.renews'}}</th>
<th>{{i18n 'discourse_subscriptions.user.subscriptions.created_at'}}</th>
@ -14,6 +15,7 @@
<td>{{subscription.id}}</td>
<td>{{subscription.product.name}}</td>
<td>{{subscription.plan.subscriptionRate}}</td>
<td>{{subscription.discounted}}</td>
<td>{{subscription.status}}</td>
<td>{{subscription.endDate}}</td>
<td>{{format-unix-date subscription.created}}</td>

View File

@ -53,6 +53,7 @@ en:
subscriptions:
id: Subscription ID
status: Status
discounted: Discounted
renews: Renews
created_at: Created
cancel: cancel
@ -68,6 +69,7 @@ en:
title: Payment
customer:
title: Customer Details
promo_code: Coupon
buttons:
subscribe: Subscribe
purchased: Purchased

View File

@ -128,7 +128,8 @@ module DiscourseSubscriptions
customer: 'cus_1234',
items: [ price: 'plan_1234' ],
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')
expect {
@ -170,6 +171,63 @@ module DiscourseSubscriptions
post "/s/create.json", params: { plan: 'plan_1234', source: 'tok_1234' }
}.to change { DiscourseSubscriptions::Customer.count }
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
describe "#finalize strong customer authenticated transaction" do