DEV Community

Daveyon Mayne ๐Ÿ˜ป
Daveyon Mayne ๐Ÿ˜ป

Posted on • Updated on

How I believe Xero pulled off implementing their Invoice Numbering System

During this lockdown, I thought I would learn accounting. I love accounting, or anything with numbers, but never fully get a chance to act on that thought.

Xero Central is my goto for anything accounting. You could pretty much read their entire documentation and API to get an understanding of how things work or accounting in general. This is what I did. I've decided to focus on the invoicing section. I also like their invoice interface that I made mine the similar (kids, do not copy ๐Ÿ˜ˆ):

Alt Biliable

This post is about how I've done the invoice numbering. I know there should be a better way but I could only come up with this method. Using Rails 6 API, I'll show you how I've pulled it off.

The Invoice Settings

This model is responsible for validating your chosen invoice sequence ie INV-001. INV- is used as the prefix and 001 is used as a numeric sequence. Sequence can only be numeric. Should you try to enter an alpha-numeric value, it will fail. Note this has nothing to do with the actual invoice number. We'll get to that in a bit.

class InvoiceSetting < ApplicationRecord
  belongs_to :organisation
  validates  :invoice_number_sequence, :quote_number_sequence, :purchase_order_number_sequence, should_be_sequencialised: true
  validate   :check_for_duplicate_numbers

  private
  # TODO: Check for credit note for invoice number
    def check_for_duplicate_numbers
      exists = Invoice.exists?(['lower(invoice_number) = ? AND organisation_id = ?', "#{(invoice_prefix + invoice_number_sequence).downcase}", organisation_id])
      if exists
        errors.add(:invoice_number_sequence, "invoice number already exists.")
      end
    end
end

So what is should_be_sequencialised?

We use a Rails concerns to implement this logic. This logic ensures the sequence value is numeric:

# /models/concerns/should_be_sequencialised_validator.rb

class ShouldBeSequencialisedValidator < ActiveModel::EachValidator
  # The Client should also ensure users only enter numeric values ie 001, 1 etc.
  def validate_each(record, attribute, value)
    begin
      Integer(value)
    rescue StandardError => e
      record.errors.add(attribute, "Invalid number. Please use numeric numbers only")
    end
  end
end

validate_each accepts three arguments: model, model's column name and the incoming value to be checked. EachValidator will loop this method, in our example, 3 times. Later I'll refactor to only validate on invoice_number_sequence.

Integer will throw an exception so it should be wrapped in catch block. When there's an error, we add the error message back to our model for the front-end to display. Your data will be rolled back. A neat feature with Rails and other frameworks.

check_for_duplicate_numbers

Should the first validator succeeds, check_for_duplicate_numbers checks for existing invoice numbers already generated. Pretty easy to understand.

That takes care of the validation but how does an invoice number increments?

Jรถrg W Mittag on Stack overflow says:

Then it's not a number but rather an "invoice identifier object", and you model it as an InvoiceIdentifier

I've "Googled" what's an InvoiceIdentifier but nothing came up. I should have asked for him to explain but I never wanted to extend the post when I've already selected an answer, so I left it as is. If invoice_number is not a string then what is it? I've created a model called invoice with a string attribute as invoice_number. I've used string because I cannot find information regarding this InvoiceIdentifier.

Let's validate our invoice model to ensure invoice_number is unique. This is only unique for each of the user's organisation:

class Invoice < ApplicationRecord
  validates   :invoice_number, uniqueness: { case_sensitive: :false, scope: :organisation_id }
  [...] # Other validations and relationships etc
end

We'll now focus our attention over to invoice_controller.rb:

Invoice Controller

  # POST /organisations/:organisation_id/invoices
  def create
    invoice = @organisation.invoices.build(invoice_params)
    if invoice.save
      return render json: invoice
    end

    render json: {error: invoice.errors.messages}
  end

I won't explain what's going on here as this is not a beginners guide. At this point we should be able to create an invoice. If it fails then our invoice_number is not unique.

How and when does the invoice number auto increments?

The Wisper gem is a great tool for listening to events. We'd listen for two events: create and update (this example for focuses on create). This is when we would increment our invoice number or leave it as is. Let's look at this IncrementInvoiceNumber class:

# /lib/invoices/increment_invoice_number.rb
module Invoices
  class IncrementInvoiceNumber
    include Wisper::Publisher

    def create(invoice, organisation)
      if invoice.save
        puts "-----INVOICE CREATED------"
        increment_invoice_number(invoice.invoice_number, organisation)
        broadcast(:invoice_created, invoice)
      else
        puts "-----INVOICE FAILED------"
        broadcast(:invoice_failed, invoice)
      end
    end

    private
      def increment_invoice_number(inv_num, organisation)
        settings = organisation.invoice_setting
        invoice_setting_prefix = settings.invoice_prefix

        arr = inv_num.split(invoice_setting_prefix)
        if arr.count == 2
          # We have an untouched invoice number
          incremented_sequence = arr[1]

          # We need to test if it's a numeric value
          begin
            Integer(incremented_sequence)
            # We can safely increment and save
            incremented_sequence = incremented_sequence.next
            organisation.invoice_setting.update(invoice_number_sequence: incremented_sequence)
          rescue StandardError => e
            # We do nothing
          end
        end

        # At this point, we have not incremented the invoice number that came in as they've entered a custom invoice number.
      end
  end
end

Our create method is self-explanatory so let's look at increment_invoice_number.

We increment the invoice number only if the prefix is the same as when you're creating a new invoice. See main image, but we still need to check as a user could simply enter INV-foo. If so, we save the invoice as is and only increment if the sequence is of a numeric value. At this point, validation has passed and invoice number is unique.

foo.next will also increment to fop but our validation checks if that section is numeric with Integer(string).

Once you've install Wisper, our create method now becomes:

 # /controllers/invoice_controller.rb

 # POST /organisations/:organisation_id/invoices
  def create
    service = Invoices::IncrementInvoiceNumber.new
    service.on(:invoice_created) { |invoice| render json: invoice }
    service.on(:invoice_failed) { | invoice |render json: {error: invoice.errors.messages} }

    service.create(@organisation.invoices.build(invoice_params), @organisation)
  end

Incrementing an invoice number only runs after an invoice is created. Is there a better way? I would love to know. If you're a Xero dev, I'd love to hear your thoughts and it does not matter what programming language you guys are using.

Oldest comments (0)