mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-20 14:54:15 +01:00
Merge branch 'monthly-payment' into staging
This commit is contained in:
commit
d24757ce1c
@ -4,9 +4,11 @@
|
||||
- Fix a bug: unable to build homepage custom stylesheet
|
||||
- Fix a bug: unable to access embedded plan views
|
||||
- Fix a bug: warning message overflow in credit wallet modal
|
||||
- Fix a bug: when a cash coupon was used, an invalid amount is shown in the statistics
|
||||
- [TODO DEPLOY] `rails fablab:stripe:plans_prices`
|
||||
- Fix a bug: when using a cash coupon, the amount shown in the statistics is invalid
|
||||
- Fix a bug: unable to create a coupon on stripe
|
||||
- [TODO DEPLOY] `rails fablab:maintenance:rebuild_stylesheet`
|
||||
- [TODO DEPLOY] `rails fablab:stripe:set_product_id`
|
||||
- [TODO DEPLOY] `rails fablab:setup:add_schedule_reference`
|
||||
|
||||
## v4.6.3 2020 October 28
|
||||
|
||||
|
@ -20,7 +20,9 @@ class API::SubscriptionsController < API::ApiController
|
||||
|
||||
@subscription = Subscription.new(subscription_params)
|
||||
is_subscribe = Subscriptions::Subscribe.new(current_user.invoicing_profile.id, user_id)
|
||||
.pay_and_save(@subscription, coupon: coupon_params[:coupon_code], invoice: true)
|
||||
.pay_and_save(@subscription, coupon: coupon_params[:coupon_code],
|
||||
invoice: true,
|
||||
schedule: params[:subscription][:payment_schedule])
|
||||
|
||||
if is_subscribe
|
||||
render :show, status: :created, location: @subscription
|
||||
|
@ -40,7 +40,7 @@ const PaymentScheduleSummary: React.FC<PaymentScheduleSummaryProps> = ({ schedul
|
||||
* Test if all payment deadlines have the same amount
|
||||
*/
|
||||
const hasEqualDeadlines = (): boolean => {
|
||||
const prices = schedule.items.map(i => i.price);
|
||||
const prices = schedule.items.map(i => i.amount);
|
||||
return prices.every(p => p === prices[0]);
|
||||
}
|
||||
/**
|
||||
@ -57,7 +57,7 @@ const PaymentScheduleSummary: React.FC<PaymentScheduleSummaryProps> = ({ schedul
|
||||
{hasEqualDeadlines() && <ul>
|
||||
<li>
|
||||
<span className="schedule-item-info">
|
||||
{t('app.shared.cart.NUMBER_monthly_payment_of_AMOUNT', { NUMBER: schedule.items.length, AMOUNT: formatPrice(schedule.items[0].price) })}
|
||||
{t('app.shared.cart.NUMBER_monthly_payment_of_AMOUNT', { NUMBER: schedule.items.length, AMOUNT: formatPrice(schedule.items[0].amount) })}
|
||||
</span>
|
||||
<span className="schedule-item-date">{t('app.shared.cart.first_debit')}</span>
|
||||
</li>
|
||||
@ -65,12 +65,12 @@ const PaymentScheduleSummary: React.FC<PaymentScheduleSummaryProps> = ({ schedul
|
||||
{!hasEqualDeadlines() && <ul>
|
||||
<li>
|
||||
<span className="schedule-item-info">{t('app.shared.cart.monthly_payment_NUMBER', { NUMBER: 1 })}</span>
|
||||
<span className="schedule-item-price">{formatPrice(schedule.items[0].price)}</span>
|
||||
<span className="schedule-item-price">{formatPrice(schedule.items[0].amount)}</span>
|
||||
<span className="schedule-item-date">{t('app.shared.cart.debit')}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className="schedule-item-info">
|
||||
{t('app.shared.cart.NUMBER_monthly_payment_of_AMOUNT', { NUMBER: schedule.items.length - 1, AMOUNT: formatPrice(schedule.items[1].price) })}
|
||||
{t('app.shared.cart.NUMBER_monthly_payment_of_AMOUNT', { NUMBER: schedule.items.length - 1, AMOUNT: formatPrice(schedule.items[1].amount) })}
|
||||
</span>
|
||||
</li>
|
||||
</ul>}
|
||||
@ -81,7 +81,7 @@ const PaymentScheduleSummary: React.FC<PaymentScheduleSummaryProps> = ({ schedul
|
||||
<li key={String(item.due_date)}>
|
||||
<span className="schedule-item-date">{formatDate(item.due_date)}</span>
|
||||
<span> </span>
|
||||
<span className="schedule-item-price">{formatPrice(item.price)}</span>
|
||||
<span className="schedule-item-price">{formatPrice(item.amount)}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
124
app/frontend/src/javascript/components/wallet-info.tsx
Normal file
124
app/frontend/src/javascript/components/wallet-info.tsx
Normal 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']));
|
@ -260,6 +260,8 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
|
||||
sample = sample.replace(/W\[([^\]]+)\]/g, '');
|
||||
// information about refunds (R[text]) - does not apply here
|
||||
sample = sample.replace(/R\[([^\]]+)\]/g, '');
|
||||
// information about payment schedules (S[text]) -does not apply here
|
||||
sample = sample.replace(/S\[([^\]]+)\]/g, '');
|
||||
}
|
||||
return sample;
|
||||
};
|
||||
|
@ -353,13 +353,13 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$stateP
|
||||
// the moment when the slot selection changed for the last time, used to trigger changes in the cart
|
||||
$scope.selectionTime = null;
|
||||
|
||||
// the last clicked event in the calender
|
||||
// the last clicked event in the calendar
|
||||
$scope.selectedEvent = null;
|
||||
|
||||
// indicates the state of the current view : calendar or plans information
|
||||
$scope.plansAreShown = false;
|
||||
|
||||
// will store the user's plan if he choosed to buy one
|
||||
// will store the user's plan if he chose to buy one
|
||||
$scope.selectedPlan = null;
|
||||
|
||||
// the moment when the plan selection changed for the last time, used to trigger changes in the cart
|
||||
@ -390,7 +390,7 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$stateP
|
||||
$scope.spaceExplicationsAlert = settingsPromise.space_explications_alert;
|
||||
|
||||
/**
|
||||
* Change the last selected slot's appearence to looks like 'added to cart'
|
||||
* Change the last selected slot's appearance to looks like 'added to cart'
|
||||
*/
|
||||
$scope.markSlotAsAdded = function () {
|
||||
$scope.selectedEvent.backgroundColor = SELECTED_EVENT_BG_COLOR;
|
||||
@ -398,7 +398,7 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$stateP
|
||||
};
|
||||
|
||||
/**
|
||||
* Change the last selected slot's appearence to looks like 'never added to cart'
|
||||
* Change the last selected slot's appearance to looks like 'never added to cart'
|
||||
*/
|
||||
$scope.markSlotAsRemoved = function (slot) {
|
||||
slot.backgroundColor = 'white';
|
||||
@ -419,7 +419,7 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$stateP
|
||||
$scope.slotCancelled = function () { $scope.markSlotAsRemoved($scope.selectedEvent); };
|
||||
|
||||
/**
|
||||
* Change the last selected slot's appearence to looks like 'currently looking for a new destination to exchange'
|
||||
* Change the last selected slot's appearance to looks like 'currently looking for a new destination to exchange'
|
||||
*/
|
||||
$scope.markSlotAsModifying = function () {
|
||||
$scope.selectedEvent.backgroundColor = '#eee';
|
||||
@ -428,7 +428,7 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$stateP
|
||||
};
|
||||
|
||||
/**
|
||||
* Change the last selected slot's appearence to looks like 'the slot being exchanged will take this place'
|
||||
* Change the last selected slot's appearance to looks like 'the slot being exchanged will take this place'
|
||||
*/
|
||||
$scope.changeModifySpaceSlot = function () {
|
||||
if ($scope.events.placable) {
|
||||
@ -538,7 +538,7 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$stateP
|
||||
};
|
||||
|
||||
/**
|
||||
* Changes the user current view from the plan subsription screen to the machine reservation agenda
|
||||
* Changes the user current view from the plan subscription screen to the machine reservation agenda
|
||||
* @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
|
||||
*/
|
||||
$scope.doNotSubscribePlan = function (e) {
|
||||
@ -612,7 +612,7 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$stateP
|
||||
/**
|
||||
* Triggered when the user clicks on a reservation slot in the agenda.
|
||||
* Defines the behavior to adopt depending on the slot status (already booked, free, ready to be reserved ...),
|
||||
* the user's subscription (current or about to be took) and the time (the user cannot modify a booked reservation
|
||||
* the user's subscription (current or about to be took), and the time (the user cannot modify a booked reservation
|
||||
* if it's too late).
|
||||
* @see http://fullcalendar.io/docs/mouse/eventClick/
|
||||
*/
|
||||
@ -622,7 +622,7 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$stateP
|
||||
};
|
||||
|
||||
/**
|
||||
* Triggered when fullCalendar tries to graphicaly render an event block.
|
||||
* Triggered when fullCalendar tries to graphically render an event block.
|
||||
* Append the event tag into the block, just after the event title.
|
||||
* @see http://fullcalendar.io/docs/event_rendering/eventRender/
|
||||
*/
|
||||
|
@ -312,24 +312,24 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
// What the bound slot
|
||||
$scope.$watch('slotSelectionTime', function (newValue, oldValue) {
|
||||
if (newValue !== oldValue) {
|
||||
return slotSelectionChanged();
|
||||
slotSelectionChanged();
|
||||
}
|
||||
});
|
||||
$scope.$watch('user', function (newValue, oldValue) {
|
||||
if (newValue !== oldValue) {
|
||||
resetCartState();
|
||||
return updateCartPrice();
|
||||
updateCartPrice();
|
||||
}
|
||||
});
|
||||
$scope.$watch('planSelectionTime', function (newValue, oldValue) {
|
||||
if (newValue !== oldValue) {
|
||||
return planSelectionChanged();
|
||||
planSelectionChanged();
|
||||
}
|
||||
});
|
||||
// watch when a coupon is applied to re-compute the total price
|
||||
$scope.$watch('coupon.applied', function (newValue, oldValue) {
|
||||
if (newValue !== oldValue) {
|
||||
return updateCartPrice();
|
||||
updateCartPrice();
|
||||
}
|
||||
});
|
||||
};
|
||||
@ -524,6 +524,7 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
*/
|
||||
const resetCartState = function () {
|
||||
$scope.selectedPlan = null;
|
||||
$scope.paidPlan = null;
|
||||
$scope.coupon.applied = null;
|
||||
$scope.events.moved = null;
|
||||
$scope.events.paid = [];
|
||||
@ -579,7 +580,7 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
const updateCartPrice = function () {
|
||||
if (Object.keys($scope.user).length > 0) {
|
||||
const r = mkReservation($scope.user, $scope.events.reserved, $scope.selectedPlan);
|
||||
return Price.compute(mkRequestParams(r, $scope.coupon.applied), function (res) {
|
||||
return Price.compute(mkRequestParams({ reservation: r }, $scope.coupon.applied), function (res) {
|
||||
$scope.amountTotal = res.price;
|
||||
$scope.schedule.payment_schedule = res.schedule;
|
||||
$scope.totalNoCoupon = res.price_without_coupon;
|
||||
@ -605,19 +606,18 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
|
||||
/**
|
||||
* Format the parameters expected by /api/prices/compute or /api/reservations and return the resulting object
|
||||
* @param reservation {Object} as returned by mkReservation()
|
||||
* @param request {{reservation: object}|{subscription: object}} as returned by mkReservation()
|
||||
* @param coupon {Object} Coupon as returned from the API
|
||||
* @return {{reservation:Object, coupon_code:string}}
|
||||
* @return {{reservation:Object, subscription: Object, coupon_code:string}}
|
||||
*/
|
||||
const mkRequestParams = function (reservation, coupon) {
|
||||
return {
|
||||
reservation,
|
||||
const mkRequestParams = function (request, coupon) {
|
||||
return Object.assign({
|
||||
coupon_code: ((coupon ? coupon.code : undefined))
|
||||
};
|
||||
}, request);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create an hash map implementing the Reservation specs
|
||||
* Create a hash map implementing the Reservation specs
|
||||
* @param member {Object} User as retrieved from the API: current user / selected user if current is admin
|
||||
* @param slots {Array<Object>} Array of fullCalendar events: slots selected on the calendar
|
||||
* @param [plan] {Object} Plan as retrieved from the API: plan to buy with the current reservation
|
||||
@ -644,6 +644,23 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
return reservation;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a hash map implementing the Subscription specs
|
||||
* @param planId {number}
|
||||
* @param userId {number}
|
||||
* @param schedule {boolean}
|
||||
* @return {{subscription: {payment_schedule: boolean, user_id: number, plan_id: number}}}
|
||||
*/
|
||||
const mkSubscription = function (planId, userId, schedule) {
|
||||
return {
|
||||
subscription: {
|
||||
plan_id: planId,
|
||||
user_id: userId,
|
||||
payment_schedule: schedule
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Open a modal window that allows the user to process a credit card payment for his current shopping cart.
|
||||
*/
|
||||
@ -656,7 +673,7 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
return reservation;
|
||||
},
|
||||
price () {
|
||||
return Price.compute(mkRequestParams(reservation, $scope.coupon.applied)).$promise;
|
||||
return Price.compute(mkRequestParams({ reservation }, $scope.coupon.applied)).$promise;
|
||||
},
|
||||
wallet () {
|
||||
return Wallet.getWalletByUser({ user_id: reservation.user_id }).$promise;
|
||||
@ -668,7 +685,11 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
return $scope.coupon.applied;
|
||||
},
|
||||
cartItems () {
|
||||
return mkRequestParams(reservation, $scope.coupon.applied);
|
||||
let request = { reservation };
|
||||
if (reservation.slots_attributes.length === 0 && reservation.plan_id) {
|
||||
request = mkSubscription($scope.selectedPlan.id, reservation.user_id, $scope.schedule.requested_schedule);
|
||||
}
|
||||
return mkRequestParams(request, $scope.coupon.applied);
|
||||
},
|
||||
schedule () {
|
||||
return $scope.schedule.payment_schedule;
|
||||
@ -717,13 +738,13 @@ 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;
|
||||
},
|
||||
price () {
|
||||
return Price.compute(mkRequestParams(reservation, $scope.coupon.applied)).$promise;
|
||||
return Price.compute(mkRequestParams({ reservation }, $scope.coupon.applied)).$promise;
|
||||
},
|
||||
wallet () {
|
||||
return Wallet.getWalletByUser({ user_id: reservation.user_id }).$promise;
|
||||
@ -741,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;
|
||||
@ -761,16 +782,13 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
// Shows the schedule info in the modal
|
||||
$scope.schedule = schedule.payment_schedule;
|
||||
|
||||
// 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');
|
||||
}
|
||||
}
|
||||
// how should we collect payments for the payment schedule
|
||||
$scope.method = {
|
||||
payment_method: 'stripe'
|
||||
};
|
||||
|
||||
// "valid" Button label
|
||||
$scope.validButtonName = '';
|
||||
|
||||
/**
|
||||
* Callback to process the local payment, triggered on button click
|
||||
@ -779,24 +797,20 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
$scope.attempting = true;
|
||||
// save subscription (if there's only a subscription selected)
|
||||
if (reservation.slots_attributes.length === 0 && selectedPlan) {
|
||||
return Subscription.save({
|
||||
coupon_code: ((coupon ? coupon.code : undefined)),
|
||||
subscription: {
|
||||
plan_id: selectedPlan.id,
|
||||
user_id: reservation.user_id,
|
||||
payment_schedule: schedule.requested_schedule
|
||||
}
|
||||
}, function (subscription) {
|
||||
$uibModalInstance.close(subscription);
|
||||
$scope.attempting = true;
|
||||
}, function (response) {
|
||||
$scope.alerts = [];
|
||||
$scope.alerts.push({ msg: _t('app.shared.cart.a_problem_occurred_during_the_payment_process_please_try_again_later'), type: 'danger' });
|
||||
$scope.attempting = false;
|
||||
});
|
||||
const sub = mkSubscription(selectedPlan.id, reservation.user_id, schedule.requested_schedule);
|
||||
|
||||
return Subscription.save(mkRequestParams(sub, coupon),
|
||||
function (subscription) {
|
||||
$uibModalInstance.close(subscription);
|
||||
$scope.attempting = true;
|
||||
}, function (response) {
|
||||
$scope.alerts = [];
|
||||
$scope.alerts.push({ msg: _t('app.shared.cart.a_problem_occurred_during_the_payment_process_please_try_again_later'), type: 'danger' });
|
||||
$scope.attempting = false;
|
||||
});
|
||||
}
|
||||
// otherwise, save the reservation (may include a subscription)
|
||||
Reservation.save(mkRequestParams($scope.reservation, coupon), function (reservation) {
|
||||
Reservation.save(mkRequestParams({ reservation: $scope.reservation }, coupon), function (reservation) {
|
||||
$uibModalInstance.close(reservation);
|
||||
$scope.attempting = true;
|
||||
}, function (response) {
|
||||
@ -805,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); });
|
||||
@ -822,9 +876,16 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
// we call the external callback if present
|
||||
if (typeof $scope.afterPayment === 'function') { $scope.afterPayment(reservation); }
|
||||
// we reset the coupon, and the cart content, and we unselect the slot
|
||||
$scope.events.reserved = [];
|
||||
$scope.coupon.applied = null;
|
||||
$scope.slot = null;
|
||||
if ($scope.slot) {
|
||||
// reservation (+ subscription)
|
||||
$scope.slot = null;
|
||||
$scope.events.reserved = [];
|
||||
} else {
|
||||
// subscription only
|
||||
$scope.events = {};
|
||||
}
|
||||
$scope.paidPlan = $scope.selectedPlan;
|
||||
$scope.selectedPlan = null;
|
||||
$scope.schedule.requested_schedule = false;
|
||||
$scope.schedule.payment_schedule = null;
|
||||
@ -848,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();
|
||||
}
|
||||
|
@ -1,7 +1,12 @@
|
||||
export interface PaymentScheduleItem {
|
||||
id: number,
|
||||
price: number,
|
||||
amount: number,
|
||||
due_date: Date
|
||||
details: {
|
||||
recurring: number,
|
||||
adjustment: number,
|
||||
other_items: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface PaymentSchedule {
|
||||
|
15
app/frontend/src/javascript/models/reservation.ts
Normal file
15
app/frontend/src/javascript/models/reservation.ts
Normal 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
|
||||
}
|
6
app/frontend/src/javascript/models/wallet.ts
Normal file
6
app/frontend/src/javascript/models/wallet.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export interface Wallet {
|
||||
id: number,
|
||||
invoicing_profile_id: number,
|
||||
amount: number,
|
||||
user_id: number
|
||||
}
|
@ -23,5 +23,6 @@
|
||||
@import "modules/tour";
|
||||
@import "modules/fab-modal";
|
||||
@import "modules/payment-schedule-summary";
|
||||
@import "modules/wallet-info";
|
||||
|
||||
@import "app.responsive";
|
||||
|
25
app/frontend/src/stylesheets/modules/wallet-info.scss
Normal file
25
app/frontend/src/stylesheets/modules/wallet-info.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -137,12 +137,18 @@
|
||||
|
||||
<script type="text/ng-template" id="addOnlineInfo.html">
|
||||
<table class="invoice-element-legend">
|
||||
<tr><td><strong>X[texte]</strong></td><td>{{ 'app.admin.invoices.add_a_notice_regarding_the_online_sales_only_if_the_invoice_is_concerned' | translate }} <mark translate>{{ 'app.admin.invoices.this_will_never_be_added_when_a_refund_notice_is_present' }}</mark> {{ 'app.admin.invoices.eg_XVL_will_add_VL_to_the_invoices_settled_with_stripe' | translate }}</td></tr>
|
||||
<tr><td><strong>X[{{ 'app.admin.invoices.text' | translate }}]</strong></td><td>{{ 'app.admin.invoices.add_a_notice_regarding_the_online_sales_only_if_the_invoice_is_concerned' | translate }} <mark translate>{{ 'app.admin.invoices.this_will_never_be_added_when_a_refund_notice_is_present' }}</mark> {{ 'app.admin.invoices.eg_XVL_will_add_VL_to_the_invoices_settled_with_stripe' | translate }}</td></tr>
|
||||
</table>
|
||||
</script>
|
||||
|
||||
<script type="text/ng-template" id="addRefundInfo.html">
|
||||
<table class="invoice-element-legend">
|
||||
<tr><td><strong>R[texte]</strong></td><td>{{ 'app.admin.invoices.add_a_notice_regarding_refunds_only_if_the_invoice_is_concerned' | translate }}<mark translate>{{ 'app.admin.invoices.this_will_never_be_added_when_an_online_sales_notice_is_present' }}</mark> {{ 'app.admin.invoices.eg_RA_will_add_A_to_the_refund_invoices' | translate }}</td></tr>
|
||||
<tr><td><strong>R[{{ 'app.admin.invoices.text' | translate }}]</strong></td><td>{{ 'app.admin.invoices.add_a_notice_regarding_refunds_only_if_the_invoice_is_concerned' | translate }}<mark translate>{{ 'app.admin.invoices.this_will_never_be_added_when_an_online_sales_notice_is_present' }}</mark> {{ 'app.admin.invoices.eg_RA_will_add_A_to_the_refund_invoices' | translate }}</td></tr>
|
||||
</table>
|
||||
</script>
|
||||
|
||||
<script type="text/ng-template" id="addPaymentScheduleInfo.html">
|
||||
<table class="invoice-element-legend">
|
||||
<tr><td><strong>S[{{ 'app.admin.invoices.text' | translate }}]</strong></td><td>{{ 'app.admin.invoices.add_a_notice_regarding_payment_schedule' | translate }}<mark translate>{{ 'app.admin.invoices.this_will_never_be_added_with_other_notices' }}</mark> {{ 'app.admin.invoices.eg_SE_to_schedules' | translate }}</td></tr>
|
||||
</table>
|
||||
</script>
|
||||
|
@ -12,6 +12,7 @@
|
||||
<li ng-click="invoice.reference.help = 'addInvoiceNumber.html'">{{ 'app.admin.invoices.num_of_invoice' | translate }}</li>
|
||||
<li ng-click="invoice.reference.help = 'addOnlineInfo.html'">{{ 'app.admin.invoices.online_sales' | translate }}</li>
|
||||
<li ng-click="invoice.reference.help = 'addRefundInfo.html'">{{ 'app.admin.invoices.refund' | translate }}</li>
|
||||
<li ng-click="invoice.reference.help = 'addPaymentScheduleInfo.html'">{{ 'app.admin.invoices.payment_schedule' | translate }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
|
@ -1,11 +1,14 @@
|
||||
<div class="widget panel b-a m m-t-lg" ng-if="user && !events.modifiable && !events.moved">
|
||||
<div class="widget panel b-a m m-t-lg" ng-if="user && !events.modifiable && !events.moved && !paidPlan">
|
||||
<div class="panel-heading b-b small">
|
||||
<h3 translate>{{ 'app.shared.cart.summary' }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="widget-content no-bg auto wrapper" ng-show="events.reserved.length == 0 && (!events.paid || events.paid.length == 0)">
|
||||
<p class="font-felt fleche-left text-lg"><img src="../../images/arrow-left.png" class="fleche-left visible-lg" />
|
||||
{{ 'app.shared.cart.select_one_or_more_slots_in_the_calendar' | translate:{SINGLE:limitToOneSlot} }}</p>
|
||||
<div class="widget-content no-bg auto wrapper" ng-show="(!events.reserved && !selectedPlan) || events.reserved.length == 0 && (!events.paid || events.paid.length == 0)">
|
||||
<p class="font-felt fleche-left text-lg">
|
||||
<img src="../../images/arrow-left.png" class="fleche-left visible-lg" />
|
||||
<span ng-show="events.reserved">{{ 'app.shared.cart.select_one_or_more_slots_in_the_calendar' | translate:{SINGLE:limitToOneSlot} }}</span>
|
||||
<span ng-hide="events.reserved" translate>{{ 'app.shared.cart.select_a_plan' }}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="widget-content no-bg auto wrapper" ng-if="events.reserved.length > 0">
|
||||
|
@ -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>
|
@ -5,20 +5,45 @@
|
||||
</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">
|
||||
<li><strong>{{slot.start_at | amDateFormat: 'LL'}} : {{slot.start_at | amDateFormat:'LT'}} - {{slot.end_at | amDateFormat:'LT'}}</strong></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div ng-if="reservation.plan_id">
|
||||
<p translate>{{ 'app.shared.valid_reservation_modal.here_is_the_subscription_summary' }}</p>
|
||||
<p>{{ plan | humanReadablePlanName }}</p>
|
||||
</div>
|
||||
<div ng-if="schedule">
|
||||
<label for="method" translate>{{ 'app.shared.valid_reservation_modal.payment_method' }}</label>
|
||||
<select id="method"
|
||||
class="form-control m-b"
|
||||
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>
|
||||
</select>
|
||||
<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>
|
||||
|
||||
<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">
|
||||
<li><strong>{{slot.start_at | amDateFormat: 'LL'}} : {{slot.start_at | amDateFormat:'LT'}} - {{slot.end_at | amDateFormat:'LT'}}</strong></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div ng-if="reservation.plan_id">
|
||||
<p translate>{{ 'app.shared.valid_reservation_modal.here_is_the_subscription_summary' }}</p>
|
||||
<p>{{ plan | humanReadablePlanName }}</p>
|
||||
</div>
|
||||
<div ng-if="schedule">
|
||||
<p translate translate-values="{DEADLINES: schedule.items.length}">{{ 'app.shared.valid_reservation_modal.payment_schedule' }}</p>
|
||||
<wallet-info current-user="currentUser"
|
||||
reservation="reservation"
|
||||
price="price"
|
||||
remaining-price="amount"
|
||||
wallet="wallet"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
@ -3,6 +3,7 @@
|
||||
# Coupon is a textual code associated with a discount rate or an amount of discount
|
||||
class Coupon < ApplicationRecord
|
||||
has_many :invoices
|
||||
has_many :payment_schedule
|
||||
|
||||
after_commit :create_stripe_coupon, on: [:create]
|
||||
after_commit :delete_stripe_coupon, on: [:destroy]
|
||||
|
@ -22,6 +22,7 @@ class Invoice < ApplicationRecord
|
||||
belongs_to :offer_day, foreign_type: 'OfferDay', foreign_key: 'invoiced_id'
|
||||
|
||||
has_one :avoir, class_name: 'Invoice', foreign_key: :invoice_id, dependent: :destroy
|
||||
has_one :payment_schedule_item
|
||||
belongs_to :operator_profile, foreign_key: :operator_profile_id, class_name: 'InvoicingProfile'
|
||||
|
||||
before_create :add_environment
|
||||
|
@ -10,6 +10,7 @@ class InvoicingProfile < ApplicationRecord
|
||||
has_one :organization, dependent: :destroy
|
||||
accepts_nested_attributes_for :organization, allow_destroy: false
|
||||
has_many :invoices, dependent: :destroy
|
||||
has_many :payment_schedules, dependent: :destroy
|
||||
|
||||
has_one :wallet, dependent: :destroy
|
||||
has_many :wallet_transactions, dependent: :destroy
|
||||
@ -17,6 +18,7 @@ class InvoicingProfile < ApplicationRecord
|
||||
has_many :history_values, dependent: :nullify
|
||||
|
||||
has_many :operated_invoices, foreign_key: :operator_profile_id, class_name: 'Invoice', dependent: :nullify
|
||||
has_many :operated_payment_schedules, foreign_key: :operator_profile_id, class_name: 'PaymentSchedule', dependent: :nullify
|
||||
|
||||
def full_name
|
||||
# if first_name or last_name is nil, the empty string will be used as a temporary replacement
|
||||
|
@ -29,6 +29,8 @@ class Machine < ApplicationRecord
|
||||
|
||||
after_create :create_statistic_subtype
|
||||
after_create :create_machine_prices
|
||||
after_create :update_stripe_product
|
||||
after_update :update_stripe_product, if: :saved_change_to_name?
|
||||
after_update :update_statistic_subtype, if: :saved_change_to_name?
|
||||
after_destroy :remove_statistic_subtype
|
||||
|
||||
@ -72,4 +74,10 @@ class Machine < ApplicationRecord
|
||||
def destroyable?
|
||||
reservations.empty?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_stripe_product
|
||||
StripeWorker.perform_async(:create_or_update_stp_product, Machine.name, id)
|
||||
end
|
||||
end
|
||||
|
@ -8,4 +8,42 @@ class PaymentSchedule < ApplicationRecord
|
||||
belongs_to :coupon
|
||||
belongs_to :invoicing_profile
|
||||
belongs_to :operator_profile, foreign_key: :operator_profile_id, class_name: 'InvoicingProfile'
|
||||
|
||||
belongs_to :subscription, foreign_type: 'Subscription', foreign_key: 'scheduled_id'
|
||||
belongs_to :reservation, foreign_type: 'Reservation', foreign_key: 'scheduled_id'
|
||||
|
||||
has_many :payment_schedule_items
|
||||
|
||||
before_create :add_environment
|
||||
after_create :update_reference, :chain_record
|
||||
|
||||
##
|
||||
# This is useful to check the first item because its amount may be different from the others
|
||||
##
|
||||
def ordered_items
|
||||
payment_schedule_items.order(due_date: :asc)
|
||||
end
|
||||
|
||||
def add_environment
|
||||
self.environment = Rails.env
|
||||
end
|
||||
|
||||
def update_reference
|
||||
self.reference = InvoiceReferenceService.generate_reference(self, payment_schedule: true)
|
||||
save
|
||||
end
|
||||
|
||||
def chain_record
|
||||
self.footprint = compute_footprint
|
||||
save!
|
||||
FootprintDebug.create!(
|
||||
footprint: footprint,
|
||||
data: FootprintService.footprint_data(PaymentSchedule, self),
|
||||
klass: PaymentSchedule.name
|
||||
)
|
||||
end
|
||||
|
||||
def compute_footprint
|
||||
FootprintService.compute_footprint(PaymentSchedule, self)
|
||||
end
|
||||
end
|
||||
|
@ -3,4 +3,5 @@
|
||||
# Represents a due date and the associated amount for a PaymentSchedule
|
||||
class PaymentScheduleItem < ApplicationRecord
|
||||
belongs_to :payment_schedule
|
||||
belongs_to :invoice
|
||||
end
|
||||
|
@ -23,9 +23,8 @@ class Plan < ApplicationRecord
|
||||
after_create :create_spaces_prices
|
||||
after_create :create_statistic_type
|
||||
after_create :set_name
|
||||
after_create :update_stripe_price
|
||||
|
||||
after_update :update_stripe_price, if: :saved_change_to_amount?
|
||||
after_create :update_stripe_product
|
||||
after_update :update_stripe_product, if: :saved_change_to_base_name?
|
||||
|
||||
validates :amount, :group, :base_name, presence: true
|
||||
validates :interval_count, numericality: { only_integer: true, greater_than_or_equal_to: 1 }
|
||||
@ -111,10 +110,6 @@ class Plan < ApplicationRecord
|
||||
StatisticType.where(statistic_index_id: stat_index.first.id).where('label LIKE ?', "%#{human_readable_duration}%").first
|
||||
end
|
||||
|
||||
def update_stripe_price
|
||||
StripeWorker.perform_async(:create_stripe_price, self)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_statistic_subtype
|
||||
@ -133,4 +128,8 @@ class Plan < ApplicationRecord
|
||||
def set_name
|
||||
update_columns(name: human_readable_name)
|
||||
end
|
||||
|
||||
def update_stripe_product
|
||||
StripeWorker.perform_async(:create_or_update_stp_product, Plan.name, id)
|
||||
end
|
||||
end
|
||||
|
@ -144,10 +144,9 @@ class Price < ApplicationRecord
|
||||
cp = cs.validate(options[:coupon_code], user.id)
|
||||
total_amount = cs.apply(total_amount, cp)
|
||||
|
||||
# == generate PaymentSchedule ()if applicable) ===
|
||||
# == generate PaymentSchedule (if applicable) ===
|
||||
schedule = if options[:payment_schedule] && plan.monthly_payment
|
||||
pss = PaymentScheduleService.new
|
||||
pss.compute(plan, _amount_no_coupon, cp)
|
||||
PaymentScheduleService.new.compute(plan, _amount_no_coupon, cp)
|
||||
else
|
||||
nil
|
||||
end
|
||||
|
@ -18,6 +18,7 @@ class Reservation < ApplicationRecord
|
||||
accepts_nested_attributes_for :tickets, allow_destroy: false
|
||||
|
||||
has_one :invoice, -> { where(type: nil) }, as: :invoiced, dependent: :destroy
|
||||
has_one :payment_schedule, as: :scheduled, dependent: :destroy
|
||||
|
||||
validates_presence_of :reservable_id, :reservable_type
|
||||
validate :machine_not_already_reserved, if: -> { reservable.is_a?(Machine) }
|
||||
|
@ -26,6 +26,8 @@ class Space < ApplicationRecord
|
||||
|
||||
after_create :create_statistic_subtype
|
||||
after_create :create_space_prices
|
||||
after_create :update_stripe_product
|
||||
after_update :update_stripe_product, if: :saved_change_to_name?
|
||||
after_update :update_statistic_subtype, if: :saved_change_to_name?
|
||||
after_destroy :remove_statistic_subtype
|
||||
|
||||
@ -60,4 +62,10 @@ class Space < ApplicationRecord
|
||||
def destroyable?
|
||||
reservations.empty?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_stripe_product
|
||||
StripeWorker.perform_async(:create_or_update_stp_product, Space.name, id)
|
||||
end
|
||||
end
|
||||
|
@ -7,6 +7,7 @@ class Subscription < ApplicationRecord
|
||||
belongs_to :plan
|
||||
belongs_to :statistic_profile
|
||||
|
||||
has_one :payment_schedule, as: :scheduled, dependent: :destroy
|
||||
has_many :invoices, as: :invoiced, dependent: :destroy
|
||||
has_many :offer_days, dependent: :destroy
|
||||
|
||||
@ -20,7 +21,7 @@ class Subscription < ApplicationRecord
|
||||
|
||||
# @param invoice if true then only the subscription is payed, without reservation
|
||||
# if false then the subscription is payed with reservation
|
||||
def save_with_payment(operator_profile_id, invoice = true, coupon_code = nil, payment_intent_id = nil)
|
||||
def save_with_payment(operator_profile_id, invoice = true, coupon_code = nil, payment_intent_id = nil, schedule = nil)
|
||||
return false unless valid?
|
||||
|
||||
set_expiration_date
|
||||
@ -33,16 +34,29 @@ class Subscription < ApplicationRecord
|
||||
# debit wallet
|
||||
wallet_transaction = debit_user_wallet
|
||||
|
||||
invoc = generate_invoice(operator_profile_id, coupon_code, payment_intent_id)
|
||||
payment = if schedule
|
||||
generate_schedule(operator_profile_id, coupon_code, payment_intent_id)
|
||||
else
|
||||
generate_invoice(operator_profile_id, coupon_code, payment_intent_id)
|
||||
end
|
||||
|
||||
if wallet_transaction
|
||||
invoc.wallet_amount = @wallet_amount_debit
|
||||
invoc.wallet_transaction_id = wallet_transaction.id
|
||||
payment.wallet_amount = @wallet_amount_debit
|
||||
payment.wallet_transaction_id = wallet_transaction.id
|
||||
end
|
||||
invoc.save
|
||||
payment.save
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
def generate_schedule(operator_profile_id, coupon_code = nil, payment_intent_id = nil)
|
||||
operator = InvoicingProfile.find(operator_profile_id)&.user
|
||||
method = operator&.admin? || (operator&.manager? && operator != user) ? nil : 'stripe' # FIXME, paiement à l'accueil
|
||||
coupon = Coupon.find_by(code: coupon_code) unless coupon_code.nil?
|
||||
|
||||
schedule = PaymentScheduleService.new.create(self, plan.amount, coupon: coupon, operator: operator, payment_method: method, user: user)
|
||||
end
|
||||
|
||||
def generate_invoice(operator_profile_id, coupon_code = nil, payment_intent_id = nil)
|
||||
coupon_id = nil
|
||||
total = plan.amount
|
||||
|
@ -28,6 +28,8 @@ class Training < ApplicationRecord
|
||||
|
||||
after_create :create_statistic_subtype
|
||||
after_create :create_trainings_pricings
|
||||
after_create :update_stripe_product
|
||||
after_update :update_stripe_product, if: :saved_change_to_name?
|
||||
after_update :update_statistic_subtype, if: :saved_change_to_name?
|
||||
after_destroy :remove_statistic_subtype
|
||||
|
||||
@ -64,4 +66,8 @@ class Training < ApplicationRecord
|
||||
TrainingsPricing.create(training: self, group: group, amount: 0)
|
||||
end
|
||||
end
|
||||
|
||||
def update_stripe_product
|
||||
StripeWorker.perform_async(:create_or_update_stp_product, Training.name, id)
|
||||
end
|
||||
end
|
||||
|
@ -9,6 +9,7 @@ class WalletTransaction < ApplicationRecord
|
||||
belongs_to :reservation
|
||||
belongs_to :transactable, polymorphic: true
|
||||
has_one :invoice
|
||||
has_one :payment_schedule
|
||||
|
||||
validates_inclusion_of :transaction_type, in: %w[credit debit]
|
||||
validates :invoicing_profile, :wallet, presence: true
|
||||
|
@ -199,8 +199,8 @@ class PDF::Invoice < Prawn::Document
|
||||
elsif cp.type == 'amount_off'
|
||||
# refunds of invoices with cash coupons: we need to ventilate coupons on paid items
|
||||
if invoice.is_a?(Avoir)
|
||||
paid_items = invoice.invoice.invoice_items.select{ |ii| ii.amount.positive? }.length
|
||||
refund_items = invoice.invoice_items.select{ |ii| ii.amount.positive? }.length
|
||||
paid_items = invoice.invoice.invoice_items.select { |ii| ii.amount.positive? }.length
|
||||
refund_items = invoice.invoice_items.select { |ii| ii.amount.positive? }.length
|
||||
|
||||
discount = ((invoice.coupon.amount_off / paid_items) * refund_items) / 100.00
|
||||
else
|
||||
|
@ -1,12 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Provides methods to generate invoice references
|
||||
# Provides methods to generate Invoice or PaymentSchedule references
|
||||
class InvoiceReferenceService
|
||||
class << self
|
||||
def generate_reference(invoice, date: DateTime.current, avoir: false)
|
||||
def generate_reference(invoice, date: DateTime.current, avoir: false, payment_schedule: false)
|
||||
pattern = Setting.get('invoice_reference')
|
||||
|
||||
reference = replace_invoice_number_pattern(pattern, invoice)
|
||||
reference = replace_invoice_number_pattern(pattern)
|
||||
reference = replace_date_pattern(reference, date)
|
||||
|
||||
if avoir
|
||||
@ -15,6 +15,15 @@ class InvoiceReferenceService
|
||||
|
||||
# remove information about online selling (X[text])
|
||||
reference.gsub!(/X\[([^\]]+)\]/, ''.to_s)
|
||||
# remove information about payment schedule (S[text])
|
||||
reference.gsub!(/S\[([^\]]+)\]/, ''.to_s)
|
||||
elsif payment_schedule
|
||||
# information about payment schedule
|
||||
reference.gsub!(/S\[([^\]]+)\]/, '\1')
|
||||
# remove information about online selling (X[text])
|
||||
reference.gsub!(/X\[([^\]]+)\]/, ''.to_s)
|
||||
# remove information about refunds (R[text])
|
||||
reference.gsub!(/R\[([^\]]+)\]/, ''.to_s)
|
||||
else
|
||||
# information about online selling (X[text])
|
||||
if invoice.paid_with_stripe?
|
||||
@ -25,6 +34,8 @@ class InvoiceReferenceService
|
||||
|
||||
# remove information about refunds (R[text])
|
||||
reference.gsub!(/R\[([^\]]+)\]/, ''.to_s)
|
||||
# remove information about payment schedule (S[text])
|
||||
reference.gsub!(/S\[([^\]]+)\]/, ''.to_s)
|
||||
end
|
||||
|
||||
reference
|
||||
@ -35,10 +46,10 @@ class InvoiceReferenceService
|
||||
|
||||
# global invoice number (nn..nn)
|
||||
reference = pattern.gsub(/n+(?![^\[]*\])/) do |match|
|
||||
pad_and_truncate(number_of_invoices(invoice, 'global'), match.to_s.length)
|
||||
pad_and_truncate(number_of_invoices('global'), match.to_s.length)
|
||||
end
|
||||
|
||||
reference = replace_invoice_number_pattern(reference, invoice)
|
||||
reference = replace_invoice_number_pattern(reference)
|
||||
replace_date_pattern(reference, invoice.created_at)
|
||||
end
|
||||
|
||||
@ -57,11 +68,10 @@ class InvoiceReferenceService
|
||||
##
|
||||
# Returns the number of current invoices in the given range around the current date.
|
||||
# If range is invalid or not specified, the total number of invoices is returned.
|
||||
# @param invoice {Invoice}
|
||||
# @param range {String} 'day', 'month', 'year'
|
||||
# @return {Integer}
|
||||
##
|
||||
def number_of_invoices(invoice, range)
|
||||
def number_of_invoices(range)
|
||||
case range.to_s
|
||||
when 'day'
|
||||
start = DateTime.current.beginning_of_day
|
||||
@ -73,7 +83,7 @@ class InvoiceReferenceService
|
||||
start = DateTime.current.beginning_of_year
|
||||
ending = DateTime.current.end_of_year
|
||||
else
|
||||
return invoice.id
|
||||
return get_max_id(Invoice) + get_max_id(PaymentSchedule)
|
||||
end
|
||||
return Invoice.count unless defined? start && defined? ending
|
||||
|
||||
@ -111,25 +121,32 @@ class InvoiceReferenceService
|
||||
##
|
||||
# Replace the invoice number elements in the provided pattern with counts from the database
|
||||
# @param reference {string}
|
||||
# @param invoice {Invoice}
|
||||
##
|
||||
def replace_invoice_number_pattern(reference, invoice)
|
||||
def replace_invoice_number_pattern(reference)
|
||||
copy = reference.dup
|
||||
|
||||
# invoice number per year (yy..yy)
|
||||
copy.gsub!(/y+(?![^\[]*\])/) do |match|
|
||||
pad_and_truncate(number_of_invoices(invoice, 'year'), match.to_s.length)
|
||||
pad_and_truncate(number_of_invoices('year'), match.to_s.length)
|
||||
end
|
||||
# invoice number per month (mm..mm)
|
||||
copy.gsub!(/m+(?![^\[]*\])/) do |match|
|
||||
pad_and_truncate(number_of_invoices(invoice, 'month'), match.to_s.length)
|
||||
pad_and_truncate(number_of_invoices('month'), match.to_s.length)
|
||||
end
|
||||
# invoice number per day (dd..dd)
|
||||
copy.gsub!(/d+(?![^\[]*\])/) do |match|
|
||||
pad_and_truncate(number_of_invoices(invoice, 'day'), match.to_s.length)
|
||||
pad_and_truncate(number_of_invoices('day'), match.to_s.length)
|
||||
end
|
||||
|
||||
copy
|
||||
end
|
||||
|
||||
##
|
||||
# Return the maximum ID from the database, for the given class
|
||||
# @param klass {ActiveRecord::Base}
|
||||
##
|
||||
def get_max_id(klass)
|
||||
ActiveRecord::Base.connection.execute("SELECT max(id) FROM #{klass.table_name}").getvalue(0, 0) || 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -28,7 +28,10 @@ class PaymentScheduleService
|
||||
items = []
|
||||
(0..deadlines - 1).each do |i|
|
||||
date = DateTime.current + i.months
|
||||
details = { recurring: per_month }
|
||||
amount = if i.zero?
|
||||
details[:adjustment] = adjustment
|
||||
details[:other_items] = other_items
|
||||
per_month + adjustment + other_items
|
||||
else
|
||||
per_month
|
||||
@ -36,9 +39,29 @@ class PaymentScheduleService
|
||||
items.push PaymentScheduleItem.new(
|
||||
amount: amount,
|
||||
due_date: date,
|
||||
payment_schedule: ps
|
||||
payment_schedule: ps,
|
||||
details: details
|
||||
)
|
||||
end
|
||||
{ payment_schedule: ps, items: items }
|
||||
end
|
||||
|
||||
def create(subscription, total, coupon: nil, operator: nil, payment_method: nil, reservation: nil, user: nil)
|
||||
schedule = compute(subscription.plan, total, coupon)
|
||||
ps = schedule[:payment_schedule]
|
||||
items = schedule[:items]
|
||||
|
||||
ps.scheduled = subscription
|
||||
ps.payment_method = payment_method
|
||||
ps.operator_profile = operator.invoicing_profile
|
||||
ps.invoicing_profile = user.invoicing_profile
|
||||
ps.save!
|
||||
items.each do |item|
|
||||
item.payment_schedule = ps
|
||||
item.save!
|
||||
end
|
||||
|
||||
StripeWorker.perform_async(:create_stripe_subscription, ps.id, reservation&.reservable&.stp_product_id)
|
||||
ps
|
||||
end
|
||||
end
|
||||
|
@ -9,11 +9,18 @@ class Subscriptions::Subscribe
|
||||
@operator_profile_id = operator_profile_id
|
||||
end
|
||||
|
||||
def pay_and_save(subscription, coupon: nil, invoice: nil, payment_intent_id: nil)
|
||||
##
|
||||
# @param subscription {Subscription}
|
||||
# @param coupon {String} coupon code
|
||||
# @param invoice {Boolean}
|
||||
# @param payment_intent_id {String} from stripe
|
||||
# @param schedule {Boolean}
|
||||
##
|
||||
def pay_and_save(subscription, coupon: nil, invoice: false, payment_intent_id: nil, schedule: false)
|
||||
return false if user_id.nil?
|
||||
|
||||
subscription.statistic_profile_id = StatisticProfile.find_by(user_id: user_id).id
|
||||
subscription.save_with_payment(operator_profile_id, invoice, coupon, payment_intent_id)
|
||||
subscription.save_with_payment(operator_profile_id, invoice, coupon, payment_intent_id, schedule)
|
||||
end
|
||||
|
||||
def extend_subscription(subscription, new_expiration_date, free_days)
|
||||
|
@ -23,6 +23,10 @@ $primary-decoration-color: <%= Stylesheet.primary_decoration_color %> !default;
|
||||
background-color: $primary;
|
||||
}
|
||||
|
||||
.header .nav.navbar-nav a.label:hover {
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
#nav .nav > li > a {
|
||||
color: $primary-text-color;
|
||||
}
|
||||
@ -166,6 +170,14 @@ h5:after {
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
.modal .modal-dialog .modal-content a {
|
||||
color: $primary;
|
||||
|
||||
&:hover {
|
||||
color: $primary-dark;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header h1,
|
||||
.fab-modal-header h1,
|
||||
.custom-invoice .modal-header h1 {
|
||||
|
@ -13,7 +13,7 @@ end
|
||||
if @amount[:schedule]
|
||||
json.schedule do
|
||||
json.items @amount[:schedule][:items] do |item|
|
||||
json.price item.amount / 100.00
|
||||
json.amount item.amount / 100.00
|
||||
json.due_date item.due_date
|
||||
end
|
||||
end
|
||||
|
@ -31,7 +31,7 @@ class StripeWorker
|
||||
stp_coupon[:percent_off] = coupon.percent_off
|
||||
elsif coupon.type == 'amount_off'
|
||||
stp_coupon[:amount_off] = coupon.amount_off
|
||||
stp_coupon[:currency] = Rails.application.secrets.stripe_currency
|
||||
stp_coupon[:currency] = Setting.get('stripe_currency')
|
||||
end
|
||||
|
||||
stp_coupon[:redeem_by] = coupon.valid_until.to_i unless coupon.valid_until.nil?
|
||||
@ -45,32 +45,79 @@ class StripeWorker
|
||||
cpn.delete
|
||||
end
|
||||
|
||||
def create_stripe_price(plan)
|
||||
product = if !plan.stp_price_id.nil?
|
||||
p = Stripe::Price.update(
|
||||
plan.stp_price_id,
|
||||
{ metadata: { archived: true } },
|
||||
{ api_key: Setting.get('stripe_secret_key') }
|
||||
)
|
||||
p.product
|
||||
else
|
||||
p = Stripe::Product.create(
|
||||
{
|
||||
name: plan.name,
|
||||
metadata: { plan_id: plan.id }
|
||||
}, { api_key: Setting.get('stripe_secret_key') }
|
||||
)
|
||||
p.id
|
||||
end
|
||||
def create_or_update_stp_product(class_name, id)
|
||||
object = class_name.constantize.find(id)
|
||||
if !object.stp_product_id.nil?
|
||||
Stripe::Product.update(
|
||||
object.stp_product_id,
|
||||
{ name: object.name },
|
||||
{ api_key: Setting.get('stripe_secret_key') }
|
||||
)
|
||||
p.product
|
||||
else
|
||||
product = Stripe::Product.create(
|
||||
{
|
||||
name: object.name,
|
||||
metadata: {
|
||||
id: object.id,
|
||||
type: class_name
|
||||
}
|
||||
}, { api_key: Setting.get('stripe_secret_key') }
|
||||
)
|
||||
object.update_attributes(stp_product_id: product.id)
|
||||
end
|
||||
end
|
||||
|
||||
price = Stripe::Price.create(
|
||||
{
|
||||
currency: Setting.get('stripe_currency'),
|
||||
unit_amount: plan.amount,
|
||||
product: product
|
||||
},
|
||||
{ api_key: Setting.get('stripe_secret_key') }
|
||||
)
|
||||
plan.update_columns(stp_price_id: price.id)
|
||||
def create_stripe_subscription(payment_schedule_id, reservable_stp_id)
|
||||
payment_schedule = PaymentSchedule.find(payment_schedule_id)
|
||||
|
||||
first_item = payment_schedule.ordered_items.first
|
||||
second_item = payment_schedule.ordered_items[1]
|
||||
|
||||
items = []
|
||||
if first_item.amount != second_item.amount
|
||||
unless first_item.details['adjustment']&.zero?
|
||||
# adjustment: when dividing the price of the plan / months, sometimes it forces us to round the amount per month.
|
||||
# The difference is invoiced here
|
||||
p1 = Stripe::Price.create({
|
||||
unit_amount: first_item.details['adjustment'],
|
||||
currency: Setting.get('stripe_currency'),
|
||||
product: payment_schedule.scheduled.plan.stp_product_id,
|
||||
nickname: "Price adjustment for payment schedule #{payment_schedule_id}"
|
||||
}, { api_key: Setting.get('stripe_secret_key') })
|
||||
items.push(price: p1[:id])
|
||||
end
|
||||
unless first_item.details['other_items']&.zero?
|
||||
# when taking a subscription at the same time of a reservation (space, machine or training), the amount of the
|
||||
# reservation is invoiced here.
|
||||
p2 = Stripe::Price.create({
|
||||
unit_amount: first_item.details['other_items'],
|
||||
currency: Setting.get('stripe_currency'),
|
||||
product: reservable_stp_id,
|
||||
nickname: "Reservations for payment schedule #{payment_schedule_id}"
|
||||
}, { api_key: Setting.get('stripe_secret_key') })
|
||||
items.push(price: p2[:id])
|
||||
end
|
||||
end
|
||||
|
||||
# subscription (recurring price)
|
||||
price = Stripe::Price.create({
|
||||
unit_amount: first_item.details['recurring'],
|
||||
currency: Setting.get('stripe_currency'),
|
||||
recurring: { interval: 'month', interval_count: 1 },
|
||||
product: payment_schedule.scheduled.plan.stp_product_id
|
||||
},
|
||||
{ api_key: Setting.get('stripe_secret_key') })
|
||||
|
||||
stp_subscription = Stripe::Subscription.create({
|
||||
customer: payment_schedule.invoicing_profile.user.stp_customer_id,
|
||||
cancel_at: payment_schedule.scheduled.expiration_date.to_i,
|
||||
promotion_code: payment_schedule.coupon&.code,
|
||||
add_invoice_items: items,
|
||||
items: [
|
||||
{ price: price[:id] }
|
||||
]
|
||||
}, { api_key: Setting.get('stripe_secret_key') })
|
||||
payment_schedule.update_attributes(stp_subscription_id: stp_subscription.id)
|
||||
end
|
||||
end
|
||||
|
@ -440,6 +440,7 @@ en:
|
||||
important_notes: "Important notes"
|
||||
address_and_legal_information: "Address and legal information"
|
||||
invoice_reference: "Invoice reference"
|
||||
text: "text"
|
||||
year: "Year"
|
||||
month: "Month"
|
||||
day: "Day"
|
||||
@ -447,6 +448,7 @@ en:
|
||||
online_sales: "Online sales"
|
||||
wallet: "Wallet"
|
||||
refund: "Refund"
|
||||
payment_schedule: "Payment schedule"
|
||||
model: "Model"
|
||||
documentation: "Documentation"
|
||||
2_digits_year: "2 digits year (eg. 70)"
|
||||
@ -469,9 +471,10 @@ en:
|
||||
eg_XVL_will_add_VL_to_the_invoices_settled_with_stripe: '(eg. X[/VL] will add "/VL" to the invoices settled with stripe)'
|
||||
add_a_notice_regarding_refunds_only_if_the_invoice_is_concerned: "Add a notice regarding refunds, only if the invoice is concerned."
|
||||
this_will_never_be_added_when_an_online_sales_notice_is_present: "This will never be added when an online sales notice is present."
|
||||
eg_RA_will_add_A_to_the_refund_invoices: '(ed. R[/A] will add "/A" to the refund invoices)'
|
||||
add_a_notice_regarding_the_wallet_only_if_the_invoice_is_concerned: "Add a notice regarding the wallet, only if the invoice is concerned."
|
||||
eg_WPM_will_add_PM_to_the_invoices_settled_with_wallet: '(eg. W[/PM] will add "/PM" to the invoices settled with wallet)'
|
||||
eg_RA_will_add_A_to_the_refund_invoices: '(eg. R[/A] will add "/A" to the refund invoices)'
|
||||
add_a_notice_regarding_payment_schedule: "Add a notice regarding the payment schedules, only for concerned documents."
|
||||
this_will_never_be_added_with_other_notices: "This will never be added when any other notice is present."
|
||||
eg_SE_to_schedules: '(eg. S[/E] will add "/E" to the payment schedules)'
|
||||
code: "Code"
|
||||
enable_the_code: "Enable the code"
|
||||
enabled: "Enabled"
|
||||
|
@ -440,6 +440,7 @@ fr:
|
||||
important_notes: "Informations importantes"
|
||||
address_and_legal_information: "Adresse et informations légales"
|
||||
invoice_reference: "Référence facture"
|
||||
text: "texte"
|
||||
year: "Année"
|
||||
month: "Mois"
|
||||
day: "Jour"
|
||||
@ -447,6 +448,7 @@ fr:
|
||||
online_sales: "Vente en ligne"
|
||||
wallet: "Porte-monnaie"
|
||||
refund: "Remboursement"
|
||||
payment_schedule: "Échéancier"
|
||||
model: "Modèle"
|
||||
documentation: "Documentation"
|
||||
2_digits_year: "Année sur 2 chiffres (ex. 70)"
|
||||
@ -470,8 +472,9 @@ fr:
|
||||
add_a_notice_regarding_refunds_only_if_the_invoice_is_concerned: "Ajoute une information relative aux remboursements, uniquement si cela concerne la facture."
|
||||
this_will_never_be_added_when_an_online_sales_notice_is_present: "Ceci ne sera jamais cumulé avec une information de vente en ligne."
|
||||
eg_RA_will_add_A_to_the_refund_invoices: '(ex. R[/A] ajoutera "/A" aux factures de remboursement)'
|
||||
add_a_notice_regarding_the_wallet_only_if_the_invoice_is_concerned: "Ajoute une information relative au paiement par le porte-monnaie, uniquement si cela concerne la facture."
|
||||
eg_WPM_will_add_PM_to_the_invoices_settled_with_wallet: '(ex. W[/PM] ajoutera "/PM" aux factures réglées avec porte-monnaie)'
|
||||
add_a_notice_regarding_payment_schedule: "Ajoute une information relative aux échéanciers de paiement, uniquement pour les documents concernés."
|
||||
this_will_never_be_added_with_other_notices: "Ceci ne sera jamais cumulé avec une autre information (remboursement ou vente en ligne)."
|
||||
eg_SE_to_schedules: '(ex. S[/É] ajoutera "/É" aux échéanciers de paiement)'
|
||||
code: "Code"
|
||||
enable_the_code: "Activer le code"
|
||||
enabled: "Activé"
|
||||
|
@ -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"
|
||||
@ -386,6 +398,7 @@ en:
|
||||
cart:
|
||||
summary: "Summary"
|
||||
select_one_or_more_slots_in_the_calendar: "Select one {SINGLE, select, true{slot} other{or more slots}} in the calendar"
|
||||
select_a_plan: "Select a plan here"
|
||||
you_ve_just_selected_the_slot: "You've just selected the slot:"
|
||||
datetime_to_time: "{START_DATETIME} to {END_TIME}" #eg: Thursday, September 4, 1986 8:30 PM to 10:00 PM
|
||||
cost_of_TYPE: "Cost of the {TYPE, select, Machine{machine slot} Training{training} Space{space slot} other{element}}"
|
||||
|
@ -129,7 +129,11 @@ fr:
|
||||
here_is_the_summary_of_the_slots_to_book_for_the_current_user: "Voici le récapitulatif des créneaux à réserver pour l'utilisateur courant :"
|
||||
subscription_confirmation: "Validation de l'abonnement"
|
||||
here_is_the_subscription_summary: "Voici le récapitulatif de l'abonnement :"
|
||||
payment_schedule: "Cet abonnement est souscrit par un échéancier de paiement sur {DEADLINES} mois. En validant, vous confirmez encaisser la première mensualité."
|
||||
payment_method: "Moyen de paiement"
|
||||
method_stripe: "Carte bancaire en ligne"
|
||||
method_check: "Par chèques"
|
||||
stripe_collection_info: "En validant, vous serez invité à saisir les informations de carte bancaire du membre. Cette carte sera prélevée automatiquement aux échéances."
|
||||
check_collection_info: "En validant, vous confirmez être en possession de {DEADLINES} chèques permettant d'encaisser l'ensemble des mensualité."
|
||||
#event edition form
|
||||
event:
|
||||
title: "Titre"
|
||||
@ -321,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"
|
||||
@ -386,6 +398,7 @@ fr:
|
||||
cart:
|
||||
summary: "Résumé"
|
||||
select_one_or_more_slots_in_the_calendar: "Sélectionnez un {SINGLE, select, true{créneau} other{ou plusieurs créneaux}} dans le calendrier"
|
||||
select_a_plan: "Sélectionnez une formule d'abonnement ici"
|
||||
you_ve_just_selected_the_slot: "Vous venez de sélectionner le créneau :"
|
||||
datetime_to_time: "{START_DATETIME} à {END_TIME}" #eg: Thursday, September 4, 1986 8:30 PM to 10:00 PM
|
||||
cost_of_TYPE: "Coût {TYPE, select, Machine{du créneau machine} Training{de la formation} Space{du créneau espace} other{de l'élément}}"
|
||||
@ -428,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."
|
||||
|
@ -1,3 +1,4 @@
|
||||
scr:
|
||||
date:
|
||||
abbr_day_names:
|
||||
- Ned
|
||||
@ -8,7 +9,7 @@
|
||||
- Pet
|
||||
- Sub
|
||||
abbr_month_names:
|
||||
-
|
||||
-
|
||||
- Jan
|
||||
- Feb
|
||||
- Mar
|
||||
@ -34,7 +35,7 @@
|
||||
long: ! '%B %e, %Y'
|
||||
short: ! '%e %b'
|
||||
month_names:
|
||||
-
|
||||
-
|
||||
- Januar
|
||||
- Februar
|
||||
- Mart
|
||||
|
@ -2,23 +2,20 @@
|
||||
|
||||
class MigratePlanStats < ActiveRecord::Migration[4.2]
|
||||
def up
|
||||
index = StatisticIndex.where({es_type_key: 'subscription'}).first
|
||||
if index
|
||||
StatisticType.where({statistic_index_id: index.id}).destroy_all
|
||||
index = StatisticIndex.where(es_type_key: 'subscription').first
|
||||
return unless index
|
||||
|
||||
Plan.all.each do |p|
|
||||
p.create_statistic_type
|
||||
end
|
||||
end
|
||||
StatisticType.where(statistic_index_id: index.id).destroy_all
|
||||
Plan.all.each(&:create_statistic_type)
|
||||
end
|
||||
|
||||
def down
|
||||
index = StatisticIndex.where({es_type_key: 'subscription'}).first
|
||||
if index
|
||||
StatisticType.where({statistic_index_id: index.id}).destroy_all
|
||||
index = StatisticIndex.where(es_type_key: 'subscription').first
|
||||
return unless index
|
||||
|
||||
StatisticType.create!({statistic_index_id: index.id, key: 'month', label: 'Abonnements mensuels', graph: true, simple: true})
|
||||
StatisticType.create!({statistic_index_id: index.id, key: 'year', label: 'Abonnements annuels', graph: true, simple: true})
|
||||
end
|
||||
StatisticType.where(statistic_index_id: index.id).destroy_all
|
||||
|
||||
StatisticType.create!(statistic_index_id: index.id, key: 'month', label: 'Abonnements mensuels', graph: true, simple: true)
|
||||
StatisticType.create!(statistic_index_id: index.id, key: 'year', label: 'Abonnements annuels', graph: true, simple: true)
|
||||
end
|
||||
end
|
||||
|
@ -16,7 +16,7 @@ class CreatePaymentSchedules < ActiveRecord::Migration[5.2]
|
||||
t.string :footprint
|
||||
t.string :environment
|
||||
t.belongs_to :invoicing_profile, foreign_key: true
|
||||
t.references :operator_profile_id, foreign_key: { to_table: 'invoicing_profiles' }
|
||||
t.references :operator_profile, foreign_key: { to_table: 'invoicing_profiles' }
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
@ -6,7 +6,9 @@ class CreatePaymentScheduleItems < ActiveRecord::Migration[5.2]
|
||||
create_table :payment_schedule_items do |t|
|
||||
t.integer :amount
|
||||
t.datetime :due_date
|
||||
t.jsonb :details, default: '{}'
|
||||
t.belongs_to :payment_schedule, foreign_key: true
|
||||
t.belongs_to :invoice, foreign_key: true
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
@ -1,9 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Save the id of the Stripe::Price associated with the current plan.
|
||||
# This is used for payment schedules
|
||||
class AddStpPriceIdToPlan < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
add_column :plans, :stp_price_id, :string
|
||||
end
|
||||
end
|
16
db/migrate/20201112092002_add_stp_product_id_to_objects.rb
Normal file
16
db/migrate/20201112092002_add_stp_product_id_to_objects.rb
Normal file
@ -0,0 +1,16 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Save the id of the Stripe::Product associated with the current plan or reservable object.
|
||||
# This is used for payment schedules.
|
||||
# Machines, Trainings and Spaces can be reserved jointly with a subscription that can have a
|
||||
# payment schedule, so we must associate them with Stripe::Product.
|
||||
# This is not the case for Events (we can't buy event+subscription) so we dot no associate a
|
||||
# Stripe::Product with the events.
|
||||
class AddStpProductIdToObjects < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
add_column :plans, :stp_product_id, :string
|
||||
add_column :machines, :stp_product_id, :string
|
||||
add_column :spaces, :stp_product_id, :string
|
||||
add_column :trainings, :stp_product_id, :string
|
||||
end
|
||||
end
|
@ -588,7 +588,7 @@ unless Setting.find_by(name: 'invoice_logo').try(:value)
|
||||
setting.save
|
||||
end
|
||||
|
||||
Setting.set('invoice_reference', 'YYMMmmmX[/VL]R[/A]') unless Setting.find_by(name: 'invoice_reference').try(:value)
|
||||
Setting.set('invoice_reference', 'YYMMmmmX[/VL]R[/A]S[/E]') unless Setting.find_by(name: 'invoice_reference').try(:value)
|
||||
|
||||
Setting.set('invoice_code-active', true) unless Setting.find_by(name: 'invoice_code-active').try(:value)
|
||||
|
||||
|
109
db/structure.sql
109
db/structure.sql
@ -108,8 +108,8 @@ SET default_tablespace = '';
|
||||
|
||||
CREATE TABLE public.abuses (
|
||||
id integer NOT NULL,
|
||||
signaled_type character varying,
|
||||
signaled_id integer,
|
||||
signaled_type character varying,
|
||||
first_name character varying,
|
||||
last_name character varying,
|
||||
email character varying,
|
||||
@ -187,8 +187,8 @@ CREATE TABLE public.addresses (
|
||||
locality character varying,
|
||||
country character varying,
|
||||
postal_code character varying,
|
||||
placeable_type character varying,
|
||||
placeable_id integer,
|
||||
placeable_type character varying,
|
||||
created_at timestamp without time zone,
|
||||
updated_at timestamp without time zone
|
||||
);
|
||||
@ -263,8 +263,8 @@ CREATE TABLE public.ar_internal_metadata (
|
||||
|
||||
CREATE TABLE public.assets (
|
||||
id integer NOT NULL,
|
||||
viewable_type character varying,
|
||||
viewable_id integer,
|
||||
viewable_type character varying,
|
||||
attachment character varying,
|
||||
type character varying,
|
||||
created_at timestamp without time zone,
|
||||
@ -504,8 +504,8 @@ ALTER SEQUENCE public.coupons_id_seq OWNED BY public.coupons.id;
|
||||
|
||||
CREATE TABLE public.credits (
|
||||
id integer NOT NULL,
|
||||
creditable_type character varying,
|
||||
creditable_id integer,
|
||||
creditable_type character varying,
|
||||
plan_id integer,
|
||||
hours integer,
|
||||
created_at timestamp without time zone,
|
||||
@ -1046,8 +1046,8 @@ ALTER SEQUENCE public.invoice_items_id_seq OWNED BY public.invoice_items.id;
|
||||
|
||||
CREATE TABLE public.invoices (
|
||||
id integer NOT NULL,
|
||||
invoiced_type character varying,
|
||||
invoiced_id integer,
|
||||
invoiced_type character varying,
|
||||
stp_invoice_id character varying,
|
||||
total integer,
|
||||
created_at timestamp without time zone,
|
||||
@ -1166,7 +1166,8 @@ CREATE TABLE public.machines (
|
||||
created_at timestamp without time zone,
|
||||
updated_at timestamp without time zone,
|
||||
slug character varying,
|
||||
disabled boolean
|
||||
disabled boolean,
|
||||
stp_product_id character varying
|
||||
);
|
||||
|
||||
|
||||
@ -1226,15 +1227,15 @@ ALTER SEQUENCE public.machines_id_seq OWNED BY public.machines.id;
|
||||
CREATE TABLE public.notifications (
|
||||
id integer NOT NULL,
|
||||
receiver_id integer,
|
||||
attached_object_type character varying,
|
||||
attached_object_id integer,
|
||||
attached_object_type character varying,
|
||||
notification_type_id integer,
|
||||
is_read boolean DEFAULT false,
|
||||
created_at timestamp without time zone,
|
||||
updated_at timestamp without time zone,
|
||||
receiver_type character varying,
|
||||
is_send boolean DEFAULT false,
|
||||
meta_data jsonb DEFAULT '"{}"'::jsonb
|
||||
meta_data jsonb DEFAULT '{}'::jsonb
|
||||
);
|
||||
|
||||
|
||||
@ -1469,7 +1470,9 @@ CREATE TABLE public.payment_schedule_items (
|
||||
id bigint NOT NULL,
|
||||
amount integer,
|
||||
due_date timestamp without time zone,
|
||||
details jsonb DEFAULT '"{}"'::jsonb,
|
||||
payment_schedule_id bigint,
|
||||
invoice_id bigint,
|
||||
created_at timestamp without time zone NOT NULL,
|
||||
updated_at timestamp without time zone NOT NULL
|
||||
);
|
||||
@ -1512,7 +1515,7 @@ CREATE TABLE public.payment_schedules (
|
||||
footprint character varying,
|
||||
environment character varying,
|
||||
invoicing_profile_id bigint,
|
||||
operator_profile_id_id bigint,
|
||||
operator_profile_id bigint,
|
||||
created_at timestamp without time zone NOT NULL,
|
||||
updated_at timestamp without time zone NOT NULL
|
||||
);
|
||||
@ -1560,7 +1563,7 @@ CREATE TABLE public.plans (
|
||||
slug character varying,
|
||||
disabled boolean,
|
||||
monthly_payment boolean,
|
||||
stp_price_id character varying
|
||||
stp_product_id character varying
|
||||
);
|
||||
|
||||
|
||||
@ -1653,8 +1656,8 @@ CREATE TABLE public.prices (
|
||||
id integer NOT NULL,
|
||||
group_id integer,
|
||||
plan_id integer,
|
||||
priceable_type character varying,
|
||||
priceable_id integer,
|
||||
priceable_type character varying,
|
||||
amount integer,
|
||||
created_at timestamp without time zone NOT NULL,
|
||||
updated_at timestamp without time zone NOT NULL
|
||||
@ -1969,8 +1972,8 @@ CREATE TABLE public.reservations (
|
||||
message text,
|
||||
created_at timestamp without time zone,
|
||||
updated_at timestamp without time zone,
|
||||
reservable_type character varying,
|
||||
reservable_id integer,
|
||||
reservable_type character varying,
|
||||
nb_reserve_places integer,
|
||||
statistic_profile_id integer
|
||||
);
|
||||
@ -2002,8 +2005,8 @@ ALTER SEQUENCE public.reservations_id_seq OWNED BY public.reservations.id;
|
||||
CREATE TABLE public.roles (
|
||||
id integer NOT NULL,
|
||||
name character varying,
|
||||
resource_type character varying,
|
||||
resource_id integer,
|
||||
resource_type character varying,
|
||||
created_at timestamp without time zone,
|
||||
updated_at timestamp without time zone
|
||||
);
|
||||
@ -2149,7 +2152,8 @@ CREATE TABLE public.spaces (
|
||||
created_at timestamp without time zone NOT NULL,
|
||||
updated_at timestamp without time zone NOT NULL,
|
||||
characteristics text,
|
||||
disabled boolean
|
||||
disabled boolean,
|
||||
stp_product_id character varying
|
||||
);
|
||||
|
||||
|
||||
@ -2679,7 +2683,8 @@ CREATE TABLE public.trainings (
|
||||
slug character varying,
|
||||
description text,
|
||||
public_page boolean DEFAULT true,
|
||||
disabled boolean
|
||||
disabled boolean,
|
||||
stp_product_id character varying
|
||||
);
|
||||
|
||||
|
||||
@ -2937,8 +2942,8 @@ CREATE TABLE public.users_roles (
|
||||
CREATE TABLE public.wallet_transactions (
|
||||
id integer NOT NULL,
|
||||
wallet_id integer,
|
||||
transactable_type character varying,
|
||||
transactable_id integer,
|
||||
transactable_type character varying,
|
||||
transaction_type character varying,
|
||||
amount integer,
|
||||
created_at timestamp without time zone NOT NULL,
|
||||
@ -4027,14 +4032,6 @@ ALTER TABLE ONLY public.roles
|
||||
ADD CONSTRAINT roles_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: schema_migrations schema_migrations_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.schema_migrations
|
||||
ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version);
|
||||
|
||||
|
||||
--
|
||||
-- Name: settings settings_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
@ -4532,6 +4529,13 @@ CREATE INDEX index_open_api_calls_count_tracings_on_open_api_client_id ON public
|
||||
CREATE INDEX index_organizations_on_invoicing_profile_id ON public.organizations USING btree (invoicing_profile_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_payment_schedule_items_on_invoice_id; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX index_payment_schedule_items_on_invoice_id ON public.payment_schedule_items USING btree (invoice_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_payment_schedule_items_on_payment_schedule_id; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
@ -4554,10 +4558,10 @@ CREATE INDEX index_payment_schedules_on_invoicing_profile_id ON public.payment_s
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_payment_schedules_on_operator_profile_id_id; Type: INDEX; Schema: public; Owner: -
|
||||
-- Name: index_payment_schedules_on_operator_profile_id; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX index_payment_schedules_on_operator_profile_id_id ON public.payment_schedules USING btree (operator_profile_id_id);
|
||||
CREATE INDEX index_payment_schedules_on_operator_profile_id ON public.payment_schedules USING btree (operator_profile_id);
|
||||
|
||||
|
||||
--
|
||||
@ -5092,6 +5096,29 @@ CREATE INDEX profiles_lower_unaccent_last_name_trgm_idx ON public.profiles USING
|
||||
CREATE INDEX projects_search_vector_idx ON public.projects USING gin (search_vector);
|
||||
|
||||
|
||||
--
|
||||
-- Name: unique_schema_migrations; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE UNIQUE INDEX unique_schema_migrations ON public.schema_migrations USING btree (version);
|
||||
|
||||
|
||||
--
|
||||
-- Name: accounting_periods accounting_periods_del_protect; Type: RULE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE RULE accounting_periods_del_protect AS
|
||||
ON DELETE TO public.accounting_periods DO INSTEAD NOTHING;
|
||||
|
||||
|
||||
--
|
||||
-- Name: accounting_periods accounting_periods_upd_protect; Type: RULE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE RULE accounting_periods_upd_protect AS
|
||||
ON UPDATE TO public.accounting_periods DO INSTEAD NOTHING;
|
||||
|
||||
|
||||
--
|
||||
-- Name: projects projects_search_content_trigger; Type: TRIGGER; Schema: public; Owner: -
|
||||
--
|
||||
@ -5275,6 +5302,14 @@ ALTER TABLE ONLY public.event_price_categories
|
||||
ADD CONSTRAINT fk_rails_4dc2c47476 FOREIGN KEY (price_category_id) REFERENCES public.price_categories(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: payment_schedule_items fk_rails_4e9d79c566; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.payment_schedule_items
|
||||
ADD CONSTRAINT fk_rails_4e9d79c566 FOREIGN KEY (invoice_id) REFERENCES public.invoices(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: payment_schedules fk_rails_552bc65163; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
@ -5347,6 +5382,14 @@ ALTER TABLE ONLY public.projects_machines
|
||||
ADD CONSTRAINT fk_rails_88b280c24c FOREIGN KEY (machine_id) REFERENCES public.machines(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: payment_schedules fk_rails_8b73dd8d7d; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.payment_schedules
|
||||
ADD CONSTRAINT fk_rails_8b73dd8d7d FOREIGN KEY (operator_profile_id) REFERENCES public.invoicing_profiles(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: availability_tags fk_rails_8cb4e921f7; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
@ -5411,14 +5454,6 @@ ALTER TABLE ONLY public.projects_themes
|
||||
ADD CONSTRAINT fk_rails_b021a22658 FOREIGN KEY (theme_id) REFERENCES public.themes(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: payment_schedules fk_rails_b38f5b39f6; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.payment_schedules
|
||||
ADD CONSTRAINT fk_rails_b38f5b39f6 FOREIGN KEY (operator_profile_id_id) REFERENCES public.invoicing_profiles(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: statistic_profiles fk_rails_bba64e5eb9; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
@ -5618,6 +5653,7 @@ INSERT INTO "schema_migrations" (version) VALUES
|
||||
('20140605125131'),
|
||||
('20140605142133'),
|
||||
('20140605151442'),
|
||||
('20140606133116'),
|
||||
('20140609092700'),
|
||||
('20140609092827'),
|
||||
('20140610153123'),
|
||||
@ -5686,12 +5722,14 @@ INSERT INTO "schema_migrations" (version) VALUES
|
||||
('20150507075620'),
|
||||
('20150512123546'),
|
||||
('20150520132030'),
|
||||
('20150520133409'),
|
||||
('20150526130729'),
|
||||
('20150527153312'),
|
||||
('20150529113555'),
|
||||
('20150601125944'),
|
||||
('20150603104502'),
|
||||
('20150603104658'),
|
||||
('20150603133050'),
|
||||
('20150604081757'),
|
||||
('20150604131525'),
|
||||
('20150608142234'),
|
||||
@ -5773,6 +5811,7 @@ INSERT INTO "schema_migrations" (version) VALUES
|
||||
('20160905142700'),
|
||||
('20160906094739'),
|
||||
('20160906094847'),
|
||||
('20160906145713'),
|
||||
('20160915105234'),
|
||||
('20161123104604'),
|
||||
('20170109085345'),
|
||||
@ -5841,6 +5880,6 @@ INSERT INTO "schema_migrations" (version) VALUES
|
||||
('20201027092149'),
|
||||
('20201027100746'),
|
||||
('20201027101809'),
|
||||
('20201027145651');
|
||||
('20201112092002');
|
||||
|
||||
|
||||
|
@ -77,5 +77,6 @@ This is currently not supported, because of some PostgreSQL specific instruction
|
||||
- `app/models/project.rb` is using the `pg_search` gem.
|
||||
- `db/migrate/20200622135401_add_pg_search_dmetaphone_support_functions.rb` is using [fuzzystrmatch](http://www.postgresql.org/docs/current/static/fuzzystrmatch.html) module and defines a PL/pgSQL function (`pg_search_dmetaphone()`);
|
||||
- `db/migrate/20200623134900_add_search_vector_to_project.rb` is using [tsvector](https://www.postgresql.org/docs/10/datatype-textsearch.html), a PostgreSQL datatype and [GIN (Generalized Inverted Index)](https://www.postgresql.org/docs/9.1/textsearch-indexes.html) a PostgreSQL index type;
|
||||
- `db/migrate/20200623141305_update_search_vector_of_projects.rb` is defines a PL/pgSQL function (`fill_search_vector_for_project()`) and create an SQL trigger for this function;
|
||||
- `db/migrate/20200623141305_update_search_vector_of_projects.rb` defines a PL/pgSQL function (`fill_search_vector_for_project()`) and create an SQL trigger for this function;
|
||||
- `db/migrate/20200629123011_update_pg_trgm.rb` is using [ALTER EXTENSION](https://www.postgresql.org/docs/10/sql-alterextension.html);
|
||||
- `db/migrate/20201027101809_create_payment_schedule_items.rb` is using [jsonb](https://www.postgresql.org/docs/9.4/static/datatype-json.html);
|
||||
|
@ -136,9 +136,9 @@ docker-compose run --rm -e VAR1=xxx -e VAR2=xxx fabmanager bundle exec rails my:
|
||||
## Easy upgrade
|
||||
|
||||
Starting with Fab-manager v4.5.0, you can upgrade Fab-manager in one single easy command, that automates the procedure below.
|
||||
To upgrade with ease, using this helper, read the GitHub release notes of the version between your current version and the target version.
|
||||
To upgrade with ease, using this command, read the GitHub release notes of all versions between your current version and the target version.
|
||||
|
||||
**You MUST append all the arguments** of easy upgrade commands, for **each versions**, to the command you run.
|
||||
**You MUST append all the arguments** of the easy upgrade commands, for **each version**, to the command you run.
|
||||
|
||||
Eg.
|
||||
If you upgrade from 1.2.3 to 1.2.5, with the following release notes:
|
||||
|
@ -101,6 +101,13 @@ namespace :fablab do
|
||||
FileUtils.mv 'tmp/invoices', 'invoices'
|
||||
end
|
||||
|
||||
desc 'add model for payment-schedules reference'
|
||||
task add_schedule_reference: :environment do
|
||||
setting = Setting.find_by(name: 'invoice_reference')
|
||||
current = setting.value
|
||||
setting.value = "#{current}S[/E]" unless /S\[([^\]]+)\]/.match?(current)
|
||||
end
|
||||
|
||||
desc 'migrate environment variables to the database (settings)'
|
||||
task env_to_db: :environment do
|
||||
include ApplicationHelper
|
||||
|
@ -53,13 +53,20 @@ namespace :fablab do
|
||||
puts 'Done'
|
||||
end
|
||||
|
||||
desc 'set stp_price_id to plans'
|
||||
task plans_prices: :environment do
|
||||
puts 'No plans, exiting...' and return if Plan.count.zero?
|
||||
|
||||
desc 'set stp_product_id to all plans/machines/trainings/spaces'
|
||||
task set_product_id: :environment do
|
||||
w = StripeWorker.new
|
||||
Plan.all.each do |p|
|
||||
w.perform(:create_stripe_price, p)
|
||||
w.perform(:create_or_update_stp_product, Plan.name, p.id)
|
||||
end
|
||||
Machine.all.each do |m|
|
||||
w.perform(:create_or_update_stp_product, Machine.name, m.id)
|
||||
end
|
||||
Training.all.each do |t|
|
||||
w.perform(:create_or_update_stp_product, Training.name, t.id)
|
||||
end
|
||||
Space.all.each do |s|
|
||||
w.perform(:create_or_update_stp_product, Space.name, s.id)
|
||||
end
|
||||
end
|
||||
|
||||
|
2
test/fixtures/history_values.yml
vendored
2
test/fixtures/history_values.yml
vendored
@ -100,7 +100,7 @@ value_history_10:
|
||||
id: 10
|
||||
setting_id: 10
|
||||
invoicing_profile_id: 1
|
||||
value: YYMMmmmX[/VL]R[/A]
|
||||
value: YYMMmmmX[/VL]R[/A]S[/E]
|
||||
created_at: 2018-12-17 11:23:01.603733000 Z
|
||||
updated_at: 2018-12-17 11:23:01.603733000 Z
|
||||
footprint: ed23a2eb1903befc977621bc3c3b19aad831fe550ebaa99e9299238b3d93c275
|
||||
|
Loading…
x
Reference in New Issue
Block a user