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.
This commit is contained in:
Justin DiRose 2020-10-29 10:31:12 -05:00 committed by GitHub
parent b950926538
commit b92627677f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 87 additions and 27 deletions

View File

@ -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

View File

@ -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,
});
},
},
});

View File

@ -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));
},
});

View File

@ -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();
});
},
},
});

View File

@ -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>

View File

@ -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>

View File

@ -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."

View File

@ -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