mirror of
https://github.com/discourse/discourse-subscriptions.git
synced 2025-07-07 15:02:23 +00:00
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");
|
this.route("subscriptions");
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
64
assets/javascripts/discourse/models/admin-coupon.js.es6
Normal file
64
assets/javascripts/discourse/models/admin-coupon.js.es6
Normal file
@ -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">
|
<ul class="nav nav-pills">
|
||||||
{{!-- {{nav-item route='adminPlugins.discourse-subscriptions.dashboard' label='discourse_subscriptions.admin.dashboard.title'}} --}}
|
{{!-- {{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.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'}}
|
{{nav-item route='adminPlugins.discourse-subscriptions.subscriptions' label='discourse_subscriptions.admin.subscriptions.title'}}
|
||||||
</ul>
|
</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;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-primary.create-coupon {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.plan-amount {
|
.plan-amount {
|
||||||
|
@ -140,6 +140,15 @@ en:
|
|||||||
active: Active
|
active: Active
|
||||||
created_at: Created
|
created_at: Created
|
||||||
recurring: Recurring Plan?
|
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:
|
subscriptions:
|
||||||
title: Subscriptions
|
title: Subscriptions
|
||||||
subscription:
|
subscription:
|
||||||
|
@ -11,6 +11,8 @@ DiscourseSubscriptions::Engine.routes.draw do
|
|||||||
resources :plans
|
resources :plans
|
||||||
resources :subscriptions, only: [:index, :destroy]
|
resources :subscriptions, only: [:index, :destroy]
|
||||||
resources :products
|
resources :products
|
||||||
|
resources :coupons, only: [:index, :create]
|
||||||
|
resource :coupons, only: [:destroy, :update]
|
||||||
end
|
end
|
||||||
|
|
||||||
namespace :user do
|
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/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' => '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/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' => 'users#show', constraints: { username: USERNAME_ROUTE_FORMAT }
|
||||||
get 'u/:username/billing/:id' => 'users#show', constraints: { username: USERNAME_ROUTE_FORMAT }
|
get 'u/:username/billing/:id' => 'users#show', constraints: { username: USERNAME_ROUTE_FORMAT }
|
||||||
end
|
end
|
||||||
|
99
spec/requests/admin/coupons_controller_spec.rb
Normal file
99
spec/requests/admin/coupons_controller_spec.rb
Normal file
@ -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…
x
Reference in New Issue
Block a user