1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-30 19:52:20 +01:00

define payment method before validating schedule

This commit is contained in:
Sylvain 2020-11-18 16:01:05 +01:00
parent a56112a47f
commit 6abee0cea0
10 changed files with 297 additions and 59 deletions

View File

@ -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<WalletInfoProps> = ({ 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 (
<div className="wallet-info">
{shouldBeShown() && <div>
{isOperatorAndClient() && <div>
<h3>{t('app.shared.wallet.wallet_info.you_have_AMOUNT_in_wallet', {AMOUNT : formatPrice(wallet.amount)})}</h3>
{!hasRemainingPrice() &&<p>
{t('app.shared.wallet.wallet_info.wallet_pay_ITEM', {ITEM: getPriceItem()})}
</p>}
{hasRemainingPrice() &&<p>
{t('app.shared.wallet.wallet_info.credit_AMOUNT_for_pay_ITEM', {
AMOUNT: formatPrice(remainingPrice),
ITEM: getPriceItem()
})}
</p>}
</div>}
{!isOperatorAndClient() && <div>
<h3>{t('app.shared.wallet.wallet_info.client_have_AMOUNT_in_wallet', {AMOUNT : formatPrice(wallet.amount)})}</h3>
{!hasRemainingPrice() &&<p>
{t('app.shared.wallet.wallet_info.client_wallet_pay_ITEM', {ITEM: getPriceItem()})}
</p>}
{hasRemainingPrice() &&<p>
{t('app.shared.wallet.wallet_info.client_credit_AMOUNT_for_pay_ITEM', {
AMOUNT: formatPrice(remainingPrice),
ITEM: getPriceItem()
})}
</p>}
</div>}
{!hasRemainingPrice() && isPaymentSchedule() &&<p className="info-deadlines">
<i className="fa fa-warning" />
<span>{t('app.shared.wallet.wallet_info.other_deadlines_no_wallet')}</span>
</p>}
</div>}
</div>
);
}
const WalletInfoWrapper: React.FC<WalletInfoProps> = ({ currentUser, reservation, $filter, price, remainingPrice, wallet }) => {
return (
<Loader>
<WalletInfo currentUser={currentUser} reservation={reservation} $filter={$filter} price={price} remainingPrice={remainingPrice} wallet={wallet} />
</Loader>
);
}
Application.Components.component('walletInfo', react2angular(WalletInfoWrapper, ['currentUser', 'price', 'remainingPrice', 'reservation', 'wallet'], ['$filter']));

View File

@ -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();
}

View File

@ -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<ReservationSlot>,
plan_id: number
payment_schedule: boolean
}

View File

@ -0,0 +1,6 @@
export interface Wallet {
id: number,
invoicing_profile_id: number,
amount: number,
user_id: number
}

View File

@ -23,5 +23,6 @@
@import "modules/tour";
@import "modules/fab-modal";
@import "modules/payment-schedule-summary";
@import "modules/wallet-info";
@import "app.responsive";

View File

@ -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;
}
}
}

View File

@ -1,10 +0,0 @@
<div ng-if="currentUser.role != 'admin'">
<h3 class="m-t-xs" ng-if="walletAmount > 0 && price > 0" ng-bind-html="'app.shared.wallet.you_have_amount_in_wallet' | translate:{ amount: numberFilter(walletAmount, 2), currency: currencySymbol }"></h3>
<p ng-if="walletAmount > 0 && price > 0 && amount === 0" class="text-italic">{{'app.shared.wallet.wallet_pay_reservation' | translate}}</p>
<p ng-if="walletAmount > 0 && amount !== 0" class="text-italic" ng-bind-html="'app.shared.stripe.credit_amount_for_pay_reservation' | translate:{ amount: numberFilter(amount, 2), currency: currencySymbol }"></p>
</div>
<div ng-if="currentUser.role == 'admin'">
<h3 class="m-t-xs" ng-if="walletAmount > 0 && price > 0" ng-bind-html="'app.shared.wallet.client_have_amount_in_wallet' | translate:{ amount: numberFilter(walletAmount, 2), currency: currencySymbol }"></h3>
<p ng-if="walletAmount > 0 && price > 0 && amount === 0" class="text-italic">{{'app.shared.wallet.client_wallet_pay_reservation' | translate}}</p>
<p ng-if="walletAmount > 0 && amount !== 0" class="text-italic" ng-bind-html="'app.shared.stripe.client_credit_amount_for_pay_reservation' | translate:{ amount: numberFilter(amount, 2), currency: currencySymbol }"></p>
</div>

View File

@ -5,8 +5,8 @@
</div>
<div class="modal-body">
<uib-alert ng-repeat="alert in alerts" type="{{alert.type}}" close="closeAlert($index)">{{alert.msg}}</uib-alert>
<ng-include src="'/shared/_wallet_amount_info.html'"></ng-include>
<div class="row">
<div ng-class="{'col-md-6': schedule, 'm-h-sm': !schedule}">
<div ng-if="reservation.slots_attributes.length > 0">
<p translate>{{ 'app.shared.valid_reservation_modal.here_is_the_summary_of_the_slots_to_book_for_the_current_user' }}</p>
<ul ng-repeat="slot in reservation.slots_attributes">
@ -18,19 +18,32 @@
<p>{{ plan | humanReadablePlanName }}</p>
</div>
<div ng-if="schedule">
<payment-schedule-summary schedule="schedule"></payment-schedule-summary>
<label for="method" translate>{{ 'app.shared.valid_reservation_modal.payment_method' }}</label>
<select id="method"
class="form-control m-b"
ng-model="payment_method">
ng-model="method.payment_method">
<option value="stripe" translate>{{ 'app.shared.valid_reservation_modal.method_stripe' }}</option>
<option value="check" translate>{{ 'app.shared.valid_reservation_modal.method_check' }}</option>
<!-- TODO, pay 1st deadline with wallet -->
<!-- TODO, notify about unable to pay with the wallet -->
<!-- TODO, compute 1st deadline with wallet -->
</select>
<p ng-show="payment_method == 'stripe'" translate>{{ 'app.shared.valid_reservation_modal.stripe_collection_info' }}</p>
<p ng-show="payment_method == 'check'" translate translate-values="{DEADLINES: schedule.items.length}">{{ 'app.shared.valid_reservation_modal.check_collection_info' }}</p>
<p ng-show="method.payment_method == 'stripe'" translate>{{ 'app.shared.valid_reservation_modal.stripe_collection_info' }}</p>
<p ng-show="method.payment_method == 'check'" translate translate-values="{DEADLINES: schedule.items.length}">{{ 'app.shared.valid_reservation_modal.check_collection_info' }}</p>
</div>
</div>
<div class="col-md-6" ng-if="schedule">
<ul class="full-schedule">
<li ng-repeat="item in schedule.items">
<span class="schedule-item-date">{{item.due_date | amDateFormat: 'L'}}</span>
<span> </span>
<span class="schedule-item-price">{{item.amount | currency}}</span>
</li>
</ul>
</div>
<wallet-info current-user="currentUser"
reservation="reservation"
price="price"
remaining-price="amount"
wallet="wallet"/>
</div>
</div>
<div class="modal-footer">

View File

@ -129,7 +129,11 @@ en:
here_is_the_summary_of_the_slots_to_book_for_the_current_user: "Here is the summary of the slots to book for the current user:"
subscription_confirmation: "Subscription confirmation"
here_is_the_subscription_summary: "Here is the subscription summary:"
payment_schedule: "This subscription is payed with a payment schedule of {DEADLINES} months. By validating, you confirm to collect the first monthly payment."
payment_method: "Payment method"
method_stripe: "Online by card"
method_check: "By check"
stripe_collection_info: "By validating, you'll be prompted for the member's card number. This card will be automatically charged at the deadlines."
check_collection_info: "By validating, you confirm that you have {DEADLINES} checks, allowing you to collect all the monthly payments."
#event edition form
event:
title: "Title"
@ -321,15 +325,23 @@ en:
amount_minimum_1: "The amount minimum is 1"
amount_confirm_is_required: "The amount confirmation is required."
amount_confirm_does_not_match: "The amount confirmation does not match."
you_have_amount_in_wallet: "You have {amount} {currency} in your wallet"
client_have_amount_in_wallet: "Client has {amount} {currency} in wallet"
wallet_pay_reservation: "You can pay your reservation directly"
client_wallet_pay_reservation: "The member can pay his reservation directly"
debit_subscription: "Pay for a subscription"
debit_reservation_training: "Pay for a training reservation"
debit_reservation_machine: "Pay for a machine reservation"
debit_reservation_event: "Pay for an event reservation"
warning_uneditable_credit: "Warning: once validated, the credited amount won't be editable anymore."
wallet_info:
you_have_AMOUNT_in_wallet: "You have {AMOUNT} on your wallet"
wallet_pay_ITEM: "You pay your {ITEM} directly."
item_reservation: "reservation"
item_subscription: "subscription"
item_first_deadline: "first deadline"
item_other: "purchase"
credit_AMOUNT_for_pay_ITEM: "You still have {AMOUNT} to pay to validate your {ITEM}."
client_have_AMOUNT_in_wallet: "The member has {AMOUNT} on his wallet"
client_wallet_pay_ITEM: "The member can directly pay his {ITEM}."
client_credit_AMOUNT_for_pay_ITEM: "{AMOUNT} are remaining to pay to validate the {ITEM}"
other_deadlines_no_wallet: "Warning: the remaining wallet balance cannot be used for the next deadlines."
#coupon (promotional) (creation/edition form)
coupon:
name: "Name"

View File

@ -325,15 +325,23 @@ fr:
amount_minimum_1: "Le montant minimum est de 1"
amount_confirm_is_required: "La confirmation du montant est requise."
amount_confirm_does_not_match: "La confirmation du montant ne correspond pas."
you_have_amount_in_wallet: "Vous avez {amount} {currency} sur votre porte-monnaie"
client_have_amount_in_wallet: "Le client a {amount} {currency} sur son porte-monnaie"
wallet_pay_reservation: "Vous pouvez payer votre réservation directement"
client_wallet_pay_reservation: "Le membre peut directement payer sa réservation"
debit_subscription: "Payer un abonnement"
debit_reservation_training: "Payer une réservation de formation"
debit_reservation_machine: "Payer une réservation de machine"
debit_reservation_event: "Payer une réservation d'événement"
warning_uneditable_credit: "Attention : une fois validé, le montant crédité ne sera plus modifiable."
wallet_info:
you_have_AMOUNT_in_wallet: "Vous avez {AMOUNT} sur votre porte-monnaie"
wallet_pay_ITEM: "Vous pouvez payer votre {ITEM} directement."
item_reservation: "réservation"
item_subscription: "abonnement"
item_first_deadline: "première échéance"
item_other: "achat"
credit_AMOUNT_for_pay_ITEM: "Il vous reste {AMOUNT} à payer pour valider votre {ITEM}."
client_have_AMOUNT_in_wallet: "Le membre a {AMOUNT} sur son porte-monnaie"
client_wallet_pay_ITEM: "Le membre peut directement payer {ITEM, select, abonnement{son} achat{son} other{sa}} {ITEM}."
client_credit_AMOUNT_for_pay_ITEM: "Il reste {AMOUNT} à payer pour valider {ITEM, select, abonnement{l'} achat{l'} other{la }}{ITEM}"
other_deadlines_no_wallet: "Attention : le solde du porte-monnaie ne pourra pas être utilisé pour les échéances suivantes."
#coupon (promotional) (creation/edition form)
coupon:
name: "Nom"
@ -433,7 +441,7 @@ fr:
do_you_really_want_to_cancel_this_reservation_html: "<p>Êtes-vous sur de vouloir annuler cette réservation ?</p><p>Attention : si cette réservation a été effectuée gratuitement, dans le cadre d'un abonnement, les crédits utilisés ne seront pas re-crédités.</p>"
reservation_was_cancelled_successfully: "La réservation a bien été annulée."
cancellation_failed: "L'annulation a échouée."
confirm_payment_of_html: "{ROLE, select, admin{Paiement sur place} other{Payer}} : {AMOUNT}" #eg. confirm my payment of $20.00
confirm_payment_of_html: "{METHOD, select, stripe{Payer par carte} other{Paiement sur place}} : {AMOUNT}" #eg. confirm my payment of $20.00
a_problem_occurred_during_the_payment_process_please_try_again_later: "Il y a eu un problème lors de la procédure de paiement. Veuillez réessayer plus tard."
none: "Aucune"
online_payment_disabled: "Le paiement par carte bancaire n'est pas disponible. Merci de contacter directement l'accueil du FabLab."