mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2024-12-03 14:24:23 +01:00
use Price.compute to generate invoice items
This commit is contained in:
parent
19de8ca319
commit
be23cf27c3
@ -51,10 +51,10 @@ class API::PaymentsController < API::ApiController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def on_reservation_success(intent)
|
def on_reservation_success(intent, details)
|
||||||
@reservation = Reservation.new(reservation_params)
|
@reservation = Reservation.new(reservation_params)
|
||||||
is_reserve = Reservations::Reserve.new(current_user.id, current_user.invoicing_profile.id)
|
is_reserve = Reservations::Reserve.new(current_user.id, current_user.invoicing_profile.id)
|
||||||
.pay_and_save(@reservation, coupon: coupon_params[:coupon_code], payment_intent_id: intent.id)
|
.pay_and_save(@reservation, payment_details: details, payment_intent_id: intent.id)
|
||||||
Stripe::PaymentIntent.update(
|
Stripe::PaymentIntent.update(
|
||||||
intent.id,
|
intent.id,
|
||||||
description: "Invoice reference: #{@reservation.invoice.reference}"
|
description: "Invoice reference: #{@reservation.invoice.reference}"
|
||||||
|
@ -29,13 +29,13 @@ class API::ReservationsController < API::ApiController
|
|||||||
# Managers can create reservations for other users
|
# Managers can create reservations for other users
|
||||||
def create
|
def create
|
||||||
user_id = current_user.admin? || current_user.manager? ? params[:reservation][:user_id] : current_user.id
|
user_id = current_user.admin? || current_user.manager? ? params[:reservation][:user_id] : current_user.id
|
||||||
amount = transaction_amount(current_user.admin? || (current_user.manager? && current_user.id != user_id), user_id)
|
price = transaction_amount(current_user.admin? || (current_user.manager? && current_user.id != user_id), user_id)
|
||||||
|
|
||||||
authorize ReservationContext.new(Reservation, amount, user_id)
|
authorize ReservationContext.new(Reservation, price[:amount], user_id)
|
||||||
|
|
||||||
@reservation = Reservation.new(reservation_params)
|
@reservation = Reservation.new(reservation_params)
|
||||||
is_reserve = Reservations::Reserve.new(user_id, current_user.invoicing_profile.id)
|
is_reserve = Reservations::Reserve.new(user_id, current_user.invoicing_profile.id)
|
||||||
.pay_and_save(@reservation, coupon: coupon_params[:coupon_code])
|
.pay_and_save(@reservation, payment_details: price[:price_details])
|
||||||
|
|
||||||
if is_reserve
|
if is_reserve
|
||||||
SubscriptionExtensionAfterReservation.new(@reservation).extend_subscription_if_eligible
|
SubscriptionExtensionAfterReservation.new(@reservation).extend_subscription_if_eligible
|
||||||
@ -73,7 +73,8 @@ class API::ReservationsController < API::ApiController
|
|||||||
# Subtract wallet amount from total
|
# Subtract wallet amount from total
|
||||||
total = price_details[:total]
|
total = price_details[:total]
|
||||||
wallet_debit = get_wallet_debit(user, total)
|
wallet_debit = get_wallet_debit(user, total)
|
||||||
total - wallet_debit
|
|
||||||
|
{ price_details: price_details, amount: (total - wallet_debit) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_wallet_debit(user, total_amount)
|
def get_wallet_debit(user, total_amount)
|
||||||
|
@ -138,10 +138,12 @@ class Price < ApplicationRecord
|
|||||||
|
|
||||||
# === apply Coupon if any ===
|
# === apply Coupon if any ===
|
||||||
_amount_no_coupon = total_amount
|
_amount_no_coupon = total_amount
|
||||||
total_amount = CouponService.new.apply(total_amount, coupon_code)
|
cs = CouponService.new
|
||||||
|
cp = cs.validate(coupon_code, user.id)
|
||||||
|
total_amount = cs.apply(total_amount, cp)
|
||||||
|
|
||||||
# return result
|
# return result
|
||||||
{ elements: all_elements, total: total_amount.to_i, before_coupon: _amount_no_coupon.to_i }
|
{ elements: all_elements, total: total_amount.to_i, before_coupon: _amount_no_coupon.to_i, coupon: cp }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
@ -33,75 +33,17 @@ class Reservation < ApplicationRecord
|
|||||||
|
|
||||||
##
|
##
|
||||||
# Generate an array of {Stripe::InvoiceItem} with the elements in the current reservation, price included.
|
# Generate an array of {Stripe::InvoiceItem} with the elements in the current reservation, price included.
|
||||||
# The training/machine price is depending of the member's group, subscription and credits already used
|
# @param payment_details {Hash} as generated by Price.compute
|
||||||
# @param on_site {Boolean} true if an admin triggered the call
|
|
||||||
# @param coupon_code {String} pass a valid code to apply a coupon
|
|
||||||
##
|
##
|
||||||
def generate_invoice_items(on_site = false, coupon_code = nil)
|
def generate_invoice_items(payment_details = nil)
|
||||||
# prepare the plan
|
|
||||||
plan = if user.subscribed_plan
|
|
||||||
user.subscribed_plan
|
|
||||||
elsif plan_id
|
|
||||||
Plan.find(plan_id)
|
|
||||||
else
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
|
|
||||||
# check that none of the reserved availabilities was locked
|
# check that none of the reserved availabilities was locked
|
||||||
slots.each do |slot|
|
slots.each do |slot|
|
||||||
raise LockedError if slot.availability.lock
|
raise LockedError if slot.availability.lock
|
||||||
end
|
end
|
||||||
|
|
||||||
case reservable
|
case reservable
|
||||||
|
|
||||||
# === Machine reservation ===
|
|
||||||
when Machine
|
|
||||||
base_amount = reservable.prices.find_by(group_id: user.group_id, plan_id: plan.try(:id)).amount
|
|
||||||
users_credits_manager = UsersCredits::Manager.new(reservation: self, plan: plan)
|
|
||||||
|
|
||||||
slots.each_with_index do |slot, index|
|
|
||||||
description = reservable.name +
|
|
||||||
" #{I18n.l slot.start_at, format: :long} - #{I18n.l slot.end_at, format: :hour_minute}"
|
|
||||||
|
|
||||||
ii_amount = base_amount # ii_amount default to base_amount
|
|
||||||
|
|
||||||
if users_credits_manager.will_use_credits?
|
|
||||||
ii_amount = index < users_credits_manager.free_hours_count ? 0 : base_amount
|
|
||||||
end
|
|
||||||
|
|
||||||
ii_amount = 0 if slot.offered && on_site # if it's a local payment and slot is offered free
|
|
||||||
|
|
||||||
invoice.invoice_items.push InvoiceItem.new(
|
|
||||||
amount: ii_amount,
|
|
||||||
description: description
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
# === Training reservation ===
|
|
||||||
when Training
|
|
||||||
base_amount = reservable.amount_by_group(user.group_id).amount
|
|
||||||
|
|
||||||
# be careful, variable plan can be the user's plan OR the plan user is currently purchasing
|
|
||||||
users_credits_manager = UsersCredits::Manager.new(reservation: self, plan: plan)
|
|
||||||
base_amount = 0 if users_credits_manager.will_use_credits?
|
|
||||||
|
|
||||||
slots.each do |slot|
|
|
||||||
description = reservable.name +
|
|
||||||
" #{I18n.l slot.start_at, format: :long} - #{I18n.l slot.end_at, format: :hour_minute}"
|
|
||||||
ii_amount = base_amount
|
|
||||||
ii_amount = 0 if slot.offered && on_site
|
|
||||||
invoice.invoice_items.push InvoiceItem.new(
|
|
||||||
amount: ii_amount,
|
|
||||||
description: description
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
# === Event reservation ===
|
# === Event reservation ===
|
||||||
when Event
|
when Event
|
||||||
amount = reservable.amount * nb_reserve_places
|
|
||||||
tickets.each do |ticket|
|
|
||||||
amount += ticket.booked * ticket.event_price_category.amount
|
|
||||||
end
|
|
||||||
slots.each do |slot|
|
slots.each do |slot|
|
||||||
description = "#{reservable.name}\n"
|
description = "#{reservable.name}\n"
|
||||||
description += if slot.start_at.to_date != slot.end_at.to_date
|
description += if slot.start_at.to_date != slot.end_at.to_date
|
||||||
@ -115,69 +57,32 @@ class Reservation < ApplicationRecord
|
|||||||
"#{I18n.l slot.start_at.to_date, format: :long} #{I18n.l slot.start_at, format: :hour_minute}" \
|
"#{I18n.l slot.start_at.to_date, format: :long} #{I18n.l slot.start_at, format: :hour_minute}" \
|
||||||
" - #{I18n.l slot.end_at, format: :hour_minute}"
|
" - #{I18n.l slot.end_at, format: :hour_minute}"
|
||||||
end
|
end
|
||||||
ii_amount = amount
|
|
||||||
ii_amount = 0 if slot.offered && on_site
|
price_slot = payment_details[:elements][:slots].detect { |p_slot| p_slot[:start_at].to_time.in_time_zone == slot[:start_at] }
|
||||||
invoice.invoice_items.push InvoiceItem.new(
|
invoice.invoice_items.push InvoiceItem.new(
|
||||||
amount: ii_amount,
|
amount: price_slot[:price],
|
||||||
description: description
|
description: description
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
# === Space|Machine|Training reservation ===
|
||||||
# === Space reservation ===
|
|
||||||
when Space
|
|
||||||
base_amount = reservable.prices.find_by(group_id: user.group_id, plan_id: plan.try(:id)).amount
|
|
||||||
users_credits_manager = UsersCredits::Manager.new(reservation: self, plan: plan)
|
|
||||||
|
|
||||||
slots.each_with_index do |slot, index|
|
|
||||||
description = reservable.name + " #{I18n.l slot.start_at, format: :long} - #{I18n.l slot.end_at, format: :hour_minute}"
|
|
||||||
|
|
||||||
ii_amount = base_amount # ii_amount default to base_amount
|
|
||||||
|
|
||||||
if users_credits_manager.will_use_credits?
|
|
||||||
ii_amount = index < users_credits_manager.free_hours_count ? 0 : base_amount
|
|
||||||
end
|
|
||||||
|
|
||||||
ii_amount = 0 if slot.offered && on_site # if it's a local payment and slot is offered free
|
|
||||||
|
|
||||||
invoice.invoice_items.push InvoiceItem.new(
|
|
||||||
amount: ii_amount,
|
|
||||||
description: description
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
# === Unknown reservation type ===
|
|
||||||
else
|
else
|
||||||
raise NotImplementedError
|
slots.each do |slot|
|
||||||
|
description = reservable.name +
|
||||||
|
" #{I18n.l slot.start_at, format: :long} - #{I18n.l slot.end_at, format: :hour_minute}"
|
||||||
|
|
||||||
|
price_slot = payment_details[:elements][:slots].detect { |p_slot| p_slot[:start_at].to_time.in_time_zone == slot[:start_at] }
|
||||||
|
invoice.invoice_items.push InvoiceItem.new(
|
||||||
|
amount: price_slot[:price],
|
||||||
|
description: description
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# === Coupon ===
|
# === Coupon ===
|
||||||
unless coupon_code.nil?
|
@coupon = payment_details[:coupon]
|
||||||
@coupon = Coupon.find_by(code: coupon_code)
|
|
||||||
raise InvalidCouponError if @coupon.nil? || @coupon.status(user.id) != 'active'
|
|
||||||
|
|
||||||
total = cart_total
|
|
||||||
|
|
||||||
discount = if @coupon.type == 'percent_off'
|
|
||||||
(total * @coupon.percent_off / 100).to_i
|
|
||||||
elsif @coupon.type == 'amount_off'
|
|
||||||
@coupon.amount_off
|
|
||||||
else
|
|
||||||
raise InvalidCouponError
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
# === Wallet ===
|
||||||
@wallet_amount_debit = wallet_amount_debit
|
@wallet_amount_debit = wallet_amount_debit
|
||||||
# if @wallet_amount_debit != 0 && !on_site
|
|
||||||
# invoice_items << Stripe::InvoiceItem.create(
|
|
||||||
# customer: user.stp_customer_id,
|
|
||||||
# amount: -@wallet_amount_debit.to_i,
|
|
||||||
# currency: Rails.application.secrets.stripe_currency,
|
|
||||||
# description: "wallet -#{@wallet_amount_debit / 100.0}"
|
|
||||||
# )
|
|
||||||
# end
|
|
||||||
|
|
||||||
true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# check reservation amount total and strip invoice total to pay is equal
|
# check reservation amount total and strip invoice total to pay is equal
|
||||||
@ -211,7 +116,7 @@ class Reservation < ApplicationRecord
|
|||||||
pending_invoice_items.each(&:delete)
|
pending_invoice_items.each(&:delete)
|
||||||
end
|
end
|
||||||
|
|
||||||
def save_with_payment(operator_profile_id, coupon_code = nil, payment_intent_id = nil)
|
def save_with_payment(operator_profile_id, payment_details, payment_intent_id = nil)
|
||||||
operator = InvoicingProfile.find(operator_profile_id)&.user
|
operator = InvoicingProfile.find(operator_profile_id)&.user
|
||||||
method = operator&.admin? || (operator&.manager? && operator != user) ? nil : 'stripe'
|
method = operator&.admin? || (operator&.manager? && operator != user) ? nil : 'stripe'
|
||||||
|
|
||||||
@ -222,7 +127,7 @@ class Reservation < ApplicationRecord
|
|||||||
stp_payment_intent_id: payment_intent_id,
|
stp_payment_intent_id: payment_intent_id,
|
||||||
payment_method: method
|
payment_method: method
|
||||||
)
|
)
|
||||||
generate_invoice_items(true, coupon_code)
|
generate_invoice_items(payment_details)
|
||||||
|
|
||||||
return false unless valid?
|
return false unless valid?
|
||||||
|
|
||||||
@ -231,18 +136,18 @@ class Reservation < ApplicationRecord
|
|||||||
subscription.attributes = { plan_id: plan_id, statistic_profile_id: statistic_profile_id, expiration_date: nil }
|
subscription.attributes = { plan_id: plan_id, statistic_profile_id: statistic_profile_id, expiration_date: nil }
|
||||||
if subscription.save_with_payment(operator_profile_id, false)
|
if subscription.save_with_payment(operator_profile_id, false)
|
||||||
invoice.invoice_items.push InvoiceItem.new(
|
invoice.invoice_items.push InvoiceItem.new(
|
||||||
amount: subscription.plan.amount,
|
amount: payment_details[:elements][:plan],
|
||||||
description: subscription.plan.name,
|
description: subscription.plan.name,
|
||||||
subscription_id: subscription.id
|
subscription_id: subscription.id
|
||||||
)
|
)
|
||||||
set_total_and_coupon(coupon_code)
|
set_total_and_coupon(payment_details[:coupon])
|
||||||
save!
|
save!
|
||||||
else
|
else
|
||||||
errors[:card] << subscription.errors[:card].join
|
errors[:card] << subscription.errors[:card].join
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
set_total_and_coupon(coupon_code)
|
set_total_and_coupon(payment_details[:coupon])
|
||||||
save!
|
save!
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -355,16 +260,13 @@ class Reservation < ApplicationRecord
|
|||||||
##
|
##
|
||||||
# Set the total price to the reservation's invoice, summing its whole items.
|
# Set the total price to the reservation's invoice, summing its whole items.
|
||||||
# Additionally a coupon may be applied to this invoice to make a discount on the total price
|
# Additionally a coupon may be applied to this invoice to make a discount on the total price
|
||||||
# @param [coupon_code] {String} optional coupon code to apply to the invoice
|
# @param [coupon] {Coupon} optional coupon to apply to the invoice
|
||||||
##
|
##
|
||||||
def set_total_and_coupon(coupon_code = nil)
|
def set_total_and_coupon(coupon = nil)
|
||||||
total = invoice.invoice_items.map(&:amount).map(&:to_i).reduce(:+)
|
total = invoice.invoice_items.map(&:amount).map(&:to_i).reduce(:+)
|
||||||
|
|
||||||
unless coupon_code.nil?
|
unless coupon.nil?
|
||||||
cp = Coupon.find_by(code: coupon_code)
|
total = CouponService.new.apply(total, coupon, user.id)
|
||||||
raise InvalidCouponError unless !cp.nil? && cp.status(user.id) == 'active'
|
|
||||||
|
|
||||||
total = CouponService.new.apply(total, cp, user.id)
|
|
||||||
invoice.coupon_id = cp.id
|
invoice.coupon_id = cp.id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -36,6 +36,21 @@ class CouponService
|
|||||||
price
|
price
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Find the coupon associated with the given code and check it is valid for the given user
|
||||||
|
# @param code {String} the literal code of the coupon
|
||||||
|
# @param user_id {Number} identifier of the user who is applying the coupon
|
||||||
|
# @return {Coupon}
|
||||||
|
##
|
||||||
|
def validate(code, user_id)
|
||||||
|
return nil unless code && user_id
|
||||||
|
|
||||||
|
coupon = Coupon.find_by(code: code)
|
||||||
|
raise InvalidCouponError if coupon.nil? || coupon.status(user_id) != 'active'
|
||||||
|
|
||||||
|
coupon
|
||||||
|
end
|
||||||
|
|
||||||
##
|
##
|
||||||
# Ventilate the discount of the provided coupon over the given amount proportionately to the invoice's total
|
# Ventilate the discount of the provided coupon over the given amount proportionately to the invoice's total
|
||||||
# @param total {Number} total amount of the invoice expressed in monetary units
|
# @param total {Number} total amount of the invoice expressed in monetary units
|
||||||
|
@ -9,8 +9,8 @@ class Reservations::Reserve
|
|||||||
@operator_profile_id = operator_profile_id
|
@operator_profile_id = operator_profile_id
|
||||||
end
|
end
|
||||||
|
|
||||||
def pay_and_save(reservation, coupon: nil, payment_intent_id: nil)
|
def pay_and_save(reservation, payment_details: nil, payment_intent_id: nil)
|
||||||
reservation.statistic_profile_id = StatisticProfile.find_by(user_id: user_id).id
|
reservation.statistic_profile_id = StatisticProfile.find_by(user_id: user_id).id
|
||||||
reservation.save_with_payment(operator_profile_id, coupon, payment_intent_id)
|
reservation.save_with_payment(operator_profile_id, payment_details, payment_intent_id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'forwardable'
|
require 'forwardable'
|
||||||
|
|
||||||
module UsersCredits
|
module UsersCredits
|
||||||
class AlreadyUpdatedError < StandardError; end
|
class AlreadyUpdatedError < StandardError; end
|
||||||
|
|
||||||
|
# You must use UsersCredits::Manager to consume the credits of a user or to reset them
|
||||||
class Manager
|
class Manager
|
||||||
extend Forwardable
|
extend Forwardable
|
||||||
attr_reader :manager
|
attr_reader :manager
|
||||||
@ -30,6 +33,8 @@ module UsersCredits
|
|||||||
def_delegators :@manager, :will_use_credits?, :free_hours_count, :update_credits, :reset_credits
|
def_delegators :@manager, :will_use_credits?, :free_hours_count, :update_credits, :reset_credits
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# The classes contained in UsersCredits::Managers are used by UsersCredits::Manager (no s) to handle the credits for
|
||||||
|
# the various kinds of reservations and for the user
|
||||||
module Managers
|
module Managers
|
||||||
# that class is responsible for resetting users_credits of a user
|
# that class is responsible for resetting users_credits of a user
|
||||||
class User
|
class User
|
||||||
@ -44,6 +49,7 @@ module UsersCredits
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Parent class of all reservations managers
|
||||||
class Reservation
|
class Reservation
|
||||||
attr_reader :reservation
|
attr_reader :reservation
|
||||||
|
|
||||||
@ -119,7 +125,7 @@ module UsersCredits
|
|||||||
return false, free_hours_count, machine_credit
|
return false, free_hours_count, machine_credit
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return false, 0
|
[false, 0]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -149,10 +155,11 @@ module UsersCredits
|
|||||||
return true, training_credit
|
return true, training_credit
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return false, nil
|
[false, nil]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# same as class Machine but for Event reservation
|
||||||
class Event < Reservation
|
class Event < Reservation
|
||||||
def will_use_credits?
|
def will_use_credits?
|
||||||
false
|
false
|
||||||
@ -161,6 +168,7 @@ module UsersCredits
|
|||||||
def update_credits; end
|
def update_credits; end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# same as class Machine but for Space reservation
|
||||||
class Space < Reservation
|
class Space < Reservation
|
||||||
# to known if a credit will be used in the context of the given reservation
|
# to known if a credit will be used in the context of the given reservation
|
||||||
def will_use_credits?
|
def will_use_credits?
|
||||||
@ -206,7 +214,7 @@ module UsersCredits
|
|||||||
return false, free_hours_count, space_credit
|
return false, free_hours_count, space_credit
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return false, 0
|
[false, 0]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
Loading…
Reference in New Issue
Block a user