# frozen_string_literal: true # 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 = Invoices::NumberService.pattern(document.created_at, 'invoice_reference') reference = replace_document_number_pattern(pattern, document) reference = replace_date_pattern(reference, date) replace_document_type_pattern(document, reference) end # @param document [PaymentDocument] def generate_order_number(document) pattern = Invoices::NumberService.pattern(document.created_at, 'invoice_order-nb') # global document number (nn..nn) reference = pattern.gsub(/n+(?![^\[]*\])/) do |match| pad_and_truncate(order_number(document, 'global'), match.to_s.length) end reference = replace_document_number_pattern(reference, document, :order_number) replace_date_pattern(reference, document.created_at) end # Generate a reference for the given document using the given document number # @param number [Integer] # @param document [PaymentDocument] def generate_numbered_reference(number, document) pattern = Invoices::NumberService.pattern(document.created_at, 'invoice_reference') reference = pattern.gsub(/n+|y+|m+|d+(?![^\[]*\])/) do |match| pad_and_truncate(number, match.to_s.length) end reference = replace_date_pattern(reference, document.created_at) replace_document_type_pattern(document, reference) end private # Output the given integer with leading zeros. If the given value is longer than the given # length, it will be truncated. # @param value [Integer] the integer to pad # @param length [Integer] the length of the resulting string. def pad_and_truncate(value, length) value.to_s.rjust(length, '0').gsub(/^.*(.{#{length},}?)$/m, '\1') end # @param document [PaymentDocument] # @param periodicity [String] 'day' | 'month' | 'year' | 'global' # @return [PaymentDocument,NilClass] def previous_document(document, periodicity) previous = document.class.base_class.where('created_at < ?', db_time(document.created_at)) .order(created_at: :desc) .limit(1) if %w[day month year].include?(periodicity) previous = previous.where('date_trunc(:periodicity, created_at) = :date', periodicity: periodicity, date: document.created_at.utc.send("beginning_of_#{periodicity}").to_date) end previous.first end # @param document [PaymentDocument] # @param periodicity [String] 'day' | 'month' | 'year' | 'global' # @return [HashFootprintable,Number>] def previous_order(document, periodicity) start = periodicity == 'global' ? nil : document.created_at.send("beginning_of_#{periodicity}") ending = document.created_at orders = orders_in_range(document, start, ending) schedules = schedules_in_range(document, start, ending) 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? last_with_number = [ orders.where.not(reference: nil).order(created_at: :desc).limit(1).first, schedules.where.not(order_number: nil).order(created_at: :desc).limit(1).first, invoices.where.not(order_number: nil).order(created_at: :desc).limit(1).first ].filter(&:present?).max_by { |item| item&.created_at } { last_order: last_with_number, unnumbered: orders_without_number(orders, schedules, invoices, last_with_number) } end def orders_without_number(orders, schedules, invoices, last_item_with_number = nil) items_after(orders.where(reference: nil), last_item_with_number).count + items_after(schedules.where(order_number: nil), last_item_with_number).count + items_after(invoices.where(order_number: nil), last_item_with_number).count end # @param items [ActiveRecord::Relation] # @param previous_item [Footprintable,NilClass] # @return [ActiveRecord::Relation] def items_after(items, previous_item = nil) return items if previous_item.nil? items.where('created_at > :date', date: previous_item&.created_at) end # @param document [PaymentDocument] invoice to exclude # @param start [Time,NilClass] # @param ending [Time] # @return [ActiveRecord::Relation,ActiveRecord::QueryMethods::WhereChain] def orders_in_range(document, start, ending) 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? orders = orders.where.not(id: document.order.id) if document.is_a?(Invoice) && document.order.present? orders end # @param document [PaymentDocument] invoice to exclude # @param start [Time,NilClass] # @param ending [Time] # @return [ActiveRecord::Relation,ActiveRecord::QueryMethods::WhereChain] def schedules_in_range(document, start, ending) 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? if document.is_a?(Invoice) && document.payment_schedule_item.present? schedules = schedules.where.not(id: document.payment_schedule_item.payment_schedule.id) end schedules end # Replace the date elements in the provided pattern with the date values, from the provided date # @param reference [String] # @param date [Time] def replace_date_pattern(reference, date) copy = reference.dup # full year (YYYY) copy.gsub!(/(?![^\[]*\])YYYY(?![^\[]*\])/, date.strftime('%Y')) # year without century (YY) copy.gsub!(/(?![^\[]*\])YY(?![^\[]*\])/, date.strftime('%y')) # abbreviated month name (MMM) # 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) copy.gsub!(/(?![^\[]*\])M(?![^\[]*\])/, date.strftime('%-m')) # day of the month, 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 # @param document [PaymentDocument] # @param periodicity [String] 'day' | 'month' | 'year' | 'global' # @return [Integer] def document_number(document, periodicity) previous = previous_document(document, periodicity) number = Invoices::NumberService.number(previous) if Invoices::NumberService.number_periodicity(previous) == periodicity number ||= 0 number + 1 end # @param document [PaymentDocument] # @param periodicity [String] 'day' | 'month' | 'year' | 'global' # @return [Integer] def order_number(document, periodicity) previous = previous_order(document, periodicity) if Invoices::NumberService.number_periodicity(previous[:last_order], 'invoice_order-nb') == periodicity number = Invoices::NumberService.number(previous[:last_order], 'invoice_order-nb') end number ||= 0 number + previous[:unnumbered] + 1 end # Replace the document number elements in the provided pattern with counts from the database # @param reference [String] # @param document [PaymentDocument] # @param numeration_method [Symbol] :document_number OR :order_number def replace_document_number_pattern(reference, document, numeration_method = :document_number) copy = reference.dup # document number per year (yy..yy) copy.gsub!(/y+(?![^\[]*\])/) do |match| pad_and_truncate(send(numeration_method, document, 'year'), match.to_s.length) end # document number per month (mm..mm) copy.gsub!(/m+(?![^\[]*\])/) do |match| pad_and_truncate(send(numeration_method, document, 'month'), match.to_s.length) end # document number per day (dd..dd) copy.gsub!(/d+(?![^\[]*\])/) do |match| pad_and_truncate(send(numeration_method, document, 'day'), match.to_s.length) end copy end # @param document [PaymentDocument] # @param pattern [String] # @return [String] def replace_document_type_pattern(document, pattern) copy = pattern.dup case document when Avoir # information about refund/avoir (R[text]) copy.gsub!(/R\[([^\]]+)\]/, '\1') # remove information about online selling (X[text]) copy.gsub!(/X\[([^\]]+)\]/, ''.to_s) # remove information about payment schedule (S[text]) copy.gsub!(/S\[([^\]]+)\]/, ''.to_s) when PaymentSchedule # information about payment schedule copy.gsub!(/S\[([^\]]+)\]/, '\1') # remove information about online selling (X[text]) copy.gsub!(/X\[([^\]]+)\]/, ''.to_s) # remove information about refunds (R[text]) copy.gsub!(/R\[([^\]]+)\]/, ''.to_s) when Invoice # information about online selling (X[text]) if document.paid_by_card? copy.gsub!(/X\[([^\]]+)\]/, '\1') else copy.gsub!(/X\[([^\]]+)\]/, ''.to_s) end # remove information about refunds (R[text]) copy.gsub!(/R\[([^\]]+)\]/, ''.to_s) # remove information about payment schedule (S[text]) copy.gsub!(/S\[([^\]]+)\]/, ''.to_s) else # maybe an Order or anything else, # remove all informations copy.gsub!(/S\[([^\]]+)\]/, ''.to_s) copy.gsub!(/X\[([^\]]+)\]/, ''.to_s) copy.gsub!(/R\[([^\]]+)\]/, ''.to_s) end copy end end end