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:
parent
7d37174b51
commit
b0afa02f1d
2
Procfile
2
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
|
||||
|
@ -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);
|
||||
}
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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({
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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 -->
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user