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:
parent
20bd3931d4
commit
6c326c7209
@ -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']));
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
@ -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)
|
||||||
##
|
##
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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}"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user