1
0
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:
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,
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']));

View File

@ -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);
});
};

View File

@ -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"

View File

@ -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)
##

View File

@ -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

View File

@ -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
)

View File

@ -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

View File

@ -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:

View File

@ -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}"