1
0
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:
Sylvain 2021-01-25 13:05:27 +01:00
parent ecdec70755
commit 26636254bd
17 changed files with 204 additions and 31 deletions

View File

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

View File

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

View File

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

View File

@ -0,0 +1,4 @@
# frozen_string_literal: true
json.title notification.notification_type
json.description t('.schedule_deadline')

View File

@ -0,0 +1,4 @@
# frozen_string_literal: true
json.title notification.notification_type
json.description t('.schedule_failed')

View File

@ -0,0 +1,4 @@
# frozen_string_literal: true
json.title notification.notification_type
json.description t('.schedule_failed')

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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