From b92627677fcea44ba7f76df80667e6d55c8704ac Mon Sep 17 00:00:00 2001 From: Justin DiRose Date: Thu, 29 Oct 2020 10:31:12 -0500 Subject: [PATCH] FEATURE: Implement refunds from dashboard (#27) An implementation of refunds from the Admin dashboard. To refund, go to Plugins > Subscriptions > Subscriptions then click the `Cancel` button. You'll be presented with a modal. If you wish to refund only the most recent payment, check the box. This only implements refunds for Subscriptions, not One Time Payments. One Time Payments will still need to be handled manually at this time. --- .../admin/subscriptions_controller.rb | 14 ++++++ ...scourse-subscriptions-subscriptions.js.es6 | 11 ++++- .../models/admin-subscription.js.es6 | 6 ++- ...scourse-subscriptions-subscriptions.js.es6 | 43 ++++++++----------- ...-discourse-subscriptions-subscriptions.hbs | 2 +- .../modal/admin-cancel-subscription.hbs | 21 +++++++++ config/locales/client.en.yml | 2 + .../admin/subscriptions_controller_spec.rb | 15 +++++++ 8 files changed, 87 insertions(+), 27 deletions(-) create mode 100644 assets/javascripts/discourse/templates/modal/admin-cancel-subscription.hbs diff --git a/app/controllers/discourse_subscriptions/admin/subscriptions_controller.rb b/app/controllers/discourse_subscriptions/admin/subscriptions_controller.rb index 046a67a..a3f7f7f 100644 --- a/app/controllers/discourse_subscriptions/admin/subscriptions_controller.rb +++ b/app/controllers/discourse_subscriptions/admin/subscriptions_controller.rb @@ -26,7 +26,9 @@ module DiscourseSubscriptions end def destroy + params.require(:id) begin + refund_subscription(params[:id]) if params[:refund] subscription = ::Stripe::Subscription.delete(params[:id]) customer = Customer.find_by( @@ -49,6 +51,18 @@ module DiscourseSubscriptions render_json_error e.message end end + + private + + # this will only refund the most recent subscription payment + def refund_subscription(subscription_id) + subscription = ::Stripe::Subscription.retrieve(subscription_id) + invoice = ::Stripe::Invoice.retrieve(subscription[:latest_invoice]) if subscription[:latest_invoice] + payment_intent = invoice[:payment_intent] if invoice[:payment_intent] + refund = ::Stripe::Refund.create({ + payment_intent: payment_intent, + }) + end end end end diff --git a/assets/javascripts/discourse/controllers/admin-plugins-discourse-subscriptions-subscriptions.js.es6 b/assets/javascripts/discourse/controllers/admin-plugins-discourse-subscriptions-subscriptions.js.es6 index fa4ba1e..f4b51f9 100644 --- a/assets/javascripts/discourse/controllers/admin-plugins-discourse-subscriptions-subscriptions.js.es6 +++ b/assets/javascripts/discourse/controllers/admin-plugins-discourse-subscriptions-subscriptions.js.es6 @@ -1,3 +1,12 @@ import Controller from "@ember/controller"; +import showModal from "discourse/lib/show-modal"; -export default Controller.extend({}); +export default Controller.extend({ + actions: { + showCancelModal(subscription) { + showModal("admin-cancel-subscription", { + model: subscription, + }); + }, + }, +}); diff --git a/assets/javascripts/discourse/models/admin-subscription.js.es6 b/assets/javascripts/discourse/models/admin-subscription.js.es6 index 73e6ecc..0641244 100644 --- a/assets/javascripts/discourse/models/admin-subscription.js.es6 +++ b/assets/javascripts/discourse/models/admin-subscription.js.es6 @@ -19,9 +19,13 @@ const AdminSubscription = EmberObject.extend({ return getURL(`/admin/users/${metadata.user_id}/${metadata.username}`); }, - destroy() { + destroy(refund) { + const data = { + refund: refund, + }; return ajax(`/s/admin/subscriptions/${this.id}`, { method: "delete", + data, }).then((result) => AdminSubscription.create(result)); }, }); diff --git a/assets/javascripts/discourse/routes/admin-plugins-discourse-subscriptions-subscriptions.js.es6 b/assets/javascripts/discourse/routes/admin-plugins-discourse-subscriptions-subscriptions.js.es6 index 1432e3c..42c6a86 100644 --- a/assets/javascripts/discourse/routes/admin-plugins-discourse-subscriptions-subscriptions.js.es6 +++ b/assets/javascripts/discourse/routes/admin-plugins-discourse-subscriptions-subscriptions.js.es6 @@ -1,6 +1,6 @@ +import I18n from "I18n"; import Route from "@ember/routing/route"; import AdminSubscription from "discourse/plugins/discourse-subscriptions/discourse/models/admin-subscription"; -import I18n from "I18n"; export default Route.extend({ model() { @@ -8,29 +8,24 @@ export default Route.extend({ }, actions: { - cancelSubscription(subscription) { - bootbox.confirm( - I18n.t( - "discourse_subscriptions.user.subscriptions.operations.destroy.confirm" - ), - I18n.t("no_value"), - I18n.t("yes_value"), - (confirmed) => { - if (confirmed) { - subscription.set("loading", true); - - subscription - .destroy() - .then((result) => subscription.set("status", result.status)) - .catch((data) => - bootbox.alert(data.jqXHR.responseJSON.errors.join("\n")) - ) - .finally(() => { - subscription.set("loading", false); - }); - } - } - ); + cancelSubscription(model) { + const subscription = model.subscription; + const refund = model.refund; + subscription.set("loading", true); + subscription + .destroy(refund) + .then((result) => { + subscription.set("status", result.status); + this.send("closeModal"); + bootbox.alert(I18n.t("discourse_subscriptions.admin.canceled")); + }) + .catch((data) => + bootbox.alert(data.jqXHR.responseJSON.errors.join("\n")) + ) + .finally(() => { + subscription.set("loading", false); + this.refresh(); + }); }, }, }); diff --git a/assets/javascripts/discourse/templates/admin/plugins-discourse-subscriptions-subscriptions.hbs b/assets/javascripts/discourse/templates/admin/plugins-discourse-subscriptions-subscriptions.hbs index ee6d91a..fcf5877 100644 --- a/assets/javascripts/discourse/templates/admin/plugins-discourse-subscriptions-subscriptions.hbs +++ b/assets/javascripts/discourse/templates/admin/plugins-discourse-subscriptions-subscriptions.hbs @@ -34,7 +34,7 @@ {{#if subscription.loading}} {{loading-spinner size="small"}} {{else}} - {{d-button disabled=subscription.canceled label="cancel" action=(route-action "cancelSubscription" subscription) icon="times"}} + {{d-button disabled=subscription.canceled label="cancel" action=(action "showCancelModal" subscription) icon="times"}} {{/if}} diff --git a/assets/javascripts/discourse/templates/modal/admin-cancel-subscription.hbs b/assets/javascripts/discourse/templates/modal/admin-cancel-subscription.hbs new file mode 100644 index 0000000..0f114c3 --- /dev/null +++ b/assets/javascripts/discourse/templates/modal/admin-cancel-subscription.hbs @@ -0,0 +1,21 @@ +
+ {{#d-modal-body rawTitle=(i18n "discourse_subscriptions.user.subscriptions.operations.destroy.confirm")}} + {{input type="checkbox" checked=refund}} + {{i18n "discourse_subscriptions.admin.ask_refund"}} + {{/d-modal-body}} + + + +
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 4cb991c..b712a78 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -148,3 +148,5 @@ en: plan: Plan status: Status created_at: Created + ask_refund: Refund the last payment made by the customer? + canceled: "The subscription is canceled." diff --git a/spec/requests/admin/subscriptions_controller_spec.rb b/spec/requests/admin/subscriptions_controller_spec.rb index 338f950..cb5b693 100644 --- a/spec/requests/admin/subscriptions_controller_spec.rb +++ b/spec/requests/admin/subscriptions_controller_spec.rb @@ -99,6 +99,21 @@ module DiscourseSubscriptions delete "/s/admin/subscriptions/sub_12345.json" }.not_to change { user.groups.count } end + + it "refunds if params[:refund] present" do + ::Stripe::Subscription + .expects(:delete) + .with('sub_12345') + .returns( + plan: { product: 'pr_34578' }, + customer: 'c_123' + ) + ::Stripe::Subscription.expects(:retrieve).with('sub_12345').returns(latest_invoice: 'in_123') + ::Stripe::Invoice.expects(:retrieve).with('in_123').returns(payment_intent: 'pi_123') + ::Stripe::Refund.expects(:create).with(payment_intent: 'pi_123') + + delete "/s/admin/subscriptions/sub_12345.json", params: { refund: true } + end end end end