From 6abee0cea07bafd549fa0b1a2c9e4248a6f68942 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 18 Nov 2020 16:01:05 +0100 Subject: [PATCH] define payment method before validating schedule --- .../src/javascript/components/wallet-info.tsx | 124 ++++++++++++++++++ .../src/javascript/directives/cart.js | 72 ++++++++-- .../src/javascript/models/reservation.ts | 15 +++ app/frontend/src/javascript/models/wallet.ts | 6 + app/frontend/src/stylesheets/application.scss | 1 + .../src/stylesheets/modules/wallet-info.scss | 25 ++++ .../templates/shared/_wallet_amount_info.html | 10 -- .../shared/valid_reservation_modal.html | 63 +++++---- config/locales/app.shared.en.yml | 22 +++- config/locales/app.shared.fr.yml | 18 ++- 10 files changed, 297 insertions(+), 59 deletions(-) create mode 100644 app/frontend/src/javascript/components/wallet-info.tsx create mode 100644 app/frontend/src/javascript/models/reservation.ts create mode 100644 app/frontend/src/javascript/models/wallet.ts create mode 100644 app/frontend/src/stylesheets/modules/wallet-info.scss delete mode 100644 app/frontend/templates/shared/_wallet_amount_info.html diff --git a/app/frontend/src/javascript/components/wallet-info.tsx b/app/frontend/src/javascript/components/wallet-info.tsx new file mode 100644 index 000000000..1a24ab7f4 --- /dev/null +++ b/app/frontend/src/javascript/components/wallet-info.tsx @@ -0,0 +1,124 @@ +/** + * This component displays a summary of the amount paid with the virtual wallet, for the current transaction + */ + +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { react2angular } from 'react2angular'; +import moment from 'moment'; +import { IApplication } from '../models/application'; +import '../lib/i18n'; +import { IFilterService } from 'angular'; +import { Loader } from './loader'; +import { Reservation } from '../models/reservation'; +import { User } from '../models/user'; +import { Wallet } from '../models/wallet'; + +declare var Application: IApplication; + +interface WalletInfoProps { + reservation: Reservation, + $filter: IFilterService, + currentUser: User, + wallet: Wallet, + price: number, + remainingPrice: number, +} + +const WalletInfo: React.FC = ({ reservation, currentUser, wallet, price, remainingPrice, $filter }) => { + const { t } = useTranslation('shared'); + + /** + * Return the formatted localized amount for the given price (eg. 20.5 => "20,50 €") + */ + const formatPrice = (price: number): string => { + return $filter('currency')(price); + } + /** + * Check if the currently connected used is also the person making the reservation. + * If the currently connected user (ie. the operator), is an admin or a manager, he may book the reservation for someone else. + */ + const isOperatorAndClient = (): boolean => { + return currentUser.id == reservation.user_id; + } + /** + * If the client has some money in his wallet & the price is not zero, then we should display this component. + */ + const shouldBeShown = (): boolean => { + return wallet.amount > 0 && price > 0; + } + /** + * If the amount in the wallet is not enough to cover the whole price, then the user must pay the remaining price + * using another payment mean. + */ + const hasRemainingPrice = (): boolean => { + return remainingPrice > 0; + } + /** + * Does the current cart contains a payment schedule? + */ + const isPaymentSchedule = (): boolean => { + return reservation.plan_id && reservation.payment_schedule; + } + /** + * Return the human-readable name of the item currently bought with the wallet + */ + const getPriceItem = (): string => { + let item = 'other'; + if (reservation.slots_attributes.length > 0) { + item = 'reservation'; + } else if (reservation.plan_id) { + if (reservation.payment_schedule) { + item = 'first_deadline'; + } + else item = 'subscription'; + } + + return t(`app.shared.wallet.wallet_info.item_${item}`); + } + + return ( +
+ {shouldBeShown() &&
+ {isOperatorAndClient() &&
+

{t('app.shared.wallet.wallet_info.you_have_AMOUNT_in_wallet', {AMOUNT : formatPrice(wallet.amount)})}

+ {!hasRemainingPrice() &&

+ {t('app.shared.wallet.wallet_info.wallet_pay_ITEM', {ITEM: getPriceItem()})} +

} + {hasRemainingPrice() &&

+ {t('app.shared.wallet.wallet_info.credit_AMOUNT_for_pay_ITEM', { + AMOUNT: formatPrice(remainingPrice), + ITEM: getPriceItem() + })} +

} +
} + {!isOperatorAndClient() &&
+

{t('app.shared.wallet.wallet_info.client_have_AMOUNT_in_wallet', {AMOUNT : formatPrice(wallet.amount)})}

+ {!hasRemainingPrice() &&

+ {t('app.shared.wallet.wallet_info.client_wallet_pay_ITEM', {ITEM: getPriceItem()})} +

} + {hasRemainingPrice() &&

+ {t('app.shared.wallet.wallet_info.client_credit_AMOUNT_for_pay_ITEM', { + AMOUNT: formatPrice(remainingPrice), + ITEM: getPriceItem() + })} +

} +
} + {!hasRemainingPrice() && isPaymentSchedule() &&

+ + {t('app.shared.wallet.wallet_info.other_deadlines_no_wallet')} +

} +
} +
+ ); +} + +const WalletInfoWrapper: React.FC = ({ currentUser, reservation, $filter, price, remainingPrice, wallet }) => { + return ( + + + + ); +} + +Application.Components.component('walletInfo', react2angular(WalletInfoWrapper, ['currentUser', 'price', 'remainingPrice', 'reservation', 'wallet'], ['$filter'])); diff --git a/app/frontend/src/javascript/directives/cart.js b/app/frontend/src/javascript/directives/cart.js index dc19f0765..a5b5065a3 100644 --- a/app/frontend/src/javascript/directives/cart.js +++ b/app/frontend/src/javascript/directives/cart.js @@ -738,7 +738,7 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs', const payOnSite = function (reservation) { $uibModal.open({ templateUrl: '/shared/valid_reservation_modal.html', - size: 'sm', + size: $scope.schedule.payment_schedule ? 'lg' : 'sm', resolve: { reservation () { return reservation; @@ -762,7 +762,7 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs', controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'Reservation', 'Subscription', 'wallet', 'helpers', '$filter', 'coupon', 'selectedPlan', 'schedule', function ($scope, $uibModalInstance, $state, reservation, price, Auth, Reservation, Subscription, wallet, helpers, $filter, coupon, selectedPlan, schedule) { // user wallet amount - $scope.walletAmount = wallet.amount; + $scope.wallet = wallet; // Global price (total of all items) $scope.price = price.price; @@ -783,18 +783,12 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs', $scope.schedule = schedule.payment_schedule; // how should we collect payments for the payment schedule - $scope.payment_method = 'stripe'; + $scope.method = { + payment_method: 'stripe' + }; - // Button label - if ($scope.amount > 0) { - $scope.validButtonName = _t('app.shared.cart.confirm_payment_of_html', { ROLE: $rootScope.currentUser.role, AMOUNT: $filter('currency')($scope.amount) }); - } else { - if ((price.price > 0) && ($scope.walletAmount === 0)) { - $scope.validButtonName = _t('app.shared.cart.confirm_payment_of_html', { ROLE: $rootScope.currentUser.role, AMOUNT: $filter('currency')(price.price) }); - } else { - $scope.validButtonName = _t('app.shared.buttons.confirm'); - } - } + // "valid" Button label + $scope.validButtonName = ''; /** * Callback to process the local payment, triggered on button click @@ -825,7 +819,47 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs', $scope.attempting = false; }); }; + /** + * Callback to close the modal without processing the payment + */ $scope.cancel = function () { $uibModalInstance.dismiss('cancel'); }; + + /* PRIVATE SCOPE */ + + /** + * Kind of constructor: these actions will be realized first when the directive is loaded + */ + const initialize = function () { + $scope.$watch('method.payment_method', function (newValue, oldValue) { + console.log(`watch triggered: ${newValue}`); + $scope.validButtonName = computeValidButtonName(); + }); + }; + + /** + * Compute the Label of the confirmation button + */ + const computeValidButtonName = function () { + let method = ''; + if (AuthService.isAuthorized(['admin', 'manager']) && $rootScope.currentUser.id !== reservation.user_id) { + method = $scope.method.payment_method; + } else { + method = 'stripe'; + } + console.log(method); + if ($scope.amount > 0) { + return _t('app.shared.cart.confirm_payment_of_html', { METHOD: method, AMOUNT: $filter('currency')($scope.amount) }); + } else { + if ((price.price > 0) && ($scope.wallet.amount === 0)) { + return _t('app.shared.cart.confirm_payment_of_html', { METHOD: method, AMOUNT: $filter('currency')(price.price) }); + } else { + return _t('app.shared.buttons.confirm'); + } + } + }; + + // # !!! MUST BE CALLED AT THE END of the controller + initialize(); } ] }).result.finally(null).then(function (reservation) { afterPayment(reservation); }); @@ -875,13 +909,23 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs', } else { if (AuthService.isAuthorized(['admin']) || (AuthService.isAuthorized('manager') && $scope.user.id !== $rootScope.currentUser.id) || - amountToPay === 0) { + (amountToPay === 0 && !hasOtherDeadlines())) { return payOnSite(reservation); } } }); }; + /** + * Check if the later deadlines of the payment schedule exists and are not equal to zero + * @return {boolean} + */ + const hasOtherDeadlines = function () { + if (!$scope.schedule.payment_schedule) return false; + if ($scope.schedule.payment_schedule.items.length < 2) return false; + return $scope.schedule.payment_schedule.items[1].amount !== 0; + }; + // !!! MUST BE CALLED AT THE END of the directive return initialize(); } diff --git a/app/frontend/src/javascript/models/reservation.ts b/app/frontend/src/javascript/models/reservation.ts new file mode 100644 index 000000000..2583dee3c --- /dev/null +++ b/app/frontend/src/javascript/models/reservation.ts @@ -0,0 +1,15 @@ +export interface ReservationSlot { + start_at: Date, + end_at: Date, + availability_id: number, + offered: boolean +} + +export interface Reservation { + user_id: number, + reservable_id: number, + reservable_type: string, + slots_attributes: Array, + plan_id: number + payment_schedule: boolean +} diff --git a/app/frontend/src/javascript/models/wallet.ts b/app/frontend/src/javascript/models/wallet.ts new file mode 100644 index 000000000..30ae30ecb --- /dev/null +++ b/app/frontend/src/javascript/models/wallet.ts @@ -0,0 +1,6 @@ +export interface Wallet { + id: number, + invoicing_profile_id: number, + amount: number, + user_id: number +} diff --git a/app/frontend/src/stylesheets/application.scss b/app/frontend/src/stylesheets/application.scss index c6a1d9f83..922f87a61 100644 --- a/app/frontend/src/stylesheets/application.scss +++ b/app/frontend/src/stylesheets/application.scss @@ -23,5 +23,6 @@ @import "modules/tour"; @import "modules/fab-modal"; @import "modules/payment-schedule-summary"; +@import "modules/wallet-info"; @import "app.responsive"; diff --git a/app/frontend/src/stylesheets/modules/wallet-info.scss b/app/frontend/src/stylesheets/modules/wallet-info.scss new file mode 100644 index 000000000..00b32f1a0 --- /dev/null +++ b/app/frontend/src/stylesheets/modules/wallet-info.scss @@ -0,0 +1,25 @@ +.wallet-info { + margin-left: 15px; + margin-right: 15px; + h3 { + margin-top: 5px; + } + p { + font-style: italic; + } + .info-deadlines { + border: 1px solid #faebcc; + padding: 15px; + color: #8a6d3b; + background-color: #fcf8e3; + border-radius: 4px; + font-style: normal; + display: flex; + + i { + vertical-align: middle; + line-height: 2.5em; + margin-right: 0.5em; + } + } +} diff --git a/app/frontend/templates/shared/_wallet_amount_info.html b/app/frontend/templates/shared/_wallet_amount_info.html deleted file mode 100644 index 40a06cf87..000000000 --- a/app/frontend/templates/shared/_wallet_amount_info.html +++ /dev/null @@ -1,10 +0,0 @@ -
-

-

{{'app.shared.wallet.wallet_pay_reservation' | translate}}

-

-
-
-

-

{{'app.shared.wallet.client_wallet_pay_reservation' | translate}}

-

-
diff --git a/app/frontend/templates/shared/valid_reservation_modal.html b/app/frontend/templates/shared/valid_reservation_modal.html index 8843a62f5..a826cf912 100644 --- a/app/frontend/templates/shared/valid_reservation_modal.html +++ b/app/frontend/templates/shared/valid_reservation_modal.html @@ -5,32 +5,45 @@