1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-21 15:54:22 +01:00

compute price according to prepaid hours

Also: handle prepaid hours decreasing when used
This commit is contained in:
Sylvain 2021-06-30 10:53:05 +02:00
parent 20bd3931d4
commit 6c326c7209
9 changed files with 100 additions and 23 deletions

View File

@ -24,9 +24,10 @@ interface PacksSummaryProps {
operator: User, operator: User,
onError: (message: string) => void, onError: (message: string) => void,
onSuccess: (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 { t } = useTranslation('logged');
const [userPacks, setUserPacks] = useState<Array<UserPack>>(null); const [userPacks, setUserPacks] = useState<Array<UserPack>>(null);
@ -42,10 +43,23 @@ const PacksSummaryComponent: React.FC<PacksSummaryProps> = ({ item, itemType, cu
useEffect(() => { useEffect(() => {
if (_.isEmpty(customer)) return; 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 }) UserPackAPI.index({ user_id: customer.id, priceable_type: itemType, priceable_id: item.id })
.then(data => setUserPacks(data)) .then(data => setUserPacks(data))
.catch(error => onError(error)); .catch(error => onError(error));
}, [item, itemType, customer]); }
/** /**
* Total of minutes used by the customer * Total of minutes used by the customer
@ -108,7 +122,10 @@ const PacksSummaryComponent: React.FC<PacksSummaryProps> = ({ item, itemType, cu
<div className="packs-summary"> <div className="packs-summary">
<h3>{t('app.logged.packs_summary.prepaid_hours')}</h3> <h3>{t('app.logged.packs_summary.prepaid_hours')}</h3>
<div className="content"> <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"> {shouldDisplayButton() && <div className="button-wrapper">
<FabButton className="buy-button" onClick={togglePacksModal} icon={<i className="fa fa-shopping-cart"/>}> <FabButton className="buy-button" onClick={togglePacksModal} icon={<i className="fa fa-shopping-cart"/>}>
{t('app.logged.packs_summary.buy_a_new_pack')} {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 ( return (
<Loader> <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> </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']));

View File

@ -415,6 +415,9 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$stat
// current machine to reserve // current machine to reserve
$scope.machine = machinePromise; $scope.machine = machinePromise;
// will be set to a Promise and resolved after the payment is sone
$scope.afterPaymentPromise = null;
// fullCalendar (v2) configuration // fullCalendar (v2) configuration
$scope.calendarConfig = CalendarConfig({ $scope.calendarConfig = CalendarConfig({
minTime: moment.duration(moment(settingsPromise.booking_window_start).format('HH:mm:ss')), minTime: moment.duration(moment(settingsPromise.booking_window_start).format('HH:mm:ss')),
@ -618,6 +621,14 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$stat
} }
refetchCalendar(); refetchCalendar();
// trigger the refresh of react components
setTimeout(() => {
$scope.afterPaymentPromise = new Promise(resolve => {
resolve();
});
$scope.$apply();
}, 50);
}); });
}; };

View File

@ -34,7 +34,8 @@
customer="ctrl.member" customer="ctrl.member"
operator="currentUser" operator="currentUser"
on-error="onError" on-error="onError"
on-success="onSuccess"> on-success="onSuccess"
refresh="afterPaymentPromise">
</packs-summary> </packs-summary>
<cart slot="selectedEvent" <cart slot="selectedEvent"

View File

@ -3,7 +3,7 @@
MINUTES_PER_HOUR = 60.0 MINUTES_PER_HOUR = 60.0
SECONDS_PER_MINUTE = 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 # A generic reservation added to the shopping cart
class CartItem::Reservation < CartItem::BaseItem class CartItem::Reservation < CartItem::BaseItem
@ -18,7 +18,7 @@ class CartItem::Reservation < CartItem::BaseItem
def price def price
base_amount = @reservable.prices.find_by(group_id: @customer.group_id, plan_id: @plan.try(:id)).amount 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 is_privileged = @operator.privileged? && @operator.id != @customer.id
prepaid_minutes = get_prepaid_minutes(@customer, @reservable) prepaid = { minutes: PrepaidPackService.minutes_available(@customer, @reservable) }
elements = { slots: [] } elements = { slots: [] }
amount = 0 amount = 0
@ -28,7 +28,7 @@ class CartItem::Reservation < CartItem::BaseItem
amount += get_slot_price(base_amount, slot, is_privileged, amount += get_slot_price(base_amount, slot, is_privileged,
elements: elements, elements: elements,
has_credits: (index < hours_available), has_credits: (index < hours_available),
prepaid_minutes: prepaid_minutes) prepaid: prepaid)
end end
{ elements: elements, amount: amount } { elements: elements, amount: amount }
@ -78,13 +78,19 @@ class CartItem::Reservation < CartItem::BaseItem
options = GET_SLOT_PRICE_DEFAULT_OPTS.merge(options) options = GET_SLOT_PRICE_DEFAULT_OPTS.merge(options)
slot_rate = options[:has_credits] || (slot[:offered] && is_privileged) ? 0 : hourly_rate 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] 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 else
slot_rate slot_rate
end end
if real_price.positive? && options[:prepaid_minutes].positive? # subtract free minutes from prepaid packs
# TODO, remove prepaid minutes 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 end
unless options[:elements].nil? unless options[:elements].nil?
@ -97,14 +103,6 @@ class CartItem::Reservation < CartItem::BaseItem
real_price real_price
end 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) # Compute the number of remaining hours in the users current credits (for machine or space)
## ##

View File

@ -56,6 +56,7 @@ class ShoppingCart
objects.push(save_item(item)) objects.push(save_item(item))
end end
update_credits(objects) update_credits(objects)
update_packs(objects)
payment = create_payment_document(price, objects, payment_id, payment_type) payment = create_payment_document(price, objects, payment_id, payment_type)
WalletService.debit_user_wallet(payment, @customer) WalletService.debit_user_wallet(payment, @customer)
@ -119,4 +120,12 @@ class ShoppingCart
UsersCredits::Manager.new(reservation: r).update_credits UsersCredits::Manager.new(reservation: r).update_credits
end end
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 end

View File

@ -190,7 +190,7 @@ class InvoicesService
invoice.invoice_items.push InvoiceItem.new( invoice.invoice_items.push InvoiceItem.new(
amount: payment_details[:elements][:pack], 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, object: pack,
main: main main: main
) )

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
SECONDS_PER_MINUTE = 60.0
# Provides methods for PrepaidPack # Provides methods for PrepaidPack
class PrepaidPackService class PrepaidPackService
class << self class << self
@ -28,5 +30,43 @@ class PrepaidPackService
.where('prepaid_packs.priceable_id = ?', priceable.id) .where('prepaid_packs.priceable_id = ?', priceable.id)
.where('prepaid_packs.priceable_type = ?', priceable.class.name) .where('prepaid_packs.priceable_type = ?', priceable.class.name)
end 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
end end

View File

@ -194,6 +194,7 @@ en:
packs_summary: packs_summary:
prepaid_hours: "Prepaid hours" prepaid_hours: "Prepaid hours"
remaining_HOURS: "You have {HOURS} prepaid hours remaining for this {ITEM, select, Machine{machine} Space{space} other{}}." 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" buy_a_new_pack: "Buy a new pack"
#book a training #book a training
trainings_reserve: trainings_reserve:

View File

@ -114,7 +114,7 @@ en:
invoice_text_example: "Our association is not subject to VAT" 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." error_invoice: "Erroneous invoice. The items below ware not booked. Please contact the FabLab for a refund."
prepaid_pack: "Prepaid pack of hours" 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 #PDF payment schedule generation
payment_schedules: payment_schedules:
schedule_reference: "Payment schedule reference: %{REF}" schedule_reference: "Payment schedule reference: %{REF}"