FEATURE: Implement refunds from dashboard (#27)

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.

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}}
         </td>
       </tr>
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 @@
+<div>
+  {{#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}}
+
+
+  <div class="modal-footer">
+    {{#if model.loading}}
+      {{loading-spinner}}
+    {{else}}
+      {{d-button
+        label="yes_value"
+        action=(route-action "cancelSubscription" (hash subscription=model refund=refund))
+        icon="times"
+        class="btn-danger"
+      }}
+      {{d-button label="no_value" action=(route-action "closeModal")}}
+    {{/if}}
+  </div>
+</div>
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'
+            )

[... diff too long, it was truncated ...]

GitHub sha: b9262767

This commit appears in #27 which was approved by romanrizzi. It was merged by justindirose.