1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-20 14:54:15 +01:00

(bug) fix invalid invoice reference

This commit is contained in:
Sylvain 2023-03-16 17:17:00 +01:00
parent abc5fa6691
commit 98e58cbc25
32 changed files with 375 additions and 139 deletions

View File

@ -2,6 +2,9 @@
- Ability to restrict machine reservations per plan
- Ability to restrict machine availabilities per plan
- Admins cannot select the date when creating a refund invoice anymore
- Fix a bug: logical sequence of invoices references is broken, when using the store module or the payments schedules
- Fix a bug: refund invoices may generate duplicates in invoices references
- Fix a security issue: updated webpack to 5.76.0 to fix [CVE-2023-28154](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-28154)
- [TODO DEPLOY] `rails db:seed`
- [TODO DEPLOY] `rails fablab:maintenance:rebuild_stylesheet`

View File

@ -63,7 +63,7 @@ class API::InvoicesController < API::ApiController
private
def avoir_params
params.require(:avoir).permit(:invoice_id, :avoir_date, :payment_method, :subscription_to_expire, :description,
params.require(:avoir).permit(:invoice_id, :payment_method, :subscription_to_expire, :description,
invoice_items_ids: [])
end

View File

@ -25,7 +25,7 @@ class API::WalletController < API::ApiController
service = WalletService.new(user: current_user, wallet: @wallet)
transaction = service.credit(credit_params[:amount].to_f)
if transaction
service.create_avoir(transaction, credit_params[:avoir_date], credit_params[:avoir_description]) if credit_params[:avoir]
service.create_avoir(transaction, credit_params[:avoir_description]) if credit_params[:avoir]
render :show
else
head :unprocessable_entity
@ -35,6 +35,6 @@ class API::WalletController < API::ApiController
private
def credit_params
params.permit(:id, :amount, :avoir, :avoir_date, :avoir_description)
params.permit(:id, :amount, :avoir, :avoir_description)
end
end

View File

@ -896,9 +896,6 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
// default: do not generate a refund invoice
$scope.generate_avoir = false;
// date of the generated refund invoice
$scope.avoir_date = null;
// optional description shown on the refund invoice
$scope.description = '';
@ -929,7 +926,6 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
{
amount: $scope.amount,
avoir: $scope.generate_avoir,
avoir_date: $scope.avoir_date,
avoir_description: $scope.description
},
function (_wallet) {

View File

@ -3,24 +3,6 @@
</div>
<div class="modal-body">
<form name="avoirForm" novalidate="novalidate">
<div class="form-group" ng-class="{'has-error': avoirForm.avoir_date.$dirty && avoirForm.avoir_date.$invalid }">
<label translate>{{ 'app.admin.invoices.creation_date_for_the_refund' }}</label>
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
<input type="text"
class="form-control"
name="avoir_date"
ng-model="avoir.avoir_date"
uib-datepicker-popup="{{datePicker.format}}"
datepicker-options="datePicker.options"
is-open="datePicker.opened"
min-date="lastClosingEnd"
placeholder="{{datePicker.format}}"
ng-click="openDatePicker($event)"
required/>
</div>
<span class="help-block" ng-show="avoirForm.avoir_date.$dirty && avoirForm.avoir_date.$error.required" translate>{{ 'app.admin.invoices.creation_date_is_required' }}</span>
</div>
<div class="form-group">
<label translate>{{ 'app.admin.invoices.refund_mode' }}</label>
<select class="form-control m-t-sm" name="payment_method" ng-model="avoir.payment_method" ng-options="mode.value as mode.name for mode in avoirModes" required></select>

View File

@ -55,25 +55,6 @@
</div>
<div ng-show="generate_avoir">
<div class="m-t" ng-class="{'has-error': walletForm.avoir_date.$dirty && walletForm.avoir_date.$invalid }">
<label for="avoir_date" translate>{{ 'app.shared.wallet.creation_date_for_the_refund' }}</label>
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
<input type="text"
class="form-control"
id="avoir_date"
name="avoir_date"
ng-model="avoir_date"
uib-datepicker-popup="{{datePicker.format}}"
datepicker-options="datePicker.options"
is-open="datePicker.opened"
placeholder="{{datePicker.format}}"
ng-click="toggleDatePicker($event)"
ng-required="generate_avoir"/>
</div>
<span class="help-block" ng-show="walletForm.avoir_date.$dirty && walletForm.avoir_date.$error.required" translate>{{ 'app.shared.wallet.creation_date_is_required' }}</span>
</div>
<div class="m-t">
<label for="description" translate>{{ 'app.shared.wallet.description_optional' }}</label>
<p translate>{{ 'app.shared.wallet.will_appear_on_the_refund_invoice' }}</p>

12
app/helpers/db_helper.rb Normal file
View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
# Helpers for database operations
module DbHelper
# Ruby times are localised and does not have the same precision as database times do comparing them in .where() clauses may
# result in unexpected results. This function worksaround this issue by converting the Time to a database-comparable format
# @param [Time]
# @return [String]
def db_time(time)
time.utc.strftime('%Y-%m-%d %H:%M:%S.%6N')
end
end

View File

@ -12,6 +12,8 @@ class Avoir < Invoice
attr_accessor :invoice_items_ids
delegate :order_number, to: :invoice
def generate_reference
super(created_at)
end

View File

@ -14,8 +14,9 @@ class Invoice < PaymentDocument
belongs_to :coupon
has_one :avoir, class_name: 'Invoice', dependent: :destroy, inverse_of: :avoir
has_one :payment_schedule_item, dependent: :nullify
has_one :payment_schedule_item, dependent: :restrict_with_error
has_one :payment_gateway_object, as: :item, dependent: :destroy
has_one :order, dependent: :restrict_with_error
belongs_to :operator_profile, class_name: 'InvoicingProfile'
has_many :accounting_lines, dependent: :destroy
@ -49,6 +50,12 @@ class Invoice < PaymentDocument
end
def order_number
return order.reference unless order.nil?
if !payment_schedule_item.nil? && !payment_schedule_item.first?
return payment_schedule_item.payment_schedule.ordered_items.first.invoice.order_number
end
PaymentDocumentService.generate_order_number(self)
end
@ -66,8 +73,7 @@ class Invoice < PaymentDocument
avoir.attributes = attrs
avoir.reference = nil
avoir.invoice_id = id
# override created_at to compute CA in stats
avoir.created_at = avoir.avoir_date
avoir.avoir_date = Time.current
avoir.total = 0
# refunds of invoices with cash coupons: we need to ventilate coupons on paid items
paid_items = 0
@ -79,7 +85,6 @@ class Invoice < PaymentDocument
refund_items += 1 unless ii.amount.zero?
avoir_ii = avoir.invoice_items.build(ii.dup.attributes)
avoir_ii.created_at = avoir.avoir_date
avoir_ii.invoice_item_id = ii.id
avoir.total += avoir_ii.amount
end

View File

@ -3,10 +3,13 @@
# Provides methods to generate Invoice, Avoir or PaymentSchedule references
class PaymentDocumentService
class << self
include DbHelper
# @param document [PaymentDocument]
# @param date [Time]
def generate_reference(document, date: Time.current)
pattern = Setting.get('invoice_reference')
pattern = Setting.get('invoice_reference').to_s
reference = replace_invoice_number_pattern(pattern, document.created_at)
reference = replace_document_number_pattern(pattern, document, document.created_at)
reference = replace_date_pattern(reference, date)
case document
@ -44,15 +47,16 @@ class PaymentDocumentService
reference
end
# @param document [PaymentDocument]
def generate_order_number(document)
pattern = Setting.get('invoice_order-nb')
# global document number (nn..nn)
reference = pattern.gsub(/n+(?![^\[]*\])/) do |match|
pad_and_truncate(number_of_invoices(document.is_a?(Order) ? 'order' : 'global'), match.to_s.length)
pad_and_truncate(number_of_order('global', document, document.created_at), match.to_s.length)
end
reference = replace_invoice_number_pattern(reference, document.created_at)
reference = replace_document_number_pattern(reference, document, document.created_at, :number_of_order)
replace_date_pattern(reference, document.created_at)
end
@ -69,25 +73,55 @@ class PaymentDocumentService
# Returns the number of current invoices in the given range around the current date.
# If range is invalid or not specified, the total number of invoices is returned.
# @param range [String] 'day', 'month', 'year'
# @param date [Date] the ending date
# @param document [PaymentDocument]
# @param date [Time] the ending date
# @return [Integer]
def number_of_invoices(range, date = Time.current)
case range.to_s
when 'day'
start = date.beginning_of_day
when 'month'
start = date.beginning_of_month
when 'year'
start = date.beginning_of_year
else
return get_max_id(Invoice) + get_max_id(PaymentSchedule) + get_max_id(Order)
end
def number_of_documents(range, document, date = Time.current)
start = case range.to_s
when 'day'
date.beginning_of_day
when 'month'
date.beginning_of_month
when 'year'
date.beginning_of_year
else
nil
end
ending = date
return Invoice.count + PaymentSchedule.count + Order.count unless defined? start
Invoice.where('created_at >= :start_date AND created_at <= :end_date', start_date: start, end_date: ending).length +
PaymentSchedule.where('created_at >= :start_date AND created_at <= :end_date', start_date: start, end_date: ending).length +
Order.where('created_at >= :start_date AND created_at <= :end_date', start_date: start, end_date: ending).length
documents = document.class.base_class
.where('created_at <= :end_date', end_date: db_time(ending))
documents = documents.where('created_at >= :start_date', start_date: db_time(start)) unless start.nil?
documents.count
end
def number_of_order(range, _document, date = Time.current)
start = case range.to_s
when 'day'
date.beginning_of_day
when 'month'
date.beginning_of_month
when 'year'
date.beginning_of_year
else
nil
end
ending = date
orders = Order.where('created_at <= :end_date', end_date: db_time(ending))
orders = orders.where('created_at >= :start_date', start_date: db_time(start)) unless start.nil?
schedules = PaymentSchedule.where('created_at <= :end_date', end_date: db_time(ending))
schedules = schedules.where('created_at >= :start_date', start_date: db_time(start)) unless start.nil?
invoices = Invoice.where(type: nil)
.where.not(id: orders.map(&:invoice_id))
.where.not(id: schedules.map(&:payment_schedule_items).flatten.map(&:invoice_id).filter(&:present?))
.where('created_at <= :end_date', end_date: db_time(ending))
invoices = invoices.where('created_at >= :start_date', start_date: db_time(start)) unless start.nil?
orders.count + schedules.count + invoices.count
end
# Replace the date elements in the provided pattern with the date values, from the provided date
@ -102,7 +136,9 @@ class PaymentDocumentService
copy.gsub!(/(?![^\[]*\])YY(?![^\[]*\])/, date.strftime('%y'))
# abbreviated month name (MMM)
copy.gsub!(/(?![^\[]*\])MMM(?![^\[]*\])/, date.strftime('%^b'))
# we cannot replace by the month name directly because it may contrains an M or a D
# so we replace it by a special indicator and, at the end, we will replace it by the abbreviated month name
copy.gsub!(/(?![^\[]*\])MMM(?![^\[]*\])/, '}~{')
# month of the year, zero-padded (MM)
copy.gsub!(/(?![^\[]*\])MM(?![^\[]*\])/, date.strftime('%m'))
# month of the year, non zero-padded (M)
@ -110,40 +146,37 @@ class PaymentDocumentService
# day of the month, zero-padded (DD)
copy.gsub!(/(?![^\[]*\])DD(?![^\[]*\])/, date.strftime('%d'))
# day of the month, non zero-padded (DD)
copy.gsub!(/(?![^\[]*\])DD(?![^\[]*\])/, date.strftime('%-d'))
# day of the month, non zero-padded (D)
copy.gsub!(/(?![^\[]*\])D(?![^\[]*\])/, date.strftime('%-d'))
# abbreviated month name (MMM) (2)
copy.gsub!(/(?![^\[]*\])}~\{(?![^\[]*\])/, date.strftime('%^b'))
copy
end
# Replace the document number elements in the provided pattern with counts from the database
# @param reference [String]
# @param document [PaymentDocument]
# @param date [Time]
def replace_invoice_number_pattern(reference, date)
# @param count_method [Symbol] :number_of_documents OR :number_of_order
def replace_document_number_pattern(reference, document, date, count_method = :number_of_documents)
copy = reference.dup
# document number per year (yy..yy)
copy.gsub!(/y+(?![^\[]*\])/) do |match|
pad_and_truncate(number_of_invoices('year', date), match.to_s.length)
pad_and_truncate(send(count_method, 'year', document, date), match.to_s.length)
end
# document number per month (mm..mm)
copy.gsub!(/m+(?![^\[]*\])/) do |match|
pad_and_truncate(number_of_invoices('month', date), match.to_s.length)
pad_and_truncate(send(count_method, 'month', document, date), match.to_s.length)
end
# document number per day (dd..dd)
copy.gsub!(/d+(?![^\[]*\])/) do |match|
pad_and_truncate(number_of_invoices('day', date), match.to_s.length)
pad_and_truncate(send(count_method, 'day', document, date), match.to_s.length)
end
copy
end
##
# Return the maximum ID from the database, for the given class
# @param klass {ActiveRecord::Base}
##
def get_max_id(klass)
ActiveRecord::Base.connection.execute("SELECT max(id) FROM #{klass.table_name}").getvalue(0, 0) || 0
end
end
end

View File

@ -80,7 +80,7 @@ class Trainings::AutoCancelService
service = WalletService.new(user: reservation.user, wallet: reservation.user.wallet)
transaction = service.credit(amount)
service.create_avoir(transaction, Time.current, I18n.t('trainings.refund_for_auto_cancel')) if transaction
service.create_avoir(transaction, I18n.t('trainings.refund_for_auto_cancel')) if transaction
end
end
end

View File

@ -50,11 +50,10 @@ class WalletService
end
## create a refund invoice associated with the given wallet transaction
def create_avoir(wallet_transaction, avoir_date, description)
def create_avoir(wallet_transaction, description)
avoir = Avoir.new
avoir.type = 'Avoir'
avoir.avoir_date = avoir_date
avoir.created_at = avoir_date
avoir.avoir_date = Time.current
avoir.description = description
avoir.payment_method = 'wallet'
avoir.subscription_to_expire = false

View File

@ -923,8 +923,6 @@ de:
deleted_user: "Gelöschter Nutzer"
refund_invoice_successfully_created: "Rückerstattungsrechnung erfolgreich erstellt."
create_a_refund_on_this_invoice: "Erstelle eine Rückerstattung mit dieser Rechnung"
creation_date_for_the_refund: "Erstellungsdatum für die Erstattung"
creation_date_is_required: "Erstellungsdatum ist erforderlich."
refund_mode: "Erstattungsmodus:"
do_you_want_to_disable_the_user_s_subscription: "Möchten Sie das Abonnement des Benutzers deaktivieren:"
elements_to_refund: "Erstattungselemente"

View File

@ -923,8 +923,6 @@ en:
deleted_user: "Deleted user"
refund_invoice_successfully_created: "Refund invoice successfully created."
create_a_refund_on_this_invoice: "Create a refund on this invoice"
creation_date_for_the_refund: "Creation date for the refund"
creation_date_is_required: "Creation date is required."
refund_mode: "Refund mode:"
do_you_want_to_disable_the_user_s_subscription: "Do you want to disabled the user's subscription:"
elements_to_refund: "Elements to refund"

View File

@ -923,8 +923,6 @@ es:
deleted_user: "Usario eliminado"
refund_invoice_successfully_created: "Factura de reembolso creada correctamente."
create_a_refund_on_this_invoice: "Crear un reembolso en esta factura"
creation_date_for_the_refund: "Fecha de creación del reembolso"
creation_date_is_required: "Se requiere la fecha de creación."
refund_mode: "Modo de reembolso:"
do_you_want_to_disable_the_user_s_subscription: "¿Quieres inhabilitar la suscripción del usuario?:"
elements_to_refund: "Elementos a reembolsar"

View File

@ -923,8 +923,6 @@ fr:
deleted_user: "Utilisateur supprimé"
refund_invoice_successfully_created: "La facture d'avoir a bien été créée."
create_a_refund_on_this_invoice: "Générer un avoir sur cette facture"
creation_date_for_the_refund: "Date d'émission de l'avoir"
creation_date_is_required: "La date d'émission est requise."
refund_mode: "Mode de remboursement :"
do_you_want_to_disable_the_user_s_subscription: "Souhaitez-vous désactiver l'abonnement de l'utilisateur :"
elements_to_refund: "Éléments à rembourser"

View File

@ -923,8 +923,6 @@
deleted_user: "Slettet bruker"
refund_invoice_successfully_created: "Refusjon ble opprettet."
create_a_refund_on_this_invoice: "Opprett en refusjon på denne fakturaen"
creation_date_for_the_refund: "Refusjonsdato"
creation_date_is_required: "Opprettelsesdato er påkrevd."
refund_mode: "Refusjonsmodus:"
do_you_want_to_disable_the_user_s_subscription: "Ønsker du å deaktivere brukerens abonnement/medlemskap:"
elements_to_refund: "Elementer for tilbakebetaling"

View File

@ -923,8 +923,6 @@ pt:
deleted_user: "Usuário deletado"
refund_invoice_successfully_created: "Restituição de fatura criada com sucesso."
create_a_refund_on_this_invoice: "Criar restituição de fatura"
creation_date_for_the_refund: "Criação de data de restituição"
creation_date_is_required: "Data de criação é obrigatório."
refund_mode: "Modo de restituição:"
do_you_want_to_disable_the_user_s_subscription: "Você deseja desativar a inscrição de usuários:"
elements_to_refund: "Elementos para restituição"

View File

@ -923,8 +923,6 @@ zu:
deleted_user: "crwdns25138:0crwdne25138:0"
refund_invoice_successfully_created: "crwdns25140:0crwdne25140:0"
create_a_refund_on_this_invoice: "crwdns25142:0crwdne25142:0"
creation_date_for_the_refund: "crwdns25144:0crwdne25144:0"
creation_date_is_required: "crwdns25146:0crwdne25146:0"
refund_mode: "crwdns25148:0crwdne25148:0"
do_you_want_to_disable_the_user_s_subscription: "crwdns25150:0crwdne25150:0"
elements_to_refund: "crwdns25152:0crwdne25152:0"

View File

@ -234,8 +234,6 @@ de:
credit_label: 'Legen Sie den Betrag der Gutschrift fest'
confirm_credit_label: 'Bestätigen Sie den Betrag der Gutschrift'
generate_a_refund_invoice: "Erstelle eine Rückerstattungs-Rechnung"
creation_date_for_the_refund: "Erstellungsdatum für die Erstattung"
creation_date_is_required: "Erstellungsdatum ist erforderlich."
description_optional: "Beschreibung (optional):"
will_appear_on_the_refund_invoice: "Wird auf der Rückerstattungsrechnung angezeigt."
to_credit: 'Guthaben'

View File

@ -234,8 +234,6 @@ en:
credit_label: 'Set the amount to be credited'
confirm_credit_label: 'Confirm the amount to be credited'
generate_a_refund_invoice: "Generate a refund invoice"
creation_date_for_the_refund: "Creation date for the refund"
creation_date_is_required: "Creation date is required."
description_optional: "Description (optional):"
will_appear_on_the_refund_invoice: "Will appear on the refund invoice."
to_credit: 'Credit'

View File

@ -234,8 +234,6 @@ es:
credit_label: 'Selecciona la cantidad a creditar'
confirm_credit_label: 'Confirma la cantidad a creditar'
generate_a_refund_invoice: "Generar informe de devolución"
creation_date_for_the_refund: "Fecha de creación del informe de devolución"
creation_date_is_required: "Se requiere fecha de creación."
description_optional: "Descripción (opcional):"
will_appear_on_the_refund_invoice: "Aparecerá en el informe de devolución."
to_credit: 'Credito'

View File

@ -234,8 +234,6 @@ fr:
credit_label: 'Indiquez le montant à créditer'
confirm_credit_label: 'Confirmez le montant à créditer'
generate_a_refund_invoice: "Générer une facture d'avoir"
creation_date_for_the_refund: "Date d'émission de l'avoir"
creation_date_is_required: "La date d'émission est requise."
description_optional: "Description (optionnelle) :"
will_appear_on_the_refund_invoice: "Apparaîtra sur la facture de remboursement."
to_credit: 'Créditer'

View File

@ -234,8 +234,6 @@
credit_label: 'Velg beløp for kreditering'
confirm_credit_label: 'Bekreft beløpet som skal krediteres'
generate_a_refund_invoice: "Genererer en refusjons- faktura"
creation_date_for_the_refund: "Refusjonsdato"
creation_date_is_required: "Opprettelsesdato er påkrevd."
description_optional: "Beskrivelse (valgfritt):"
will_appear_on_the_refund_invoice: "Vises på refusjonsfakturaen."
to_credit: 'Kreditt'

View File

@ -234,8 +234,6 @@ pt:
credit_label: 'Digite a quantia a ser creditada'
confirm_credit_label: 'Confirme a quantia a ser creditada'
generate_a_refund_invoice: "Gerar uma fatura de reembolso"
creation_date_for_the_refund: "Data de criação de reembolso"
creation_date_is_required: "Data de criação é obrigatório."
description_optional: "Descrição (opcional):"
will_appear_on_the_refund_invoice: "Aparecerá na fatura de reembolso."
to_credit: 'Crédito'

View File

@ -234,8 +234,6 @@ zu:
credit_label: 'crwdns29096:0crwdne29096:0'
confirm_credit_label: 'crwdns29098:0crwdne29098:0'
generate_a_refund_invoice: "crwdns29100:0crwdne29100:0"
creation_date_for_the_refund: "crwdns29102:0crwdne29102:0"
creation_date_is_required: "crwdns29104:0crwdne29104:0"
description_optional: "crwdns29106:0crwdne29106:0"
will_appear_on_the_refund_invoice: "crwdns29108:0crwdne29108:0"
to_credit: 'crwdns29110:0crwdne29110:0'

View File

@ -4,8 +4,8 @@
module InvoiceHelper
# Force the invoice generation worker to run NOW and check the resulting file generated.
# Delete the file afterwards.
# @param invoice {Invoice}
# @param &block an optional block may be provided for additional specific assertions on the invoices PDF lines
# @param invoice [Invoice]
# @yield an optional block may be provided for additional specific assertions on the invoices PDF lines
def assert_invoice_pdf(invoice)
assert_not_nil invoice, 'Invoice was not created'
@ -27,6 +27,43 @@ module InvoiceHelper
File.delete(invoice.file)
end
# @param customer [User]
# @param operator [User]
# @return [Invoice] saved
def sample_reservation_invoice(customer, operator)
machine = Machine.first
slot = Availabilities::AvailabilitiesService.new(operator)
.machines([machine], customer, { start: Time.current, end: 1.year.from_now })
.find { |s| !s.full?(machine) }
reservation = Reservation.new(
reservable: machine,
slots_reservations: [SlotsReservation.new({ slot_id: slot.id })],
statistic_profile: customer.statistic_profile
)
reservation.save
invoice = Invoice.new(
invoicing_profile: customer.invoicing_profile,
statistic_profile: customer.statistic_profile,
operator_profile: operator.invoicing_profile,
payment_method: '',
invoice_items: [InvoiceItem.new(
amount: 1000,
description: "reservation #{machine.name}",
object: reservation,
main: true
)]
)
unless operator.privileged?
invoice.payment_method = 'card'
invoice.payment_gateway_object = PaymentGatewayObject.new(
gateway_object_id: 'pi_3LpALs2sOmf47Nz91QyFI7nP',
gateway_object_type: 'Stripe::PaymentIntent'
)
end
invoice.save
invoice
end
private
def generate_pdf(invoice)

View File

@ -0,0 +1,43 @@
# frozen_string_literal: true
# Provides methods to help testing payment schedules
module PaymentScheduleHelper
# Force the payment schedule generation worker to run NOW and check the resulting file generated.
# Delete the file afterwards.
# @param schedule [PaymentSchedule]
def assert_schedule_pdf(schedule)
assert_not_nil schedule, 'Schedule was not created'
generate_schedule_pdf(schedule)
assert File.exist?(schedule.file), 'Schedule PDF was not generated'
File.delete(schedule.file)
end
# @param customer [User]
# @param operator [User]
# @return [PaymentSchedule] saved
def sample_schedule(customer, operator)
plan = plans(:plan_schedulable)
subscription = Subscription.new(plan: plan, statistic_profile_id: customer.statistic_profile, start_at: Time.current)
subscription.save
options = { payment_method: '' }
unless operator.privileged?
options = { payment_method: 'card', payment_id: 'pi_3LpALs2sOmf47Nz91QyFI7nP', payment_type: 'Stripe::PaymentIntent' }
end
schedule = PaymentScheduleService.new.create([subscription], 113_600, customer, operator: operator, **options)
schedule.save
first_item = schedule.ordered_items.first
PaymentScheduleService.new.generate_invoice(first_item, **options)
first_item.update(state: 'paid', payment_method: operator.privileged? ? 'check' : 'card')
schedule
end
private
def generate_schedule_pdf(schedule)
schedule_worker = PaymentScheduleWorker.new
schedule_worker.perform(schedule.id)
end
end

View File

@ -67,24 +67,4 @@ class InvoicesTest < ActionDispatch::IntegrationTest
# Check footprint
assert avoir.check_footprint
end
test 'admin fails generates a refund in closed period' do
date = Time.zone.parse('2015-10-01T13:09:55+01:00')
post '/api/invoices', params: { avoir: {
avoir_date: date,
payment_method: 'cash',
description: 'Unable to refund',
invoice_id: 5,
invoice_items_ids: [5],
subscription_to_expire: false
} }.to_json, headers: default_headers
# Check response format & status
assert_equal 422, response.status, response.body
assert_equal Mime[:json], response.content_type
# Check the error was handled
assert_match(/#{I18n.t('errors.messages.in_closed_period')}/, response.body)
end
end

View File

@ -84,6 +84,7 @@ class Reservations::PaymentScheduleTest < ActionDispatch::IntegrationTest
assert payment_schedule.check_footprint
assert_equal @user_without_subscription.invoicing_profile.id, payment_schedule.invoicing_profile_id
assert_equal @admin.invoicing_profile.id, payment_schedule.operator_profile_id
assert_schedule_pdf(payment_schedule)
# Check the answer
result = json_response(response.body)

View File

@ -0,0 +1,188 @@
# frozen_string_literal: true
require 'test_helper'
class PaymentDocumentServiceTest < ActiveSupport::TestCase
setup do
@admin = User.find_by(username: 'admin')
@acamus = User.find_by(username: 'acamus')
@machine = Machine.first
# From the fixtures,
# - invoice_reference = YYMMmmmX[/VL]R[/A]
# - invoice_order-nb = nnnnnn-MM-YY
end
test 'invoice for local payment' do
invoice = sample_reservation_invoice(@acamus, @admin)
assert_equal "#{Time.current.strftime('%y%m')}001", invoice.reference
assert_equal "000018-#{Time.current.strftime('%m-%y')}", invoice.order_number
end
test 'invoice with custom format' do
travel_to(Time.current.beginning_of_month)
Setting.set('invoice_reference', 'YYYYMMMDdddddX[/VL]R[/A]S[/E]')
Setting.set('invoice_order-nb', 'yyyy-YYYY')
invoice = sample_reservation_invoice(@acamus, @admin)
assert_equal "#{Time.current.strftime('%Y%^b%-d')}00001", invoice.reference
assert_equal "0001-#{Time.current.strftime('%Y')}", invoice.order_number
travel_back
end
test 'invoice with other custom format' do
travel_to(Time.current.beginning_of_year)
Setting.set('invoice_reference', 'YYMDDyyyyX[/VL]R[/A]S[/E]')
Setting.set('invoice_order-nb', 'DMYYYYnnnnnn')
invoice = sample_reservation_invoice(@acamus, @admin)
assert_equal "#{Time.current.strftime('%y%-m%d')}0001", invoice.reference
assert_equal "#{Time.current.strftime('%-d%-m%Y')}000018", invoice.order_number
travel_back
end
test 'invoice for online card payment' do
invoice = sample_reservation_invoice(@acamus, @acamus)
assert_equal "#{Time.current.strftime('%y%m')}001/VL", invoice.reference
assert_equal "000018-#{Time.current.strftime('%m-%y')}", invoice.order_number
end
test 'refund' do
invoice = sample_reservation_invoice(@acamus, @admin)
assert_equal "#{Time.current.strftime('%y%m')}001", invoice.reference
assert_equal "000018-#{Time.current.strftime('%m-%y')}", invoice.order_number
refund = invoice.build_avoir(payment_method: 'wallet', invoice_items_ids: invoice.invoice_items.map(&:id))
refund.save
refund.reload
assert_equal "#{Time.current.strftime('%y%m')}002/A", refund.reference
assert_equal "000018-#{Time.current.strftime('%m-%y')}", refund.order_number
end
test 'payment schedule' do
Setting.set('invoice_reference', 'YYMMmmmX[/VL]R[/A]S[/E]')
schedule = sample_schedule(@acamus, @admin)
assert_equal "#{Time.current.strftime('%y%m')}001/E", schedule.reference
first_item = schedule.ordered_items.first
assert_equal "#{Time.current.strftime('%y%m')}001", first_item.invoice.reference
assert_equal "000018-#{Time.current.strftime('%m-%y')}", first_item.invoice.order_number
second_item = schedule.ordered_items[1]
PaymentScheduleService.new.generate_invoice(second_item, payment_method: 'check')
assert_equal "#{Time.current.strftime('%y%m')}002", second_item.invoice.reference
assert_equal "000018-#{Time.current.strftime('%m-%y')}", second_item.invoice.order_number
third_item = schedule.ordered_items[2]
PaymentScheduleService.new.generate_invoice(third_item, payment_method: 'check')
assert_equal "#{Time.current.strftime('%y%m')}003", third_item.invoice.reference
assert_equal "000018-#{Time.current.strftime('%m-%y')}", third_item.invoice.order_number
fourth_item = schedule.ordered_items[3]
PaymentScheduleService.new.generate_invoice(fourth_item, payment_method: 'check')
assert_equal "#{Time.current.strftime('%y%m')}004", fourth_item.invoice.reference
assert_equal "000018-#{Time.current.strftime('%m-%y')}", fourth_item.invoice.order_number
fifth_item = schedule.ordered_items[2]
PaymentScheduleService.new.generate_invoice(fifth_item, payment_method: 'check')
assert_equal "#{Time.current.strftime('%y%m')}005", fifth_item.invoice.reference
assert_equal "000018-#{Time.current.strftime('%m-%y')}", fifth_item.invoice.order_number
end
test 'order' do
cart = Cart::FindOrCreateService.new(users(:user_2)).call(nil)
cart = Cart::AddItemService.new.call(cart, Product.find_by(slug: 'panneaux-de-mdf'), 1)
Checkout::PaymentService.new.payment(cart, @admin, nil)
assert_equal "000018-#{Time.current.strftime('%m-%y')}", cart.reference # here reference = order number
assert_equal "000018-#{Time.current.strftime('%m-%y')}", cart.invoice.order_number
assert_equal "#{Time.current.strftime('%y%m')}001", cart.invoice.reference
end
test 'multiple items logical sequence' do
Setting.set('invoice_reference', 'YYMMmmmX[/VL]R[/A]S[/E]')
invoice = sample_reservation_invoice(@acamus, @admin)
assert_equal "#{Time.current.strftime('%y%m')}001", invoice.reference
assert_equal "000018-#{Time.current.strftime('%m-%y')}", invoice.order_number
refund = invoice.build_avoir(payment_method: 'wallet', invoice_items_ids: invoice.invoice_items.map(&:id))
refund.save
refund.reload
assert_equal "#{Time.current.strftime('%y%m')}002/A", refund.reference
assert_equal "000018-#{Time.current.strftime('%m-%y')}", refund.order_number
invoice = sample_reservation_invoice(@acamus, @admin)
assert_equal "#{Time.current.strftime('%y%m')}003", invoice.reference
assert_equal "000019-#{Time.current.strftime('%m-%y')}", invoice.order_number
invoice = sample_reservation_invoice(@acamus, @acamus)
assert_equal "#{Time.current.strftime('%y%m')}004/VL", invoice.reference
assert_equal "000020-#{Time.current.strftime('%m-%y')}", invoice.order_number
invoice = sample_reservation_invoice(@acamus, @admin)
assert_equal "#{Time.current.strftime('%y%m')}005", invoice.reference
assert_equal "000021-#{Time.current.strftime('%m-%y')}", invoice.order_number
invoice = sample_reservation_invoice(@acamus, @admin)
assert_equal "#{Time.current.strftime('%y%m')}006", invoice.reference
assert_equal "000022-#{Time.current.strftime('%m-%y')}", invoice.order_number
invoice = sample_reservation_invoice(@acamus, @acamus)
assert_equal "#{Time.current.strftime('%y%m')}007/VL", invoice.reference
assert_equal "000023-#{Time.current.strftime('%m-%y')}", invoice.order_number
invoice = sample_reservation_invoice(@acamus, @acamus)
assert_equal "#{Time.current.strftime('%y%m')}008/VL", invoice.reference
assert_equal "000024-#{Time.current.strftime('%m-%y')}", invoice.order_number
refund = invoice.build_avoir(payment_method: 'wallet', invoice_items_ids: invoice.invoice_items.map(&:id))
refund.save
refund.reload
assert_equal "#{Time.current.strftime('%y%m')}009/A", refund.reference
assert_equal "000024-#{Time.current.strftime('%m-%y')}", refund.order_number
invoice = sample_reservation_invoice(@acamus, @acamus)
assert_equal "#{Time.current.strftime('%y%m')}010/VL", invoice.reference
assert_equal "000025-#{Time.current.strftime('%m-%y')}", invoice.order_number
invoice2 = sample_reservation_invoice(@acamus, @admin)
assert_equal "#{Time.current.strftime('%y%m')}011", invoice2.reference
assert_equal "000026-#{Time.current.strftime('%m-%y')}", invoice2.order_number
refund = invoice.build_avoir(payment_method: 'wallet', invoice_items_ids: invoice.invoice_items.map(&:id))
refund.save
refund.reload
assert_equal "#{Time.current.strftime('%y%m')}012/A", refund.reference
assert_equal "000025-#{Time.current.strftime('%m-%y')}", refund.order_number
refund = invoice2.build_avoir(payment_method: 'wallet', invoice_items_ids: invoice.invoice_items.map(&:id))
refund.save
refund.reload
assert_equal "#{Time.current.strftime('%y%m')}013/A", refund.reference
assert_equal "000026-#{Time.current.strftime('%m-%y')}", refund.order_number
schedule = sample_schedule(@acamus, @admin)
assert_equal "#{Time.current.strftime('%y%m')}001/E", schedule.reference
assert_equal "#{Time.current.strftime('%y%m')}014", schedule.ordered_items.first.invoice.reference
assert_equal "000027-#{Time.current.strftime('%m-%y')}", schedule.ordered_items.first.invoice.order_number
schedule = sample_schedule(users(:user_2), users(:user_2))
assert_equal "#{Time.current.strftime('%y%m')}002/E", schedule.reference
assert_equal "#{Time.current.strftime('%y%m')}015/VL", schedule.ordered_items.first.invoice.reference
assert_equal "000028-#{Time.current.strftime('%m-%y')}", schedule.ordered_items.first.invoice.order_number
invoice = sample_reservation_invoice(@acamus, @acamus)
assert_equal "#{Time.current.strftime('%y%m')}016/VL", invoice.reference
assert_equal "000029-#{Time.current.strftime('%m-%y')}", invoice.order_number
cart = Cart::FindOrCreateService.new(users(:user_2)).call(nil)
cart = Cart::AddItemService.new.call(cart, Product.find_by(slug: 'panneaux-de-mdf'), 1)
Checkout::PaymentService.new.payment(cart, @admin, nil)
assert_equal "000030-#{Time.current.strftime('%m-%y')}", cart.reference # here reference = order number
assert_equal "000030-#{Time.current.strftime('%m-%y')}", cart.invoice.order_number
assert_equal "#{Time.current.strftime('%y%m')}017", cart.invoice.reference
cart = Cart::FindOrCreateService.new(users(:user_2)).call(nil)
cart = Cart::AddItemService.new.call(cart, Product.find_by(slug: 'panneaux-de-mdf'), 1)
Checkout::PaymentService.new.payment(cart, @admin, nil)
assert_equal "000031-#{Time.current.strftime('%m-%y')}", cart.reference # here reference = order number
assert_equal "000031-#{Time.current.strftime('%m-%y')}", cart.invoice.order_number
assert_equal "#{Time.current.strftime('%y%m')}018", cart.invoice.reference
invoice = sample_reservation_invoice(@acamus, @admin)
assert_equal "#{Time.current.strftime('%y%m')}019", invoice.reference
assert_equal "000032-#{Time.current.strftime('%m-%y')}", invoice.order_number
end
end

View File

@ -10,8 +10,9 @@ require 'rails/test_help'
require 'vcr'
require 'sidekiq/testing'
require 'minitest/reporters'
require 'helpers/invoice_helper'
require 'helpers/archive_helper'
require 'helpers/invoice_helper'
require 'helpers/payment_schedule_helper'
require 'fileutils'
VCR.configure do |config|
@ -31,8 +32,9 @@ Minitest::Reporters.use! [Minitest::Reporters::DefaultReporter.new(color: true)]
class ActiveSupport::TestCase
include ActionDispatch::TestProcess
include InvoiceHelper
include ArchiveHelper
include InvoiceHelper
include PaymentScheduleHelper
# Add more helper methods to be used by all tests here...
ActiveRecord::Migration.check_pending!