From 3a910b1182ca381edcf1f2c5056064c96d443f3a Mon Sep 17 00:00:00 2001 From: Sylvain Date: Mon, 24 May 2021 11:19:59 +0200 Subject: [PATCH] script to fix existing invoices w/o invoiced_id --- CHANGELOG.md | 1 + .../tasks/fablab/fix_invoices.rake | 122 +++++++++++------- 2 files changed, 75 insertions(+), 48 deletions(-) rename db/migrate/20210521073742_fix_invoices_without_invoiced_id.rb => lib/tasks/fablab/fix_invoices.rake (53%) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1435cb3d..5c3295609 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Fix a bug: unable to use run.fab.mn - Fix a security issue: updated puma to 4.3.8 to fix [CVE-2019-16770](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-16770) - Fix a security issue: updated nokogiri to 1.11.4 to fix [GHSA-7rrm-v45f-jp64](https://github.com/advisories/GHSA-7rrm-v45f-jp64) +- [TODO DEPLOY] `rails fablab:fix_invoices` ## v4.7.9 2021 May 17 diff --git a/db/migrate/20210521073742_fix_invoices_without_invoiced_id.rb b/lib/tasks/fablab/fix_invoices.rake similarity index 53% rename from db/migrate/20210521073742_fix_invoices_without_invoiced_id.rb rename to lib/tasks/fablab/fix_invoices.rake index 5e070412f..6c58b8c84 100644 --- a/db/migrate/20210521073742_fix_invoices_without_invoiced_id.rb +++ b/lib/tasks/fablab/fix_invoices.rake @@ -1,58 +1,81 @@ # frozen_string_literal: true -# This migration will ensure data integrity for invoices. -# A bug introduced with v4.7.0 has made invoices without invoiced_id for Reservations. -# This issue is concerning slots restricted to subscribers, when the restriction was manually overridden by an admin. -class FixInvoicesWithoutInvoicedId < ActiveRecord::Migration[5.2] - def up +# This take will ensure data integrity for invoices. +# A bug introduced with v4.3.0 has made invoices without saving the associated Reservation (Invoice.invoiced_id is null). +# This issue is concerning slots restricted to subscribers, when the restriction is manually overridden by an admin. +namespace :fablab do + desc 'Remove the invoices w/o reservation or regenerate the reservation' + task fix_invoices: :environment do |_task, _args| return unless Invoice.where(invoiced_id: nil).count.positive? + include ActionView::Helpers::NumberHelper + # check the footprints and save the archives check_footprints - periods = backup_and_remove_periods + ActiveRecord::Base.transaction do + periods = backup_and_remove_periods - # fix invoices data - Invoice.where(invoiced_id: nil).each do |invoice| - if invoice.total.zero? - puts "Invoice #{invoice.id} has total = 0, destroying..." - invoice.destroy - next - end - if invoice.invoiced_type != 'Reservation' - STDERR.puts "WARNING: Invoice #{invoice.id} is not about a reservation, ignoring..." - next - end - - ii = invoice.invoice_items.where(subscription_id: nil).first - reservable = find_reservable(ii) - if reservable - if reservable.is_a? Event - STDERR.puts "WARNING: invoice #{invoice.id} may be linked to the Event #{reservable.id}. This is unsupported, ignoring..." + # fix invoices data + Invoice.where(invoiced_id: nil).each do |invoice| + if invoice.total.zero? + puts "Invoice #{invoice.id} has total = 0, destroying..." + invoice.destroy next end - reservation = ::Reservation.create!( - reservable_id: reservable.id, - reservable_type: reservable.class.name, - slots_attributes: slots_attributes(invoice, reservable), - statistic_profile_id: StatisticProfile.find_by(user: invoice.user).id - ) - invoice.update_attributes(invoiced: reservation) - else - STDERR.puts "WARNING: Unable to guess the reservable for invoice #{invoice.id}, ignoring..." + if invoice.invoiced_type != 'Reservation' + STDERR.puts "WARNING: Invoice #{invoice.id} is about #{invoice.invoiced_type}. Please handle manually." + STDERR.puts 'Ignoring...' + next + end + + ii = invoice.invoice_items.where(subscription_id: nil).first + puts '==============================================' + puts "Invoice #{invoice.id} (# #{invoice.reference})" + puts "Total: #{number_to_currency(invoice.total / 100.0)}" + puts "Subject: #{ii.description}." + puts "Customer: #{invoice.invoicing_profile.full_name} (#{invoice.invoicing_profile.email})" + puts "Operator: #{invoice.operator_profile&.user&.profile&.full_name} (#{invoice.operator_profile&.user&.email})" + puts "Date: #{invoice.created_at}" + + print 'Delete [d] OR create the missing reservation [c]? > ' + confirm = STDIN.gets.chomp + if confirm == 'd' + puts "Destroying #{invoice.id}..." + invoice.destroy + elsif confirm == 'c' + reservable = find_reservable(ii) + if reservable + if reservable.is_a? Event + STDERR.puts "WARNING: invoice #{invoice.id} is linked to Event #{reservable.id}. This is unsupported, please handle manually." + STDERR.puts 'Ignoring...' + next + end + reservation = ::Reservation.create!( + reservable_id: reservable.id, + reservable_type: reservable.class.name, + slots_attributes: slots_attributes(invoice, reservable), + statistic_profile_id: StatisticProfile.find_by(user: invoice.user).id + ) + invoice.update_attributes(invoiced: reservation) + else + STDERR.puts "WARNING: Unable to guess the reservable for invoice #{invoice.id}, please handle manually." + STDERR.puts 'Ignoring...' + end + else + puts "Operation #{confirm} unknown. Ignoring invoice #{invoice.id}..." + end end + + # chain records + puts 'Chaining all record. This may take a while...' + InvoiceItem.order(:id).all.each(&:chain_record) + Invoice.order(:id).all.each(&:chain_record) + + # re-create all archives from the memory dump + restore_periods(periods) end - - # chain records - puts 'Chaining all record. This may take a while...' - InvoiceItem.order(:id).all.each(&:chain_record) - Invoice.order(:id).all.each(&:chain_record) - - # re-create all archives from the memory dump - restore_periods(periods) end - def down; end - private def check_footprints @@ -111,7 +134,6 @@ class FixInvoicesWithoutInvoicedId < ActiveRecord::Migration[5.2] execute("CREATE RULE accounting_periods_del_protect AS ON DELETE TO #{AccountingPeriod.arel_table.name} DO INSTEAD NOTHING;") end - def find_reservable(invoice_item) descr = /^([a-zA-Z\u00C0-\u017F]+\s+)+/.match(invoice_item.description)[0].strip[/(.*)\s/, 1] reservable = InvoiceItem.where('description LIKE ?', "#{descr}%") @@ -123,17 +145,21 @@ class FixInvoicesWithoutInvoicedId < ActiveRecord::Migration[5.2] reservable ||= [Machine, Training, Space].map { |c| c.where('name LIKE ?', "#{descr}%") } .filter { |r| r.count.positive? } .first - &.first + &.first reservable || Event.where('title LIKE ?', "#{descr}%").first end def find_slots(invoice) invoice.invoice_items.map do |ii| - # FIXME, datetime parsing is KO - start = DateTime.parse(ii.description) - end_time = DateTime.parse(/- (.+)$/.match(ii.description)[1]) - [start, DateTime.new(start.year, start.month, start.day, end_time.hour, end_time.min, end_time.sec, end_time.zone)] + description = ii.description + # DateTime.parse only works with english dates, so translate the month name + month_idx = I18n.t('date.month_names').find_index { |month| month && description.include?(month) } + description.gsub!(/#{I18n.t('date.month_names')[month_idx]}/, I18n.t('date.month_names', locale: :en)[month_idx]) + + start = DateTime.parse(description) + end_time = DateTime.parse(/- (.+)$/.match(description)[1]) + [start, DateTime.new(start.year, start.month, start.day, end_time.hour, end_time.min, end_time.sec, DateTime.current.zone)] end end