From 1b1eb215e4fd366439c12dfbfb85588249f1a1fd Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Thu, 28 Jun 2018 09:32:58 +1000 Subject: [PATCH] various --- .../discourse_donations/charges_controller.rb | 68 ++++++--- app/services/discourse_donations/stripe.rb | 137 ++++++++++++++---- .../discourse/components/donation-list.js.es6 | 3 + .../discourse/components/donation-row.js.es6 | 96 ++++++++++++ .../discourse/components/stripe-card.js.es6 | 34 +++-- .../controllers/cancel-subscription.js.es6 | 12 ++ .../discourse/controllers/donate.js.es6 | 46 +++++- .../discourse/helpers/donation.js.es6 | 56 ------- .../discourse/routes/donate.js.es6 | 21 ++- .../templates/components/donation-list.hbs | 7 +- .../templates/components/donation-row.hbs | 39 +++++ .../templates/components/stripe-card.hbs | 1 + .../discourse/templates/donate.hbs | 30 ++-- .../templates/modal/cancel-subscription.hbs | 11 ++ assets/stylesheets/discourse-donations.scss | 35 ++++- config/locales/client.en.yml | 13 +- config/locales/server.en.yml | 6 +- config/routes.rb | 3 + 18 files changed, 478 insertions(+), 140 deletions(-) create mode 100644 assets/javascripts/discourse/components/donation-row.js.es6 create mode 100644 assets/javascripts/discourse/controllers/cancel-subscription.js.es6 delete mode 100644 assets/javascripts/discourse/helpers/donation.js.es6 create mode 100644 assets/javascripts/discourse/templates/components/donation-row.hbs create mode 100644 assets/javascripts/discourse/templates/modal/cancel-subscription.hbs diff --git a/app/controllers/discourse_donations/charges_controller.rb b/app/controllers/discourse_donations/charges_controller.rb index 1c79371..017be3f 100644 --- a/app/controllers/discourse_donations/charges_controller.rb +++ b/app/controllers/discourse_donations/charges_controller.rb @@ -1,14 +1,20 @@ module DiscourseDonations class ChargesController < ::ApplicationController skip_before_action :verify_authenticity_token, only: [:create] + + before_action :ensure_logged_in, only: [:cancel_subscription] before_action :set_user, only: [:index, :create] - before_action :set_email, only: [:create] + before_action :set_email, only: [:index, :create, :cancel_subscription] def index - if @user && @user.stripe_customer_id - result = DiscourseDonations::Stripe.new(secret_key, stripe_options).list(@user) - else - result = {} + result = {} + + if current_user + stripe = DiscourseDonations::Stripe.new(secret_key, stripe_options) + + list_result = stripe.list(current_user, email: current_user.email) + + result = list_result if list_result.present? end render json: success_json.merge(result) @@ -36,7 +42,7 @@ module DiscourseDonations end Rails.logger.debug "Creating a Stripe payment" - payment = DiscourseDonations::Stripe.new(secret_key, stripe_options) + stripe = DiscourseDonations::Stripe.new(secret_key, stripe_options) result = {} begin @@ -48,17 +54,27 @@ module DiscourseDonations } if user_params[:type] === 'once' - result[:payment] = payment.charge(@user, opts) + result[:charge] = stripe.charge(@user, opts) else opts[:type] = user_params[:type] - result[:subscription] = payment.subscribe(@user, opts) + + subscription = stripe.subscribe(@user, opts) + + if subscription && subscription['id'] + invoices = stripe.invoices_for_subscription(@user, + email: opts[:email], + subscription_id: subscription['id'] + ) + end + + result[:subscription] = {} + result[:subscription][:subscription] = subscription if subscription + result[:subscription][:invoices] = invoices if invoices end rescue ::Stripe::CardError => e err = e.json_body[:error] - puts "HERE IS THE ERROR: #{e.inspect}" - output['messages'] << "There was an error (#{err[:type]})." output['messages'] << "Error code: #{err[:code]}" if err[:code] output['messages'] << "Decline code: #{err[:decline_code]}" if err[:decline_code] @@ -68,18 +84,16 @@ module DiscourseDonations end if (result[:charge] && result[:charge]['paid'] == true) || - (result[:subscription] && result[:subscription]['status'] === 'active') + (result[:subscription] && result[:subscription][:subscription] && + result[:subscription][:subscription]['status'] === 'active') output['messages'] << I18n.t('donations.payment.success') - if result[:charge] + if (result[:charge] && result[:charge]['receipt_number']) || + (result[:subscription] && result[:subscription][:invoices].first['receipt_number']) output['messages'] << " #{I18n.t('donations.payment.receipt_sent', email: @email)}" end - if result[:subscription] - output['messages'] << " #{I18n.t('donations.payment.invoice_sent', email: @email)}" - end - output['charge'] = result[:charge] if result[:charge] output['subscription'] = result[:subscription] if result[:subscription] @@ -95,6 +109,20 @@ module DiscourseDonations render json: output end + def cancel_subscription + params.require(:subscription_id) + + stripe = DiscourseDonations::Stripe.new(secret_key, stripe_options) + + result = stripe.cancel_subscription(params[:subscription_id]) + + if result[:success] + render json: success_json.merge(subscription: result[:subscription]) + else + render json: failed_json.merge(message: result[:message]) + end + end + private def create_account @@ -132,16 +160,20 @@ module DiscourseDonations user = current_user if user_params[:user_id].present? - user = User.find(user_params[:user_id]) + if record = User.find_by(user_params[:user_id]) + user = record + end end @user = user end def set_email + email = nil + if user_params[:email].present? email = user_params[:email] - else + elsif @user email = @user.try(:email) end diff --git a/app/services/discourse_donations/stripe.rb b/app/services/discourse_donations/stripe.rb index c43a9cf..cf7288e 100644 --- a/app/services/discourse_donations/stripe.rb +++ b/app/services/discourse_donations/stripe.rb @@ -9,7 +9,13 @@ module DiscourseDonations end def checkoutCharge(user = nil, email, token, amount) - customer = customer(user, email, token) + customer = customer(user, + email: email, + source: token, + create: true + ) + + return if !customer charge = ::Stripe::Charge.create( customer: customer.id, @@ -22,7 +28,13 @@ module DiscourseDonations end def charge(user = nil, opts) - customer = customer(user, opts[:email], opts[:token]) + customer = customer(user, + email: opts[:email], + source: opts[:token], + create: true + ) + + return if !customer @charge = ::Stripe::Charge.create( customer: customer.id, @@ -36,28 +48,40 @@ module DiscourseDonations end def subscribe(user = nil, opts) - customer = customer(user, opts[:email], opts[:token]) + customer = customer(user, + email: opts[:email], + source: opts[:token], + create: true + ) + + return if !customer + + type = opts[:type] + amount = opts[:amount] plans = ::Stripe::Plan.list - type = opts[:type] - plan_id = create_plan_id(type) + plan_id = create_plan_id(type, amount) unless plans.data && plans.data.any? { |p| p['id'] === plan_id } - result = create_plan(type, opts[:amount]) + result = create_plan(type, amount) + plan_id = result['id'] end - @subscription = ::Stripe::Subscription.create( + ::Stripe::Subscription.create( customer: customer.id, - items: [{ plan: plan_id }] + items: [{ + plan: plan_id + }] ) - - @subscription end - def list(user) - customer = customer(user) - result = {} + def list(user, opts = {}) + customer = customer(user, opts) + + return if !customer + + result = { customer: customer } raw_invoices = ::Stripe::Invoice.list(customer: customer.id) raw_invoices = raw_invoices.is_a?(Object) ? raw_invoices['data'] : [] @@ -66,7 +90,8 @@ module DiscourseDonations raw_charges = raw_charges.is_a?(Object) ? raw_charges['data'] : [] if raw_invoices.any? - raw_subscriptions = ::Stripe::Subscription.list(customer: customer.id)['data'] + raw_subscriptions = ::Stripe::Subscription.list(customer: customer.id, status: 'all') + raw_subscriptions = raw_subscriptions.is_a?(Object) ? raw_subscriptions['data'] : [] if raw_subscriptions.any? subscriptions = [] @@ -97,22 +122,82 @@ module DiscourseDonations result end - def customer(user, email = nil, source = nil) - if user && user.stripe_customer_id - ::Stripe::Customer.retrieve(user.stripe_customer_id) + def invoices_for_subscription(user, opts) + customer = customer(user, + email: opts[:email] + ) + + invoices = [] + + if customer + result = ::Stripe::Invoice.list( + customer: customer.id, + subscription: opts[:subscription_id] + ) + + invoices = result['data'] if result['data'] + end + + invoices + end + + def cancel_subscription(subscription_id) + if subscription = ::Stripe::Subscription.retrieve(subscription_id) + result = subscription.delete + + if result['status'] === 'canceled' + { success: true, subscription: subscription } + else + { success: false, message: I18n.t('donations.subscription.error.not_cancelled') } + end else + { success: false, message: I18n.t('donations.subscription.error.not_found') } + end + end + + def customer(user, opts = {}) + customer = nil + + if user && user.stripe_customer_id + begin + customer = ::Stripe::Customer.retrieve(user.stripe_customer_id) + rescue ::Stripe::StripeError => e + user.custom_fields['stripe_customer_id'] = nil + user.save_custom_fields(true) + customer = nil + end + end + + if !customer && opts[:email] + begin + customers = ::Stripe::Customer.list(email: opts[:email]) + + if customers && customers['data'] + customer = customers['data'].first if customers['data'].any? + end + + if customer && user + user.custom_fields['stripe_customer_id'] = customer.id + user.save_custom_fields(true) + end + rescue ::Stripe::StripeError => e + customer = nil + end + end + + if !customer && opts[:create] customer = ::Stripe::Customer.create( - email: email, - source: source + email: opts[:email], + source: opts[:source] ) if user user.custom_fields['stripe_customer_id'] = customer.id user.save_custom_fields(true) end - - customer end + + customer end def successful? @@ -120,7 +205,7 @@ module DiscourseDonations end def create_plan(type, amount) - id = create_plan_id(type) + id = create_plan_id(type, amount) nickname = id.gsub(/_/, ' ').titleize products = ::Stripe::Product.list(type: 'service') @@ -151,15 +236,15 @@ module DiscourseDonations end def product_id - @product_id ||= "#{SiteSetting.title}_recurring_donation" + @product_id ||= "#{SiteSetting.title}_recurring_donation".freeze end def product_name - @product_name ||= I18n.t('discourse_donations.recurring', site_title: SiteSetting.title) + @product_name ||= I18n.t('donations.recurring', site_title: SiteSetting.title) end - def create_plan_id(type) - "discourse_donation_recurring_#{type}" + def create_plan_id(type, amount) + "discourse_donation_recurring_#{type}_#{amount}".freeze end end end diff --git a/assets/javascripts/discourse/components/donation-list.js.es6 b/assets/javascripts/discourse/components/donation-list.js.es6 index 744ebe3..3b9b4da 100644 --- a/assets/javascripts/discourse/components/donation-list.js.es6 +++ b/assets/javascripts/discourse/components/donation-list.js.es6 @@ -1,3 +1,6 @@ +import { ajax } from 'discourse/lib/ajax'; +import { popupAjaxError } from 'discourse/lib/ajax-error'; + export default Ember.Component.extend({ classNames: 'donation-list', hasSubscriptions: Ember.computed.notEmpty('subscriptions'), diff --git a/assets/javascripts/discourse/components/donation-row.js.es6 b/assets/javascripts/discourse/components/donation-row.js.es6 new file mode 100644 index 0000000..56533f5 --- /dev/null +++ b/assets/javascripts/discourse/components/donation-row.js.es6 @@ -0,0 +1,96 @@ +import { ajax } from 'discourse/lib/ajax'; +import { popupAjaxError } from 'discourse/lib/ajax-error'; +import { formatAnchor, formatAmount } from '../lib/donation-utilities'; +import { default as computed, observes, on } from 'ember-addons/ember-computed-decorators'; +import showModal from "discourse/lib/show-modal"; + +export default Ember.Component.extend({ + classNameBindings: [':donation-row', 'canceled', 'updating'], + includePrefix: Ember.computed.or('invoice', 'charge'), + canceled: Ember.computed.equal('subscription.status', 'canceled'), + + @computed('subscription', 'invoice', 'charge', 'customer') + data(subscription, invoice, charge, customer) { + if (subscription) { + return $.extend({}, subscription.plan, { + anchor: subscription.billing_cycle_anchor + }); + } else if (invoice) { + let receiptSent = false; + + if (invoice.receipt_number && customer.email) { + receiptSent = true; + } + + return $.extend({}, invoice.lines.data[0], { + anchor: invoice.date, + invoiceLink: invoice.invoice_pdf, + receiptSent + }); + } else if (charge) { + let receiptSent = false; + + if (charge.receipt_number && charge.receipt_email) { + receiptSent = true; + } + + return $.extend({}, charge, { + anchor: charge.created, + receiptSent + }); + } + }, + + @computed('data.currency') + currency(currency) { + return currency ? currency.toUpperCase() : null; + }, + + @computed('data.amount', 'currency') + amount(amount, currency) { + return formatAmount(amount, currency); + }, + + @computed('data.interval') + interval(interval) { + return interval || 'once'; + }, + + @computed('data.anchor', 'interval') + period(anchor, interval) { + return I18n.t(`discourse_donations.period.${interval}`, { + anchor: formatAnchor(interval, moment.unix(anchor)) + }) + }, + + cancelSubscription() { + const subscriptionId = this.get('subscription.id'); + this.set('updating', true); + + ajax('/donate/charges/cancel-subscription', { + data: { + subscription_id: subscriptionId + }, + method: 'put' + }).then(result => { + if (result.success) { + this.set('subscription', result.subscription); + } + }).catch(popupAjaxError).finally(() => { + this.set('updating', false); + }); + }, + + actions: { + cancelSubscription() { + showModal('cancel-subscription', { + model: { + currency: this.get('currency'), + amount: this.get('amount'), + period: this.get('period'), + confirm: () => this.cancelSubscription() + } + }); + } + } +}) diff --git a/assets/javascripts/discourse/components/stripe-card.js.es6 b/assets/javascripts/discourse/components/stripe-card.js.es6 index 2eb2611..06d4ad3 100644 --- a/assets/javascripts/discourse/components/stripe-card.js.es6 +++ b/assets/javascripts/discourse/components/stripe-card.js.es6 @@ -175,7 +175,7 @@ export default Ember.Component.extend({ const transactionFeeEnabled = settings.discourse_donations_enable_transaction_fee; let amount = transactionFeeEnabled ? this.get('totalAmount') : this.get('amount'); - if (zeroDecimalCurrencies.indexOf(setting.discourse_donations_currency) === -1) { + if (zeroDecimalCurrencies.indexOf(settings.discourse_donations_currency) === -1) { amount = amount * 100; } @@ -189,22 +189,26 @@ export default Ember.Component.extend({ }; if(!self.get('paymentSuccess')) { - ajax('/donate/charges', { data: params, method: 'post' }).then(d => { - let donation = d.donation; - - if (donation) { - if (donation.object === 'subscription') { - let subscriptions = this.get('subscriptions') || []; - subscriptions.push(donation); - this.set('subscriptions', subscriptions); - } else if (donation.object === 'charge') { - let charges = this.get('charges') || []; - charges.push(donation); - this.set('charges', charges); - } + ajax('/donate/charges', { + data: params, + method: 'post' + }).then(result => { + if (result.subscription) { + let subscription = $.extend({}, result.subscription, { + new: true + }); + this.get('subscriptions').unshiftObject(subscription); } - self.concatMessages(d.messages); + if (result.charge) { + let charge = $.extend({}, result.charge, { + new: true + }); + this.get('charges').unshiftObject(charge); + } + + self.concatMessages(result.messages); + self.endTranscation(); }); } diff --git a/assets/javascripts/discourse/controllers/cancel-subscription.js.es6 b/assets/javascripts/discourse/controllers/cancel-subscription.js.es6 new file mode 100644 index 0000000..57a6565 --- /dev/null +++ b/assets/javascripts/discourse/controllers/cancel-subscription.js.es6 @@ -0,0 +1,12 @@ +export default Ember.Controller.extend({ + actions: { + confirm() { + this.get('model.confirm')(); + this.send('closeModal'); + }, + + cancel() { + this.send('closeModal'); + } + } +}) diff --git a/assets/javascripts/discourse/controllers/donate.js.es6 b/assets/javascripts/discourse/controllers/donate.js.es6 index daecc05..2073af0 100644 --- a/assets/javascripts/discourse/controllers/donate.js.es6 +++ b/assets/javascripts/discourse/controllers/donate.js.es6 @@ -1,11 +1,53 @@ import { default as computed } from 'ember-addons/ember-computed-decorators'; +import { popupAjaxError } from 'discourse/lib/ajax-error'; +import { ajax } from 'discourse/lib/ajax'; +import { getOwner } from 'discourse-common/lib/get-owner'; export default Ember.Controller.extend({ loadingDonations: false, - - @computed('charges', 'subscriptions') + loadDonationsDisabled: Ember.computed.not('emailVaild'), + + @computed('charges.[]', 'subscriptions.[]') hasDonations(charges, subscriptions) { return (charges && charges.length > 0) || (subscriptions && subscriptions.length > 0); + }, + + @computed('email') + emailVaild(email) { + return emailValid(email); + }, + + actions: { + loadDonations() { + let email = this.get('email'); + + this.set('loadingDonations', true); + + ajax('/donate/charges', { + data: { email }, + type: 'GET' + }).then((result) => { + this.setProperties({ + charges: Ember.A(result.charges), + subscriptions: Ember.A(result.subscriptions), + customer: result.customer + }); + }).catch(popupAjaxError).finally(() => { + this.setProperties({ + loadingDonations: false, + hasEmailResult: true + }); + + Ember.run.later(() => { + this.set('hasEmailResult', false); + }, 6000) + }) + }, + + showLogin() { + const controller = getOwner(this).lookup('route:application'); + controller.send('showLogin'); + } } }) diff --git a/assets/javascripts/discourse/helpers/donation.js.es6 b/assets/javascripts/discourse/helpers/donation.js.es6 deleted file mode 100644 index cd372c5..0000000 --- a/assets/javascripts/discourse/helpers/donation.js.es6 +++ /dev/null @@ -1,56 +0,0 @@ -import { registerHelper } from "discourse-common/lib/helpers"; -import { formatAnchor, formatAmount } from '../lib/donation-utilities'; - -registerHelper("donation-subscription", function([subscription]) { - let currency = subscription.plan.currency.toUpperCase(); - let html = currency; - - html += ` ${formatAmount(subscription.plan.amount, currency)} `; - - html += I18n.t(`discourse_donations.period.${subscription.plan.interval}`, { - anchor: formatAnchor(subscription.plan.interval, moment.unix(subscription.billing_cycle_anchor)) - }); - - return new Handlebars.SafeString(html); -}); - -registerHelper("donation-invoice", function([invoice]) { - let details = invoice.lines.data[0]; - let html = I18n.t('discourse_donations.invoice_prefix'); - let currency = details.currency.toUpperCase(); - - html += ` ${currency}`; - - html += ` ${formatAmount(details.amount, currency)} `; - - html += I18n.t(`discourse_donations.period.once`, { - anchor: formatAnchor('once', moment.unix(invoice.date)) - }); - - if (invoice.invoice_pdf) { - html += ` (${I18n.t('discourse_donations.invoice')})`; - } - - return new Handlebars.SafeString(html); -}); - -registerHelper("donation-charge", function([charge]) { - let html = I18n.t('discourse_donations.invoice_prefix'); - let currency = charge.currency.toUpperCase(); - - html += ` ${currency}`; - - html += ` ${formatAmount(charge.amount, currency)} `; - - html += I18n.t(`discourse_donations.period.once`, { - anchor: formatAnchor('once', moment.unix(charge.created)) - }); - - if (charge.receipt_email) { - html += `. ${I18n.t('discourse_donations.receipt', { - email: charge.receipt_email - })}`; - } - - return new Handlebars.SafeString(html); -}); diff --git a/assets/javascripts/discourse/routes/donate.js.es6 b/assets/javascripts/discourse/routes/donate.js.es6 index 90b12e0..04d7994 100644 --- a/assets/javascripts/discourse/routes/donate.js.es6 +++ b/assets/javascripts/discourse/routes/donate.js.es6 @@ -4,15 +4,24 @@ import { ajax } from 'discourse/lib/ajax'; export default DiscourseRoute.extend({ setupController(controller) { + let charges = []; + let subscriptions = []; + let customer = {}; + controller.set('loadingDonations', true); - ajax('/donate/charges').then((result) => { - if (result && (result.charges || result.subscriptions)) { - controller.setProperties({ - charges: result.charges, - subscriptions: result.subscriptions - }); + ajax('/donate/charges').then((result) => { + if (result) { + charges = result.charges; + subscriptions = result.subscriptions; + customer = result.customer; } + + controller.setProperties({ + charges: Ember.A(charges), + subscriptions: Ember.A(subscriptions), + customer + }); }).catch(popupAjaxError).finally(() => { controller.set('loadingDonations', false); }) diff --git a/assets/javascripts/discourse/templates/components/donation-list.hbs b/assets/javascripts/discourse/templates/components/donation-list.hbs index ae2706e..4a23964 100644 --- a/assets/javascripts/discourse/templates/components/donation-list.hbs +++ b/assets/javascripts/discourse/templates/components/donation-list.hbs @@ -3,12 +3,11 @@
{{i18n 'discourse_donations.donations.subscriptions'}}