From b0afa02f1dd9f51bff91f788e7ec3d11742b0a49 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 4 Nov 2020 16:22:31 +0100 Subject: [PATCH] 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 --- Procfile | 2 +- .../src/javascript/components/plan-card.tsx | 2 +- .../src/javascript/controllers/plans.js | 267 ++---------------- .../src/javascript/directives/cart.js | 119 +++++--- app/frontend/templates/plans/index.html | 40 +-- app/frontend/templates/shared/_cart.html | 29 +- .../shared/valid_reservation_modal.html | 1 + app/services/payment_schedule_service.rb | 2 +- config/locales/app.public.en.yml | 9 - config/locales/app.public.fr.yml | 9 - config/locales/app.shared.en.yml | 6 +- config/locales/app.shared.fr.yml | 6 +- 12 files changed, 141 insertions(+), 351 deletions(-) diff --git a/Procfile b/Procfile index bbda6f44f..e68a73938 100644 --- a/Procfile +++ b/Procfile @@ -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 diff --git a/app/frontend/src/javascript/components/plan-card.tsx b/app/frontend/src/javascript/components/plan-card.tsx index f8d45a99d..1de4a3d5d 100644 --- a/app/frontend/src/javascript/components/plan-card.tsx +++ b/app/frontend/src/javascript/components/plan-card.tsx @@ -36,7 +36,7 @@ const PlanCard: React.FC = ({ 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); } /** diff --git a/app/frontend/src/javascript/controllers/plans.js b/app/frontend/src/javascript/controllers/plans.js index 6237290d7..1dcd96cd1 100644 --- a/app/frontend/src/javascript/controllers/plans.js +++ b/app/frontend/src/javascript/controllers/plans.js @@ -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, 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 diff --git a/app/frontend/src/javascript/directives/cart.js b/app/frontend/src/javascript/directives/cart.js index f53b84764..9e35c2f6e 100644 --- a/app/frontend/src/javascript/directives/cart.js +++ b/app/frontend/src/javascript/directives/cart.js @@ -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({ diff --git a/app/frontend/templates/plans/index.html b/app/frontend/templates/plans/index.html index 3aab02673..65162aad2 100644 --- a/app/frontend/templates/plans/index.html +++ b/app/frontend/templates/plans/index.html @@ -104,40 +104,12 @@ -
-
-

{{ 'app.public.plans.summary' }}

-
- -
- {{ 'app.public.plans.you_ve_just_selected_a_subscription_html' }} - -
- {{ selectedPlan | humanReadablePlanName }} -
{{ 'app.public.plans.subscription_price' | translate }} {{selectedPlan.amount | currency}}
-
- - - - - -
- - -
+
diff --git a/app/frontend/templates/shared/_cart.html b/app/frontend/templates/shared/_cart.html index c4ef8f812..d1c91ee02 100644 --- a/app/frontend/templates/shared/_cart.html +++ b/app/frontend/templates/shared/_cart.html @@ -60,7 +60,7 @@
-
{{ 'app.shared.cart.you_ve_just_selected_a_' | translate }}
{{ 'app.shared.cart._subscription' }} :
+
{{ 'app.shared.cart.you_ve_just_selected_a_subscription_html' }}
{{selectedPlan | humanReadablePlanName }}
@@ -72,7 +72,32 @@
- + diff --git a/app/services/payment_schedule_service.rb b/app/services/payment_schedule_service.rb index 8251837ef..eb31184be 100644 --- a/app/services/payment_schedule_service.rb +++ b/app/services/payment_schedule_service.rb @@ -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 diff --git a/config/locales/app.public.en.yml b/config/locales/app.public.en.yml index 5db4eeba7..9be6cee90 100644 --- a/config/locales/app.public.en.yml +++ b/config/locales/app.public.en.yml @@ -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 subscription:" - 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 subscription:" 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" diff --git a/config/locales/app.public.fr.yml b/config/locales/app.public.fr.yml index b0cc61a76..ec7297d12 100644 --- a/config/locales/app.public.fr.yml +++ b/config/locales/app.public.fr.yml @@ -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 abonnement :" - 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 l'abonnement :" 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" diff --git a/config/locales/app.shared.en.yml b/config/locales/app.shared.en.yml index d92656993..3d59b4291 100644 --- a/config/locales/app.shared.en.yml +++ b/config/locales/app.shared.en.yml @@ -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 subscription:" + 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" diff --git a/config/locales/app.shared.fr.yml b/config/locales/app.shared.fr.yml index b32f7a3df..f369d84ce 100644 --- a/config/locales/app.shared.fr.yml +++ b/config/locales/app.shared.fr.yml @@ -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 abonnement :" + 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"