mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-19 13:54:25 +01:00
generate the invoices for each schedule item and notify if something goes wrong
This commit is contained in:
parent
ecdec70755
commit
26636254bd
@ -55,6 +55,9 @@ class NotificationType
|
||||
notify_user_role_update
|
||||
notify_admin_members_stripe_sync
|
||||
notify_user_when_payment_schedule_ready
|
||||
notify_admin_payment_schedule_failed
|
||||
notify_member_payment_schedule_failed
|
||||
notify_admin_payment_schedule_check_deadline
|
||||
]
|
||||
# deprecated:
|
||||
# - notify_member_subscribed_plan_is_changed
|
||||
|
@ -6,6 +6,10 @@ class PaymentScheduleItem < Footprintable
|
||||
belongs_to :invoice
|
||||
after_create :chain_record
|
||||
|
||||
def first?
|
||||
payment_schedule.ordered_items.first == self
|
||||
end
|
||||
|
||||
def self.columns_out_of_footprint
|
||||
%w[invoice_id]
|
||||
end
|
||||
|
@ -69,67 +69,135 @@ class PaymentScheduleService
|
||||
ps
|
||||
end
|
||||
|
||||
##
|
||||
# Generate the invoice associated with the given PaymentScheduleItem, with the children elements (InvoiceItems).
|
||||
# @param stp_invoice is used to determine if the invoice was paid using stripe
|
||||
##
|
||||
def generate_invoice(payment_schedule_item, stp_invoice = nil)
|
||||
# build the base invoice
|
||||
invoice = Invoice.new(
|
||||
invoiced: payment_schedule_item.payment_schedule.scheduled,
|
||||
invoicing_profile: payment_schedule_item.payment_schedule.invoicing_profile,
|
||||
statistic_profile: payment_schedule_item.payment_schedule.user.statistic_profile,
|
||||
statistic_profile: payment_schedule_item.payment_schedule.user&.statistic_profile,
|
||||
operator_profile_id: payment_schedule_item.payment_schedule.operator_profile_id,
|
||||
stp_payment_intent_id: stp_invoice&.payment_intent,
|
||||
payment_method: stp_invoice ? 'stripe' : nil
|
||||
)
|
||||
# complete the invoice with some InvoiceItem
|
||||
if payment_schedule_item.first?
|
||||
complete_first_invoice(payment_schedule_item, invoice)
|
||||
else
|
||||
complete_next_invoice(payment_schedule_item, invoice)
|
||||
end
|
||||
|
||||
generate_invoice_items(invoice, payment_schedule_item, reservation: reservation, subscription: subscription)
|
||||
InvoicesService.set_total_and_coupon(invoice, user, payment_details[:coupon])
|
||||
invoice
|
||||
# set the total and apply any coupon
|
||||
user = payment_schedule_item.payment_schedule.user
|
||||
coupon = payment_schedule_item.payment_schedule.coupon
|
||||
set_total_and_coupon(payment_schedule_item, invoice, user, coupon)
|
||||
|
||||
# save the results
|
||||
invoice.save
|
||||
payment_schedule_item.update_attributes(invoice_id: invoice.id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def 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
|
||||
##
|
||||
# The first PaymentScheduleItem contains references to the reservation price (if any) and to the adjustement price
|
||||
# for the subscription (if any)
|
||||
##
|
||||
def complete_first_invoice(payment_schedule_item, invoice)
|
||||
# sub-prices for the subscription and the reservation
|
||||
details = {}
|
||||
if payment_schedule_item.payment_schedule.scheduled_type == Subscription.name
|
||||
details[:subscription] = payment_schedule_item.details['recurring'] + payment_schedule_item.details['adjustment']
|
||||
else
|
||||
details[:reservation] = payment_schedule_item.details['other_items']
|
||||
end
|
||||
|
||||
return unless subscription || reservation&.plan_id
|
||||
# the subscription and reservation items
|
||||
subscription = Subscription.find(payment_schedule_item.details['subscription_id'])
|
||||
if payment_schedule_item.payment_schedule.scheduled_type == Reservation.name
|
||||
reservation = payment_schedule_item.payment_schedule.scheduled
|
||||
end
|
||||
|
||||
subscription = reservation.generate_subscription if !subscription && reservation.plan_id
|
||||
InvoicesService.generate_subscription_item(invoice, subscription, payment_details)
|
||||
# build the invoice items
|
||||
generate_invoice_items(invoice, details, subscription: subscription, reservation: reservation)
|
||||
end
|
||||
|
||||
##
|
||||
# The later PaymentScheduleItems only contain references to the subscription (which is recurring)
|
||||
##
|
||||
def complete_next_invoice(payment_schedule_item, invoice)
|
||||
# the subscription item
|
||||
subscription = Subscription.find(payment_schedule_item.details['subscription_id'])
|
||||
|
||||
# sub-price for the subscription
|
||||
details = { subscription: payment_schedule_item.details['recurring'] }
|
||||
|
||||
# build the invoice item
|
||||
generate_invoice_items(invoice, details, subscription: subscription)
|
||||
end
|
||||
|
||||
##
|
||||
# Generate an array of InvoiceItem according to the provided parameters and saves them in invoice.invoice_items
|
||||
##
|
||||
def generate_invoice_items(invoice, payment_details, reservation: nil, subscription: nil)
|
||||
generate_reservation_item(invoice, reservation, payment_details) if reservation
|
||||
|
||||
return unless subscription
|
||||
|
||||
generate_subscription_item(invoice, subscription, payment_details)
|
||||
end
|
||||
|
||||
##
|
||||
# Generate a single InvoiceItem for the given reservation and save it in invoice.invoice_items.
|
||||
# This method must be called only with a valid reservation
|
||||
##
|
||||
def generate_reservation_item(invoice, reservation, payment_details)
|
||||
raise TypeError unless [Space, Machine, Training].include? reservation.reservable.class
|
||||
|
||||
description = reservation.reservable.name
|
||||
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
|
||||
)
|
||||
description += " #{I18n.l slot.start_at, format: :long} - #{I18n.l slot.end_at, format: :hour_minute}\n"
|
||||
end
|
||||
|
||||
invoice.invoice_items.push InvoiceItem.new(
|
||||
amount: payment_details[:reservation],
|
||||
description: description
|
||||
)
|
||||
end
|
||||
|
||||
##
|
||||
# Generate an 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)
|
||||
def generate_subscription_item(invoice, subscription, payment_details)
|
||||
raise TypeError unless subscription
|
||||
|
||||
invoice.invoice_items.push InvoiceItem.new(
|
||||
amount: payment_details[:elements][:plan],
|
||||
amount: payment_details[:subscription],
|
||||
description: subscription.plan.name,
|
||||
subscription_id: subscription.id
|
||||
)
|
||||
end
|
||||
|
||||
##
|
||||
# Set the total price to the invoice, summing all sub-items.
|
||||
# Additionally a coupon may be applied to this invoice to make a discount on the total price
|
||||
##
|
||||
def set_total_and_coupon(payment_schedule_item, invoice, user, coupon = nil)
|
||||
return unless invoice
|
||||
|
||||
total = invoice.invoice_items.map(&:amount).map(&:to_i).reduce(:+)
|
||||
|
||||
unless coupon.nil?
|
||||
if (coupon.validity_per_user == 'once' && payment_schedule_item.first?) || coupon.validity_per_user == 'forever'
|
||||
total = CouponService.new.apply(total, coupon, user.id)
|
||||
invoice.coupon_id = coupon.id
|
||||
end
|
||||
end
|
||||
|
||||
invoice.total = total
|
||||
end
|
||||
end
|
||||
|
@ -0,0 +1,4 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.title notification.notification_type
|
||||
json.description t('.schedule_deadline')
|
@ -0,0 +1,4 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.title notification.notification_type
|
||||
json.description t('.schedule_failed')
|
@ -0,0 +1,4 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.title notification.notification_type
|
||||
json.description t('.schedule_failed')
|
@ -0,0 +1,10 @@
|
||||
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
|
||||
|
||||
<p>
|
||||
<%= t('.body.remember',
|
||||
REFERENCE: @attached_object.payment_schedule.reference,
|
||||
AMOUNT: number_to_currency(@attached_object.amount),
|
||||
DATE: I18n.l @attached_object.due_date, format: :long) %>
|
||||
<%= t('.body.date') %>
|
||||
</p>
|
||||
<p><%= t('.body.confirm') %></p>
|
@ -0,0 +1,10 @@
|
||||
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
|
||||
|
||||
<p>
|
||||
<%= t('.body.remember',
|
||||
REFERENCE: @attached_object.payment_schedule.reference,
|
||||
AMOUNT: number_to_currency(@attached_object.amount),
|
||||
DATE: I18n.l @attached_object.due_date, format: :long) %>
|
||||
<%= t('.body.error') %>
|
||||
</p>
|
||||
<p><%= t('.body.action') %></p>
|
@ -1,4 +1,4 @@
|
||||
<%# this is a mail template of notifcation notify_admin_user_wallet_is_credited %>
|
||||
<%# this is a mail template of notification notify_admin_user_wallet_is_credited %>
|
||||
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
|
||||
<p>
|
||||
<%= t('.body.wallet_credit_html',
|
||||
|
@ -0,0 +1,10 @@
|
||||
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
|
||||
|
||||
<p>
|
||||
<%= t('.body.remember',
|
||||
REFERENCE: @attached_object.payment_schedule.reference,
|
||||
AMOUNT: number_to_currency(@attached_object.amount),
|
||||
DATE: I18n.l @attached_object.due_date, format: :long) %>
|
||||
<%= t('.body.error') %>
|
||||
</p>
|
||||
<p><%= t('.body.action') %></p>
|
@ -9,23 +9,27 @@ class PaymentScheduleItemWorker
|
||||
PaymentScheduleItem.where(due_date: [DateTime.current.at_beginning_of_day, DateTime.current.end_of_day], state: 'new').each do |psi|
|
||||
# the following depends on the payment method (stripe/check)
|
||||
if psi.payment_schedule.payment_method == 'stripe'
|
||||
### Stripe
|
||||
stripe_key = Setting.get('stripe_secret_key')
|
||||
stp_suscription = Stripe::Subscription.retrieve(psi.payment_schedule.stp_subscription_id, api_key: stripe_key)
|
||||
stp_invoice = Stripe::Invoice.retrieve(stp_suscription.latest_invoice, api_key: stripe_key)
|
||||
if stp_invoice.status == 'paid'
|
||||
##### Stripe / Successfully paid
|
||||
PaymentScheduleService.new.generate_invoice(psi, stp_invoice)
|
||||
psi.update_attributes(state: 'paid')
|
||||
else
|
||||
NotificationCenter.call type: 'notify_admin_payment_schedule_failed', # TODO
|
||||
##### Stripe / Payment error
|
||||
NotificationCenter.call type: 'notify_admin_payment_schedule_failed',
|
||||
receiver: User.admins_and_managers,
|
||||
attached_object: psi
|
||||
NotificationCenter.call type: 'notify_member_payment_schedule_failed', # TODO
|
||||
NotificationCenter.call type: 'notify_member_payment_schedule_failed',
|
||||
receiver: psi.payment_schedule.user,
|
||||
attached_object: psi
|
||||
psi.update_attributes(state: 'pending')
|
||||
end
|
||||
else
|
||||
NotificationCenter.call type: 'notify_admin_payment_schedule_check', # TODO
|
||||
### Check
|
||||
NotificationCenter.call type: 'notify_admin_payment_schedule_check_deadline',
|
||||
receiver: User.admins_and_managers,
|
||||
attached_object: psi
|
||||
psi.update_attributes(state: 'pending')
|
||||
|
@ -359,6 +359,8 @@ en:
|
||||
validity_per_user: "Validity per user"
|
||||
once: "Just once"
|
||||
forever: "Each use"
|
||||
warn_validity_once: "Please note that when this coupon will be used with a payment schedule, the discount will be applied to the first deadline only."
|
||||
warn_validity_forever: "Please note that when this coupon will be used with a payment schedule, the discount will be applied to each deadlines."
|
||||
validity_per_user_is_required: "Validity per user is required."
|
||||
valid_until: "Valid until (included)"
|
||||
leave_empty_for_no_limit: "Do not specify any limit by leaving the field empty."
|
||||
|
@ -359,6 +359,8 @@ fr:
|
||||
validity_per_user: "Validité par utilisateur"
|
||||
once: "Une seule fois"
|
||||
forever: "À chaque utilisation"
|
||||
warn_validity_once: "Veuillez noter que lors de l'utilisation de ce code promo avec un échéancier de paiement, la réduction sera appliquée uniquement à la première échéance."
|
||||
warn_validity_forever: "Veuillez noter que lors de l'utilisation de ce code promo avec un échéancier de paiement, la réduction sera appliquée à chaque échéance."
|
||||
validity_per_user_is_required: "La validité par utilisateur est requise."
|
||||
valid_until: "Valable jusqu'au (inclus)"
|
||||
leave_empty_for_no_limit: "Laissez vide pour ne pas spécifier de limite."
|
||||
|
@ -358,6 +358,12 @@ en:
|
||||
all_members_sync: "All members were successfully synchronized on Stripe."
|
||||
notify_user_when_payment_schedule_ready:
|
||||
your_schedule_is_ready_html: "Your payment schedule #%{REFERENCE}, of %{AMOUNT}, is ready. <a href='api/payment_schedules/%{SCHEDULE_ID}/download' target='_blank'>Click here to download</a>."
|
||||
notify_admin_payment_schedule_failed:
|
||||
schedule_failed: "Failed card debit for the %{DATE} deadline, for schedule %{REFERENCE}"
|
||||
notify_member_payment_schedule_failed:
|
||||
schedule_failed: "Failed card debit for the %{DATE} deadline, for your schedule %{REFERENCE}"
|
||||
notify_admin_payment_schedule_check_deadline:
|
||||
schedule_deadline: "You must cash the check for the %{DATE} deadline, for schedule %{REFERENCE}"
|
||||
#statistics tools for admins
|
||||
statistics:
|
||||
subscriptions: "Subscriptions"
|
||||
|
@ -358,6 +358,12 @@ fr:
|
||||
all_members_sync: "Tous les membres ont été synchronisés avec succès sur Stripe."
|
||||
notify_user_when_payment_schedule_ready:
|
||||
your_schedule_is_ready_html: "Votre échéancier n°%{REFERENCE}, d'un montant de %{AMOUNT}, est prêt. <a href='api/payment_schedules/%{SCHEDULE_ID}/download' target='_blank'>Cliquez ici pour le télécharger</a>."
|
||||
notify_admin_payment_schedule_failed:
|
||||
schedule_failed: "Échec du prélèvement par carte de l'échéance du %{DATE}, pour l'échéancier %{REFERENCE}"
|
||||
notify_member_payment_schedule_failed:
|
||||
schedule_failed: "Échec du prélèvement par carte de l'échéance du %{DATE}, pour votre échéancier %{REFERENCE}"
|
||||
notify_admin_payment_schedule_check_deadline:
|
||||
schedule_deadline: "Vous devez encaisser le chèque de l'échéance du %{DATE}, pour l'échéancier %{REFERENCE}"
|
||||
#statistics tools for admins
|
||||
statistics:
|
||||
subscriptions: "Abonnements"
|
||||
|
@ -293,5 +293,23 @@ en:
|
||||
please_find_attached_html: "Please find attached your payment schedule, issued on {DATE}, with an amount of {AMOUNT} concerning your {TYPE, select, Reservation{reservation} other{subscription}}." #messageFormat interpolation
|
||||
schedule_in_your_dashboard_html: "You can find this payment schedule at any time from %{DASHBOARD} on the Fab Lab's website."
|
||||
your_dashboard: "your dashboard"
|
||||
notify_admin_payment_schedule_failed:
|
||||
subject: "Card debit failure"
|
||||
body:
|
||||
remember: "In accordance with the %{REFERENCE} payment schedule, a debit by card of %{AMOUNT} was scheduled on %{DATE}."
|
||||
error: "Unfortunately, this card debit was unable to complete successfully."
|
||||
action: "Please go to your payment schedule management interface as soon as possible to resolve the problem."
|
||||
notify_member_payment_schedule_failed:
|
||||
subject: "Card debit failure"
|
||||
body:
|
||||
remember: "In accordance with your %{REFERENCE} payment schedule, a debit by card of %{AMOUNT} was scheduled on %{DATE}."
|
||||
error: "Unfortunately, this card debit was unable to complete successfully."
|
||||
action: "Please contact the manager of your FabLab as soon as possible, otherwise your subscription may be interrupted."
|
||||
notify_admin_payment_schedule_check_deadline:
|
||||
subject: "Payment deadline"
|
||||
body:
|
||||
remember: "In accordance with the %{REFERENCE} payment schedule, %{AMOUNT} was due to be debited on %{DATE}."
|
||||
date: "This is a reminder to cash the scheduled check as soon as possible."
|
||||
confirm: "Do not forget to confirm the receipt in your payment schedule management interface, so that the corresponding invoice will be generated."
|
||||
shared:
|
||||
hello: "Hello %{user_name}"
|
||||
|
@ -293,5 +293,23 @@ fr:
|
||||
please_find_attached_html: "Vous trouverez en pièce jointe votre échéancier de paiement du {DATE}, d'un montant de {AMOUNT} concernant votre {TYPE, select, Reservation{réservation} other{abonnement}}." #messageFormat interpolation
|
||||
schedule_in_your_dashboard_html: "Vous pouvez à tout moment retrouver votre échéancier dans %{DASHBOARD} sur le site du Fab Lab."
|
||||
your_dashboard: "votre tableau de bord"
|
||||
notify_admin_payment_schedule_failed:
|
||||
subject: "Échec du prélèvement par carte"
|
||||
body:
|
||||
remember: "Conformément à l'échéancier de paiement %{REFERENCE}, un prélèvement par carte de %{AMOUNT} était prévu le %{DATE}."
|
||||
error: "Malheureusement, ce prélèvement n'a pas pu être effectué correctement."
|
||||
action: "Veuillez vous rendre au plus tôt dans votre interface de gestion des échéanciers pour régler le problème."
|
||||
notify_member_payment_schedule_failed:
|
||||
subject: "Échec du prélèvement par carte"
|
||||
body:
|
||||
remember: "Conformément à votre échéancier de paiement %{REFERENCE}, un prélèvement par carte de %{AMOUNT} était prévu le %{DATE}."
|
||||
error: "Malheureusement, ce prélèvement n'a pas pu être effectué correctement."
|
||||
action: "Veuillez prendre contact avec le gestionnaire de votre FabLab au plus tôt, faute de quoi votre abonnement risque d'être interrompu."
|
||||
notify_admin_payment_schedule_check_deadline:
|
||||
subject: "Échéance d'encaissement"
|
||||
body:
|
||||
remember: "Conformément à l'échéancier de paiement %{REFERENCE}, une échéance de %{AMOUNT} était prévu pour être prélevée le %{DATE}."
|
||||
date: "Ceci est un rappel d'encaisser le chèque prévu dès que possible."
|
||||
confirm: "N'oubliez pas de confirmer l'encaissement dans votre interface de gestion des échéanciers, afin que la facture correspondante soit générée."
|
||||
shared:
|
||||
hello: "Bonjour %{user_name}"
|
||||
|
Loading…
x
Reference in New Issue
Block a user