diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cfa2f97a..a991d3f5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,9 @@ - Fix a bug: unable to cancel the upgrade before it begins - Fix a bug: unable to use run.fab.mn - Fix a bug: typo in allow/prevent booking overlapping slots +- Fix a bug: in the admin calendar, the trainings' info panel shows "duration: null minutes" - `SUPERADMIN_EMAIL` renamed to `ADMINSYS_EMAIL` +- `scripts/run-tests.sh` renamed to `scripts/tests.sh` - [BREAKING CHANGE] GET `open_api/v1/invoices` won't return `stp_invoice_id` OR `stp_payment_intent_id` anymore. The new field `payment_gateway_object` will contain some similar data if the invoice was paid online by card. - [TODO DEPLOY] `rails fablab:stripe:set_gateway` - [TODO DEPLOY] `rails fablab:maintenance:rebuild_stylesheet` diff --git a/app/controllers/api/local_payment_controller.rb b/app/controllers/api/local_payment_controller.rb new file mode 100644 index 000000000..dbb4b1bec --- /dev/null +++ b/app/controllers/api/local_payment_controller.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +# API Controller for handling local payments (at the reception) or when the amount = 0 +class API::LocalPaymentController < API::PaymentsController + def confirm_payment + cart = shopping_cart + price = debit_amount(cart) + + authorize LocalPaymentContext.new(cart, price[:amount]) + + if cart.reservation + res = on_reservation_success(nil, nil, price[:details], cart) + elsif cart.subscription + res = on_subscription_success(nil, nil, price[:details], cart) + end + + render res + end + + protected + + def shopping_cart + cs = CartService.new(current_user) + cs.from_hash(params) + end +end diff --git a/app/controllers/api/payments_controller.rb b/app/controllers/api/payments_controller.rb index 6fb1d1296..cad46b5fe 100644 --- a/app/controllers/api/payments_controller.rb +++ b/app/controllers/api/payments_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Abstract API Controller to be extended by each gateway, for handling the payments processes in the front-end +# Abstract API Controller to be extended by each payment gateway/mean, for handling the payments processes in the front-end class API::PaymentsController < API::ApiController before_action :authenticate_user! @@ -21,9 +21,7 @@ class API::PaymentsController < API::ApiController wallet_amount >= total_amount ? total_amount : wallet_amount end - def card_amount - cs = CartService.new(current_user) - cart = cs.from_hash(params[:cart_items]) + def debit_amount(cart) price_details = cart.total # Subtract wallet amount from total @@ -32,30 +30,33 @@ class API::PaymentsController < API::ApiController { amount: total - wallet_debit, details: price_details } end - def check_coupon - return if coupon_params[:coupon_code].nil? - - coupon = Coupon.find_by(code: coupon_params[:coupon_code]) - raise InvalidCouponError if coupon.nil? || coupon.status(current_user.id) != 'active' + def shopping_cart + cs = CartService.new(current_user) + cs.from_hash(params[:cart_items]) end - def check_plan - plan_id = (cart_items_params[:subscription][:plan_id] if cart_items_params[:subscription]) + # @param cart {ShoppingCart} + def check_coupon(cart) + return if cart.coupon.nil? - return unless plan_id + cart.coupon.coupon + end - plan = Plan.find(plan_id) + # @param cart {ShoppingCart} + def check_plan(cart) + return unless cart.subscription + + plan = cart.subscription.plan raise InvalidGroupError if plan.group_id != current_user.group_id end - def on_reservation_success(gateway_item_id, gateway_item_type, details) - @reservation = Reservation.new(reservation_params) - if params[:cart_items][:subscription] && params[:cart_items][:subscription][:plan_id] - @reservation.plan_id = params[:cart_items][:subscription][:plan_id] - end - payment_method = params[:cart_items][:reservation][:payment_method] || 'card' + def on_reservation_success(gateway_item_id, gateway_item_type, details, cart) + @reservation = cart.reservation.to_reservation + @reservation.plan_id = cart.subscription.plan.id if cart.subscription + + payment_method = cart.payment_method || 'card' user_id = if current_user.admin? || current_user.manager? - params[:cart_items][:reservation][:user_id] + cart.customer.id else current_user.id end @@ -64,7 +65,7 @@ class API::PaymentsController < API::ApiController payment_details: details, payment_id: gateway_item_id, payment_type: gateway_item_type, - schedule: params[:cart_items][:payment_schedule], + schedule: cart.payment_schedule.requested, payment_method: payment_method) post_reservation_save(gateway_item_id, gateway_item_type) @@ -77,10 +78,10 @@ class API::PaymentsController < API::ApiController end end - def on_subscription_success(gateway_item_id, gateway_item_type, details) - @subscription = Subscription.new(subscription_params) + def on_subscription_success(gateway_item_id, gateway_item_type, details, cart) + @subscription = cart.subscription.to_subscription user_id = if current_user.admin? || current_user.manager? - params[:cart_items][:customer_id] + cart.customer.id else current_user.id end @@ -89,8 +90,8 @@ class API::PaymentsController < API::ApiController payment_details: details, payment_id: gateway_item_id, payment_type: gateway_item_type, - schedule: params[:cart_items][:subscription][:payment_schedule], - payment_method: 'card') + schedule: cart.payment_schedule.requested, + payment_method: cart.payment_method || 'card') post_subscription_save(gateway_item_id, gateway_item_type) @@ -100,27 +101,4 @@ class API::PaymentsController < API::ApiController { json: @subscription.errors, status: :unprocessable_entity } end end - - def reservation_params - params[:cart_items].require(:reservation).permit(:reservable_id, :reservable_type, :nb_reserve_places, - tickets_attributes: %i[event_price_category_id booked], - slots_attributes: %i[id start_at end_at availability_id offered]) - end - - def subscription_params - params[:cart_items].require(:subscription).permit(:plan_id) - end - - def cart_items_params - params.require(:cart_items).permit(subscription: :plan_id, - reservation: [ - :reservable_id, :reservable_type, :nb_reserve_places, - tickets_attributes: %i[event_price_category_id booked], - slots_attributes: %i[id start_at end_at availability_id offered] - ]) - end - - def coupon_params - params.require(:cart_items).permit(:coupon_code) - end end diff --git a/app/controllers/api/payzen_controller.rb b/app/controllers/api/payzen_controller.rb index 555a3693f..1191a764a 100644 --- a/app/controllers/api/payzen_controller.rb +++ b/app/controllers/api/payzen_controller.rb @@ -18,8 +18,9 @@ class API::PayzenController < API::PaymentsController end def create_payment - amount = card_amount - @id = PayZen::Helper.generate_ref(cart_items_params, params[:customer_id]) + cart = shopping_cart + amount = debit_amount(cart) + @id = PayZen::Helper.generate_ref(params[:cart_items], params[:customer_id]) client = PayZen::Charge.new @result = client.create_payment(amount: amount[:amount], @@ -29,7 +30,7 @@ class API::PayzenController < API::PaymentsController end def create_token - @id = PayZen::Helper.generate_ref(cart_items_params, params[:customer_id]) + @id = PayZen::Helper.generate_ref(params[:cart_items], params[:customer_id]) client = PayZen::Charge.new @result = client.create_token(order_id: @id, customer: PayZen::Helper.generate_customer(params[:customer_id], current_user.id, params[:cart_items])) @@ -46,13 +47,14 @@ class API::PayzenController < API::PaymentsController client = PayZen::Order.new order = client.get(params[:order_id], operation_type: 'DEBIT') - amount = card_amount + cart = shopping_cart + amount = debit_amount(cart) if order['answer']['transactions'].first['status'] == 'PAID' - if params[:cart_items][:reservation] - res = on_reservation_success(params[:order_id], amount[:details]) - elsif params[:cart_items][:subscription] - res = on_subscription_success(params[:order_id], amount[:details]) + if cart.reservation + res = on_reservation_success(params[:order_id], amount[:details], cart) + elsif cart.subscription + res = on_subscription_success(params[:order_id], amount[:details], cart) end end @@ -63,12 +65,12 @@ class API::PayzenController < API::PaymentsController private - def on_reservation_success(order_id, details) - super(order_id, 'PayZen::Order', details) + def on_reservation_success(order_id, details, cart) + super(order_id, 'PayZen::Order', details, cart) end - def on_subscription_success(order_id, details) - super(order_id, 'PayZen::Order', details) + def on_subscription_success(order_id, details, cart) + super(order_id, 'PayZen::Order', details, cart) end def error_handling diff --git a/app/controllers/api/prices_controller.rb b/app/controllers/api/prices_controller.rb index 83a74612a..eb0ace25d 100644 --- a/app/controllers/api/prices_controller.rb +++ b/app/controllers/api/prices_controller.rb @@ -47,18 +47,4 @@ class API::PricesController < API::ApiController def price_params params.require(:price).permit(:amount) end - - def compute_reservation_price_params - params.require(:reservation).permit(:reservable_id, :reservable_type, :plan_id, :user_id, :nb_reserve_places, :payment_schedule, - tickets_attributes: %i[event_price_category_id booked], - slots_attributes: %i[id start_at end_at availability_id offered]) - end - - def compute_subscription_price_params - params.require(:subscription).permit(:plan_id, :user_id, :payment_schedule) - end - - def coupon_params - params.permit(:coupon_code) - end end diff --git a/app/controllers/api/reservations_controller.rb b/app/controllers/api/reservations_controller.rb index db8df7565..92c159b8b 100644 --- a/app/controllers/api/reservations_controller.rb +++ b/app/controllers/api/reservations_controller.rb @@ -24,35 +24,6 @@ class API::ReservationsController < API::ApiController def show; end - # Admins can create any reservations. Members can directly create reservations if total = 0, - # otherwise, they must use payments_controller#confirm_payment. - # Managers can create reservations for other users - def create - user_id = current_user.admin? || current_user.manager? ? params[:customer_id] : current_user.id - price = transaction_amount(user_id) - - authorize ReservationContext.new(Reservation, price[:amount], user_id) - - @reservation = Reservation.new(reservation_params) - @reservation.plan_id = params[:subscription][:plan_id] if params[:subscription] && params[:subscription][:plan_id] - - is_reserve = Reservations::Reserve.new(user_id, current_user.invoicing_profile.id) - .pay_and_save(@reservation, - payment_details: price[:price_details], - schedule: params[:payment_schedule], - payment_method: params[:payment_method]) - - if is_reserve - SubscriptionExtensionAfterReservation.new(@reservation).extend_subscription_if_eligible - - render :show, status: :created, location: @reservation - else - render json: @reservation.errors, status: :unprocessable_entity - end - rescue InvalidCouponError - render json: { coupon_code: 'wrong coupon code or expired' }, status: :unprocessable_entity - end - def update authorize @reservation if @reservation.update(reservation_params) @@ -64,30 +35,6 @@ class API::ReservationsController < API::ApiController private - def transaction_amount(user_id) - user = User.find(user_id) - cs = CartService.new(current_user) - cart = cs.from_hash(customer_id: user_id, - subscription: { - plan_id: params[:subscription] ? params[:subscription][:plan_id] : nil - }, - reservation: reservation_params, - coupon_code: coupon_params[:coupon_code], - payment_schedule: params[:payment_schedule]) - price_details = cart.total - - # Subtract wallet amount from total - total = price_details[:total] - wallet_debit = get_wallet_debit(user, total) - - { price_details: price_details, amount: (total - wallet_debit) } - end - - def get_wallet_debit(user, total_amount) - wallet_amount = (user.wallet.amount * 100).to_i - wallet_amount >= total_amount ? total_amount : wallet_amount - end - def set_reservation @reservation = Reservation.find(params[:id]) end @@ -97,8 +44,4 @@ class API::ReservationsController < API::ApiController tickets_attributes: %i[event_price_category_id booked], slots_attributes: %i[id start_at end_at availability_id offered]) end - - def coupon_params - params.permit(:coupon_code) - end end diff --git a/app/controllers/api/stripe_controller.rb b/app/controllers/api/stripe_controller.rb index fd0fed599..453d7cd70 100644 --- a/app/controllers/api/stripe_controller.rb +++ b/app/controllers/api/stripe_controller.rb @@ -16,11 +16,12 @@ class API::StripeController < API::PaymentsController intent = nil # stripe's payment intent res = nil # json of the API answer + cart = shopping_cart begin - amount = card_amount + amount = debit_amount(cart) if params[:payment_method_id].present? - check_coupon - check_plan + check_coupon(cart) + check_plan(cart) # Create the PaymentIntent intent = Stripe::PaymentIntent.create( @@ -46,10 +47,10 @@ class API::StripeController < API::PaymentsController end if intent&.status == 'succeeded' - if params[:cart_items][:reservation] - res = on_reservation_success(intent, amount[:details]) - elsif params[:cart_items][:subscription] - res = on_subscription_success(intent, amount[:details]) + if cart.reservation + res = on_reservation_success(intent, amount[:details], cart) + elsif cart.subscription + res = on_subscription_success(intent, amount[:details], cart) end end @@ -79,12 +80,13 @@ class API::StripeController < API::PaymentsController key = Setting.get('stripe_secret_key') intent = Stripe::SetupIntent.retrieve(params[:setup_intent_id], api_key: key) - amount = card_amount + cart = shopping_cart + amount = debit_amount(cart) if intent&.status == 'succeeded' - if params[:cart_items][:reservation] - res = on_reservation_success(intent, amount[:details]) - elsif params[:cart_items][:subscription] - res = on_subscription_success(intent, amount[:details]) + if cart.reservation + res = on_reservation_success(intent, amount[:details], cart) + elsif cart.subscription + res = on_subscription_success(intent, amount[:details], cart) end end @@ -126,12 +128,12 @@ class API::StripeController < API::PaymentsController ) end - def on_reservation_success(intent, details) - super(intent.id, intent.class.name, details) + def on_reservation_success(intent, details, cart) + super(intent.id, intent.class.name, details, cart) end - def on_subscription_success(intent, details) - super(intent.id, intent.class.name, details) + def on_subscription_success(intent, details, cart) + super(intent.id, intent.class.name, details, cart) end def generate_payment_response(intent, res = nil) diff --git a/app/controllers/api/subscriptions_controller.rb b/app/controllers/api/subscriptions_controller.rb index 442995cb4..bca08d5e5 100644 --- a/app/controllers/api/subscriptions_controller.rb +++ b/app/controllers/api/subscriptions_controller.rb @@ -9,28 +9,6 @@ class API::SubscriptionsController < API::ApiController authorize @subscription end - # Admins can create any subscriptions. Members can directly create subscriptions if total = 0, - # otherwise, they must use payments_controller#confirm_payment. - # Managers can create subscriptions for other users - def create - user_id = current_user.admin? || current_user.manager? ? params[:customer_id] : current_user.id - transaction = transaction_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, payment_details: transaction[:details], - schedule: params[:subscription][:payment_schedule], - payment_method: params[:subscription][:payment_method]) - - if is_subscribe - render :show, status: :created, location: @subscription - else - render json: @subscription.errors, status: :unprocessable_entity - end - end - def update authorize @subscription @@ -50,42 +28,11 @@ class API::SubscriptionsController < API::ApiController private - def transaction_amount(user_id) - cs = CartService.new(current_user) - cart = cs.from_hash(customer_id: user_id, - subscription: { - plan_id: subscription_params[:plan_id] - }, - coupon_code: coupon_params[:coupon_code], - payment_schedule: params[:payment_schedule]) - price_details = cart.total - user = User.find(user_id) - - # Subtract wallet amount from total - total = price_details[:total] - wallet_debit = get_wallet_debit(user, total) - { amount: total - wallet_debit, details: price_details } - end - - def get_wallet_debit(user, total_amount) - wallet_amount = (user.wallet.amount * 100).to_i - wallet_amount >= total_amount ? total_amount : wallet_amount - end - # Use callbacks to share common setup or constraints between actions. def set_subscription @subscription = Subscription.find(params[:id]) end - # Never trust parameters from the scary internet, only allow the white list through. - def subscription_params - params.require(:subscription).permit(:plan_id) - end - - def coupon_params - params.permit(:coupon_code) - end - def subscription_update_params params.require(:subscription).permit(:expired_at) end diff --git a/app/frontend/src/javascript/controllers/events.js.erb b/app/frontend/src/javascript/controllers/events.js.erb index 489563da7..48696c6e0 100644 --- a/app/frontend/src/javascript/controllers/events.js.erb +++ b/app/frontend/src/javascript/controllers/events.js.erb @@ -657,12 +657,12 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' * @param reservation {Object} as returned by mkReservation() * @param coupon {Object} Coupon as returned from the API * @param paymentMethod {string} 'card' | '' - * @return {{reservation:Object, coupon_code:string}} + * @return {CartItems} */ const mkCartItems = function (reservation, coupon, paymentMethod = '') { return { customer_id: $scope.ctrl.member.id, - reservation, + items: [reservation], coupon_code: ((coupon ? coupon.code : undefined)), payment_method: paymentMethod, }; @@ -733,8 +733,8 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' return mkCartItems(reservation, $scope.coupon.applied); }, }, - controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'Reservation', 'wallet', 'helpers', '$filter', 'coupon', 'cartItems', - function ($scope, $uibModalInstance, $state, reservation, price, Auth, Reservation, wallet, helpers, $filter, coupon, cartItems) { + controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'LocalPayment', 'wallet', 'helpers', '$filter', 'coupon', 'cartItems', + function ($scope, $uibModalInstance, $state, reservation, price, Auth, LocalPayment, wallet, helpers, $filter, coupon, cartItems) { // User's wallet amount $scope.wallet = wallet; @@ -767,7 +767,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' // Callback to validate the payment $scope.ok = function () { $scope.attempting = true; - return Reservation.save(mkCartItems($scope.reservation, coupon), function (reservation) { + return LocalPayment.confirm(cartItems, function (reservation) { $uibModalInstance.close(reservation); return $scope.attempting = true; } diff --git a/app/frontend/src/javascript/directives/cart.js b/app/frontend/src/javascript/directives/cart.js index 0d8ca2794..27179e5de 100644 --- a/app/frontend/src/javascript/directives/cart.js +++ b/app/frontend/src/javascript/directives/cart.js @@ -687,16 +687,13 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs', * @return {CartItems} */ const mkCartItems = function (items, paymentMethod = '') { - const cartItems = { + return { customer_id: $scope.user.id, + items, payment_schedule: $scope.schedule.requested_schedule, payment_method: paymentMethod, coupon_code: (($scope.coupon.applied ? $scope.coupon.applied.code : undefined)) }; - for (const item of items) { - Object.assign(cartItems, item); - } - return cartItems; }; /** @@ -745,8 +742,8 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs', return $scope.settings; } }, - controller: ['$scope', '$uibModalInstance', '$state', 'price', 'Auth', 'Reservation', 'Subscription', 'wallet', 'helpers', '$filter', 'coupon', 'selectedPlan', 'schedule', 'cartItems', 'user', 'settings', - function ($scope, $uibModalInstance, $state, price, Auth, Reservation, Subscription, wallet, helpers, $filter, coupon, selectedPlan, schedule, cartItems, user, settings) { + controller: ['$scope', '$uibModalInstance', '$state', 'price', 'Auth', 'LocalPayment', 'wallet', 'helpers', '$filter', 'coupon', 'selectedPlan', 'schedule', 'cartItems', 'user', 'settings', + function ($scope, $uibModalInstance, $state, price, Auth, LocalPayment, wallet, helpers, $filter, coupon, selectedPlan, schedule, cartItems, user, settings) { // user wallet amount $scope.wallet = wallet; @@ -783,6 +780,30 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs', // the customer $scope.user = user; + /** + * Check if the shopping cart contains a reservation + * @return {Reservation|boolean} + */ + $scope.reservation = (function () { + const item = cartItems.items.find(i => i.reservation); + if (item && item.reservation.slots_attributes.length > 0) { + return item.reservation; + } + return false; + })(); + + /** + * Check if the shopping cart contains a subscription + * @return {Subscription|boolean} + */ + $scope.subscription = (function () { + const item = cartItems.items.find(i => i.subscription); + if (item && item.subscription.plan_id) { + return item.subscription; + } + return false; + })(); + /** * Callback to process the local payment, triggered on button click */ @@ -796,22 +817,7 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs', } } $scope.attempting = true; - // save subscription (if there's only a subscription selected) - if ((!$scope.cartItems.reservation || $scope.cartItems.reservation.slots_attributes.length === 0) && selectedPlan) { - const sub = mkSubscription(selectedPlan.id); - - return Subscription.save(mkCartItems([sub], $scope.method.payment_method), - function (subscription) { - $uibModalInstance.close(subscription); - $scope.attempting = true; - }, function (response) { - $scope.alerts = []; - $scope.alerts.push({ msg: _t('app.shared.cart.a_problem_occurred_during_the_payment_process_please_try_again_later'), type: 'danger' }); - $scope.attempting = false; - }); - } - // otherwise, save the reservation (may include a subscription) - Reservation.save(cartItems, function (reservation) { + LocalPayment.confirm(cartItems, function (reservation) { $uibModalInstance.close(reservation); $scope.attempting = true; }, function (response) { diff --git a/app/frontend/src/javascript/models/payment.ts b/app/frontend/src/javascript/models/payment.ts index 0958cb9b7..16777e46c 100644 --- a/app/frontend/src/javascript/models/payment.ts +++ b/app/frontend/src/javascript/models/payment.ts @@ -21,8 +21,7 @@ export enum PaymentMethod { export interface CartItems { customer_id: number, - reservation?: Reservation, - subscription?: SubscriptionRequest, + items: Array, coupon_code?: string, payment_schedule?: boolean, payment_method: PaymentMethod diff --git a/app/frontend/src/javascript/services/local_payment.js b/app/frontend/src/javascript/services/local_payment.js new file mode 100644 index 000000000..1c85e689c --- /dev/null +++ b/app/frontend/src/javascript/services/local_payment.js @@ -0,0 +1,13 @@ +'use strict'; + +Application.Services.factory('LocalPayment', ['$resource', function ($resource) { + return $resource('/api/local_payment', + {}, { + confirm: { + method: 'POST', + url: '/api/local_payment/confirm_payment', + isArray: false + } + } + ); +}]); diff --git a/app/frontend/templates/admin/calendar/calendar.html b/app/frontend/templates/admin/calendar/calendar.html index fee9dc7bf..b15596f49 100644 --- a/app/frontend/templates/admin/calendar/calendar.html +++ b/app/frontend/templates/admin/calendar/calendar.html @@ -58,12 +58,12 @@ -
+

{{ 'app.admin.calendar.info' }}

-
{{ 'app.admin.calendar.slot_duration' }}
+
{{ 'app.admin.calendar.slot_duration' }}
{{ 'app.admin.calendar.tags' }}
    diff --git a/app/frontend/templates/shared/valid_reservation_modal.html b/app/frontend/templates/shared/valid_reservation_modal.html index 4bfb9b26a..76a6e5fb5 100644 --- a/app/frontend/templates/shared/valid_reservation_modal.html +++ b/app/frontend/templates/shared/valid_reservation_modal.html @@ -1,19 +1,19 @@