mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-21 15:54:22 +01:00
refactoring of reservation:pay_and_save
TODO: debug with tests, refactor subscription:pay_and_save on the same template
This commit is contained in:
parent
6c8d65fba1
commit
40c78974b8
@ -106,7 +106,8 @@ class API::PaymentsController < API::ApiController
|
|||||||
.pay_and_save(@reservation,
|
.pay_and_save(@reservation,
|
||||||
payment_details: details,
|
payment_details: details,
|
||||||
payment_intent_id: intent.id,
|
payment_intent_id: intent.id,
|
||||||
schedule: params[:cart_items][:reservation][:payment_schedule])
|
schedule: params[:cart_items][:reservation][:payment_schedule],
|
||||||
|
payment_method: params[:cart_items][:reservation][:payment_method])
|
||||||
if intent.class == Stripe::PaymentIntent
|
if intent.class == Stripe::PaymentIntent
|
||||||
Stripe::PaymentIntent.update(
|
Stripe::PaymentIntent.update(
|
||||||
intent.id,
|
intent.id,
|
||||||
|
@ -37,7 +37,8 @@ class API::ReservationsController < API::ApiController
|
|||||||
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,
|
.pay_and_save(@reservation,
|
||||||
payment_details: price[:price_details],
|
payment_details: price[:price_details],
|
||||||
schedule: params[:reservation][:payment_schedule])
|
schedule: params[:reservation][:payment_schedule],
|
||||||
|
payment_method: params[:reservation][:payment_method])
|
||||||
|
|
||||||
if is_reserve
|
if is_reserve
|
||||||
SubscriptionExtensionAfterReservation.new(@reservation).extend_subscription_if_eligible
|
SubscriptionExtensionAfterReservation.new(@reservation).extend_subscription_if_eligible
|
||||||
|
@ -33,6 +33,13 @@ class PaymentSchedule < ApplicationRecord
|
|||||||
save
|
save
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_wallet_transaction(amount, transaction_id)
|
||||||
|
raise InvalidFootprintError unless check_footprint
|
||||||
|
|
||||||
|
update_columns(wallet_amount: amount, wallet_transaction_id: transaction_id)
|
||||||
|
chain_record
|
||||||
|
end
|
||||||
|
|
||||||
def chain_record
|
def chain_record
|
||||||
self.footprint = compute_footprint
|
self.footprint = compute_footprint
|
||||||
save!
|
save!
|
||||||
@ -46,4 +53,8 @@ class PaymentSchedule < ApplicationRecord
|
|||||||
def compute_footprint
|
def compute_footprint
|
||||||
FootprintService.compute_footprint(PaymentSchedule, self)
|
FootprintService.compute_footprint(PaymentSchedule, self)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def check_footprint
|
||||||
|
payment_schedule_items.map(&:check_footprint).all? && footprint == compute_footprint
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -4,4 +4,23 @@
|
|||||||
class PaymentScheduleItem < ApplicationRecord
|
class PaymentScheduleItem < ApplicationRecord
|
||||||
belongs_to :payment_schedule
|
belongs_to :payment_schedule
|
||||||
belongs_to :invoice
|
belongs_to :invoice
|
||||||
|
after_create :chain_record
|
||||||
|
|
||||||
|
def chain_record
|
||||||
|
self.footprint = compute_footprint
|
||||||
|
save!
|
||||||
|
FootprintDebug.create!(
|
||||||
|
footprint: footprint,
|
||||||
|
data: FootprintService.footprint_data(PaymentScheduleItem, self),
|
||||||
|
klass: PaymentScheduleItem.name
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_footprint
|
||||||
|
footprint == compute_footprint
|
||||||
|
end
|
||||||
|
|
||||||
|
def compute_footprint
|
||||||
|
FootprintService.compute_footprint(PaymentScheduleItem, self)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -30,133 +30,33 @@ class Reservation < ApplicationRecord
|
|||||||
after_commit :notify_member_create_reservation, on: :create
|
after_commit :notify_member_create_reservation, on: :create
|
||||||
after_commit :notify_admin_member_create_reservation, on: :create
|
after_commit :notify_admin_member_create_reservation, on: :create
|
||||||
after_save :update_event_nb_free_places, if: proc { |reservation| reservation.reservable_type == 'Event' }
|
after_save :update_event_nb_free_places, if: proc { |reservation| reservation.reservable_type == 'Event' }
|
||||||
after_create :debit_user_wallet
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Generate an array of {Stripe::InvoiceItem} with the elements in the current reservation, price included.
|
# These checks will run before the invoice/payment-schedule is generated
|
||||||
# @param payment_details {Hash} as generated by Price.compute
|
|
||||||
##
|
##
|
||||||
def generate_invoice_items(payment_details = nil)
|
def pre_check
|
||||||
# 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
|
|
||||||
# === Event reservation ===
|
|
||||||
when Event
|
|
||||||
slots.each do |slot|
|
|
||||||
description = "#{reservable.name}\n"
|
|
||||||
description += if slot.start_at.to_date != slot.end_at.to_date
|
|
||||||
I18n.t('events.from_STARTDATE_to_ENDDATE',
|
|
||||||
STARTDATE: I18n.l(slot.start_at.to_date, format: :long),
|
|
||||||
ENDDATE: I18n.l(slot.end_at.to_date, format: :long)) + ' ' +
|
|
||||||
I18n.t('events.from_STARTTIME_to_ENDTIME',
|
|
||||||
STARTTIME: I18n.l(slot.start_at, format: :hour_minute),
|
|
||||||
ENDTIME: I18n.l(slot.end_at, format: :hour_minute))
|
|
||||||
else
|
|
||||||
"#{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}"
|
|
||||||
end
|
|
||||||
|
|
||||||
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
|
|
||||||
# === Space|Machine|Training reservation ===
|
|
||||||
else
|
|
||||||
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
|
|
||||||
|
|
||||||
# === Coupon ===
|
|
||||||
@coupon = payment_details[:coupon]
|
|
||||||
|
|
||||||
# === Wallet ===
|
|
||||||
@wallet_amount_debit = wallet_amount_debit
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# check reservation amount total and strip invoice total to pay is equal
|
## Generate the subscription associated with for the current reservation
|
||||||
# @param stp_invoice[Stripe::Invoice]
|
def generate_subscription
|
||||||
# @param coupon_code[String]
|
return unless plan_id
|
||||||
# return Boolean
|
|
||||||
def is_equal_reservation_total_and_stp_invoice_total(stp_invoice, coupon_code = nil)
|
self.subscription = Subscription.find_or_initialize_by(statistic_profile_id: statistic_profile_id)
|
||||||
compute_amount_total_to_pay(coupon_code) == stp_invoice.total
|
subscription.attributes = { plan_id: plan_id, statistic_profile_id: statistic_profile_id, expiration_date: nil }
|
||||||
|
|
||||||
|
subscription.init_save
|
||||||
|
subscription
|
||||||
end
|
end
|
||||||
|
|
||||||
def clear_payment_info(card, invoice)
|
##
|
||||||
card&.delete
|
# These actions will be realized after the reservation is initially saved (on creation)
|
||||||
if invoice
|
##
|
||||||
invoice.closed = true
|
def post_save
|
||||||
invoice.save
|
|
||||||
end
|
|
||||||
rescue Stripe::InvalidRequestError => e
|
|
||||||
logger.error e
|
|
||||||
rescue Stripe::AuthenticationError => e
|
|
||||||
logger.error e
|
|
||||||
rescue Stripe::APIConnectionError => e
|
|
||||||
logger.error e
|
|
||||||
rescue Stripe::StripeError => e
|
|
||||||
logger.error e
|
|
||||||
rescue StandardError => e
|
|
||||||
logger.error e
|
|
||||||
end
|
|
||||||
|
|
||||||
def clean_pending_strip_invoice_items
|
|
||||||
pending_invoice_items = Stripe::InvoiceItem.list(
|
|
||||||
{ customer: user.stp_customer_id, limit: 100 },
|
|
||||||
{ api_key: Setting.get('stripe_secret_key') }
|
|
||||||
).data.select { |ii| ii.invoice.nil? }
|
|
||||||
pending_invoice_items.each(&:delete)
|
|
||||||
end
|
|
||||||
|
|
||||||
def save_with_payment(operator_profile_id, payment_details, payment_intent_id = nil, schedule: false)
|
|
||||||
operator = InvoicingProfile.find(operator_profile_id)&.user
|
|
||||||
method = operator&.admin? || (operator&.manager? && operator != user) ? nil : 'stripe'
|
|
||||||
|
|
||||||
build_invoice(
|
|
||||||
invoicing_profile: user.invoicing_profile,
|
|
||||||
statistic_profile: user.statistic_profile,
|
|
||||||
operator_profile_id: operator_profile_id,
|
|
||||||
stp_payment_intent_id: payment_intent_id,
|
|
||||||
payment_method: method
|
|
||||||
)
|
|
||||||
generate_invoice_items(payment_details)
|
|
||||||
|
|
||||||
return false unless valid?
|
|
||||||
|
|
||||||
if plan_id
|
|
||||||
self.subscription = Subscription.find_or_initialize_by(statistic_profile_id: statistic_profile_id)
|
|
||||||
subscription.attributes = { plan_id: plan_id, statistic_profile_id: statistic_profile_id, expiration_date: nil }
|
|
||||||
if subscription.save_with_payment(operator_profile_id, invoice: false, schedule: schedule)
|
|
||||||
invoice.invoice_items.push InvoiceItem.new(
|
|
||||||
amount: payment_details[:elements][:plan],
|
|
||||||
description: subscription.plan.name,
|
|
||||||
subscription_id: subscription.id
|
|
||||||
)
|
|
||||||
set_total_and_coupon(payment_details[:coupon])
|
|
||||||
save!
|
|
||||||
else
|
|
||||||
errors[:card] << subscription.errors[:card].join
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
else
|
|
||||||
set_total_and_coupon(payment_details[:coupon])
|
|
||||||
save!
|
|
||||||
end
|
|
||||||
|
|
||||||
UsersCredits::Manager.new(reservation: self).update_credits
|
UsersCredits::Manager.new(reservation: self).update_credits
|
||||||
true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# @param canceled if true, count the number of seats for this reservation, including canceled seats
|
# @param canceled if true, count the number of seats for this reservation, including canceled seats
|
||||||
@ -219,61 +119,4 @@ class Reservation < ApplicationRecord
|
|||||||
receiver: User.admins_and_managers,
|
receiver: User.admins_and_managers,
|
||||||
attached_object: self
|
attached_object: self
|
||||||
end
|
end
|
||||||
|
|
||||||
def cart_total
|
|
||||||
total = (invoice.invoice_items.map(&:amount).map(&:to_i).reduce(:+) or 0)
|
|
||||||
if plan_id.present?
|
|
||||||
plan = Plan.find(plan_id)
|
|
||||||
total += plan.amount
|
|
||||||
end
|
|
||||||
total
|
|
||||||
end
|
|
||||||
|
|
||||||
def wallet_amount_debit
|
|
||||||
total = cart_total
|
|
||||||
total = CouponService.new.apply(total, @coupon, user.id) if @coupon
|
|
||||||
|
|
||||||
wallet_amount = (user.wallet.amount * 100).to_i
|
|
||||||
|
|
||||||
wallet_amount >= total ? total : wallet_amount
|
|
||||||
end
|
|
||||||
|
|
||||||
def debit_user_wallet
|
|
||||||
return unless @wallet_amount_debit.present? && @wallet_amount_debit != 0
|
|
||||||
|
|
||||||
amount = @wallet_amount_debit / 100.0
|
|
||||||
wallet_transaction = WalletService.new(user: user, wallet: user.wallet).debit(amount, self)
|
|
||||||
# wallet debit success
|
|
||||||
raise DebitWalletError unless wallet_transaction
|
|
||||||
|
|
||||||
invoice.set_wallet_transaction(@wallet_amount_debit, wallet_transaction.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
# this function only use for compute total of reservation before save
|
|
||||||
def compute_amount_total_to_pay(coupon_code = nil)
|
|
||||||
total = invoice.invoice_items.map(&:amount).map(&:to_i).reduce(:+)
|
|
||||||
unless coupon_code.nil?
|
|
||||||
cp = Coupon.find_by(code: coupon_code)
|
|
||||||
raise InvalidCouponError unless !cp.nil? && cp.status(user.id) == 'active'
|
|
||||||
|
|
||||||
total = CouponService.new.apply(total, cp, user.id)
|
|
||||||
end
|
|
||||||
total - wallet_amount_debit
|
|
||||||
end
|
|
||||||
|
|
||||||
##
|
|
||||||
# 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
|
|
||||||
# @param [coupon] {Coupon} optional coupon to apply to the invoice
|
|
||||||
##
|
|
||||||
def set_total_and_coupon(coupon = nil)
|
|
||||||
total = invoice.invoice_items.map(&:amount).map(&:to_i).reduce(:+)
|
|
||||||
|
|
||||||
unless coupon.nil?
|
|
||||||
total = CouponService.new.apply(total, coupon, user.id)
|
|
||||||
invoice.coupon_id = coupon.id
|
|
||||||
end
|
|
||||||
|
|
||||||
invoice.total = total
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
@ -19,6 +19,22 @@ class Subscription < ApplicationRecord
|
|||||||
after_save :notify_admin_subscribed_plan
|
after_save :notify_admin_subscribed_plan
|
||||||
after_save :notify_partner_subscribed_plan, if: :of_partner_plan?
|
after_save :notify_partner_subscribed_plan, if: :of_partner_plan?
|
||||||
|
|
||||||
|
##
|
||||||
|
# Set the inner properties of the subscription, init the user's credits and save the subscription
|
||||||
|
# into the DB
|
||||||
|
# @return {boolean} true, if the operation succeeded
|
||||||
|
##
|
||||||
|
def init_save
|
||||||
|
return false unless valid?
|
||||||
|
|
||||||
|
set_expiration_date
|
||||||
|
return false unless save
|
||||||
|
|
||||||
|
UsersCredits::Manager.new(user: user).reset_credits
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO, remove this method, refactor like services/Reservations::Reserve
|
||||||
# @param invoice if true then only the subscription is payed, without reservation
|
# @param invoice if true then only the subscription is payed, without reservation
|
||||||
# if false then the subscription is payed with reservation
|
# if false then the subscription is payed with reservation
|
||||||
# @param payment_method is only used for schedules
|
# @param payment_method is only used for schedules
|
||||||
|
@ -59,4 +59,137 @@ class InvoicesService
|
|||||||
end
|
end
|
||||||
{ direction: direction, order_key: order_key }
|
{ direction: direction, order_key: order_key }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Create a Stripe::Invoice with an associated array of Stripe::InvoiceItem matching the given parameters
|
||||||
|
# @param payment_details {Hash} as generated by Price.compute
|
||||||
|
# @param operator_profile_id {Number} ID of the user that operates the invoice generation (may be an admin, a manager or the customer himself)
|
||||||
|
# @param reservation {Reservation} the booking reservation, if any
|
||||||
|
# @param subscription {Subscription} the booking subscription, if any
|
||||||
|
# @param payment_intent_id {String} ID of the Stripe::PaymentIntend, if the current invoice is paid by stripe
|
||||||
|
##
|
||||||
|
def self.create(payment_details, operator_profile_id, reservation: nil, subscription: nil, payment_intent_id: nil)
|
||||||
|
user = reservation&.user || subscription&.user
|
||||||
|
operator = InvoicingProfile.find(operator_profile_id)&.user
|
||||||
|
method = operator&.admin? || (operator&.manager? && operator != user) ? nil : 'stripe'
|
||||||
|
|
||||||
|
invoice = Invoice.new(
|
||||||
|
invoiced: subscription || reservation,
|
||||||
|
invoicing_profile: user.invoicing_profile,
|
||||||
|
statistic_profile: user.statistic_profile,
|
||||||
|
operator_profile_id: operator_profile_id,
|
||||||
|
stp_payment_intent_id: payment_intent_id,
|
||||||
|
payment_method: method
|
||||||
|
)
|
||||||
|
|
||||||
|
InvoicesService.generate_invoice_items(invoice, payment_details, reservation: reservation, subscription: subscription)
|
||||||
|
InvoicesService.set_total_and_coupon(invoice, user, payment_details[:coupon])
|
||||||
|
invoice
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Generate an array of {Stripe::InvoiceItem} with the elements in provided reservation, price included.
|
||||||
|
# @param invoice {Invoice} the parent invoice
|
||||||
|
# @param payment_details {Hash} as generated by Price.compute
|
||||||
|
##
|
||||||
|
def self.generate_invoice_items(invoice, payment_details, reservation: nil, subscription: nil)
|
||||||
|
if reservation
|
||||||
|
case reservation.reservable
|
||||||
|
# === Event reservation ===
|
||||||
|
when Event
|
||||||
|
InvoicesService.generate_event_item(invoice, reservation, payment_details)
|
||||||
|
# === Space|Machine|Training reservation ===
|
||||||
|
else
|
||||||
|
InvoicesService.generate_generic_item(invoice, reservation, payment_details)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return unless subscription || reservation&.plan_id
|
||||||
|
|
||||||
|
subscription = reservation.generate_subscription if !subscription && reservation.plan_id
|
||||||
|
InvoicesService.generate_subscription_item(invoice, subscription, payment_details)
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Generate Stripe::InvoiceItem for each slot in the given reservation and save them in invoice.invoice_items.
|
||||||
|
# This method must be called if reservation.reservable is an Event
|
||||||
|
##
|
||||||
|
def self.generate_event_item(invoice, reservation, payment_details)
|
||||||
|
raise TypeError unless reservation.reservable.class == Event
|
||||||
|
|
||||||
|
reservation.slots.each do |slot|
|
||||||
|
description = "#{reservation.reservable.name}\n"
|
||||||
|
description += if slot.start_at.to_date != slot.end_at.to_date
|
||||||
|
I18n.t('events.from_STARTDATE_to_ENDDATE',
|
||||||
|
STARTDATE: I18n.l(slot.start_at.to_date, format: :long),
|
||||||
|
ENDDATE: I18n.l(slot.end_at.to_date, format: :long)) + ' ' +
|
||||||
|
I18n.t('events.from_STARTTIME_to_ENDTIME',
|
||||||
|
STARTTIME: I18n.l(slot.start_at, format: :hour_minute),
|
||||||
|
ENDTIME: I18n.l(slot.end_at, format: :hour_minute))
|
||||||
|
else
|
||||||
|
"#{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}"
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
##
|
||||||
|
# Generate Stripe::InvoiceItem for each slot in the given reservation and save them in invoice.invoice_items.
|
||||||
|
# This method must be called if reservation.reservable is a Space, a Machine or a Training
|
||||||
|
##
|
||||||
|
def self.generate_generic_item(invoice, reservation, payment_details)
|
||||||
|
raise TypeError unless [Space, Machine, Training].include? reservation.reservable.class
|
||||||
|
|
||||||
|
reservation.slots.each do |slot|
|
||||||
|
description = reservation.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
|
||||||
|
|
||||||
|
##
|
||||||
|
# Generate a Stripe::InvoiceItem for the given subscription and save it in invoice.invoice_items.
|
||||||
|
# This method must be called only with a valid subscription
|
||||||
|
##
|
||||||
|
def self.generate_subscription_item(invoice, subscription, payment_details)
|
||||||
|
raise TypeError unless subscription
|
||||||
|
|
||||||
|
invoice.invoice_items.push InvoiceItem.new(
|
||||||
|
amount: payment_details[:elements][:plan],
|
||||||
|
description: subscription.plan.name,
|
||||||
|
subscription_id: subscription.id
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# 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
|
||||||
|
# @param invoice {Invoice} the invoice to fill
|
||||||
|
# @param user {User} the customer
|
||||||
|
# @param [coupon] {Coupon} optional coupon to apply to the invoice
|
||||||
|
##
|
||||||
|
def self.set_total_and_coupon(invoice, user, coupon = nil)
|
||||||
|
return unless invoice
|
||||||
|
|
||||||
|
total = invoice.invoice_items.map(&:amount).map(&:to_i).reduce(:+)
|
||||||
|
|
||||||
|
unless coupon.nil?
|
||||||
|
total = CouponService.new.apply(total, coupon, user.id)
|
||||||
|
invoice.coupon_id = coupon.id
|
||||||
|
end
|
||||||
|
|
||||||
|
invoice.total = total
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -47,11 +47,11 @@ class PaymentScheduleService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create(subscription, total, coupon: nil, operator: nil, payment_method: nil, reservation: nil, user: nil)
|
def create(subscription, total, coupon: nil, operator: nil, payment_method: nil, reservation: nil, user: nil)
|
||||||
schedule = compute(subscription.plan, total, coupon)
|
schedule = compute(reservation ? reservation.subscription.plan : subscription.plan, total, coupon)
|
||||||
ps = schedule[:payment_schedule]
|
ps = schedule[:payment_schedule]
|
||||||
items = schedule[:items]
|
items = schedule[:items]
|
||||||
|
|
||||||
ps.scheduled = subscription
|
ps.scheduled = reservation || subscription
|
||||||
ps.payment_method = payment_method
|
ps.payment_method = payment_method
|
||||||
ps.operator_profile = operator.invoicing_profile
|
ps.operator_profile = operator.invoicing_profile
|
||||||
ps.invoicing_profile = user.invoicing_profile
|
ps.invoicing_profile = user.invoicing_profile
|
||||||
|
@ -9,8 +9,87 @@ class Reservations::Reserve
|
|||||||
@operator_profile_id = operator_profile_id
|
@operator_profile_id = operator_profile_id
|
||||||
end
|
end
|
||||||
|
|
||||||
def pay_and_save(reservation, payment_details: nil, payment_intent_id: nil, schedule: false)
|
##
|
||||||
|
# Confirm the payment of the given reservation, generate the associated documents and save teh record into
|
||||||
|
# the database.
|
||||||
|
##
|
||||||
|
def pay_and_save(reservation, payment_details: nil, payment_intent_id: nil, schedule: false, payment_method: nil)
|
||||||
|
user = User.find(user_id)
|
||||||
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, payment_details, payment_intent_id, schedule: schedule)
|
|
||||||
|
reservation.pre_check
|
||||||
|
payment = if schedule
|
||||||
|
generate_schedule(reservation: reservation,
|
||||||
|
total: payment_details[:before_coupon],
|
||||||
|
operator_profile_id: operator_profile_id,
|
||||||
|
user: user,
|
||||||
|
payment_method: payment_method,
|
||||||
|
coupon_code: payment_details[:coupon])
|
||||||
|
else
|
||||||
|
generate_invoice(reservation, operator_profile_id, payment_details, payment_intent_id)
|
||||||
|
end
|
||||||
|
payment.save
|
||||||
|
debit_user_wallet(payment, user, reservation)
|
||||||
|
reservation.post_save
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Generate the invoice for the given reservation+subscription
|
||||||
|
##
|
||||||
|
def generate_schedule(reservation: nil, total: nil, operator_profile_id: nil, user: nil, payment_method: nil, coupon_code: nil)
|
||||||
|
operator = InvoicingProfile.find(operator_profile_id)&.user
|
||||||
|
coupon = Coupon.find_by(code: coupon_code) unless coupon_code.nil?
|
||||||
|
|
||||||
|
PaymentScheduleService.new.create(
|
||||||
|
nil,
|
||||||
|
total,
|
||||||
|
coupon: coupon,
|
||||||
|
operator: operator,
|
||||||
|
payment_method: payment_method,
|
||||||
|
user: user,
|
||||||
|
reservation: reservation
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Generate the invoice for the given reservation
|
||||||
|
##
|
||||||
|
def generate_invoice(reservation, operator_profile_id, payment_details, payment_intent_id = nil)
|
||||||
|
InvoicesService.create(
|
||||||
|
payment_details,
|
||||||
|
operator_profile_id,
|
||||||
|
reservation: reservation,
|
||||||
|
payment_intent_id: payment_intent_id
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Compute the amount decreased from the user's wallet, if applicable
|
||||||
|
# @param payment {Invoice|PaymentSchedule}
|
||||||
|
# @param user {User} the customer
|
||||||
|
# @param coupon {Coupon|String} Coupon object or code
|
||||||
|
##
|
||||||
|
def wallet_amount_debit(payment, user, coupon = nil)
|
||||||
|
total = payment.total
|
||||||
|
total = CouponService.new.apply(total, coupon, user.id) if coupon
|
||||||
|
|
||||||
|
wallet_amount = (user.wallet.amount * 100).to_i
|
||||||
|
|
||||||
|
wallet_amount >= total ? total : wallet_amount
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Subtract the amount of the current reservation from the customer's wallet
|
||||||
|
##
|
||||||
|
def debit_user_wallet(payment, user, reservation)
|
||||||
|
wallet_amount = wallet_amount_debit(payment, user)
|
||||||
|
return unless wallet_amount.present? && wallet_amount != 0
|
||||||
|
|
||||||
|
amount = wallet_amount / 100.0
|
||||||
|
wallet_transaction = WalletService.new(user: user, wallet: user.wallet).debit(amount, reservation)
|
||||||
|
# wallet debit success
|
||||||
|
raise DebitWalletError unless wallet_transaction
|
||||||
|
|
||||||
|
payment.set_wallet_transaction(wallet_amount, wallet_transaction.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
# frozen_string_literal:true
|
# frozen_string_literal:true
|
||||||
|
|
||||||
|
# From this migration, if the current Invoice is payed with Stripe, it will be stored in database
|
||||||
|
# using stp_payment_intent_id instead of stp_invoice_id
|
||||||
class AddStpPaymentIntentIdToInvoices < ActiveRecord::Migration[4.2]
|
class AddStpPaymentIntentIdToInvoices < ActiveRecord::Migration[4.2]
|
||||||
def change
|
def change
|
||||||
add_column :invoices, :stp_payment_intent_id, :string
|
add_column :invoices, :stp_payment_intent_id, :string
|
||||||
|
@ -9,6 +9,7 @@ class CreatePaymentScheduleItems < ActiveRecord::Migration[5.2]
|
|||||||
t.jsonb :details, default: '{}'
|
t.jsonb :details, default: '{}'
|
||||||
t.belongs_to :payment_schedule, foreign_key: true
|
t.belongs_to :payment_schedule, foreign_key: true
|
||||||
t.belongs_to :invoice, foreign_key: true
|
t.belongs_to :invoice, foreign_key: true
|
||||||
|
t.string :footprint
|
||||||
|
|
||||||
t.timestamps
|
t.timestamps
|
||||||
end
|
end
|
||||||
|
@ -1473,6 +1473,7 @@ CREATE TABLE public.payment_schedule_items (
|
|||||||
details jsonb DEFAULT '"{}"'::jsonb,
|
details jsonb DEFAULT '"{}"'::jsonb,
|
||||||
payment_schedule_id bigint,
|
payment_schedule_id bigint,
|
||||||
invoice_id bigint,
|
invoice_id bigint,
|
||||||
|
footprint character varying,
|
||||||
created_at timestamp without time zone NOT NULL,
|
created_at timestamp without time zone NOT NULL,
|
||||||
updated_at timestamp without time zone NOT NULL
|
updated_at timestamp without time zone NOT NULL
|
||||||
);
|
);
|
||||||
|
@ -60,6 +60,7 @@ class Subscriptions::CreateAsAdminTest < ActionDispatch::IntegrationTest
|
|||||||
test 'admin takes a subscription with a payment schedule' do
|
test 'admin takes a subscription with a payment schedule' do
|
||||||
user = User.find_by(username: 'jdupond')
|
user = User.find_by(username: 'jdupond')
|
||||||
plan = Plan.find_by(group_id: user.group.id, type: 'Plan', base_name: 'Abonnement mensualisable')
|
plan = Plan.find_by(group_id: user.group.id, type: 'Plan', base_name: 'Abonnement mensualisable')
|
||||||
|
invoice_count = Invoice.count
|
||||||
payment_schedule_count = PaymentSchedule.count
|
payment_schedule_count = PaymentSchedule.count
|
||||||
payment_schedule_items_count = PaymentScheduleItem.count
|
payment_schedule_items_count = PaymentScheduleItem.count
|
||||||
|
|
||||||
@ -106,6 +107,7 @@ class Subscriptions::CreateAsAdminTest < ActionDispatch::IntegrationTest
|
|||||||
# Check generalities
|
# Check generalities
|
||||||
assert_equal 201, response.status, response.body
|
assert_equal 201, response.status, response.body
|
||||||
assert_equal Mime[:json], response.content_type
|
assert_equal Mime[:json], response.content_type
|
||||||
|
assert_equal invoice_count, Invoice.count, "an invoice was generated but it shouldn't"
|
||||||
assert_equal payment_schedule_count + 1, PaymentSchedule.count, 'missing the payment schedule'
|
assert_equal payment_schedule_count + 1, PaymentSchedule.count, 'missing the payment schedule'
|
||||||
assert_equal payment_schedule_items_count + 12, PaymentScheduleItem.count, 'missing some payment schedule items'
|
assert_equal payment_schedule_items_count + 12, PaymentScheduleItem.count, 'missing some payment schedule items'
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user