From 609d19e5d17519b1360957084414ddeea7920cef Mon Sep 17 00:00:00 2001 From: Sylvain Date: Mon, 21 Dec 2020 16:12:34 +0100 Subject: [PATCH] refactored subscription process + renew (update) subscription/ offre free days --- app/controllers/api/payments_controller.rb | 9 +- .../api/subscriptions_controller.rb | 9 +- app/frontend/src/javascript/api/api-client.ts | 7 ++ .../javascript/controllers/admin/members.js | 1 - .../src/javascript/directives/cart.js | 31 ++++--- .../admin/subscriptions/expired_at_modal.html | 1 + app/models/subscription.rb | 83 +------------------ app/services/reservations/reserve.rb | 33 +------- app/services/subscriptions/subscribe.rb | 72 +++++++++++++--- app/services/wallet_service.rb | 30 +++++++ config/locales/app.admin.en.yml | 1 + config/locales/app.admin.fr.yml | 1 + 12 files changed, 134 insertions(+), 144 deletions(-) diff --git a/app/controllers/api/payments_controller.rb b/app/controllers/api/payments_controller.rb index 2bc906e1f..ad1eaa2dd 100644 --- a/app/controllers/api/payments_controller.rb +++ b/app/controllers/api/payments_controller.rb @@ -49,7 +49,7 @@ class API::PaymentsController < API::ApiController if params[:cart_items][:reservation] res = on_reservation_success(intent, amount[:details]) elsif params[:cart_items][:subscription] - res = on_subscription_success(intent) + res = on_subscription_success(intent, amount[:details]) end end @@ -84,7 +84,7 @@ class API::PaymentsController < API::ApiController if params[:cart_items][:reservation] res = on_reservation_success(intent, amount[:details]) elsif params[:cart_items][:subscription] - res = on_subscription_success(intent) + res = on_subscription_success(intent, amount[:details]) end end @@ -125,7 +125,7 @@ class API::PaymentsController < API::ApiController end end - def on_subscription_success(intent) + def on_subscription_success(intent, details) @subscription = Subscription.new(subscription_params) user_id = if current_user.admin? || current_user.manager? params[:cart_items][:subscription][:user_id] @@ -134,8 +134,7 @@ class API::PaymentsController < API::ApiController end is_subscribe = Subscriptions::Subscribe.new(current_user.invoicing_profile.id, user_id) .pay_and_save(@subscription, - coupon: coupon_params[:coupon_code], - invoice: true, + payment_details: details, payment_intent_id: intent.id, schedule: params[:cart_items][:subscription][:payment_schedule], payment_method: 'stripe') diff --git a/app/controllers/api/subscriptions_controller.rb b/app/controllers/api/subscriptions_controller.rb index f79017acd..db8c1ce12 100644 --- a/app/controllers/api/subscriptions_controller.rb +++ b/app/controllers/api/subscriptions_controller.rb @@ -14,14 +14,13 @@ class API::SubscriptionsController < API::ApiController # Managers can create subscriptions for other users def create user_id = current_user.admin? || current_user.manager? ? params[:subscription][:user_id] : current_user.id - amount = transaction_amount(current_user.admin? || (current_user.manager? && current_user.id != user_id), user_id) + transaction = transaction_amount(current_user.admin? || (current_user.manager? && current_user.id != user_id), user_id) - authorize SubscriptionContext.new(Subscription, amount, user_id) + authorize SubscriptionContext.new(Subscription, transaction[:amount], user_id) @subscription = Subscription.new(subscription_params) is_subscribe = Subscriptions::Subscribe.new(current_user.invoicing_profile.id, user_id) - .pay_and_save(@subscription, coupon: coupon_params[:coupon_code], - invoice: true, + .pay_and_save(@subscription, payment_details: transaction[:details], schedule: params[:subscription][:payment_schedule], payment_method: params[:subscription][:payment_method]) @@ -65,7 +64,7 @@ class API::SubscriptionsController < API::ApiController # Subtract wallet amount from total total = price_details[:total] wallet_debit = get_wallet_debit(user, total) - total - wallet_debit + { amount: total - wallet_debit, details: price_details } end def get_wallet_debit(user, total_amount) diff --git a/app/frontend/src/javascript/api/api-client.ts b/app/frontend/src/javascript/api/api-client.ts index ad4aa60f2..eaead8232 100644 --- a/app/frontend/src/javascript/api/api-client.ts +++ b/app/frontend/src/javascript/api/api-client.ts @@ -19,6 +19,13 @@ client.interceptors.response.use(function (response) { }); function extractHumanReadableMessage(error: any): string { + if (error.match(/^/)) { + // parse ruby error pages + const parser = new DOMParser(); + const htmlDoc = parser.parseFromString(error, 'text/html'); + return htmlDoc.querySelector('h2').textContent; + } + if (typeof error === 'string') return error; let message = ''; diff --git a/app/frontend/src/javascript/controllers/admin/members.js b/app/frontend/src/javascript/controllers/admin/members.js index 114502eae..3f105e484 100644 --- a/app/frontend/src/javascript/controllers/admin/members.js +++ b/app/frontend/src/javascript/controllers/admin/members.js @@ -761,7 +761,6 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state', size: 'lg', controller: ['$scope', '$uibModalInstance', 'Subscription', function ($scope, $uibModalInstance, Subscription) { $scope.new_expired_at = angular.copy(subscription.expired_at); - $scope.scheduled = subscription.scheduled; $scope.free = free; $scope.datePicker = { opened: false, diff --git a/app/frontend/src/javascript/directives/cart.js b/app/frontend/src/javascript/directives/cart.js index dea22cab0..a209e394a 100644 --- a/app/frontend/src/javascript/directives/cart.js +++ b/app/frontend/src/javascript/directives/cart.js @@ -716,9 +716,14 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs', * Open a modal window that allows the user to process a credit card payment for his current shopping cart. */ const payByStripe = function (reservation) { - $scope.toggleStripeModal(() => { - $scope.stripe.cartItems = mkCartItems(reservation, 'stripe'); - }); + // check that the online payment is enabled + if ($scope.settings.online_payment_module !== 'true') { + growl.error(_t('app.shared.cart.online_payment_disabled')); + } else { + $scope.toggleStripeModal(() => { + $scope.stripe.cartItems = mkCartItems(reservation, 'stripe'); + }); + } }; /** * Open a modal window that allows the user to process a local payment for his current shopping cart (admin only). @@ -751,10 +756,13 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs', }, user () { return $scope.user; + }, + settings () { + return $scope.settings; } }, - controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'Reservation', 'Subscription', 'wallet', 'helpers', '$filter', 'coupon', 'selectedPlan', 'schedule', 'cartItems', 'user', - function ($scope, $uibModalInstance, $state, reservation, price, Auth, Reservation, Subscription, wallet, helpers, $filter, coupon, selectedPlan, schedule, cartItems, user) { + controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'Reservation', 'Subscription', 'wallet', 'helpers', '$filter', 'coupon', 'selectedPlan', 'schedule', 'cartItems', 'user', 'settings', + function ($scope, $uibModalInstance, $state, reservation, price, Auth, Reservation, Subscription, wallet, helpers, $filter, coupon, selectedPlan, schedule, cartItems, user, settings) { // user wallet amount $scope.wallet = wallet; @@ -797,7 +805,12 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs', */ $scope.ok = function () { if ($scope.schedule && $scope.method.payment_method === 'stripe') { - return $scope.toggleStripeModal(); + // check that the online payment is enabled + if (settings.online_payment_module !== 'true') { + return growl.error(_t('app.shared.cart.online_payment_disabled')); + } else { + return $scope.toggleStripeModal(); + } } $scope.attempting = true; // save subscription (if there's only a subscription selected) @@ -927,11 +940,7 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs', const amountToPay = helpers.getAmountToPay($scope.amountTotal, wallet.amount); if ((AuthService.isAuthorized(['member']) && amountToPay > 0) || (AuthService.isAuthorized('manager') && $scope.user.id === $rootScope.currentUser.id && amountToPay > 0)) { - if ($scope.settings.online_payment_module !== 'true') { - growl.error(_t('app.shared.cart.online_payment_disabled')); - } else { - return payByStripe(reservation); - } + return payByStripe(reservation); } else { if (AuthService.isAuthorized(['admin']) || (AuthService.isAuthorized('manager') && $scope.user.id !== $rootScope.currentUser.id) || diff --git a/app/frontend/templates/admin/subscriptions/expired_at_modal.html b/app/frontend/templates/admin/subscriptions/expired_at_modal.html index 6d02cd49f..02e0b71ac 100644 --- a/app/frontend/templates/admin/subscriptions/expired_at_modal.html +++ b/app/frontend/templates/admin/subscriptions/expired_at_modal.html @@ -10,6 +10,7 @@

{{ 'app.admin.members_edit.you_intentionally_decide_to_extend_the_user_s_subscription_by_charging_him_again_for_his_current_subscription' }}

{{ 'app.admin.members_edit.credits_will_be_reset' }}

+

{{ 'app.admin.members_edit.payment_scheduled' }}

diff --git a/app/models/subscription.rb b/app/models/subscription.rb index 540396345..f47a82816 100644 --- a/app/models/subscription.rb +++ b/app/models/subscription.rb @@ -20,8 +20,7 @@ class Subscription < ApplicationRecord after_save :notify_partner_subscribed_plan, if: :of_partner_plan? ## - # Set the inner properties of the subscription, init the user's credits and save the subscription - # into the DB + # Set the inner properties of the subscription, init the user's credits and save the subscription into the DB # @return {boolean} true, if the operation succeeded ## def init_save @@ -34,86 +33,6 @@ class Subscription < ApplicationRecord true end - # TODO, remove this method, refactor like services/Reservations::Reserve - # @param invoice if true then only the subscription is payed, without reservation - # if false then the subscription is payed with reservation - # @param payment_method is only used for schedules - def save_with_payment(operator_profile_id, invoice: true, coupon_code: nil, payment_intent_id: nil, schedule: nil, payment_method: nil) - return false unless valid? - - set_expiration_date - return false unless save - - UsersCredits::Manager.new(user: user).reset_credits - if invoice - @wallet_amount_debit = get_wallet_amount_debit - - # debit wallet - wallet_transaction = debit_user_wallet - - payment = if schedule - generate_schedule(operator_profile_id, payment_method, coupon_code) - else - generate_invoice(operator_profile_id, coupon_code, payment_intent_id) - end - - if wallet_transaction - payment.wallet_amount = @wallet_amount_debit - payment.wallet_transaction_id = wallet_transaction.id - end - payment.save - end - true - end - - def generate_schedule(operator_profile_id, payment_method, coupon_code = nil) - operator = InvoicingProfile.find(operator_profile_id)&.user - coupon = Coupon.find_by(code: coupon_code) unless coupon_code.nil? - - PaymentScheduleService.new.create( - self, - plan.amount, - coupon: coupon, - operator: operator, - payment_method: payment_method, - user: user - ) - end - - def generate_invoice(operator_profile_id, coupon_code = nil, payment_intent_id = nil) - coupon_id = nil - total = plan.amount - operator = InvoicingProfile.find(operator_profile_id)&.user - method = operator&.admin? || (operator&.manager? && operator != user) ? nil : 'stripe' - - unless coupon_code.nil? - @coupon = Coupon.find_by(code: coupon_code) - - unless @coupon.nil? - total = CouponService.new.apply(plan.amount, @coupon, user.id) - coupon_id = @coupon.id - end - end - - invoice = Invoice.new( - invoiced_id: id, - invoiced_type: 'Subscription', - invoicing_profile: user.invoicing_profile, - statistic_profile: user.statistic_profile, - total: total, - coupon_id: coupon_id, - operator_profile_id: operator_profile_id, - stp_payment_intent_id: payment_intent_id, - payment_method: method - ) - invoice.invoice_items.push InvoiceItem.new( - amount: plan.amount, - description: plan.name, - subscription_id: id - ) - invoice - end - def generate_and_save_invoice(operator_profile_id) generate_invoice(operator_profile_id).save end diff --git a/app/services/reservations/reserve.rb b/app/services/reservations/reserve.rb index 77599952b..e2bf3d225 100644 --- a/app/services/reservations/reserve.rb +++ b/app/services/reservations/reserve.rb @@ -29,11 +29,13 @@ class Reservations::Reserve generate_invoice(reservation, operator_profile_id, payment_details, payment_intent_id) end payment.save - debit_user_wallet(payment, user, reservation) + WalletService.debit_user_wallet(payment, user, reservation) reservation.post_save true end + private + ## # Generate the invoice for the given reservation+subscription ## @@ -64,33 +66,4 @@ class Reservations::Reserve ) end - ## - # Compute the amount decreased from the user's wallet, if applicable - # @param payment {Invoice|PaymentSchedule} - # @param user {User} the customer - # @param coupon {Coupon|String} Coupon object or code - ## - def wallet_amount_debit(payment, user, coupon = nil) - total = payment.total - total = CouponService.new.apply(total, coupon, user.id) if coupon - - wallet_amount = (user.wallet.amount * 100).to_i - - wallet_amount >= total ? total : wallet_amount - end - - ## - # Subtract the amount of the current reservation from the customer's wallet - ## - def debit_user_wallet(payment, user, reservation) - wallet_amount = wallet_amount_debit(payment, user) - return unless wallet_amount.present? && wallet_amount != 0 - - amount = wallet_amount / 100.0 - wallet_transaction = WalletService.new(user: user, wallet: user.wallet).debit(amount, reservation) - # wallet debit success - raise DebitWalletError unless wallet_transaction - - payment.set_wallet_transaction(wallet_amount, wallet_transaction.id) - end end diff --git a/app/services/subscriptions/subscribe.rb b/app/services/subscriptions/subscribe.rb index bcfc9fc82..56b7ad921 100644 --- a/app/services/subscriptions/subscribe.rb +++ b/app/services/subscriptions/subscribe.rb @@ -11,22 +11,31 @@ class Subscriptions::Subscribe ## # @param subscription {Subscription} - # @param coupon {String} coupon code - # @param invoice {Boolean} + # @param payment_details {Hash} as generated by Price.compute # @param payment_intent_id {String} from stripe # @param schedule {Boolean} # @param payment_method {String} only for schedules ## - def pay_and_save(subscription, coupon: nil, invoice: false, payment_intent_id: nil, schedule: false, payment_method: nil) + def pay_and_save(subscription, payment_details: nil, payment_intent_id: nil, schedule: false, payment_method: nil) return false if user_id.nil? subscription.statistic_profile_id = StatisticProfile.find_by(user_id: user_id).id - subscription.save_with_payment(operator_profile_id, - invoice: invoice, - coupon_code: coupon, - payment_intent_id: payment_intent_id, - schedule: schedule, - payment_method: payment_method) + subscription.init_save + user = User.find(user_id) + + payment = if schedule + generate_schedule(subscription: subscription, + total: payment_details[:before_coupon], + operator_profile_id: operator_profile_id, + user: user, + payment_method: payment_method, + coupon_code: payment_details[:coupon]) + else + generate_invoice(subscription, operator_profile_id, payment_details, payment_intent_id) + end + payment.save + WalletService.debit_user_wallet(payment, user, subscription) + true end def extend_subscription(subscription, new_expiration_date, free_days) @@ -38,10 +47,53 @@ class Subscriptions::Subscribe expiration_date: new_expiration_date ) if new_sub.save - new_sub.user.generate_subscription_invoice(operator_profile_id) + schedule = subscription.payment_schedule + details = Price.compute(true, new_sub.user, nil, [], plan_id: subscription.plan_id) + payment = if schedule + generate_schedule(subscription: new_sub, + total: details[:before_coupon], + operator_profile_id: operator_profile_id, + user: new_sub.user, + payment_method: schedule.payment_method) + else + generate_invoice(subscription, operator_profile_id, details) + end + payment.save UsersCredits::Manager.new(user: new_sub.user).reset_credits return new_sub end false end + + private + + ## + # Generate the invoice for the given subscription + ## + def generate_schedule(subscription: nil, total: nil, operator_profile_id: nil, user: nil, payment_method: nil, coupon_code: nil) + operator = InvoicingProfile.find(operator_profile_id)&.user + coupon = Coupon.find_by(code: coupon_code) unless coupon_code.nil? + + PaymentScheduleService.new.create( + subscription, + total, + coupon: coupon, + operator: operator, + payment_method: payment_method, + user: user + ) + end + + ## + # Generate the invoice for the given subscription + ## + def generate_invoice(subscription, operator_profile_id, payment_details, payment_intent_id = nil) + InvoicesService.create( + payment_details, + operator_profile_id, + subscription: subscription, + payment_intent_id: payment_intent_id + ) + end + end diff --git a/app/services/wallet_service.rb b/app/services/wallet_service.rb index cdde0cef7..87ad0348f 100644 --- a/app/services/wallet_service.rb +++ b/app/services/wallet_service.rb @@ -72,4 +72,34 @@ class WalletService ii.invoice = avoir ii.save! end + + ## + # Compute the amount decreased from the user's wallet, if applicable + # @param payment {Invoice|PaymentSchedule} + # @param user {User} the customer + # @param coupon {Coupon|String} Coupon object or code + ## + def self.wallet_amount_debit(payment, user, coupon = nil) + total = payment.total + total = CouponService.new.apply(total, coupon, user.id) if coupon + + wallet_amount = (user.wallet.amount * 100).to_i + + wallet_amount >= total ? total : wallet_amount + end + + ## + # Subtract the amount of the transactable item (Subscription|Reservation) from the customer's wallet + ## + def self.debit_user_wallet(payment, user, transactable) + wallet_amount = WalletService.wallet_amount_debit(payment, user) + return unless wallet_amount.present? && wallet_amount != 0 + + amount = wallet_amount / 100.0 + wallet_transaction = WalletService.new(user: user, wallet: user.wallet).debit(amount, transactable) + # wallet debit success + raise DebitWalletError unless wallet_transaction + + payment.set_wallet_transaction(wallet_amount, wallet_transaction.id) + end end diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index 2b0e33058..788c2f3d4 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -815,6 +815,7 @@ en: credits_will_remain_unchanged: "The balance of free credits (training / machines / spaces) of the user will remain unchanged." you_intentionally_decide_to_extend_the_user_s_subscription_by_charging_him_again_for_his_current_subscription: "You intentionally decide to extend the user's subscription by charging him again for his current subscription." credits_will_be_reset: "The balance of free credits (training / machines / spaces) of the user will be reset, unused credits will be lost." + payment_scheduled: "If the previous subscription was charged through a payment schedule, this one will be charged the same way." until_expiration_date: "Until (expiration date):" you_successfully_changed_the_expiration_date_of_the_user_s_subscription: "You successfully changed the expiration date of the user's subscription" a_problem_occurred_while_saving_the_date: "A problem occurred while saving the date." diff --git a/config/locales/app.admin.fr.yml b/config/locales/app.admin.fr.yml index cdb80eac5..69d3ab126 100644 --- a/config/locales/app.admin.fr.yml +++ b/config/locales/app.admin.fr.yml @@ -815,6 +815,7 @@ fr: credits_will_remain_unchanged: "Le solde de crédits gratuits (formations/machines/espaces) de l'utilisateur restera inchangé." you_intentionally_decide_to_extend_the_user_s_subscription_by_charging_him_again_for_his_current_subscription: "Vous décidez délibérément d'étendre l'abonnement de l'utilisateur en lui faisant repayer le prix de l'abonnement qu'il possède actuellement." credits_will_be_reset: "Le solde de crédits gratuits (formations/machines/espaces) de l'utilisateur sera remis à zéro, ses crédits non utilisés seront perdu." + payment_scheduled: "Si l'abonnement précédent a été facturé via un échéancier de paiement mensualisé, celui-ci sera facturé de la même façon." until_expiration_date: "Jusqu'à (date d'expiration) :" you_successfully_changed_the_expiration_date_of_the_user_s_subscription: "Vous avez bien modifié la date d'expiration de l'abonnement de l'utilisateur" a_problem_occurred_while_saving_the_date: "Il y a eu un problème lors de l'enregistrement de la date."