1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2024-12-11 22:24:21 +01:00

use Price.compute to generate invoice items

This commit is contained in:
Sylvain 2020-05-06 12:43:47 +02:00
parent 19de8ca319
commit be23cf27c3
7 changed files with 65 additions and 137 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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