Module: Invoicing::TimeDependent

Extended by:
ActiveSupport::Concern
Defined in:
lib/invoicing/time_dependent.rb

Overview

Time-dependent value objects

This module implements the notion of a value (or a set of values) which may change at certain points in time, and for which it is important to have a full history of values for every point in time. It is used in the invoicing framework as basis for tax rates, prices, commissions etc.

Background

To illustrate the need for this tool, consider for example the case of a tax rate. Say the rate is currently 10%, and in a naive implementation you simply store the value 0.1 in a constant. Whenever you need to calculate tax on a price, you multiply the price with the constant, and store the result together with the price in the database. Then, one day the government decides to increase the tax rate to 12%. On the day the change takes effect, you change the value of the constant to 0.12.

This naive implementation has a number of problems, which are addressed by this module:

  • With a constant, you have no way of informing users what a price will be after an upcoming tax change. Using TimeDependent allows you to query the value on any date in the past or future, and show it to users as appropriate. You also gain the ability to process back-dated or future-dated transactions if this should be necessary.

  • With a constant, you have no explicit information in your database informing you which rate was applied for a particular tax calculation. You may be able to infer the rate from the prices you store, but this may not be enough in cases where there is additional metadata attached to tax rates (e.g. if there are different tax rates for different types of product). With TimeDependent you can have an explicit reference to the tax object which formed the basis of a calculation, giving you a much better audit trail.

  • If there are different tax categories (e.g. a reduced rate for products of type A, and a higher rate for type B), the government may not only change the rates themselves, but also decide to reclassify product X as type B rather than type A. In any case you will need to store the type of each of your products; however, TimeDependent tries to minimize the amount of reclassifying you need to do, should it become necessary.

Data Structure

TimeDependent objects are special ActiveRecord::Base objects. One database table is used, and each row in that table represents the value (e.g. the tax rate or the price) during a particular period of time. If there are multiple different values at the same time (e.g. a reduced tax rate and a higher rate), each of these is also represented as a separate row. That way you can refer to a TimeDependent object from another model object (such as storing the tax category for a product), and refer simultaneously to the type of tax applicable for this product and the period for which this classification is valid.

If a rate change is announced, it important that the actual values in the table are not changed in order to preserve historical information. Instead, add another row (or several rows), taking effect on the appropriate date. However, it is usually not necessary to update your other model objects to refer to these new rows; instead, each TimeDependent object which expires has a reference to the new TimeDependent objects which replaces it. TimeDependent provides methods for finding the current (or future) rate by following this chain of replacements.

Example

To illustrate, take as example the rate of VAT (Value Added Tax) in the United Kingdom. The main tax rate was at 17.5% until 1 December 2008, when it was changed to 15%. On 1 January 2010 it is due to be changed back to 17.5%. At the same time, there are a reduced rates of 5% and 0% on certain goods; while the main rate was changed, the reduced rates stayed unchanged.

The table of TimeDependent records will look something like this:

+----+-------+---------------+------------+---------------------+---------------------+----------------+
| id | value | description   | is_default | valid_from          | valid_until         | replaced_by_id |
+----+-------+---------------+------------+---------------------+---------------------+----------------+
|  1 | 0.175 | Standard rate |          1 | 1991-04-01 00:00:00 | 2008-12-01 00:00:00 |              4 |
|  2 |  0.05 | Reduced rate  |          0 | 1991-04-01 00:00:00 | NULL                |           NULL |
|  3 |   0.0 | Zero rate     |          0 | 1991-04-01 00:00:00 | NULL                |           NULL |
|  4 |  0.15 | Standard rate |          1 | 2008-12-01 00:00:00 | 2010-01-01 00:00:00 |              5 |
|  5 | 0.175 | Standard rate |          1 | 2010-01-01 00:00:00 | NULL                |           NULL |
+----+-------+---------------+------------+---------------------+---------------------+----------------+

Graphically, this may be illustrated as:

          1991-04-01             2008-12-01             2010-01-01
                   :                      :                      :
Standard rate: 17.5% -----------------> 15% ---------------> 17.5% ----------------->
                   :                      :                      :
Zero rate:        0% --------------------------------------------------------------->
                   :                      :                      :
Reduced rate:     5% --------------------------------------------------------------->

It is a deliberate choice that a TimeDependent object references its successor, and not its predecessor. This is so that you can classify your items based on the current classification, and be sure that if the current rate expires there is an unambiguous replacement for it. On the other hand, it is usually not important to know what the rate for a particular item would have been at some point in the past.

Now consider a slightly more complicated (fictional) example, in which a UK court rules that teacakes have been incorrectly classified for VAT purposes, namely that they should have been zero-rated while actually they had been standard-rated. The court also decides that all sales of teacakes before 1 Dec 2008 should maintain their old standard-rated status, while sales from 1 Dec 2008 onwards should be zero-rated.

Assume you have an online shop in which you sell teacakes and other goods (both standard-rated and zero-rated). You can handle this reclassification (in addition to the standard VAT rate change above) as follows:

          1991-04-01             2008-12-01             2010-01-01
                   :                      :                      :
Standard rate: 17.5% -----------------> 15% ---------------> 17.5% ----------------->
                   :                      :                      :
Teacakes:      17.5% ------------.        :                      :
                   :              \_      :                      :
Zero rate:        0% ---------------+->  0% ---------------------------------------->
                   :                      :                      :
Reduced rate:     5% --------------------------------------------------------------->

Then you just need to update the teacake products in your database, which previously referred to the 17.5% object valid from 1991-04-01, to refer to the special teacake rate. None of the other products need to be modified. This way, the teacakes will automatically switch to the 0% rate on 2008-12-01. If you add any new teacake products to the database after December 2008, you can refer either to the teacake rate or to the new 0% rate which takes effect on 2008-12-01; it won't make any difference.

Usage notes

This implementation is designed for tables with a small number of rows (no more than a few dozen) and very infrequent changes. To reduce database load, it caches model objects very aggressively; you will need to restart your Ruby interpreter after making a change to the data as the cache is not cleared between requests. This is ok because you shouldn't be lightheartedly modifying TimeDependent data anyway; a database migration as part of an explicitly deployed release is probably the best way of introducing a rate change (that way you can also check it all looks correct on your staging server before making the rate change public).

A model object using TimeDependent must inherit from ActiveRecord::Base and must have at least the following columns (although columns may have different names, if declared to acts_as_time_dependent):

  • id – An integer primary key

  • valid_from – A column of type datetime, which must not be NULL. It contains the moment at which the rate takes effect. The oldest valid_from dates in the table should be in the past by a safe margin.

  • valid_until – A column of type datetime, which contains the moment from which the rate is no longer valid. It may be NULL, in which case the the rate is taken to be “valid until further notice”. If it is not NULL, it must contain a date strictly later than valid_from.

  • replaced_by_id – An integer, foreign key reference to the id column in this same table. If valid_until is NULL, replaced_by_id must also be NULL. If valid_until is non-NULL, replaced_by_id may or may not be NULL; if it refers to a replacement object, the valid_from value of that replacement object must be equal to the valid_until value of this object.

Optionally, the table may have further columns:

  • value – The actual (usually numeric) value for which we're going to all this effort, e.g. a tax rate percentage or a price in some currency unit.

  • is_default – A boolean column indicating whether or not this object should be considered a default during its period of validity. This may be useful if there are several different rates in effect at the same time (such as standard, reduced and zero rate in the example above). If this column is used, there should be exactly one default rate at any given point in time, otherwise results are undefined.

Apart from these requirements, a TimeDependent object is a normal model object, and you may give it whatever extra metadata you want, and make references to it from any other model object.

Defined Under Namespace

Modules: ActMethods, ClassMethods Classes: ClassInfo

Instance Method Summary collapse

Instance Method Details

#changes_until(point_in_time) ⇒ Object

Examines the replacement chain from this record into the future, during the period starting with this record's valid_from and ending at point_in_time. If this record stays valid until after point_in_time, an empty list is returned. Otherwise the sequence of replacement records is returned in the list. If a record expires before point_in_time and without replacement, a nil element is inserted as the last element of the list.


352
353
354
355
356
357
358
359
360
361
362
363
# File 'lib/invoicing/time_dependent.rb', line 352

def changes_until(point_in_time)
  info = time_dependent_class_info
  changes = []
  record = self
  while !record.nil?
    valid_until = info.get(record, :valid_until)
    break if valid_until.nil? || (valid_until > point_in_time)
    record = record.replaced_by
    changes << record
  end
  changes
end

#predecessorsObject

Returns a list of objects of the same type as this object, which refer to this object through their replaced_by_id values. In other words, this method returns all records which are direct predecessors of the current record in the replacement chain.


298
299
300
# File 'lib/invoicing/time_dependent.rb', line 298

def predecessors
  time_dependent_class_info.predecessors(self)
end

#record_at(point_in_time) ⇒ Object

Translates this record into its replacement for a given point in time, if necessary/possible.

  • If this record is still valid at the given date/time, this method just returns self.

  • If this record is no longer valid at the given date/time, the record which has been marked as this rate's replacement for the given point in time is returned.

  • If this record has expired and there is no valid replacement, nil is returned.

  • On the other hand, if the given date is at a time before this record becomes valid, we try to follow the chain of predecessors records. If there is an unambiguous predecessor record which is valid at the given point in time, it is returned; otherwise nil is returned.


311
312
313
314
315
316
317
318
319
320
321
322
323
324
# File 'lib/invoicing/time_dependent.rb', line 311

def record_at(point_in_time)
  valid_from  = time_dependent_class_info.get(self, :valid_from)
  valid_until = time_dependent_class_info.get(self, :valid_until)

  if valid_from > point_in_time
    (predecessors.size == 1) ? predecessors[0].record_at(point_in_time) : nil
  elsif valid_until.nil? || (valid_until > point_in_time)
    self
  elsif replaced_by.nil?
    nil
  else
    replaced_by.record_at(point_in_time)
  end
end

#record_nowObject

Returns self if this record is currently valid, otherwise its past or future replacement (see record_at). If there is no valid replacement, nil is returned.


328
329
330
# File 'lib/invoicing/time_dependent.rb', line 328

def record_now
  record_at Time.now
end

#value_at(point_in_time) ⇒ Object

Finds this record's replacement for a given point in time (see record_at), then returns the value in its value column. If value was renamed to another_method_name (option to acts_as_time_dependent), then another_method_name_at is defined as an alias for value_at.


335
336
337
# File 'lib/invoicing/time_dependent.rb', line 335

def value_at(point_in_time)
  time_dependent_class_info.get(record_at(point_in_time), :value)
end

#value_nowObject

Returns value_at for the current date/time. If value was renamed to another_method_name (option to acts_as_time_dependent), then another_method_name_now is defined as an alias for value_now.


342
343
344
# File 'lib/invoicing/time_dependent.rb', line 342

def value_now
  value_at Time.now
end