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:
parent
abc5fa6691
commit
98e58cbc25
@ -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`
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
|
@ -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
12
app/helpers/db_helper.rb
Normal 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
|
@ -12,6 +12,8 @@ class Avoir < Invoice
|
||||
|
||||
attr_accessor :invoice_items_ids
|
||||
|
||||
delegate :order_number, to: :invoice
|
||||
|
||||
def generate_reference
|
||||
super(created_at)
|
||||
end
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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)
|
||||
|
43
test/helpers/payment_schedule_helper.rb
Normal file
43
test/helpers/payment_schedule_helper.rb
Normal 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
|
@ -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
|
||||
|
@ -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)
|
||||
|
188
test/services/payment_document_service_test.rb
Normal file
188
test/services/payment_document_service_test.rb
Normal 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
|
@ -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!
|
||||
|
Loading…
x
Reference in New Issue
Block a user