FEATURE: Allow creation of coupons in admin panel (#43)
Adds full support to create coupon/promo codes in the Admin > Plugins > Subscriptions section of the plugin. The Create Coupon button opens a form on the same page, and the active checkboxes toggle the active status of the coupon code.
This commit is contained in:
parent
a6de862ba9
commit
400313cded
|
@ -0,0 +1,72 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseSubscriptions
|
||||
module Admin
|
||||
class CouponsController < ::Admin::AdminController
|
||||
include DiscourseSubscriptions::Stripe
|
||||
include DiscourseSubscriptions::Group
|
||||
before_action :set_api_key
|
||||
|
||||
def index
|
||||
begin
|
||||
promo_codes = ::Stripe::PromotionCode.list({ limit: 100 })[:data]
|
||||
promo_codes = promo_codes.select { |code| code[:coupon][:valid] == true }
|
||||
render_json_dump promo_codes
|
||||
rescue ::Stripe::InvalidRequestError => e
|
||||
render_json_error e.message
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
params.require([:promo, :discount_type, :discount, :active])
|
||||
begin
|
||||
coupon_params = {
|
||||
duration: 'forever',
|
||||
}
|
||||
|
||||
case params[:discount_type]
|
||||
when 'amount'
|
||||
coupon_params[:amount_off] = params[:discount].to_i * 100
|
||||
coupon_params[:currency] = SiteSetting.discourse_subscriptions_currency
|
||||
when 'percent'
|
||||
coupon_params[:percent_off] = params[:discount]
|
||||
end
|
||||
|
||||
coupon = ::Stripe::Coupon.create(coupon_params)
|
||||
|
||||
promo_code = ::Stripe::PromotionCode.create({ coupon: coupon[:id], code: params[:promo] }) if coupon.present?
|
||||
|
||||
render_json_dump promo_code
|
||||
rescue ::Stripe::InvalidRequestError => e
|
||||
render_json_error e.message
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
params.require([:id, :active])
|
||||
begin
|
||||
promo_code = ::Stripe::PromotionCode.update(
|
||||
params[:id],
|
||||
{
|
||||
active: params[:active]
|
||||
}
|
||||
)
|
||||
|
||||
render_json_dump promo_code
|
||||
rescue ::Stripe::InvalidRequestError => e
|
||||
render_json_error e.message
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
params.require(:coupon_id)
|
||||
begin
|
||||
coupon = ::Stripe::Coupon.delete(params[:coupon_id])
|
||||
render_json_dump coupon
|
||||
rescue ::Stripe::InvalidRequestError => e
|
||||
render_json_error e.message
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,32 @@
|
|||
import Component from "@ember/component";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
|
||||
export default Component.extend({
|
||||
@discourseComputed
|
||||
discountTypes() {
|
||||
return [
|
||||
{ id: "amount", name: "Amount" },
|
||||
{ id: "percent", name: "Percent" },
|
||||
];
|
||||
},
|
||||
discountType: "amount",
|
||||
discount: null,
|
||||
promoCode: null,
|
||||
active: false,
|
||||
|
||||
actions: {
|
||||
createNewCoupon() {
|
||||
const createParams = {
|
||||
promo: this.promoCode,
|
||||
discount_type: this.discountType,
|
||||
discount: this.discount,
|
||||
active: this.active,
|
||||
};
|
||||
|
||||
this.create(createParams);
|
||||
},
|
||||
cancelCreate() {
|
||||
this.cancel();
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,42 @@
|
|||
import Controller from "@ember/controller";
|
||||
import AdminCoupon from "discourse/plugins/discourse-subscriptions/discourse/models/admin-coupon";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
|
||||
export default Controller.extend({
|
||||
creating: null,
|
||||
|
||||
actions: {
|
||||
openCreateForm() {
|
||||
this.set("creating", true);
|
||||
},
|
||||
closeCreateForm() {
|
||||
this.set("creating", false);
|
||||
},
|
||||
createNewCoupon(params) {
|
||||
AdminCoupon.save(params)
|
||||
.then(() => {
|
||||
this.send("closeCreateForm");
|
||||
this.send("reloadModel");
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
},
|
||||
deleteCoupon(coupon) {
|
||||
AdminCoupon.destroy(coupon)
|
||||
.then(() => {
|
||||
this.send("reloadModel");
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
},
|
||||
toggleActive(coupon) {
|
||||
const couponData = {
|
||||
id: coupon.id,
|
||||
active: !coupon.active,
|
||||
};
|
||||
AdminCoupon.update(couponData)
|
||||
.then(() => {
|
||||
this.send("reloadModel");
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
},
|
||||
},
|
||||
});
|
|
@ -14,6 +14,8 @@ export default {
|
|||
});
|
||||
});
|
||||
|
||||
this.route("coupons");
|
||||
|
||||
this.route("subscriptions");
|
||||
});
|
||||
},
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
import { ajax } from "discourse/lib/ajax";
|
||||
import EmberObject from "@ember/object";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
|
||||
const AdminCoupon = EmberObject.extend({
|
||||
@discourseComputed("coupon.amount_off", "coupon.percent_off")
|
||||
discount(amount_off, percent_off) {
|
||||
if (amount_off) {
|
||||
return `${parseFloat(amount_off * 0.01).toFixed(2)}`;
|
||||
} else if (percent_off) {
|
||||
return `${percent_off}%`;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
AdminCoupon.reopenClass({
|
||||
list() {
|
||||
return ajax("/s/admin/coupons", {
|
||||
method: "get",
|
||||
}).then((result) => {
|
||||
if (result === null) {
|
||||
return { unconfigured: true };
|
||||
}
|
||||
return result.map((coupon) => AdminCoupon.create(coupon));
|
||||
});
|
||||
},
|
||||
save(params) {
|
||||
const data = {
|
||||
promo: params.promo,
|
||||
discount_type: params.discount_type,
|
||||
discount: params.discount,
|
||||
active: params.active,
|
||||
};
|
||||
|
||||
return ajax("/s/admin/coupons", {
|
||||
method: "post",
|
||||
data,
|
||||
}).then((coupon) => AdminCoupon.create(coupon));
|
||||
},
|
||||
|
||||
update(params) {
|
||||
const data = {
|
||||
id: params.id,
|
||||
active: params.active,
|
||||
};
|
||||
|
||||
return ajax("/s/admin/coupons", {
|
||||
method: "put",
|
||||
data,
|
||||
}).then((coupon) => AdminCoupon.create(coupon));
|
||||
},
|
||||
|
||||
destroy(params) {
|
||||
const data = {
|
||||
coupon_id: params.coupon.id,
|
||||
};
|
||||
return ajax("/s/admin/coupons", {
|
||||
method: "delete",
|
||||
data,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default AdminCoupon;
|
|
@ -0,0 +1,14 @@
|
|||
import Route from "@ember/routing/route";
|
||||
import AdminCoupon from "discourse/plugins/discourse-subscriptions/discourse/models/admin-coupon";
|
||||
|
||||
export default Route.extend({
|
||||
model() {
|
||||
return AdminCoupon.list();
|
||||
},
|
||||
|
||||
actions: {
|
||||
reloadModel() {
|
||||
this.refresh();
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,44 @@
|
|||
{{#if model.unconfigured}}
|
||||
<p>{{i18n 'discourse_subscriptions.admin.unconfigured'}}</p>
|
||||
<p><a href="https://meta.discourse.org/t/discourse-subscriptions/140818/">Discourse Subscriptions on Meta</a></p>
|
||||
{{else}}
|
||||
{{#if model}}
|
||||
<table class="table discourse-patrons-table">
|
||||
<thead>
|
||||
<th>{{i18n 'discourse_subscriptions.admin.coupons.code'}}</th>
|
||||
<th>{{i18n 'discourse_subscriptions.admin.coupons.discount'}}</th>
|
||||
<th>{{i18n 'discourse_subscriptions.admin.coupons.times_redeemed'}}</th>
|
||||
<th>{{i18n 'discourse_subscriptions.admin.coupons.active'}}</th>
|
||||
<th>{{i18n 'discourse_subscriptions.admin.coupons.actions'}}</th>
|
||||
</thead>
|
||||
{{#each model as |coupon|}}
|
||||
<tr>
|
||||
<td>{{coupon.code}}</td>
|
||||
<td>{{coupon.discount}}</td>
|
||||
<td>{{coupon.times_redeemed}}</td>
|
||||
<td>{{input type="checkbox" checked=coupon.active click=(action "toggleActive" coupon)}}</td>
|
||||
<td>
|
||||
{{d-button
|
||||
action=(action "deleteCoupon")
|
||||
actionParam=coupon
|
||||
icon="trash-alt"
|
||||
class="btn-danger btn btn-icon btn-no-text"}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</table>
|
||||
{{/if}}
|
||||
|
||||
{{#unless creating}}
|
||||
{{d-button
|
||||
action=(action "openCreateForm")
|
||||
label="discourse_subscriptions.admin.coupons.create"
|
||||
title="discourse_subscriptions.admin.coupons.create"
|
||||
icon="plus"
|
||||
class="btn btn-icon btn-primary create-coupon"}}
|
||||
{{/unless}}
|
||||
|
||||
{{#if creating}}
|
||||
{{create-coupon-form cancel=(action 'closeCreateForm') create=(action 'createNewCoupon')}}
|
||||
{{/if}}
|
||||
{{/if}}
|
|
@ -4,6 +4,7 @@
|
|||
<ul class="nav nav-pills">
|
||||
{{!-- {{nav-item route='adminPlugins.discourse-subscriptions.dashboard' label='discourse_subscriptions.admin.dashboard.title'}} --}}
|
||||
{{nav-item route='adminPlugins.discourse-subscriptions.products' label='discourse_subscriptions.admin.products.title'}}
|
||||
{{nav-item route='adminPlugins.discourse-subscriptions.coupons' label='discourse_subscriptions.admin.coupons.title'}}
|
||||
{{nav-item route='adminPlugins.discourse-subscriptions.subscriptions' label='discourse_subscriptions.admin.subscriptions.title'}}
|
||||
</ul>
|
||||
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
<div class='create-coupon-form'>
|
||||
<form class="form-horizontal">
|
||||
<p>
|
||||
<label for="promo_code">{{i18n 'discourse_subscriptions.admin.coupons.promo_code'}}</label>
|
||||
{{input type="text" name="promo_code" value=promoCode}}
|
||||
</p>
|
||||
<p>
|
||||
<label for="amount">{{i18n 'discourse_subscriptions.admin.coupons.discount'}}</label>
|
||||
{{combo-box
|
||||
content=discountTypes
|
||||
value=discountType
|
||||
onChange=(action (mut discountType))
|
||||
}}
|
||||
{{input class="discount-amount" type="text" name="amount" value=discount}}
|
||||
</p>
|
||||
<p>
|
||||
<label for="active">
|
||||
{{i18n 'discourse_subscriptions.admin.coupons.active'}}
|
||||
</label>
|
||||
{{input type="checkbox" name="active" checked=active}}
|
||||
</p>
|
||||
</form>
|
||||
|
||||
{{d-button
|
||||
action=(action "createNewCoupon")
|
||||
label="discourse_subscriptions.admin.coupons.create"
|
||||
title="discourse_subscriptions.admin.coupons.create"
|
||||
icon="plus"
|
||||
class="btn-primary btn btn-icon"}}
|
||||
{{d-button
|
||||
action=(action "cancelCreate")
|
||||
label="cancel"
|
||||
title="cancel"
|
||||
icon="times"
|
||||
class="btn btn-icon"}}
|
||||
</div>
|
|
@ -22,6 +22,10 @@ textarea[readonly] {
|
|||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-primary.create-coupon {
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.plan-amount {
|
||||
|
|
|
@ -140,6 +140,15 @@ en:
|
|||
active: Active
|
||||
created_at: Created
|
||||
recurring: Recurring Plan?
|
||||
coupons:
|
||||
title: Coupons
|
||||
code: Promo Code
|
||||
discount: Discount
|
||||
times_redeemed: Times Redeemed
|
||||
active: Active?
|
||||
actions: Actions
|
||||
create: Create Coupon
|
||||
promo_code: Promo Code (case-insensitive)
|
||||
subscriptions:
|
||||
title: Subscriptions
|
||||
subscription:
|
||||
|
|
|
@ -11,6 +11,8 @@ DiscourseSubscriptions::Engine.routes.draw do
|
|||
resources :plans
|
||||
resources :subscriptions, only: [:index, :destroy]
|
||||
resources :products
|
||||
resources :coupons, only: [:index, :create]
|
||||
resource :coupons, only: [:destroy, :update]
|
||||
end
|
||||
|
||||
namespace :user do
|
||||
|
|
|
@ -35,6 +35,7 @@ Discourse::Application.routes.append do
|
|||
get '/admin/plugins/discourse-subscriptions/subscriptions' => 'admin/plugins#index', constraints: AdminConstraint.new
|
||||
get '/admin/plugins/discourse-subscriptions/plans' => 'admin/plugins#index', constraints: AdminConstraint.new
|
||||
get '/admin/plugins/discourse-subscriptions/plans/:plan_id' => 'admin/plugins#index', constraints: AdminConstraint.new
|
||||
get '/admin/plugins/discourse-subscriptions/coupons' => 'admin/plugins#index', constraints: AdminConstraint.new
|
||||
get 'u/:username/billing' => 'users#show', constraints: { username: USERNAME_ROUTE_FORMAT }
|
||||
get 'u/:username/billing/:id' => 'users#show', constraints: { username: USERNAME_ROUTE_FORMAT }
|
||||
end
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
module DiscourseSubscriptions
|
||||
RSpec.describe Admin::CouponsController do
|
||||
it 'is a subclass of AdminController' do
|
||||
expect(DiscourseSubscriptions::Admin::CouponsController < ::Admin::AdminController).to eq(true)
|
||||
end
|
||||
|
||||
context 'when unauthenticated' do
|
||||
it "does nothing" do
|
||||
::Stripe::PromotionCode.expects(:list).never
|
||||
get "/s/admin/coupons.json"
|
||||
expect(response.status).to eq(403)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authenticated' do
|
||||
let(:admin) { Fabricate(:admin) }
|
||||
|
||||
before { sign_in(admin) }
|
||||
|
||||
describe "#index" do
|
||||
it "returns a list of promo codes" do
|
||||
::Stripe::PromotionCode.expects(:list).with(limit: 100).returns({
|
||||
data: [{
|
||||
id: 'promo_123',
|
||||
coupon: {
|
||||
valid: true
|
||||
}
|
||||
}]
|
||||
})
|
||||
|
||||
get "/s/admin/coupons.json"
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.parsed_body[0]['id']).to eq('promo_123')
|
||||
end
|
||||
|
||||
it "only returns valid promo codes" do
|
||||
::Stripe::PromotionCode.expects(:list).with(limit: 100).returns({
|
||||
data: [{
|
||||
id: 'promo_123',
|
||||
coupon: {
|
||||
valid: false
|
||||
}
|
||||
}]
|
||||
})
|
||||
|
||||
get "/s/admin/coupons.json"
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.parsed_body).to be_blank
|
||||
end
|
||||
end
|
||||
|
||||
describe "#create" do
|
||||
it "creates a coupon with an amount off" do
|
||||
::Stripe::Coupon.expects(:create).returns(id: 'coup_123')
|
||||
::Stripe::PromotionCode.expects(:create).returns({
|
||||
code: 'p123',
|
||||
coupon: {
|
||||
amount_off: 2000
|
||||
}
|
||||
})
|
||||
|
||||
post "/s/admin/coupons.json", params: {
|
||||
promo: 'p123',
|
||||
discount_type: 'amount',
|
||||
discount: '2000',
|
||||
active: true,
|
||||
}
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.parsed_body['code']).to eq('p123')
|
||||
expect(response.parsed_body['coupon']['amount_off']).to eq(2000)
|
||||
end
|
||||
|
||||
it "creates a coupon with a percent off" do
|
||||
::Stripe::Coupon.expects(:create).returns(id: 'coup_123')
|
||||
::Stripe::PromotionCode.expects(:create).returns({
|
||||
code: 'p123',
|
||||
coupon: {
|
||||
percent_off: 20
|
||||
}
|
||||
})
|
||||
|
||||
post "/s/admin/coupons.json", params: {
|
||||
promo: 'p123',
|
||||
discount_type: 'percent',
|
||||
discount: '20',
|
||||
active: true,
|
||||
}
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.parsed_body['code']).to eq('p123')
|
||||
expect(response.parsed_body['coupon']['percent_off']).to eq(20)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue