diff --git a/app/controllers/api/prices_controller.rb b/app/controllers/api/prices_controller.rb index d3fb8ef2c..9cc7db30c 100644 --- a/app/controllers/api/prices_controller.rb +++ b/app/controllers/api/prices_controller.rb @@ -14,7 +14,7 @@ class API::PricesController < API::ApiController @prices = @prices.where(priceable_id: params[:priceable_id]) if params[:priceable_id] end if params[:plan_id] - plan_id = if params[:plan_id] =~ /no|nil|null|undefined/i + plan_id = if /no|nil|null|undefined/i.match?(params[:plan_id]) nil else params[:plan_id] @@ -37,7 +37,11 @@ class API::PricesController < API::ApiController end def compute - price_parameters = compute_price_params + price_parameters = if params[:reservation] + compute_reservation_price_params + elsif params[:subscription] + compute_subscription_price_params + end # user user = User.find(price_parameters[:user_id]) # reservable @@ -74,12 +78,16 @@ class API::PricesController < API::ApiController params.require(:price).permit(:amount) end - def compute_price_params + 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 diff --git a/app/frontend/src/javascript/api/payment.ts b/app/frontend/src/javascript/api/payment.ts index 98bfee334..08e0b4739 100644 --- a/app/frontend/src/javascript/api/payment.ts +++ b/app/frontend/src/javascript/api/payment.ts @@ -4,7 +4,7 @@ import { CartItems, PaymentConfirmation } from '../models/payment'; export default class PaymentAPI { static async confirm (stp_payment_method_id: string, cart_items: CartItems): Promise { - const res: AxiosResponse = await apiClient.post(`/api/payment/confirm`, { + const res: AxiosResponse = await apiClient.post(`/api/payments/confirm_payment`, { payment_method_id: stp_payment_method_id, cart_items }); diff --git a/app/frontend/src/javascript/api/price.ts b/app/frontend/src/javascript/api/price.ts index 1cff11435..c0e1b0f91 100644 --- a/app/frontend/src/javascript/api/price.ts +++ b/app/frontend/src/javascript/api/price.ts @@ -1,18 +1,12 @@ import apiClient from './api-client'; import { AxiosResponse } from 'axios'; -import wrapPromise, { IWrapPromise } from '../lib/wrap-promise'; import { CartItems } from '../models/payment'; import { ComputePriceResult } from '../models/price'; export default class PriceAPI { - async compute (cartItems: CartItems): Promise { + static async compute (cartItems: CartItems): Promise { const res: AxiosResponse = await apiClient.post(`/api/prices/compute`, cartItems); - return res?.data?.custom_asset; - } - - static compute (cartItems: CartItems): IWrapPromise { - const api = new PriceAPI(); - return wrapPromise(api.compute(cartItems)); + return res?.data; } } diff --git a/app/frontend/src/javascript/api/wallet.ts b/app/frontend/src/javascript/api/wallet.ts index afb7b1499..590085611 100644 --- a/app/frontend/src/javascript/api/wallet.ts +++ b/app/frontend/src/javascript/api/wallet.ts @@ -4,14 +4,9 @@ import wrapPromise, { IWrapPromise } from '../lib/wrap-promise'; import { Wallet } from '../models/wallet'; export default class WalletAPI { - async getByUser (user_id: number): Promise { + static async getByUser (user_id: number): Promise { const res: AxiosResponse = await apiClient.get(`/api/wallet/by_user/${user_id}`); return res?.data; } - - static getByUser (user_id: number): IWrapPromise { - const api = new WalletAPI(); - return wrapPromise(api.getByUser(user_id)); - } } diff --git a/app/frontend/src/javascript/components/stripe-modal.tsx b/app/frontend/src/javascript/components/stripe-modal.tsx index 62c9df12f..fb1e9a1ef 100644 --- a/app/frontend/src/javascript/components/stripe-modal.tsx +++ b/app/frontend/src/javascript/components/stripe-modal.tsx @@ -3,7 +3,7 @@ * Supports Strong-Customer Authentication (SCA). */ -import React, { ChangeEvent, ReactNode, useEffect, useState } from 'react'; +import React, { ReactNode, useEffect, useState } from 'react'; import { react2angular } from 'react2angular'; import { Loader } from './loader'; import { IApplication } from '../models/application'; @@ -43,27 +43,43 @@ interface StripeModalProps { const cgvFile = CustomAssetAPI.get(CustomAssetName.CgvFile); const StripeModal: React.FC = ({ isOpen, toggleModal, afterSuccess, cartItems, currentUser, schedule , processPayment = true }) => { + // customer's wallet + const [wallet, setWallet] = useState(null); + // server-computed price with all details + const [price, setPrice] = useState(null); + // remaining price = total price - wallet amount const [remainingPrice, setRemainingPrice] = useState(0); - const userWallet = WalletAPI.getByUser(cartItems.reservation?.user_id || cartItems.subscription?.user_id); - const priceInfo = PriceAPI.compute(cartItems); - - const { t } = useTranslation('shared'); - - const cgv = cgvFile.read(); - const wallet = userWallet.read(); - const price = priceInfo.read(); - + // is the component ready to display? + const [ready, setReady] = useState(false); + // errors to display in the UI (stripe errors mainly) const [errors, setErrors] = useState(null); + // are we currently processing the payment (ie. the form was submit, but the process is still running)? const [submitState, setSubmitState] = useState(false); + // did the user accepts the terms of services (CGV)? const [tos, setTos] = useState(false); + const { t } = useTranslation('shared'); + const cgv = cgvFile.read(); + + /** - * Refresh the remaining price on each display + * On each display: + * - Refresh the wallet + * - Refresh the price + * - Refresh the remaining price */ useEffect(() => { - const wLib = new WalletLib(wallet); - setRemainingPrice(wLib.computeRemainingPrice(price.price)); - }) + if (!cartItems) return; + WalletAPI.getByUser(cartItems.reservation?.user_id || cartItems.subscription?.user_id).then((wallet) => { + setWallet(wallet); + PriceAPI.compute(cartItems).then((res) => { + setPrice(res); + const wLib = new WalletLib(wallet); + setRemainingPrice(wLib.computeRemainingPrice(res.price)); + setReady(true); + }) + }) + }, [cartItems]); /** * Check if there is currently an error to display @@ -82,7 +98,7 @@ const StripeModal: React.FC = ({ isOpen, toggleModal, afterSuc /** * Triggered when the user accepts or declines the Terms of Sales */ - const toggleTos = (event: ChangeEvent): void => { + const toggleTos = (): void => { setTos(!tos); } @@ -114,6 +130,9 @@ const StripeModal: React.FC = ({ isOpen, toggleModal, afterSuc ); } + /** + * Set the component as 'currently submitting' + */ const handleSubmit = (): void => { setSubmitState(true); } @@ -153,8 +172,8 @@ const StripeModal: React.FC = ({ isOpen, toggleModal, afterSuc closeButton={false} customFooter={logoFooter()} className="stripe-modal"> - - + {ready && + = ({ isOpen, toggleModal, afterSuc className="validate-btn"> {t('app.shared.stripe.confirm_payment_of_', { AMOUNT: formatPrice(remainingPrice) })} - + } ); } diff --git a/app/frontend/src/javascript/directives/cart.js b/app/frontend/src/javascript/directives/cart.js index 411ac175b..7c593afe4 100644 --- a/app/frontend/src/javascript/directives/cart.js +++ b/app/frontend/src/javascript/directives/cart.js @@ -79,6 +79,9 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs', cartItems: null }; + // currently logged-in user + $scope.currentUser = $rootScope.currentUser; + /** * Add the provided slot to the shopping cart (state transition from free to 'about to be reserved') * and increment the total amount of the cart if needed. @@ -312,9 +315,12 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs', /** * This will open/close the stripe payment modal */ - $scope.toggleStripeModal = () => { + $scope.toggleStripeModal = (beforeApply) => { setTimeout(() => { $scope.stripe.showModal = !$scope.stripe.showModal; + if (typeof beforeApply === 'function') { + beforeApply(); + } $scope.$apply(); }, 50); }; @@ -693,8 +699,15 @@ 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.stripe.cartItems = mkRequestParams({ reservation }, $scope.coupon.applied); - $scope.toggleStripeModal(); + $scope.toggleStripeModal(() => { + let request = { reservation }; + if (reservation.slots_attributes.length === 0 && reservation.plan_id) { + request = mkSubscription($scope.selectedPlan.id, reservation.user_id, $scope.schedule.requested_schedule, 'stripe'); + } else { + request.reservation.payment_method = 'stripe'; + } + $scope.stripe.cartItems = mkRequestParams(request, $scope.coupon.applied); + }); }; /** * Open a modal window that allows the user to process a local payment for his current shopping cart (admin only). diff --git a/app/frontend/templates/shared/_cart.html b/app/frontend/templates/shared/_cart.html index f752fdd8b..f2e42372a 100644 --- a/app/frontend/templates/shared/_cart.html +++ b/app/frontend/templates/shared/_cart.html @@ -203,7 +203,7 @@