1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2024-11-29 10:24:20 +01:00

use cart directive on the plan subscription page

+ fix payment schedule compute
+ fix price for monthly-payments plans in plan-card
+ TODO: valid_reservation_modal.html
+ TODO: Stripe processing
This commit is contained in:
Sylvain 2020-11-04 16:22:31 +01:00
parent 7d37174b51
commit b0afa02f1d
12 changed files with 141 additions and 351 deletions

View File

@ -1,4 +1,4 @@
#web: bundle exec rails server puma -p $PORT
web: bundle exec rails server puma -p $PORT
worker: bundle exec sidekiq -C ./config/sidekiq.yml
wp-client: bin/webpack-dev-server
wp-server: SERVER_BUNDLE_ONLY=yes bin/webpack --watch

View File

@ -36,7 +36,7 @@ const PlanCard: React.FC<PlanCardProps> = ({ plan, user, operator, onSelectPlan,
* Return the formatted localized amount, divided by the number of months (eg. 120 => "10,00 € / month")
*/
const monthlyAmount = (): string => {
const monthly = Math.ceil(plan.amount / moment.duration(plan.interval_count, plan.interval).asMonths());
const monthly = plan.amount / moment.duration(plan.interval_count, plan.interval).asMonths();
return $filter('currency')(monthly);
}
/**

View File

@ -42,17 +42,16 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop
// plan to subscribe (shopping cart)
$scope.selectedPlan = null;
// the moment when the plan selection changed for the last time, used to trigger changes in the cart
$scope.planSelectionTime = null;
// the application global settings
$scope.settings = settingsPromise;
// Discount coupon to apply to the basket, if any
$scope.coupon =
{ applied: null };
// Storage for the total price (plan price + coupon, if any) and of the payment schedule
$scope.cart = {
total: null,
payment_schedule: false,
schedule: null
};
// text that appears in the bottom-right box of the page (subscriptions rules details)
$scope.subscriptionExplicationsAlert = subscriptionExplicationsPromise.setting.value;
@ -79,8 +78,7 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop
if ($scope.isAuthenticated()) {
if ($scope.selectedPlan !== plan) {
$scope.selectedPlan = plan;
$scope.cart.payment_schedule = plan.monthly_payment;
updateCartPrice();
$scope.planSelectionTime = new Date();
} else {
$scope.selectedPlan = null;
}
@ -91,18 +89,6 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop
}, 50);
};
/**
* This will update the payment_schedule setting when the user toggles the switch button
* @param checked {Boolean}
*/
$scope.togglePaymentSchedule = (checked) => {
setTimeout(() => {
$scope.cart.payment_schedule = checked;
updateCartPrice();
$scope.$apply();
}, 50);
};
/**
* Check if the provided plan is currently selected
* @param plan {Object} Resource plan
@ -111,29 +97,6 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop
return $scope.selectedPlan === plan;
};
/**
* Callback to trigger the payment process of the subscription
*/
$scope.openSubscribePlanModal = function () {
Wallet.getWalletByUser({ user_id: $scope.ctrl.member.id }, function (wallet) {
const amountToPay = helpers.getAmountToPay($scope.cart.total, wallet.amount);
if ((AuthService.isAuthorized('member') && amountToPay > 0) ||
(AuthService.isAuthorized('manager') && $scope.ctrl.member.id === $rootScope.currentUser.id && amountToPay > 0)) {
if (settingsPromise.online_payment_module !== 'true') {
growl.error(_t('app.public.plans.online_payment_disabled'));
} else {
return payByStripe();
}
} else {
if (AuthService.isAuthorized('admin') ||
(AuthService.isAuthorized('manager') && $scope.ctrl.member.id !== $rootScope.currentUser.id) ||
amountToPay === 0) {
return payOnSite();
}
}
});
};
/**
* Return the group object, identified by the ID set in $scope.group.id
*/
@ -198,6 +161,18 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop
*/
$scope.filterDisabledPlans = function (plan) { return !plan.disabled; };
/**
* Once the subscription is confirmed (payment process successfully completed), make the plan as subscribed,
* and update the user's subscription
*/
$scope.afterPayment = function () {
$scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan);
Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan);
$scope.paid.plan = angular.copy($scope.selectedPlan);
$scope.selectedPlan = null;
$scope.coupon.applied = null;
};
/* PRIVATE SCOPE */
/**
@ -225,210 +200,6 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop
}
$scope.$on('devise:new-session', function (event, user) { if (user.role !== 'admin') { $scope.ctrl.member = user; } });
// watch when a coupon is applied to re-compute the total price
$scope.$watch('coupon.applied', function (newValue, oldValue) {
if ((newValue !== null) || (oldValue !== null)) {
return updateCartPrice();
}
});
};
/**
* Compute the total amount for the current reservation according to the previously set parameters
* and assign the result in $scope.reserve.amountTotal
*/
const updateCartPrice = function () {
// first we check the selection of a user
if (Object.keys($scope.ctrl.member).length > 0 && $scope.selectedPlan) {
const r = mkReservation($scope.ctrl.member, $scope.selectedPlan);
Price.compute(mkRequestParams(r, $scope.coupon.applied), function (res) {
$scope.cart.total = res.price;
$scope.cart.schedule = res.schedule;
});
} else {
return $scope.reserve.amountTotal = null;
}
};
/**
* Format the parameters expected by /api/prices/compute or /api/reservations and return the resulting object
* @param reservation {Object} as returned by mkReservation()
* @param coupon {Object} Coupon as returned from the API
* @return {{reservation:Object, coupon_code:string}}
*/
const mkRequestParams = function (reservation, coupon) {
return {
reservation,
coupon_code: ((coupon ? coupon.code : undefined))
};
};
/**
* Create an hash map implementing the Reservation specs
* @param member {Object} User as retrieved from the API: current user / selected user if current is admin
* @param [plan] {Object} Plan as retrieved from the API: plan to buy with the current reservation
* @return {{user_id:Number, slots_attributes:Array<Object>, plan_id:Number|null}}
*/
const mkReservation = function (member, plan) {
return {
user_id: member.id,
slots_attributes: [],
plan_id: ((plan ? plan.id : undefined)),
payment_schedule: $scope.cart.payment_schedule
};
};
/**
* Open a modal window which trigger the stripe payment process
*/
const payByStripe = function () {
$uibModal.open({
templateUrl: '/stripe/payment_modal.html',
size: 'md',
resolve: {
selectedPlan () { return $scope.selectedPlan; },
member () { return $scope.ctrl.member; },
price () { return $scope.cart.total; },
wallet () {
return Wallet.getWalletByUser({ user_id: $scope.ctrl.member.id }).$promise;
},
coupon () { return $scope.coupon.applied; },
stripeKey: ['Setting', function (Setting) { return Setting.get({ name: 'stripe_public_key' }).$promise; }]
},
controller: ['$scope', '$uibModalInstance', '$state', 'selectedPlan', 'member', 'price', 'Subscription', 'CustomAsset', 'wallet', 'helpers', '$filter', 'coupon', 'stripeKey',
function ($scope, $uibModalInstance, $state, selectedPlan, member, price, Subscription, CustomAsset, wallet, helpers, $filter, coupon, stripeKey) {
// User's wallet amount
$scope.walletAmount = wallet.amount;
// Final price to pay by the user
$scope.amount = helpers.getAmountToPay(price, wallet.amount);
// The plan that the user is about to subscribe
$scope.selectedPlan = selectedPlan;
// Used in wallet info template to interpolate some translations
$scope.numberFilter = $filter('number');
// Cart items
$scope.cartItems = {
coupon_code: ((coupon ? coupon.code : undefined)),
subscription: {
plan_id: selectedPlan.id
}
};
// stripe publishable key
$scope.stripeKey = stripeKey.setting.value;
// retrieve the CGV
CustomAsset.get({ name: 'cgv-file' }, function (cgv) { $scope.cgv = cgv.custom_asset; });
/**
* Callback for a click on the 'proceed' button.
* Handle the stripe's card tokenization process response and save the subscription to the API with the
* card token just created.
*/
$scope.onPaymentSuccess = function (response) {
$uibModalInstance.close(response);
};
}
]
}).result.finally(null).then(function (subscription) {
$scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan);
Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan);
$scope.paid.plan = angular.copy($scope.selectedPlan);
$scope.selectedPlan = null;
$scope.coupon.applied = null;
});
};
/**
* Open a modal window which trigger the local payment process
*/
const payOnSite = function () {
$uibModal.open({
templateUrl: '/plans/payment_modal.html',
size: 'sm',
resolve: {
selectedPlan () { return $scope.selectedPlan; },
member () { return $scope.ctrl.member; },
price () { return $scope.cart.total; },
wallet () {
return Wallet.getWalletByUser({ user_id: $scope.ctrl.member.id }).$promise;
},
coupon () { return $scope.coupon.applied; }
},
controller: ['$scope', '$uibModalInstance', '$state', 'selectedPlan', 'member', 'price', 'Subscription', 'wallet', 'helpers', '$filter', 'coupon',
function ($scope, $uibModalInstance, $state, selectedPlan, member, price, Subscription, wallet, helpers, $filter, coupon) {
// user wallet amount
$scope.walletAmount = wallet.amount;
// subscription price, coupon subtracted if any
$scope.price = price;
// price to pay
$scope.amount = helpers.getAmountToPay($scope.price, wallet.amount);
// Used in wallet info template to interpolate some translations
$scope.numberFilter = $filter('number');
// The plan that the user is about to subscribe
$scope.plan = selectedPlan;
// The member who is subscribing a plan
$scope.member = member;
// Button label
if ($scope.amount > 0) {
$scope.validButtonName = _t('app.public.plans.confirm_payment_of_html', { ROLE: $scope.currentUser.role, AMOUNT: $filter('currency')($scope.amount) });
} else {
if ((price.price > 0) && ($scope.walletAmount === 0)) {
$scope.validButtonName = _t('app.public.plans.confirm_payment_of_html', { ROLE: $scope.currentUser.role, AMOUNT: $filter('currency')(price.price) });
} else {
$scope.validButtonName = _t('app.shared.buttons.confirm');
}
}
/**
* Callback for the 'proceed' button.
* Save the subscription to the API
*/
$scope.ok = function () {
$scope.attempting = true;
Subscription.save({
coupon_code: ((coupon ? coupon.code : undefined)),
subscription: {
plan_id: selectedPlan.id,
user_id: member.id
}
}
, function (data) { // success
$uibModalInstance.close(data);
}
, function (data, status) { // failed
$scope.alerts = [];
$scope.alerts.push({ msg: _t('app.public.plans.an_error_occured_during_the_payment_process_please_try_again_later'), type: 'danger' });
$scope.attempting = false;
}
);
};
/**
* Callback for the 'cancel' button.
* Close the modal box.
*/
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
}
]
}).result.finally(null).then(function (subscription) {
$scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan);
Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan);
$scope.ctrl.member = null;
$scope.paid.plan = angular.copy($scope.selectedPlan);
$scope.selectedPlan = null;
return $scope.coupon.applied = null;
});
};
// !!! MUST BE CALLED AT THE END of the controller

View File

@ -67,6 +67,12 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
// Global config: delay in hours before a booking while the cancellation is forbidden
$scope.cancelBookingDelay = parseInt($scope.settings.booking_cancel_delay);
// Payment schedule
$scope.schedule = {
requested_schedule: false, // does the user requests a payment schedule for his subscription
payment_schedule: null // the effective computed payment schedule
};
/**
* 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.
@ -107,9 +113,13 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
*/
$scope.isSlotsValid = function () {
let isValid = true;
angular.forEach($scope.events.reserved, function (m) {
if (!m.isValid) { return isValid = false; }
});
if ($scope.events) {
angular.forEach($scope.events.reserved, function (m) {
if (!m.isValid) {
return isValid = false;
}
});
}
return isValid;
};
@ -143,48 +153,58 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
const slotValidations = [];
let slotNotValid;
let slotNotValidError;
$scope.events.reserved.forEach(function (slot) {
if (slot.plan_ids.length > 0) {
if (
($scope.selectedPlan && _.includes(slot.plan_ids, $scope.selectedPlan.id)) ||
($scope.user.subscribed_plan && _.includes(slot.plan_ids, $scope.user.subscribed_plan.id))
) {
slotValidations.push(true);
} else {
slotNotValid = slot;
if ($scope.selectedPlan && !_.includes(slot.plan_ids, $scope.selectedPlan.id)) {
slotNotValidError = 'selectedPlanError';
if ($scope.events.reserved) {
$scope.events.reserved.forEach(function (slot) {
if (slot.plan_ids.length > 0) {
if (
($scope.selectedPlan && _.includes(slot.plan_ids, $scope.selectedPlan.id)) ||
($scope.user.subscribed_plan && _.includes(slot.plan_ids, $scope.user.subscribed_plan.id))
) {
slotValidations.push(true);
} else {
slotNotValid = slot;
if ($scope.selectedPlan && !_.includes(slot.plan_ids, $scope.selectedPlan.id)) {
slotNotValidError = 'selectedPlanError';
}
if ($scope.user.subscribed_plan && !_.includes(slot.plan_ids, $scope.user.subscribed_plan.id)) {
slotNotValidError = 'userPlanError';
}
if (!$scope.selectedPlan || !$scope.user.subscribed_plan) {
slotNotValidError = 'noPlanError';
}
slotValidations.push(false);
}
if ($scope.user.subscribed_plan && !_.includes(slot.plan_ids, $scope.user.subscribed_plan.id)) {
slotNotValidError = 'userPlanError';
}
if (!$scope.selectedPlan || !$scope.user.subscribed_plan) {
slotNotValidError = 'noPlanError';
}
slotValidations.push(false);
}
}
});
const hasPlanForSlot = slotValidations.every(function (a) { return a; });
if (!hasPlanForSlot) {
if (!AuthService.isAuthorized(['admin', 'manager'])) {
return growl.error(_t('app.shared.cart.slot_restrict_subscriptions_must_select_plan'));
});
const hasPlanForSlot = slotValidations.every(function (a) {
return a;
});
if (!hasPlanForSlot) {
if (!AuthService.isAuthorized(['admin', 'manager'])) {
return growl.error(_t('app.shared.cart.slot_restrict_subscriptions_must_select_plan'));
} else {
const modalInstance = $uibModal.open({
animation: true,
templateUrl: '/shared/_reserve_slot_without_plan.html',
size: 'md',
controller: 'ReserveSlotWithoutPlanController',
resolve: {
slot: function () {
return slotNotValid;
},
slotNotValidError: function () {
return slotNotValidError;
}
}
});
modalInstance.result.then(function (res) {
return paySlots();
});
}
} else {
const modalInstance = $uibModal.open({
animation: true,
templateUrl: '/shared/_reserve_slot_without_plan.html',
size: 'md',
controller: 'ReserveSlotWithoutPlanController',
resolve: {
slot: function () { return slotNotValid; },
slotNotValidError: function () { return slotNotValidError; }
}
});
modalInstance.result.then(function (res) {
return paySlots();
});
return paySlots();
}
} else {
} else if ($scope.selectedPlan) {
return paySlots();
}
} else {
@ -271,6 +291,18 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
return false;
};
/**
* This will update the payment_schedule setting when the user toggles the switch button
* @param checked {Boolean}
*/
$scope.togglePaymentSchedule = (checked) => {
setTimeout(() => {
$scope.schedule.requested_schedule = checked;
updateCartPrice();
$scope.$apply();
}, 50);
};
/* PRIVATE SCOPE */
/**
@ -528,6 +560,7 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
if (Auth.isAuthenticated()) {
if ($scope.selectedPlan !== $scope.plan) {
$scope.selectedPlan = $scope.plan;
$scope.schedule.requested_schedule = $scope.plan.monthly_payment;
} else {
$scope.selectedPlan = null;
}
@ -548,6 +581,7 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
const r = mkReservation($scope.user, $scope.events.reserved, $scope.selectedPlan);
return Price.compute(mkRequestParams(r, $scope.coupon.applied), function (res) {
$scope.amountTotal = res.price;
$scope.schedule.payment_schedule = res.schedule;
$scope.totalNoCoupon = res.price_without_coupon;
setSlotsDetails(res.details);
});
@ -595,7 +629,8 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
reservable_id: $scope.reservableId,
reservable_type: $scope.reservableType,
slots_attributes: [],
plan_id: ((plan ? plan.id : undefined))
plan_id: ((plan ? plan.id : undefined)),
payment_schedule: $scope.schedule.requested_schedule
};
angular.forEach(slots, function (slot) {
reservation.slots_attributes.push({

View File

@ -104,40 +104,12 @@
</div>
</section>
<section class="widget panel b-a m m-t-lg" ng-if="selectedPlan && ctrl.member">
<div class="panel-heading b-b">
<h3 translate>{{ 'app.public.plans.summary' }}</h3>
</div>
<div class="widget-content no-bg auto wrapper">
<span translate>{{ 'app.public.plans.you_ve_just_selected_a_subscription_html' }}</span>
<div class="well well-warning m-t-sm">
<i class="font-sbold">{{ selectedPlan | humanReadablePlanName }}</i>
<div class="font-sbold">{{ 'app.public.plans.subscription_price' | translate }} {{selectedPlan.amount | currency}}</div>
</div>
<coupon show="!ctrl.member.subscribed_plan" coupon="coupon.applied" total="selectedPlan.amount" user-id="{{ctrl.member.id}}"></coupon>
<label for="payment_schedule" translate>{{ 'app.public.plans.monthly_payment' }}</label>
<switch checked="cart.payment_schedule" id="payment_schedule" on-change="togglePaymentSchedule" class-name="'v-middle'" ng-if="selectedPlan.monthly_payment"></switch>
</div>
<div class="widget-footer">
<div ng-if="cart.schedule">
<h4 translate>{{ 'app.public.plans.your_payment_schedule' }}</h4>
<ul>
<li ng-repeat="item in cart.schedule.items">
<span>{{item.due_date | amDateFormat:'L'}}</span>
<span>{{item.price | currency}}</span>
</li>
</ul>
</div>
<button class="btn btn-valid btn-info btn-block p-l text-u-c r-b" ng-click="openSubscribePlanModal()" ng-if="!ctrl.member.subscribed_plan">
{{ 'app.public.plans.confirm_and_pay' | translate }} {{cart.total | currency}}
</button>
</div>
</section>
<cart events="{}"
user="ctrl.member"
plan="selectedPlan"
plan-selection-time="planSelectionTime"
settings="settings"
after-payment="afterPayment"></cart>
<section class="widget panel b-a m m-t-lg" ng-if="paid.plan">

View File

@ -60,7 +60,7 @@
</div>
<div ng-if="selectedPlan">
<div class="m-t-md m-b-sm text-base">{{ 'app.shared.cart.you_ve_just_selected_a_' | translate }} <br> <span class="font-sbold" translate>{{ 'app.shared.cart._subscription' }}</span> :</div>
<div class="m-t-md m-b-sm text-base" translate>{{ 'app.shared.cart.you_ve_just_selected_a_subscription_html' }}</div>
<div class="panel panel-default bg-light m-n">
<div class="panel-body m-b-md">
<div class="font-sbold text-u-c">{{selectedPlan | humanReadablePlanName }}</div>
@ -72,7 +72,32 @@
</div>
<div class="panel-footer no-padder" ng-if="events.reserved.length > 0">
<div class="widget-content no-bg auto wrapper" ng-if="selectedPlan && !events.reserved">
<span translate>{{ 'app.shared.cart.you_ve_just_selected_a_subscription_html' }}</span>
<div class="well well-warning m-t-sm">
<i class="font-sbold">{{ selectedPlan | humanReadablePlanName }}</i>
<div class="font-sbold">{{ 'app.shared.cart.subscription_price' | translate }} {{selectedPlan.amount | currency}}</div>
</div>
<coupon show="!user.subscribed_plan" coupon="coupon.applied" total="totalNoCoupon" user-id="{{user.id}}"></coupon>
<div ng-if="selectedPlan.monthly_payment">
<label for="payment_schedule" translate>{{ 'app.shared.cart.monthly_payment' }}</label>
<switch checked="schedule.requested_schedule" id="'payment_schedule'" on-change="togglePaymentSchedule" class-name="'v-middle'"></switch>
</div>
</div>
<div ng-if="schedule.payment_schedule">
<h4 translate>{{ 'app.shared.cart.your_payment_schedule' }}</h4>
<ul>
<li ng-repeat="item in schedule.payment_schedule.items">
<span>{{item.due_date | amDateFormat:'L'}}</span>
<span>{{item.price | currency}}</span>
</li>
</ul>
</div>
<div class="widget-footer no-padder" ng-if="events.reserved.length > 0 || (selectedPlan && !events.reserved)">
<button class="btn btn-valid btn-info btn-block p-l btn-lg text-u-c r-b text-base" ng-click="payCart()" ng-if="isSlotsValid() && (!modePlans || selectedPlan)">{{ 'app.shared.cart.confirm_and_pay' | translate }} {{amountTotal | currency}}</button>
</div>

View File

@ -15,3 +15,4 @@
<button class="btn btn-info" ng-click="ok()" ng-disabled="attempting" ng-bind-html="validButtonName"></button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
</div>
<!-- TODO, add plan: see app/frontend/templates/plans/payment_modal.html then delete it -->

View File

@ -17,7 +17,7 @@ class PaymentScheduleService
end
ps = PaymentSchedule.new(scheduled: plan, total: price, coupon: coupon)
deadlines = plan.duration / 1.month
per_month = price / deadlines
per_month = (price / deadlines).truncate
adjustment = if per_month * deadlines != price
price - (per_month * deadlines)
else

View File

@ -246,13 +246,7 @@ en:
his_group: "{GENDER, select, male{His} female{Her} other{Its}} group"
he_wants_to_change_group: "{ROLE, select, member{I want} other{The user wants}} to change group"
change_my_group: "Change {ROLE, select, member{my} other{{GENDER, select, male{his} female{her} other{its}}}} group"
summary: "Summary"
your_subscription_has_expired_on_the_DATE: "Your subscription has expired on the {DATE}"
subscription_price: "Subscription price"
you_ve_just_selected_a_subscription_html: "You've just selected a <strong>subscription</strong>:"
monthly_payment: "Pay by monthly schedule"
confirm_and_pay: "Confirm and pay"
your_payment_schedule: "Your payment schedule"
you_ve_just_payed_the_subscription_html: "You've just paid the <strong>subscription</strong>:"
thank_you_your_subscription_is_successful: "Thank you. Your subscription is successful!"
your_invoice_will_be_available_soon_from_your_dashboard: "Your invoice will be available soon from your dashboard"
@ -260,11 +254,8 @@ en:
the_user_s_group_was_successfully_changed: "The user's group was successfully changed."
an_error_prevented_your_group_from_being_changed: "An error prevented your group from being changed."
an_error_prevented_to_change_the_user_s_group: "An error prevented to change the user's group."
an_error_occured_during_the_payment_process_please_try_again_later: "An error occurred during the payment process. Please try again later."
subscription_confirmation: "Subscription confirmation"
here_is_the_NAME_subscription_summary: "Here is the {NAME}'s subscription summary:"
confirm_payment_of_html: "{ROLE, select, admin{Cash} other{Pay}}: {AMOUNT}" #(contexte : validate a payment of $20,00)
online_payment_disabled: "Payment by credit card is not available. Please contact the FabLab's reception directly."
#Fablab's events list
events_list:
the_fablab_s_events: "The Fablab's events"

View File

@ -246,13 +246,7 @@ fr:
his_group: "Son groupe"
he_wants_to_change_group: "{ROLE, select, member{Je veux} other{L'utilisateur veut}} changer de groupe"
change_my_group: "Changer {ROLE, select, member{mon} other{{GENDER, select, other{son}}}} groupe"
summary: "Résumé"
your_subscription_has_expired_on_the_DATE: "Votre abonnement a expiré au {DATE}"
subscription_price: "Coût de l'abonnement"
you_ve_just_selected_a_subscription_html: "Vous venez de sélectionner un <strong>abonnement</strong> :"
monthly_payment: "Payer par échéancier mensuel"
your_payment_schedule: "Votre échéancier de paiement"
confirm_and_pay: "Valider et payer"
you_ve_just_payed_the_subscription_html: "Vous venez de régler <strong>l'abonnement</strong> :"
thank_you_your_subscription_is_successful: "Merci. Votre abonnement a bien été pris en compte !"
your_invoice_will_be_available_soon_from_your_dashboard: "Votre facture sera bientôt disponible depuis votre tableau de bord"
@ -260,11 +254,8 @@ fr:
the_user_s_group_was_successfully_changed: "Le groupe de l'utilisateur a bien été changé."
an_error_prevented_your_group_from_being_changed: "Une erreur a empêché votre changement de groupe."
an_error_prevented_to_change_the_user_s_group: "Une erreur a empêché le changement de groupe de l'utilisateur."
an_error_occured_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."
subscription_confirmation: "Validation de l'abonnement"
here_is_the_NAME_subscription_summary: "Voici le récapitulatif de l'abonnement de {NAME} :"
confirm_payment_of_html: "{ROLE, select, admin{Encaisser} other{Payer}} : {AMOUNT}" #(contexte : validate a payment of $20,00)
online_payment_disabled: "Le paiement par carte bancaire n'est pas disponible. Merci de contacter directement l'accueil du FabLab."
#Fablab's events list
events_list:
the_fablab_s_events: "Les événements du Fab Lab"

View File

@ -391,9 +391,11 @@ en:
to_benefit_from_attractive_prices: "To benefit from attractive prices"
view_our_subscriptions: "View our subscriptions"
or: "or"
you_ve_just_selected_a_: "You've just selected a"
_subscription: "subscription"
cost_of_the_subscription: "Cost of the subscription"
subscription_price: "Subscription price"
you_ve_just_selected_a_subscription_html: "You've just selected a <strong>subscription</strong>:"
monthly_payment: "Pay by monthly schedule"
your_payment_schedule: "Your payment schedule"
confirm_and_pay: "Confirm and pay"
you_have_settled_the_following_TYPE: "You have settled the following {TYPE, select, Machine{machine slots} Training{training} other{elements}}:"
you_have_settled_a_: "You have settled a"

View File

@ -391,9 +391,11 @@ fr:
to_benefit_from_attractive_prices: "Pour bénéficier de prix avantageux"
view_our_subscriptions: "Consultez nos abonnements"
or: "ou"
you_ve_just_selected_a_: "Vous venez de sélectionner un"
_subscription: "abonnement"
cost_of_the_subscription: "Coût de l'abonnement"
subscription_price: "Coût de l'abonnement"
you_ve_just_selected_a_subscription_html: "Vous venez de sélectionner un <strong>abonnement</strong> :"
monthly_payment: "Payer par échéancier mensuel"
your_payment_schedule: "Votre échéancier de paiement"
confirm_and_pay: "Valider et payer"
you_have_settled_the_following_TYPE: "Vous avez réglé {TYPE, select, Machine{les créneaux machines suivants} Training{la formation suivante} other{les éléments suivants}} :"
you_have_settled_a_: "Vous avez réglé un"