Skip to content

Sample Project Allocation

Val Huber edited this page Nov 15, 2021 · 33 revisions

Allocation

This illustrates the allocation pattern:

cd examples/payment_allocation/tests
python add_payment.py

Allocation is a pattern (more examples below), where:

A Provider allocates to a list of Recipients, creating Allocation rows.

The Allocation Pattern; Examples

The allocation pattern may be unfamiliar, but experience has shown it is valuable in about 1/3 of applications.
Some examples are briefly described below.

Benefits, over Time Period

In an unemployment system, benefits are allocated to a set of time periods. The allocation pattern eliminated hundreds of lines of code that had taken months to develop, was running in minutes instead of seconds, and was getting the wrong answer.

Budget, to Departments, to G/L

In a state government, chosen projects are allocated budget for an approved project. Moreover, the allocation was "chained" - the budget was allocated to each departments General Ledger. Allocation enabled a project to be completed in a weekend that had been running for months.

Resources, to Emergency Events

To manage emergencies such as wild fires, resources (e.g, personnel, bulldozers, aircraft) were allocated to incidents.

Bonus, to Employees

Our favorite - project completes and the department is awarded a bonus; it is allocated to the employees in the department.

Bank Payment, to Accounts

A large logistics company allocates bank payments to a set of customer accounts. A project was completed in a weekend that previously required 9 months.

Sample - Allocate Payment to Outstanding Orders

For example, imagine a Customer has a set of outstanding Orders, and pays all/several off with a periodic Payment.

Data Model

Requirements

When the Payment is inserted, our system must:

  1. Allocate the Payment to Orders that have AmountOwed, oldest first
  2. Keep track of how the Payment is allocated, by creating a PaymentAllocation
  3. As the Payment is allocated,
    1. Update the Order.AmountOwed, and
    2. Adjust the Customer.Balance

Logic

We create the following rules:

Observe that the allocation rule looks very much like our pattern:

RuleExtension.allocate(provider=Payment,
                       recipients=unpaid_orders,
                       creating_allocation=PaymentAllocation)

Logic Logging

The console log panel at the bottom illustrates how rules log their execution, including the rule and row state. Indention indicates rule chaining - how values changed by 1 rule can trigger other rules, even across tables.

The dotted lines show you can correlate your rules to actual execution. We'll explore the logic execution more below, but it's useful to see how it looks during actual development.

Sample Transactions in add_payment.py

Let's explore examples/payment_allocation/tests/add_payment.py; here's the key segment that inserts a payment:

cust_alfki = session.query(models.Customer).filter(models.Customer.Id == "ALFKI").one()

new_payment = models.Payment(Amount=1000)
cust_alfki.PaymentList.append(new_payment)

session.add(new_payment)
session.commit()

The test illustrates allocation logic for our inserted payment, which operates as follows:

  1. The triggering event is the insertion of a Payment, which triggers:
  2. The allocate rule (line 28). It performs the allocation:
    1. Obtains the list of recipient orders by calling the functionunpaid_orders (line 9)
    2. For each recipient (Order),
      1. Creates a PaymentAllocation, links it to the Order and Payment,
      2. Invokes while_calling_allocator, which
        1. Reduces Payment.AmountUnAllocated
        2. Inserts the PaymentAllocation, which runs the following rules:
          • r1 PaymentAllocation.AmountAllocated is derived (formula, line 25); this triggers the next rule...
          • r2 Order.AmountPaid is adjusted (sum rule, line 23); that triggers...
          • r3 Order.AmountOwed is derived (formula rule, line 22); that triggers
          • r4 Customer.Balance is adjusted (sum rule, line 20)
        3. Returns whether thePayment.AmountUnAllocated has remaining value ( > 0 ).
      3. Tests the returned result
        1. If true (allocation remains), the loop continues for the next recipient
        2. Otherwise, the allocation loop is terminated

Default while_calling_allocator

This example does not supply an optional argument: while_calling_allocator. The logic_bank/extensions/allocate.py provides a default, called while_calling_allocator_default.

This default presumes the attribute names shown in the code below. If these do not match your attribute names, copy / alter this implementation to your own, and specify it on the constructor (line 15).

    def while_calling_allocator_default(self, allocation_logic_row, provider_logic_row) -> bool:
        """
        Called for each created allocation, to
            * insert the created allocation (triggering rules that compute `Allocation.AmountAllocated`)
            * reduce Provider.AmountUnAllocated
            * return boolean indicating whether Provider.AmountUnAllocated > 0 (remains)

        This uses default names:
            * provider.Amount
            * provider.AmountUnallocated
            * allocation.AmountAllocated

        To use your names, copy this code and alter as as required

        :param allocation_logic_row: allocation row being created
        :param provider_logic_row: provider
        :return: provider has AmountUnAllocated remaining
        """

        if provider_logic_row.row.AmountUnAllocated is None:
            provider_logic_row.row.AmountUnAllocated = provider_logic_row.row.Amount  # initialization

        allocation_logic_row.insert(reason="Allocate " + provider_logic_row.name)  # triggers rules, eg AmountAllocated

        provider_logic_row.row.AmountUnAllocated = \
            provider_logic_row.row.AmountUnAllocated - allocation_logic_row.row.AmountAllocated

        return provider_logic_row.row.AmountUnAllocated > 0  # terminate allocation loop if none left

Log Output

Logic operation is visible in the log

Note: the test program examples/payment_allocation/tests/add_payment.py shows some test data in comments at the end

Logic Phase:		BEFORE COMMIT          						 - 2020-12-23 05:56:45,682 - logic_logger - DEBUG
Logic Phase:		ROW LOGIC (sqlalchemy before_flush)			 - 2020-12-23 05:56:45,682 - logic_logger - DEBUG
..Customer[ALFKI] {Update - client} Id: ALFKI, CompanyName: Alfreds Futterkiste, Balance: 1016.00, CreditLimit: 2000.00  row@: 0x10abbea00 - 2020-12-23 05:56:45,682 - logic_logger - DEBUG
..Payment[None] {Insert - client} Id: None, Amount: 1000, AmountUnAllocated: None, CustomerId: None, CreatedOn: None  row@: 0x10970f610 - 2020-12-23 05:56:45,682 - logic_logger - DEBUG
..Payment[None] {BEGIN Allocate Rule, creating: PaymentAllocation} Id: None, Amount: 1000, AmountUnAllocated: None, CustomerId: None, CreatedOn: None  row@: 0x10970f610 - 2020-12-23 05:56:45,683 - logic_logger - DEBUG
....PaymentAllocation[None] {Insert - Allocate Payment} Id: None, AmountAllocated: None, OrderId: None, PaymentId: None  row@: 0x10abbe700 - 2020-12-23 05:56:45,684 - logic_logger - DEBUG
....PaymentAllocation[None] {Formula AmountAllocated} Id: None, AmountAllocated: 100.00, OrderId: None, PaymentId: None  row@: 0x10abbe700 - 2020-12-23 05:56:45,684 - logic_logger - DEBUG
......Order[10692] {Update - Adjusting Order} Id: 10692, CustomerId: ALFKI, OrderDate: 2013-10-03, AmountTotal: 878.00, AmountPaid:  [778.00-->] 878.00, AmountOwed: 100.00  row@: 0x10ac82370 - 2020-12-23 05:56:45,685 - logic_logger - DEBUG
......Order[10692] {Formula AmountOwed} Id: 10692, CustomerId: ALFKI, OrderDate: 2013-10-03, AmountTotal: 878.00, AmountPaid:  [778.00-->] 878.00, AmountOwed:  [100.00-->] 0.00  row@: 0x10ac82370 - 2020-12-23 05:56:45,685 - logic_logger - DEBUG
........Customer[ALFKI] {Update - Adjusting Customer} Id: ALFKI, CompanyName: Alfreds Futterkiste, Balance:  [1016.00-->] 916.00, CreditLimit: 2000.00  row@: 0x10abbea00 - 2020-12-23 05:56:45,685 - logic_logger - DEBUG
....PaymentAllocation[None] {Insert - Allocate Payment} Id: None, AmountAllocated: None, OrderId: None, PaymentId: None  row@: 0x10ac6a850 - 2020-12-23 05:56:45,686 - logic_logger - DEBUG
....PaymentAllocation[None] {Formula AmountAllocated} Id: None, AmountAllocated: 330.00, OrderId: None, PaymentId: None  row@: 0x10ac6a850 - 2020-12-23 05:56:45,686 - logic_logger - DEBUG
......Order[10702] {Update - Adjusting Order} Id: 10702, CustomerId: ALFKI, OrderDate: 2013-10-13, AmountTotal: 330.00, AmountPaid:  [0.00-->] 330.00, AmountOwed: 330.00  row@: 0x10ac824f0 - 2020-12-23 05:56:45,686 - logic_logger - DEBUG
......Order[10702] {Formula AmountOwed} Id: 10702, CustomerId: ALFKI, OrderDate: 2013-10-13, AmountTotal: 330.00, AmountPaid:  [0.00-->] 330.00, AmountOwed:  [330.00-->] 0.00  row@: 0x10ac824f0 - 2020-12-23 05:56:45,686 - logic_logger - DEBUG
........Customer[ALFKI] {Update - Adjusting Customer} Id: ALFKI, CompanyName: Alfreds Futterkiste, Balance:  [916.00-->] 586.00, CreditLimit: 2000.00  row@: 0x10abbea00 - 2020-12-23 05:56:45,686 - logic_logger - DEBUG
....PaymentAllocation[None] {Insert - Allocate Payment} Id: None, AmountAllocated: None, OrderId: None, PaymentId: None  row@: 0x10ac6a9d0 - 2020-12-23 05:56:45,687 - logic_logger - DEBUG
....PaymentAllocation[None] {Formula AmountAllocated} Id: None, AmountAllocated: 570.00, OrderId: None, PaymentId: None  row@: 0x10ac6a9d0 - 2020-12-23 05:56:45,687 - logic_logger - DEBUG
......Order[10835] {Update - Adjusting Order} Id: 10835, CustomerId: ALFKI, OrderDate: 2014-01-15, AmountTotal: 851.00, AmountPaid:  [0.00-->] 570.00, AmountOwed: 851.00  row@: 0x10ac82550 - 2020-12-23 05:56:45,688 - logic_logger - DEBUG
......Order[10835] {Formula AmountOwed} Id: 10835, CustomerId: ALFKI, OrderDate: 2014-01-15, AmountTotal: 851.00, AmountPaid:  [0.00-->] 570.00, AmountOwed:  [851.00-->] 281.00  row@: 0x10ac82550 - 2020-12-23 05:56:45,688 - logic_logger - DEBUG
........Customer[ALFKI] {Update - Adjusting Customer} Id: ALFKI, CompanyName: Alfreds Futterkiste, Balance:  [586.00-->] 16.00, CreditLimit: 2000.00  row@: 0x10abbea00 - 2020-12-23 05:56:45,688 - logic_logger - DEBUG
..Payment[None] {END Allocate Rule, creating: PaymentAllocation} Id: None, Amount: 1000, AmountUnAllocated: 0.00, CustomerId: None, CreatedOn: None  row@: 0x10970f610 - 2020-12-23 05:56:45,688 - logic_logger - DEBUG
Logic Phase:		COMMIT   									 - 2020-12-23 05:56:45,689 - logic_logger - DEBUG
Logic Phase:		FLUSH   (sqlalchemy flush processing       	 - 2020-12-23 05:56:45,689 - logic_logger - DEBUG

add_payment, update completed

Key Points

Allocation illustrates some key points regarding logic.

Extensibility

While Allocation is part of Logic Bank, you could have recognized the pattern yourself, and provided the implementation. This is enabled since Event rules can invoke Python. You can make your Python code generic, using meta data (from SQLAlchemy), parameters, etc.

Rule Chaining

Note how the created PaymentAllocation row triggered the more standard rules such as sums and formulas. This required no special machinery: rules watch and react to changes in data - if you change the data, rules will "notice" that, and fire. Automatically.

Clone this wiki locally