mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-20 14:54:15 +01:00
compute price according to prepaid hours
Also: handle prepaid hours decreasing when used
This commit is contained in:
parent
20bd3931d4
commit
6c326c7209
@ -24,9 +24,10 @@ interface PacksSummaryProps {
|
||||
operator: User,
|
||||
onError: (message: string) => void,
|
||||
onSuccess: (message: string) => void,
|
||||
refresh?: Promise<void>
|
||||
}
|
||||
|
||||
const PacksSummaryComponent: React.FC<PacksSummaryProps> = ({ item, itemType, customer, operator, onError, onSuccess }) => {
|
||||
const PacksSummaryComponent: React.FC<PacksSummaryProps> = ({ item, itemType, customer, operator, onError, onSuccess, refresh }) => {
|
||||
const { t } = useTranslation('logged');
|
||||
|
||||
const [userPacks, setUserPacks] = useState<Array<UserPack>>(null);
|
||||
@ -42,10 +43,23 @@ const PacksSummaryComponent: React.FC<PacksSummaryProps> = ({ item, itemType, cu
|
||||
useEffect(() => {
|
||||
if (_.isEmpty(customer)) return;
|
||||
|
||||
getUserPacksData();
|
||||
}, [item, itemType, customer]);
|
||||
|
||||
useEffect(() => {
|
||||
if (refresh instanceof Promise) {
|
||||
refresh.then(getUserPacksData);
|
||||
}
|
||||
}, [refresh]);
|
||||
|
||||
/**
|
||||
* Fetch the user packs data from the API
|
||||
*/
|
||||
const getUserPacksData = (): void => {
|
||||
UserPackAPI.index({ user_id: customer.id, priceable_type: itemType, priceable_id: item.id })
|
||||
.then(data => setUserPacks(data))
|
||||
.catch(error => onError(error));
|
||||
}, [item, itemType, customer]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Total of minutes used by the customer
|
||||
@ -108,7 +122,10 @@ const PacksSummaryComponent: React.FC<PacksSummaryProps> = ({ item, itemType, cu
|
||||
<div className="packs-summary">
|
||||
<h3>{t('app.logged.packs_summary.prepaid_hours')}</h3>
|
||||
<div className="content">
|
||||
<span className="remaining-hours">{t('app.logged.packs_summary.remaining_HOURS', { HOURS: totalHours(), ITEM: itemType })}</span>
|
||||
<span className="remaining-hours">
|
||||
{totalHours() > 0 && t('app.logged.packs_summary.remaining_HOURS', { HOURS: totalHours(), ITEM: itemType })}
|
||||
{totalHours() === 0 && t('app.logged.packs_summary.no_hours', { ITEM: itemType })}
|
||||
</span>
|
||||
{shouldDisplayButton() && <div className="button-wrapper">
|
||||
<FabButton className="buy-button" onClick={togglePacksModal} icon={<i className="fa fa-shopping-cart"/>}>
|
||||
{t('app.logged.packs_summary.buy_a_new_pack')}
|
||||
@ -128,12 +145,12 @@ const PacksSummaryComponent: React.FC<PacksSummaryProps> = ({ item, itemType, cu
|
||||
);
|
||||
}
|
||||
|
||||
export const PacksSummary: React.FC<PacksSummaryProps> = ({ item, itemType, customer, operator, onError, onSuccess }) => {
|
||||
export const PacksSummary: React.FC<PacksSummaryProps> = ({ item, itemType, customer, operator, onError, onSuccess, refresh }) => {
|
||||
return (
|
||||
<Loader>
|
||||
<PacksSummaryComponent item={item} itemType={itemType} customer={customer} operator={operator} onError={onError} onSuccess={onSuccess} />
|
||||
<PacksSummaryComponent item={item} itemType={itemType} customer={customer} operator={operator} onError={onError} onSuccess={onSuccess} refresh={refresh} />
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
|
||||
Application.Components.component('packsSummary', react2angular(PacksSummary, ['item', 'itemType', 'customer', 'operator', 'onError', 'onSuccess']));
|
||||
Application.Components.component('packsSummary', react2angular(PacksSummary, ['item', 'itemType', 'customer', 'operator', 'onError', 'onSuccess', 'refresh']));
|
||||
|
@ -415,6 +415,9 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$stat
|
||||
// current machine to reserve
|
||||
$scope.machine = machinePromise;
|
||||
|
||||
// will be set to a Promise and resolved after the payment is sone
|
||||
$scope.afterPaymentPromise = null;
|
||||
|
||||
// fullCalendar (v2) configuration
|
||||
$scope.calendarConfig = CalendarConfig({
|
||||
minTime: moment.duration(moment(settingsPromise.booking_window_start).format('HH:mm:ss')),
|
||||
@ -618,6 +621,14 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$stat
|
||||
}
|
||||
|
||||
refetchCalendar();
|
||||
|
||||
// trigger the refresh of react components
|
||||
setTimeout(() => {
|
||||
$scope.afterPaymentPromise = new Promise(resolve => {
|
||||
resolve();
|
||||
});
|
||||
$scope.$apply();
|
||||
}, 50);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -34,7 +34,8 @@
|
||||
customer="ctrl.member"
|
||||
operator="currentUser"
|
||||
on-error="onError"
|
||||
on-success="onSuccess">
|
||||
on-success="onSuccess"
|
||||
refresh="afterPaymentPromise">
|
||||
</packs-summary>
|
||||
|
||||
<cart slot="selectedEvent"
|
||||
|
@ -3,7 +3,7 @@
|
||||
MINUTES_PER_HOUR = 60.0
|
||||
SECONDS_PER_MINUTE = 60.0
|
||||
|
||||
GET_SLOT_PRICE_DEFAULT_OPTS = { has_credits: false, elements: nil, is_division: true, prepaid_minutes: 0 }.freeze
|
||||
GET_SLOT_PRICE_DEFAULT_OPTS = { has_credits: false, elements: nil, is_division: true, prepaid: { minutes: 0 } }.freeze
|
||||
|
||||
# A generic reservation added to the shopping cart
|
||||
class CartItem::Reservation < CartItem::BaseItem
|
||||
@ -18,7 +18,7 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
def price
|
||||
base_amount = @reservable.prices.find_by(group_id: @customer.group_id, plan_id: @plan.try(:id)).amount
|
||||
is_privileged = @operator.privileged? && @operator.id != @customer.id
|
||||
prepaid_minutes = get_prepaid_minutes(@customer, @reservable)
|
||||
prepaid = { minutes: PrepaidPackService.minutes_available(@customer, @reservable) }
|
||||
|
||||
elements = { slots: [] }
|
||||
amount = 0
|
||||
@ -28,7 +28,7 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
amount += get_slot_price(base_amount, slot, is_privileged,
|
||||
elements: elements,
|
||||
has_credits: (index < hours_available),
|
||||
prepaid_minutes: prepaid_minutes)
|
||||
prepaid: prepaid)
|
||||
end
|
||||
|
||||
{ elements: elements, amount: amount }
|
||||
@ -78,13 +78,19 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
options = GET_SLOT_PRICE_DEFAULT_OPTS.merge(options)
|
||||
|
||||
slot_rate = options[:has_credits] || (slot[:offered] && is_privileged) ? 0 : hourly_rate
|
||||
slot_minutes = (slot[:end_at].to_time - slot[:start_at].to_time) / SECONDS_PER_MINUTE
|
||||
# apply the base price to the real slot duration
|
||||
real_price = if options[:is_division]
|
||||
(slot_rate / MINUTES_PER_HOUR) * ((slot[:end_at].to_time - slot[:start_at].to_time) / SECONDS_PER_MINUTE)
|
||||
(slot_rate / MINUTES_PER_HOUR) * slot_minutes
|
||||
else
|
||||
slot_rate
|
||||
end
|
||||
if real_price.positive? && options[:prepaid_minutes].positive?
|
||||
# TODO, remove prepaid minutes
|
||||
# subtract free minutes from prepaid packs
|
||||
if real_price.positive? && options[:prepaid][:minutes] >= slot_minutes
|
||||
consumed = slot_minutes
|
||||
consumed = options[:prepaid][:minutes] if slot_minutes > options[:prepaid][:minutes]
|
||||
real_price = (slot_minutes - consumed) * (slot_rate / MINUTES_PER_HOUR)
|
||||
options[:prepaid][:minutes] -= consumed
|
||||
end
|
||||
|
||||
unless options[:elements].nil?
|
||||
@ -97,14 +103,6 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
real_price
|
||||
end
|
||||
|
||||
def get_prepaid_minutes(user, priceable)
|
||||
user_packs = PrepaidPackService.user_packs(user, priceable)
|
||||
total_available = user_packs.map { |up| up.prepaid_pack.minutes }.reduce(:+) || 0
|
||||
total_used = user_packs.map(&:minutes_used).reduce(:+) || 0
|
||||
|
||||
total_available - total_used
|
||||
end
|
||||
|
||||
##
|
||||
# Compute the number of remaining hours in the users current credits (for machine or space)
|
||||
##
|
||||
|
@ -56,6 +56,7 @@ class ShoppingCart
|
||||
objects.push(save_item(item))
|
||||
end
|
||||
update_credits(objects)
|
||||
update_packs(objects)
|
||||
|
||||
payment = create_payment_document(price, objects, payment_id, payment_type)
|
||||
WalletService.debit_user_wallet(payment, @customer)
|
||||
@ -119,4 +120,12 @@ class ShoppingCart
|
||||
UsersCredits::Manager.new(reservation: r).update_credits
|
||||
end
|
||||
end
|
||||
|
||||
# Handle the update of the user's prepaid-packs
|
||||
# The total booked minutes are subtracted from the user's prepaid minutes
|
||||
def update_packs(objects)
|
||||
objects.filter { |o| o.is_a? Reservation }.each do |reservation|
|
||||
PrepaidPackService.update_user_minutes(@customer, reservation)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -190,7 +190,7 @@ class InvoicesService
|
||||
|
||||
invoice.invoice_items.push InvoiceItem.new(
|
||||
amount: payment_details[:elements][:pack],
|
||||
description: I18n.t('invoices.pack_item', COUNT: pack.prepaid_pack.minutes / 60),
|
||||
description: I18n.t('invoices.pack_item', COUNT: pack.prepaid_pack.minutes / 60, ITEM: pack.prepaid_pack.priceable.name),
|
||||
object: pack,
|
||||
main: main
|
||||
)
|
||||
|
@ -1,5 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
SECONDS_PER_MINUTE = 60.0
|
||||
|
||||
# Provides methods for PrepaidPack
|
||||
class PrepaidPackService
|
||||
class << self
|
||||
@ -28,5 +30,43 @@ class PrepaidPackService
|
||||
.where('prepaid_packs.priceable_id = ?', priceable.id)
|
||||
.where('prepaid_packs.priceable_type = ?', priceable.class.name)
|
||||
end
|
||||
|
||||
# subtract the number of used prepaid minutes from the user's count
|
||||
def update_user_minutes(user, reservation)
|
||||
# total number of minutes available in user's packs
|
||||
available_minutes = minutes_available(user, reservation.reservable)
|
||||
return if available_minutes.zero?
|
||||
|
||||
# total number of minutes in the reservation's slots
|
||||
slots_minutes = reservation.slots.map do |slot|
|
||||
(slot.end_at.to_time - slot.start_at.to_time) / SECONDS_PER_MINUTE
|
||||
end
|
||||
reservation_minutes = slots_minutes.reduce(:+) || 0
|
||||
|
||||
# total number of prepaid minutes used by this reservation
|
||||
consumed = reservation_minutes
|
||||
consumed = available_minutes if reservation_minutes > available_minutes
|
||||
|
||||
# subtract the consumed minutes for user's current packs
|
||||
packs = user_packs(user, reservation.reservable).order(minutes_used: :desc)
|
||||
packs.each do |pack|
|
||||
pack_available = pack.prepaid_pack.minutes - pack.minutes_used
|
||||
remaining = pack_available - consumed
|
||||
remaining = 0 if remaining.negative?
|
||||
pack_consumed = pack.prepaid_pack.minutes - remaining
|
||||
pack.update_attributes(minutes_used: pack_consumed)
|
||||
|
||||
consumed -= pack_consumed
|
||||
end
|
||||
end
|
||||
|
||||
## Total number of prepaid minutes available
|
||||
def minutes_available(user, priceable)
|
||||
user_packs = user_packs(user, priceable)
|
||||
total_available = user_packs.map { |up| up.prepaid_pack.minutes }.reduce(:+) || 0
|
||||
total_used = user_packs.map(&:minutes_used).reduce(:+) || 0
|
||||
|
||||
total_available - total_used
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -194,6 +194,7 @@ en:
|
||||
packs_summary:
|
||||
prepaid_hours: "Prepaid hours"
|
||||
remaining_HOURS: "You have {HOURS} prepaid hours remaining for this {ITEM, select, Machine{machine} Space{space} other{}}."
|
||||
no_hours: "You don't have any prepaid hours for this {ITEM, select, Machine{machine} Space{space} other{}}."
|
||||
buy_a_new_pack: "Buy a new pack"
|
||||
#book a training
|
||||
trainings_reserve:
|
||||
|
@ -114,7 +114,7 @@ en:
|
||||
invoice_text_example: "Our association is not subject to VAT"
|
||||
error_invoice: "Erroneous invoice. The items below ware not booked. Please contact the FabLab for a refund."
|
||||
prepaid_pack: "Prepaid pack of hours"
|
||||
pack_item: "Pack of %{COUNT} hours"
|
||||
pack_item: "Pack of %{COUNT} hours for the %{ITEM}"
|
||||
#PDF payment schedule generation
|
||||
payment_schedules:
|
||||
schedule_reference: "Payment schedule reference: %{REF}"
|
||||
|
Loading…
x
Reference in New Issue
Block a user