Automation: Agile, TDD Process
This project illustrates how API Logic Server Extensible Automation, coupled with an Agile (TDD) Process, can dramatically improve Time to Market and Reduce Risk:
-
Instant App: API Logic Server creates an Admin App (and underlying API) with a single command.
-
Customer Collaboration: the app (Working Software, Now) drives collaboration, resulting in Features (Stories), Scenarios (tests), and Design Specifications that define how data is computed, validated, and processed (e.g., issues email or messages, auditing, etc.).
-
Iteration: the Design Specifications often translate directly into Executable Rules, automated in API Logic Server. These can be easily altered as further collaboration uncovers clarifications (no archaeology).
-
Transparency: the TDD (Test Driven Development) Report documents the functionality of the system as Features (Stories) and Scenarios (tests) that confirm its operation. The report includes the underlying Rules, extending transparency to the implementation level.
Key Takeaway: automation drives Time to Market, and provides working software rapidly; it also drives TDD collaboration to define systems that meet actual needs, reducing requirements risk.
Sample Project
This is the sample project from API Logic Server, based on the Northwind database (sqlite database located in the database
folder - no installation required):
Verify Installation
You can confirm its working by installing and running as described here.
Process Overview
The created project provides the User Interface and API described below, and implements the transactional logic described in the TDD Report. It was created, customized and tested as described in the subsections below.
1. Create Api Logic Project
API Logic Server is used once you have a preliminary database design. Use your existing procedures for database design. Include at least minimal test data.
Then (presuming API Logic Server is installed), create the project with this command, using venv
based installs:
ApiLogicServer create --db_url= --project_name=TDD
or, like this, using docker-based installs:
ApiLogicServer create --db_url= --project_name=/localhost/TDD
1a. Creates Admin App
The Agile objective of collaboration is typically best-served with running screens. The problem is, it takes quite a long time to create the API and screens to reach this point. And this work can be wasted if there were misunderstandings.
Ideally, User Interface creation would be automatic.
So, the API Logic Server create
command above builds first-cut screens, automatically from the data model.
The app shown below (more detail here) is suitable for initial business user collaboration (further discussed below), and basic back office data maintenance.
You can customize it by editing a simple yaml
file (e.g, field captions, ordering etc.)
Key Takeaway: Admin App Automation enables collaboration, instantly.
1b. Also creates API
It is not difficult to create a single endpoint API. The problem is that it's quite a bit more work to create an endpoint for each table, with support for related data, pagination, filtering and sorting.
Ideally, API creation would be automatic.
So, the API Logic Server create
command above builds such an API instantly, suitable for application integration, and creating custom User Interfaces. The API enforces the business logic described below.
The created project is customizable, using a standard IDE.
Key Takeaway: automatic API creation, with support for related data, pagination, filtering and sorting.
2. Collaborate using Admin App
As noted above, running screens are an excellent way to engage business user collaboration and ensure the system meets actual user needs. Such collaboration typically leads in two important directions, as described below.
2a. Iterate Data Model
You may discover that the data model is incorrect ("Wait! Customers have multiple addresses!!").
In a conventional system, this would mean revising the API and App. However, since these are created instantly through automation, such iterations are trivial. Just rebuild.
2b. Define Behave Scenarios
User Interfaces also spark insight about the Features ("Place Order") and Scenarios ("Check Credit"): "When the customer places an order, we need to reject it if it exceeds the credit limit". Capture these as described below.
TDD is designed for business user collaboration by making Features and Scenarios transparent. So, the start of Behave is to define one or more .feature
files.
For example, see the place_order.feature
, as tested by the Bad Order: Custom Service
Scenario, below.
For more on TDD, see here.
Add Custom Service
While the automatically-created API is a great start, you may uncover a need for a custom service. This is easy to add - it's only about 10 lines of Python (api/customize_api.py
), since the logic (discussed below) is enforced in the underlying data access. For details, see here.
2c. Logic Specification
We now choose a scenario (e.g, Bad Order
), and engage business users for a clear understanding of check credit. This follows a familiar step-wise definition of terms, which we capture in text as shown below.
Note this "cocktail napkin spec" is short, yet clear. That's because instead of diving unto unecessary technical detail of how (such as pseudocode), it focuses on what.
3a. Declare Logic (from spec)
Business Logic is the heart of the system, enforcing our business policies. These consist of multi-table constraints and derivations, and actions such as sending email and messages. A core TDD objective is to define and test such behavior.
It's generally accepted that such domain-specific logic must require domain-specific code. The problem is that this is:
- slow (it's often nearly half the system)
- opaque to business users
- painful to maintain - it's no secret that developers hate maintenance, since it's less coding than "archaeology": read the existing code to understand where to insert the new logic
Ideally, our logic specification is executable.
So, API Logic Server provides Logic Automation, where logic is implemented as:
-
Spreadsheet-like rules for multi-table derivations and constraints, and
-
Python, to implement logic not addressed in rules such as sending email or messages
So, instead of several hundred lines of code, we declared 5 rules (more details here).
Rules are entered in Python, with code completion. 5 key rules are shown below. Oserve how they exactly correspond to our specification, and are executable by the API Logic Server rules engine:
Unlike manual code, logic is declarative:
- automatically reused - it is enforced as part of the API, so automatically shared across all screens and services.
- automatically ordered - maintenance is simply altering the rules; the system computes their execution order by automatically discovering their dependencies. No more archaeology.
- transparent - business users can read the spreadsheet-like rules. We'll exploit this in the TDD Report, described below.
Key Takeaway: spreadsheet-like rules can dramatically reduce the effort for backend logic, and make it transparent
Key Takeaway: keep your Logic Specification high level (what not how -- think spreadsheet), and your specification will often map directly to executable rules.
3b. Code/Run TDD Scenarios
Implement the actual scenarios (tests) in Python (place_order.py
), using annotations (@when
) to match scenarios and implementations. In this project, the implementation is basically calling APIs to get old data, run transactions, and check results.
Execute the tests using the pre-supplied Launch Configurations (shown at the bottom):
- Run Launch Configuration
API Logic Server
- Run Launch Configuration
Debug Behave Logic
The rules fire as transactions are run, and produce files later used in Report Behave Logic (described below):
test/api_logic_server_behave/behave.log
- summarizes test success / failureapi_logic_server_behave/scenario_logic_logs/Bad_Order_Custom_Service.log
- Logic Log output.- The code on line 121 signals the name of Logic Log
- Note the Logic Log actually consists of 2 sections:
- The first shows each rule firing, including complete old/new row values, with indentation for
multi-table chaining
- The "Rules Fired" summarizes which rules actually fired, representing a confirmation of our Logic Specification
- The first shows each rule firing, including complete old/new row values, with indentation for
You can use the debugger to stop in a test and verify results
4. Create TDD/Logic Report
This is pretty interesting: a record of all our Features and Scenarios, including transparent underlying logic. The problem is that it's buried in some text files inside our project.
Ideally, publishing this in a transparent manner (e.g., a wiki accessible via the Browser) would be a great asset to the team.
So, this project provides report_behave_logic.py
to create a TDD Report - including logic - as a wiki file.
To run it, use Launch Configuration Report Behave Logic
:
- Reads your current
readme.md
file (text like you are reading now), and - Appends the TDD Report: by processing the files created in step 3c
- Reading the
behave.log
, and - Injecting the
scenario_logic_logs
files
- Reading the
- Creates the output report as a wiki file named
report_behave_logic.md
Key Takeaway: TDD makes requirements and tests transparent; rules make your logic transparent; combine them both into the TDD Report.
TDD Report
Feature: About Sample
Scenario: Transaction Processing
Scenario: Transaction Processing
Given Sample Database
When Transactions are submitted
Then Enforce business policies with Logic (rules + code)
Tests - and their logic - are transparent.. click to see Logic
Rules Used in Scenario: Transaction Processing
Logic Log in Scenario: Transaction Processing
AbstractRule Bank[0x10f151af0] (loaded 2022-03-22 20:10:53.857548
Mapped Class[Customer] rules
Constraint Function: None
Derive Customer.Balance as Sum(Order.AmountTotal Where <function declare_logic.<locals>.<lambda> at 0x10f1da940>
Derive Customer.UnpaidOrderCount as Count(<class 'database.models.Order'> Where <function declare_logic.<locals>.<lambda> at 0x10f2ca310>
Derive Customer.OrderCount as Count(<class 'database.models.Order'> Where None
Mapped Class[Order] rules
Derive Order.AmountTotal as Sum(OrderDetail.Amount Where None
RowEvent Order.congratulate_sales_rep()
Derive Order.OrderDetailCount as Count(<class 'database.models.OrderDetail'> Where None
Mapped Class[OrderDetail] rules
Derive OrderDetail.Amount as Formula (1): as_expression=lambda row: row.UnitPrice * row.Qua [...
Derive OrderDetail.UnitPrice as Copy(Product.UnitPrice
Derive OrderDetail.ShippedDate as Formula (2): row.Order.ShippedDat
Mapped Class[Product] rules
Derive Product.UnitsShipped as Sum(OrderDetail.Quantity Where <function declare_logic.<locals>.<lambda> at 0x10f2ca1f0>
Derive Product.UnitsInStock as Formula (1): <function
Mapped Class[Employee] rules
Constraint Function: <function declare_logic.<locals>.raise_over_20_percent at 0x10f2caa60>
RowEvent Employee.audit_by_event()
Copy to: EmployeeAudi
Logic Bank - 21 rules loaded - 2022-03-22 20:11:03,026 - logic_logger - INF
Feature: Application Integration
Scenario: GET Customer
Scenario: GET Customer
Given Customer Account: VINET
When GET Orders API
Then VINET retrieved
Scenario: GET Department
Scenario: GET Department
Given Department 2
When GET Department with SubDepartments API
Then SubDepartments returned
Feature: Place Order
Scenario: Good Order Custom Service
Scenario: Good Order Custom Service
Given Customer Account: ALFKI
When Good Order Placed
Then Logic adjusts Balance (demo: chain up)
Then Logic adusts Products Reordered
Then Logic adjusts aggregates down on delete order
Tests - and their logic - are transparent.. click to see Logic
Logic Doc for scenario: Good Order Custom Service
We place an Order with an Order Detail. It's one transaction.
Note how the Order.OrderTotal
and Customer.Balance
are adjusted as Order Details are processed.
Similarly, the Product.UnitsShipped
is adjusted, and used to recompute UnitsInStock
![](https://github.com/valhuber/TDD/raw/main/images/declare-logic.png?raw=true&raw=true)
Key Takeaway: sum/count aggregates (e.g.,
Customer.Balance
) automate chain up multi-table transactions.
Inspect the log for send mail.
The congratulate_sales_rep
event illustrates logic
Extensibility
- using Python to provide logic not covered by rules, like non-database operations such as sending email or messages.
There are actually multiple kinds of events:
- Before row logic
- After row logic
- On commit, after all row logic has completed (as here), so that your code "sees" the full logic results
Events are passed the row
and old_row
, as well as logic_row
which enables you to test the actual operation, chaining nest level, etc.
You can set breakpoints in events, and inspect these.
Rules Used in Scenario: Good Order Custom Service
Customer
1. Derive Customer.UnpaidOrderCount as Count(<class 'database.models.Order'> Where <function declare_logic.<locals>.<lambda> at 0x10f2ca310>)
2. Derive Customer.OrderCount as Count(<class 'database.models.Order'> Where None)
3. Derive Customer.Balance as Sum(Order.AmountTotal Where <function declare_logic.<locals>.<lambda> at 0x10f1da940>)
Order
4. Derive Order.OrderDetailCount as Count(<class 'database.models.OrderDetail'> Where None)
5. Derive Order.AmountTotal as Sum(OrderDetail.Amount Where None)
6. RowEvent Order.congratulate_sales_rep()
Product
7. Derive Product.UnitsShipped as Sum(OrderDetail.Quantity Where <function declare_logic.<locals>.<lambda> at 0x10f2ca1f0>)
8. Derive Product.UnitsInStock as Formula (1): <function>
Logic Log in Scenario: Good Order Custom Service
Logic Phase: ROW LOGIC(session=0x10f9f6e20) (sqlalchemy before_flush) - 2022-03-22 20:11:03,203 - logic_logger - INF
..Order[11124] {Delete - client} Id: 11124, CustomerId: ALFKI, EmployeeId: 1, OrderDate: None, RequiredDate: None, ShippedDate: None, ShipVia: None, Freight: 11.0000000000, ShipName: None, ShipAddress: None, ShipCity: None, ShipRegion: None, ShipPostalCode: None, ShipCountry: None, AmountTotal: 56.00, Country: None, City: None, Ready: True, OrderDetailCount: 2 row: 0x10fa06c10 session: 0x10f9f6e20 - 2022-03-22 20:11:03,204 - logic_logger - INF
....Customer[ALFKI] {Update - Adjusting Customer: Balance, UnpaidOrderCount, OrderCount} Id: ALFKI, CompanyName: Alfreds Futterkiste, ContactName: Maria Anders, ContactTitle: Sales Representative, Address: Obere Str. 57A, City: Berlin, Region: Western Europe, PostalCode: 12209, Country: Germany, Phone: 030-0074321, Fax: 030-0076545, Balance: [2158.0000000000-->] 2102.0000000000, CreditLimit: 2300.0000000000, OrderCount: [16-->] 15, UnpaidOrderCount: [11-->] 10 row: 0x10fa2f100 session: 0x10f9f6e20 - 2022-03-22 20:11:03,205 - logic_logger - INF
..OrderDetail[2249] {Delete - client} Id: 2249, OrderId: 11124, ProductId: 1, UnitPrice: 18.0000000000, Quantity: 1, Discount: 0.0, Amount: 18.0000000000, ShippedDate: None row: 0x10fa27c40 session: 0x10f9f6e20 - 2022-03-22 20:11:03,206 - logic_logger - INF
....Product[1] {Update - Adjusting Product: UnitsShipped} Id: 1, ProductName: Chai, SupplierId: 1, CategoryId: 1, QuantityPerUnit: 10 boxes x 20 bags, UnitPrice: 18.0000000000, UnitsInStock: 39, UnitsOnOrder: 0, ReorderLevel: 10, Discontinued: 0, UnitsShipped: [0-->] -1 row: 0x10fa27040 session: 0x10f9f6e20 - 2022-03-22 20:11:03,208 - logic_logger - INF
....Product[1] {Formula UnitsInStock} Id: 1, ProductName: Chai, SupplierId: 1, CategoryId: 1, QuantityPerUnit: 10 boxes x 20 bags, UnitPrice: 18.0000000000, UnitsInStock: [39-->] 40, UnitsOnOrder: 0, ReorderLevel: 10, Discontinued: 0, UnitsShipped: [0-->] -1 row: 0x10fa27040 session: 0x10f9f6e20 - 2022-03-22 20:11:03,208 - logic_logger - INF
..OrderDetail[2249] {No adjustment on deleted parent: Order} Id: 2249, OrderId: 11124, ProductId: 1, UnitPrice: 18.0000000000, Quantity: 1, Discount: 0.0, Amount: 18.0000000000, ShippedDate: None row: 0x10fa27c40 session: 0x10f9f6e20 - 2022-03-22 20:11:03,209 - logic_logger - INF
..OrderDetail[2250] {Delete - client} Id: 2250, OrderId: 11124, ProductId: 2, UnitPrice: 19.0000000000, Quantity: 2, Discount: 0.0, Amount: 38.0000000000, ShippedDate: None row: 0x10fa27be0 session: 0x10f9f6e20 - 2022-03-22 20:11:03,209 - logic_logger - INF
....Product[2] {Update - Adjusting Product: UnitsShipped} Id: 2, ProductName: Chang, SupplierId: 1, CategoryId: 1, QuantityPerUnit: 24 - 12 oz bottles, UnitPrice: 19.0000000000, UnitsInStock: 15, UnitsOnOrder: 40, ReorderLevel: 25, Discontinued: 0, UnitsShipped: [2-->] 0 row: 0x10fa3b100 session: 0x10f9f6e20 - 2022-03-22 20:11:03,210 - logic_logger - INF
....Product[2] {Formula UnitsInStock} Id: 2, ProductName: Chang, SupplierId: 1, CategoryId: 1, QuantityPerUnit: 24 - 12 oz bottles, UnitPrice: 19.0000000000, UnitsInStock: [15-->] 17, UnitsOnOrder: 40, ReorderLevel: 25, Discontinued: 0, UnitsShipped: [2-->] 0 row: 0x10fa3b100 session: 0x10f9f6e20 - 2022-03-22 20:11:03,210 - logic_logger - INF
..OrderDetail[2250] {No adjustment on deleted parent: Order} Id: 2250, OrderId: 11124, ProductId: 2, UnitPrice: 19.0000000000, Quantity: 2, Discount: 0.0, Amount: 38.0000000000, ShippedDate: None row: 0x10fa27be0 session: 0x10f9f6e20 - 2022-03-22 20:11:03,211 - logic_logger - INF
Logic Phase: COMMIT(session=0x10f9f6e20) - 2022-03-22 20:11:03,211 - logic_logger - INF
..Order[11124] {Commit Event} Id: 11124, CustomerId: ALFKI, EmployeeId: 1, OrderDate: None, RequiredDate: None, ShippedDate: None, ShipVia: None, Freight: 11.0000000000, ShipName: None, ShipAddress: None, ShipCity: None, ShipRegion: None, ShipPostalCode: None, ShipCountry: None, AmountTotal: 56.00, Country: None, City: None, Ready: True, OrderDetailCount: 2 row: 0x10fa06c10 session: 0x10f9f6e20 - 2022-03-22 20:11:03,212 - logic_logger - INF
Scenario: Bad Order Custom Service
Scenario: Bad Order Custom Service
Given Customer Account: ALFKI
When Order Placed with excessive quantity
Then Rejected per Credit Limit
Then exceeds credit in response
Tests - and their logic - are transparent.. click to see Logic
Logic Doc for scenario: Bad Order Custom Service
Familiar logic pattern: constrain a derived result
Rules Used in Scenario: Bad Order Custom Service
Customer
1. Derive Customer.UnpaidOrderCount as Count(<class 'database.models.Order'> Where <function declare_logic.<locals>.<lambda> at 0x10f2ca310>)
2. Derive Customer.OrderCount as Count(<class 'database.models.Order'> Where None)
3. Constraint Function: None
4. Derive Customer.Balance as Sum(Order.AmountTotal Where <function declare_logic.<locals>.<lambda> at 0x10f1da940>)
Order
5. Derive Order.OrderDetailCount as Count(<class 'database.models.OrderDetail'> Where None)
6. Derive Order.AmountTotal as Sum(OrderDetail.Amount Where None)
OrderDetail
7. Derive OrderDetail.ShippedDate as Formula (2): row.Order.ShippedDate
8. Derive OrderDetail.UnitPrice as Copy(Product.UnitPrice)
9. Derive OrderDetail.Amount as Formula (1): as_expression=lambda row: row.UnitPrice * row.Qua [...]
Product
10. Derive Product.UnitsShipped as Sum(OrderDetail.Quantity Where <function declare_logic.<locals>.<lambda> at 0x10f2ca1f0>)
11. Derive Product.UnitsInStock as Formula (1): <function>
Logic Log in Scenario: Bad Order Custom Service
Logic Phase: ROW LOGIC(session=0x10fa573d0) (sqlalchemy before_flush) - 2022-03-22 20:11:03,267 - logic_logger - INF
..Order[None] {Insert - client} Id: None, CustomerId: ALFKI, EmployeeId: 1, OrderDate: None, RequiredDate: None, ShippedDate: None, ShipVia: None, Freight: 10, ShipName: None, ShipAddress: None, ShipCity: None, ShipRegion: None, ShipPostalCode: None, ShipCountry: None, AmountTotal: None, Country: None, City: None, Ready: None, OrderDetailCount: None row: 0x10fa57ca0 session: 0x10fa573d0 - 2022-03-22 20:11:03,267 - logic_logger - INF
....Customer[ALFKI] {Update - Adjusting Customer: UnpaidOrderCount, OrderCount} Id: ALFKI, CompanyName: Alfreds Futterkiste, ContactName: Maria Anders, ContactTitle: Sales Representative, Address: Obere Str. 57A, City: Berlin, Region: Western Europe, PostalCode: 12209, Country: Germany, Phone: 030-0074321, Fax: 030-0076545, Balance: 2102.0000000000, CreditLimit: 2300.0000000000, OrderCount: [15-->] 16, UnpaidOrderCount: [10-->] 11 row: 0x10fa504c0 session: 0x10fa573d0 - 2022-03-22 20:11:03,270 - logic_logger - INF
..OrderDetail[None] {Insert - client} Id: None, OrderId: None, ProductId: 1, UnitPrice: None, Quantity: 1111, Discount: 0, Amount: None, ShippedDate: None row: 0x10fa57370 session: 0x10fa573d0 - 2022-03-22 20:11:03,271 - logic_logger - INF
..OrderDetail[None] {copy_rules for role: Product - UnitPrice} Id: None, OrderId: None, ProductId: 1, UnitPrice: 18.0000000000, Quantity: 1111, Discount: 0, Amount: None, ShippedDate: None row: 0x10fa57370 session: 0x10fa573d0 - 2022-03-22 20:11:03,273 - logic_logger - INF
..OrderDetail[None] {Formula Amount} Id: None, OrderId: None, ProductId: 1, UnitPrice: 18.0000000000, Quantity: 1111, Discount: 0, Amount: 19998.0000000000, ShippedDate: None row: 0x10fa57370 session: 0x10fa573d0 - 2022-03-22 20:11:03,273 - logic_logger - INF
....Product[1] {Update - Adjusting Product: UnitsShipped} Id: 1, ProductName: Chai, SupplierId: 1, CategoryId: 1, QuantityPerUnit: 10 boxes x 20 bags, UnitPrice: 18.0000000000, UnitsInStock: 40, UnitsOnOrder: 0, ReorderLevel: 10, Discontinued: 0, UnitsShipped: [-1-->] 1110 row: 0x10fa50e50 session: 0x10fa573d0 - 2022-03-22 20:11:03,273 - logic_logger - INF
....Product[1] {Formula UnitsInStock} Id: 1, ProductName: Chai, SupplierId: 1, CategoryId: 1, QuantityPerUnit: 10 boxes x 20 bags, UnitPrice: 18.0000000000, UnitsInStock: [40-->] -1071, UnitsOnOrder: 0, ReorderLevel: 10, Discontinued: 0, UnitsShipped: [-1-->] 1110 row: 0x10fa50e50 session: 0x10fa573d0 - 2022-03-22 20:11:03,274 - logic_logger - INF
....Order[None] {Update - Adjusting Order: AmountTotal, OrderDetailCount} Id: None, CustomerId: ALFKI, EmployeeId: 1, OrderDate: None, RequiredDate: None, ShippedDate: None, ShipVia: None, Freight: 10, ShipName: None, ShipAddress: None, ShipCity: None, ShipRegion: None, ShipPostalCode: None, ShipCountry: None, AmountTotal: [None-->] 19998.0000000000, Country: None, City: None, Ready: None, OrderDetailCount: [None-->] 1 row: 0x10fa57ca0 session: 0x10fa573d0 - 2022-03-22 20:11:03,275 - logic_logger - INF
......Customer[ALFKI] {Update - Adjusting Customer: Balance} Id: ALFKI, CompanyName: Alfreds Futterkiste, ContactName: Maria Anders, ContactTitle: Sales Representative, Address: Obere Str. 57A, City: Berlin, Region: Western Europe, PostalCode: 12209, Country: Germany, Phone: 030-0074321, Fax: 030-0076545, Balance: [2102.0000000000-->] 22100.0000000000, CreditLimit: 2300.0000000000, OrderCount: 16, UnpaidOrderCount: 11 row: 0x10fa504c0 session: 0x10fa573d0 - 2022-03-22 20:11:03,275 - logic_logger - INF
......Customer[ALFKI] {Constraint Failure: balance (22100.0000000000) exceeds credit (2300.0000000000)} Id: ALFKI, CompanyName: Alfreds Futterkiste, ContactName: Maria Anders, ContactTitle: Sales Representative, Address: Obere Str. 57A, City: Berlin, Region: Western Europe, PostalCode: 12209, Country: Germany, Phone: 030-0074321, Fax: 030-0076545, Balance: [2102.0000000000-->] 22100.0000000000, CreditLimit: 2300.0000000000, OrderCount: 16, UnpaidOrderCount: 11 row: 0x10fa504c0 session: 0x10fa573d0 - 2022-03-22 20:11:03,276 - logic_logger - INF
Scenario: Alter Item Qty to exceed credit
Scenario: Alter Item Qty to exceed credit
Given Customer Account: ALFKI
When Order Detail Quantity altered very high
Then Rejected per Credit Limit
Then exceeds credit in response
Tests - and their logic - are transparent.. click to see Logic
Logic Doc for scenario: Alter Item Qty to exceed credit
Same constraint as above.
Key Takeaway: Automatic Reuse (design one, solve many)
Rules Used in Scenario: Alter Item Qty to exceed credit
Customer
1. Derive Customer.UnpaidOrderCount as Count(<class 'database.models.Order'> Where <function declare_logic.<locals>.<lambda> at 0x10f2ca310>)
2. Derive Customer.OrderCount as Count(<class 'database.models.Order'> Where None)
3. Constraint Function: None
4. Derive Customer.Balance as Sum(Order.AmountTotal Where <function declare_logic.<locals>.<lambda> at 0x10f1da940>)
Order
5. Derive Order.OrderDetailCount as Count(<class 'database.models.OrderDetail'> Where None)
6. Derive Order.AmountTotal as Sum(OrderDetail.Amount Where None)
OrderDetail
7. Derive OrderDetail.Amount as Formula (1): as_expression=lambda row: row.UnitPrice * row.Qua [...]
Product
8. Derive Product.UnitsShipped as Sum(OrderDetail.Quantity Where <function declare_logic.<locals>.<lambda> at 0x10f2ca1f0>)
9. Derive Product.UnitsInStock as Formula (1): <function>
Logic Log in Scenario: Alter Item Qty to exceed credit
Logic Phase: ROW LOGIC(session=0x10fa5bbe0) (sqlalchemy before_flush) - 2022-03-22 20:11:03,320 - logic_logger - INF
..OrderDetail[1040] {Update - client} Id: 1040, OrderId: 10643, ProductId: 28, UnitPrice: 45.6000000000, Quantity: [15-->] 1110, Discount: 0.25, Amount: 684.0000000000, ShippedDate: None row: 0x10fa27820 session: 0x10fa5bbe0 - 2022-03-22 20:11:03,321 - logic_logger - INF
..OrderDetail[1040] {Formula Amount} Id: 1040, OrderId: 10643, ProductId: 28, UnitPrice: 45.6000000000, Quantity: [15-->] 1110, Discount: 0.25, Amount: [684.0000000000-->] 50616.0000000000, ShippedDate: None row: 0x10fa27820 session: 0x10fa5bbe0 - 2022-03-22 20:11:03,321 - logic_logger - INF
..OrderDetail[1040] {Prune Formula: ShippedDate [['Order.ShippedDate']]} Id: 1040, OrderId: 10643, ProductId: 28, UnitPrice: 45.6000000000, Quantity: [15-->] 1110, Discount: 0.25, Amount: [684.0000000000-->] 50616.0000000000, ShippedDate: None row: 0x10fa27820 session: 0x10fa5bbe0 - 2022-03-22 20:11:03,321 - logic_logger - INF
....Product[28] {Update - Adjusting Product: UnitsShipped} Id: 28, ProductName: Rössle Sauerkraut, SupplierId: 12, CategoryId: 7, QuantityPerUnit: 25 - 825 g cans, UnitPrice: 45.6000000000, UnitsInStock: 26, UnitsOnOrder: 0, ReorderLevel: 0, Discontinued: 1, UnitsShipped: [0-->] 1095 row: 0x10fa5bc70 session: 0x10fa5bbe0 - 2022-03-22 20:11:03,322 - logic_logger - INF
....Product[28] {Formula UnitsInStock} Id: 28, ProductName: Rössle Sauerkraut, SupplierId: 12, CategoryId: 7, QuantityPerUnit: 25 - 825 g cans, UnitPrice: 45.6000000000, UnitsInStock: [26-->] -1069, UnitsOnOrder: 0, ReorderLevel: 0, Discontinued: 1, UnitsShipped: [0-->] 1095 row: 0x10fa5bc70 session: 0x10fa5bbe0 - 2022-03-22 20:11:03,323 - logic_logger - INF
....Order[10643] {Update - Adjusting Order: AmountTotal} Id: 10643, CustomerId: ALFKI, EmployeeId: 6, OrderDate: 2013-08-25, RequiredDate: 2013-10-13, ShippedDate: None, ShipVia: 1, Freight: 29.4600000000, ShipName: Alfreds Futterkiste, ShipAddress: Obere Str. 57, ShipCity: Berlin, ShipRegion: Western Europe, ShipPostalCode: 12209, ShipCountry: Germany, AmountTotal: [1086.00-->] 51018.0000000000, Country: None, City: None, Ready: True, OrderDetailCount: 3 row: 0x10fa3b190 session: 0x10fa5bbe0 - 2022-03-22 20:11:03,325 - logic_logger - INF
......Customer[ALFKI] {Update - Adjusting Customer: Balance} Id: ALFKI, CompanyName: Alfreds Futterkiste, ContactName: Maria Anders, ContactTitle: Sales Representative, Address: Obere Str. 57A, City: Berlin, Region: Western Europe, PostalCode: 12209, Country: Germany, Phone: 030-0074321, Fax: 030-0076545, Balance: [2102.0000000000-->] 52034.0000000000, CreditLimit: 2300.0000000000, OrderCount: 15, UnpaidOrderCount: 10 row: 0x10fa57280 session: 0x10fa5bbe0 - 2022-03-22 20:11:03,326 - logic_logger - INF
......Customer[ALFKI] {Constraint Failure: balance (52034.0000000000) exceeds credit (2300.0000000000)} Id: ALFKI, CompanyName: Alfreds Futterkiste, ContactName: Maria Anders, ContactTitle: Sales Representative, Address: Obere Str. 57A, City: Berlin, Region: Western Europe, PostalCode: 12209, Country: Germany, Phone: 030-0074321, Fax: 030-0076545, Balance: [2102.0000000000-->] 52034.0000000000, CreditLimit: 2300.0000000000, OrderCount: 15, UnpaidOrderCount: 10 row: 0x10fa57280 session: 0x10fa5bbe0 - 2022-03-22 20:11:03,327 - logic_logger - INF
Scenario: Alter Required Date - adjust logic pruned
Scenario: Alter Required Date - adjust logic pruned
Given Customer Account: ALFKI
When Order RequiredDate altered (2013-10-13)
Then Balance not adjusted
Tests - and their logic - are transparent.. click to see Logic
Logic Doc for scenario: Alter Required Date - adjust logic pruned
We set Order.RequiredDate
.
This is a normal update. Nothing depends on the columns altered, so this has no effect on the related Customer, Order Details or Products. Contrast this to the Cascade Update Test and the Custom Service test, where logic chaining affects related rows. Only the commit event fires.
Key Takeaway: rule pruning automatically avoids unnecessary SQL overhead.
Rules Used in Scenario: Alter Required Date - adjust logic pruned
Customer
1. Derive Customer.UnpaidOrderCount as Count(<class 'database.models.Order'> Where <function declare_logic.<locals>.<lambda> at 0x10f2ca310>)
2. Derive Customer.Balance as Sum(Order.AmountTotal Where <function declare_logic.<locals>.<lambda> at 0x10f1da940>)
3. Derive Customer.OrderCount as Count(<class 'database.models.Order'> Where None)
Order
4. RowEvent Order.congratulate_sales_rep()
Logic Log in Scenario: Alter Required Date - adjust logic pruned
Logic Phase: ROW LOGIC(session=0x10fa50d60) (sqlalchemy before_flush) - 2022-03-22 20:11:03,371 - logic_logger - INF
..Order[10643] {Update - client} Id: 10643, CustomerId: ALFKI, EmployeeId: 6, OrderDate: 2013-08-25, RequiredDate: [2013-10-13-->] 2013-10-13 00:00:00, ShippedDate: None, ShipVia: 1, Freight: 29.4600000000, ShipName: Alfreds Futterkiste, ShipAddress: Obere Str. 57, ShipCity: Berlin, ShipRegion: Western Europe, ShipPostalCode: 12209, ShipCountry: Germany, AmountTotal: 1086.00, Country: None, City: None, Ready: True, OrderDetailCount: 3 row: 0x10fa579d0 session: 0x10fa50d60 - 2022-03-22 20:11:03,372 - logic_logger - INF
Logic Phase: COMMIT(session=0x10fa50d60) - 2022-03-22 20:11:03,373 - logic_logger - INF
..Order[10643] {Commit Event} Id: 10643, CustomerId: ALFKI, EmployeeId: 6, OrderDate: 2013-08-25, RequiredDate: [2013-10-13-->] 2013-10-13 00:00:00, ShippedDate: None, ShipVia: 1, Freight: 29.4600000000, ShipName: Alfreds Futterkiste, ShipAddress: Obere Str. 57, ShipCity: Berlin, ShipRegion: Western Europe, ShipPostalCode: 12209, ShipCountry: Germany, AmountTotal: 1086.00, Country: None, City: None, Ready: True, OrderDetailCount: 3 row: 0x10fa579d0 session: 0x10fa50d60 - 2022-03-22 20:11:03,373 - logic_logger - INF
Scenario: Set Shipped - adjust logic reuse
Scenario: Set Shipped - adjust logic reuse
Given Customer Account: ALFKI
When Order ShippedDate altered (2013-10-13)
Then Balance reduced 1086
Tests - and their logic - are transparent.. click to see Logic
Logic Doc for scenario: Set Shipped - adjust logic reuse
We set Order.ShippedDate
.
This cascades to the Order Details, where it adjusts the Product.UnitsShipped
and recomputes UnitsInStock
, as above
Key Takeaway: parent references (e.g.,
OrderDetail.ShippedDate
) automate chain-down multi-table transactions.
Key Takeaway: Automatic Reuse (design one, solve many)
Rules Used in Scenario: Set Shipped - adjust logic reuse
Customer
1. Derive Customer.UnpaidOrderCount as Count(<class 'database.models.Order'> Where <function declare_logic.<locals>.<lambda> at 0x10f2ca310>)
2. Derive Customer.OrderCount as Count(<class 'database.models.Order'> Where None)
3. Derive Customer.Balance as Sum(Order.AmountTotal Where <function declare_logic.<locals>.<lambda> at 0x10f1da940>)
Order
4. Derive Order.OrderDetailCount as Count(<class 'database.models.OrderDetail'> Where None)
5. Derive Order.AmountTotal as Sum(OrderDetail.Amount Where None)
6. RowEvent Order.congratulate_sales_rep()
OrderDetail
7. Derive OrderDetail.ShippedDate as Formula (2): row.Order.ShippedDate
Product
8. Derive Product.UnitsShipped as Sum(OrderDetail.Quantity Where <function declare_logic.<locals>.<lambda> at 0x10f2ca1f0>)
9. Derive Product.UnitsInStock as Formula (1): <function>
Logic Log in Scenario: Set Shipped - adjust logic reuse
Logic Phase: ROW LOGIC(session=0x10fa5b310) (sqlalchemy before_flush) - 2022-03-22 20:11:03,459 - logic_logger - INF
..Order[10643] {Update - client} Id: 10643, CustomerId: ALFKI, EmployeeId: 6, OrderDate: 2013-08-25, RequiredDate: 2013-10-13, ShippedDate: [None-->] 2013-10-13, ShipVia: 1, Freight: 29.4600000000, ShipName: Alfreds Futterkiste, ShipAddress: Obere Str. 57, ShipCity: Berlin, ShipRegion: Western Europe, ShipPostalCode: 12209, ShipCountry: Germany, AmountTotal: 1086.00, Country: None, City: None, Ready: True, OrderDetailCount: 3 row: 0x10fa27e20 session: 0x10fa5b310 - 2022-03-22 20:11:03,460 - logic_logger - INF
....Customer[ALFKI] {Update - Adjusting Customer: Balance, UnpaidOrderCount} Id: ALFKI, CompanyName: Alfreds Futterkiste, ContactName: Maria Anders, ContactTitle: Sales Representative, Address: Obere Str. 57A, City: Berlin, Region: Western Europe, PostalCode: 12209, Country: Germany, Phone: 030-0074321, Fax: 030-0076545, Balance: [2102.0000000000-->] 1016.0000000000, CreditLimit: 2300.0000000000, OrderCount: 15, UnpaidOrderCount: [10-->] 9 row: 0x10fa27d60 session: 0x10fa5b310 - 2022-03-22 20:11:03,461 - logic_logger - INF
....OrderDetail[1040] {Update - Cascading Order.ShippedDate (,...)} Id: 1040, OrderId: 10643, ProductId: 28, UnitPrice: 45.6000000000, Quantity: 15, Discount: 0.25, Amount: 684.0000000000, ShippedDate: None row: 0x10f991c10 session: 0x10fa5b310 - 2022-03-22 20:11:03,463 - logic_logger - INF
....OrderDetail[1040] {Prune Formula: Amount [['UnitPrice', 'Quantity']]} Id: 1040, OrderId: 10643, ProductId: 28, UnitPrice: 45.6000000000, Quantity: 15, Discount: 0.25, Amount: 684.0000000000, ShippedDate: None row: 0x10f991c10 session: 0x10fa5b310 - 2022-03-22 20:11:03,463 - logic_logger - INF
....OrderDetail[1040] {Formula ShippedDate} Id: 1040, OrderId: 10643, ProductId: 28, UnitPrice: 45.6000000000, Quantity: 15, Discount: 0.25, Amount: 684.0000000000, ShippedDate: [None-->] 2013-10-13 row: 0x10f991c10 session: 0x10fa5b310 - 2022-03-22 20:11:03,463 - logic_logger - INF
......Product[28] {Update - Adjusting Product: UnitsShipped} Id: 28, ProductName: Rössle Sauerkraut, SupplierId: 12, CategoryId: 7, QuantityPerUnit: 25 - 825 g cans, UnitPrice: 45.6000000000, UnitsInStock: 26, UnitsOnOrder: 0, ReorderLevel: 0, Discontinued: 1, UnitsShipped: [0-->] -15 row: 0x10fa27d00 session: 0x10fa5b310 - 2022-03-22 20:11:03,464 - logic_logger - INF
......Product[28] {Formula UnitsInStock} Id: 28, ProductName: Rössle Sauerkraut, SupplierId: 12, CategoryId: 7, QuantityPerUnit: 25 - 825 g cans, UnitPrice: 45.6000000000, UnitsInStock: [26-->] 41, UnitsOnOrder: 0, ReorderLevel: 0, Discontinued: 1, UnitsShipped: [0-->] -15 row: 0x10fa27d00 session: 0x10fa5b310 - 2022-03-22 20:11:03,465 - logic_logger - INF
....OrderDetail[1041] {Update - Cascading Order.ShippedDate (,...)} Id: 1041, OrderId: 10643, ProductId: 39, UnitPrice: 18.0000000000, Quantity: 21, Discount: 0.25, Amount: 378.0000000000, ShippedDate: None row: 0x10f991bb0 session: 0x10fa5b310 - 2022-03-22 20:11:03,465 - logic_logger - INF
....OrderDetail[1041] {Prune Formula: Amount [['UnitPrice', 'Quantity']]} Id: 1041, OrderId: 10643, ProductId: 39, UnitPrice: 18.0000000000, Quantity: 21, Discount: 0.25, Amount: 378.0000000000, ShippedDate: None row: 0x10f991bb0 session: 0x10fa5b310 - 2022-03-22 20:11:03,466 - logic_logger - INF
....OrderDetail[1041] {Formula ShippedDate} Id: 1041, OrderId: 10643, ProductId: 39, UnitPrice: 18.0000000000, Quantity: 21, Discount: 0.25, Amount: 378.0000000000, ShippedDate: [None-->] 2013-10-13 row: 0x10f991bb0 session: 0x10fa5b310 - 2022-03-22 20:11:03,466 - logic_logger - INF
......Product[39] {Update - Adjusting Product: UnitsShipped} Id: 39, ProductName: Chartreuse verte, SupplierId: 18, CategoryId: 1, QuantityPerUnit: 750 cc per bottle, UnitPrice: 18.0000000000, UnitsInStock: 69, UnitsOnOrder: 0, ReorderLevel: 5, Discontinued: 0, UnitsShipped: [0-->] -21 row: 0x10fa5b070 session: 0x10fa5b310 - 2022-03-22 20:11:03,467 - logic_logger - INF
......Product[39] {Formula UnitsInStock} Id: 39, ProductName: Chartreuse verte, SupplierId: 18, CategoryId: 1, QuantityPerUnit: 750 cc per bottle, UnitPrice: 18.0000000000, UnitsInStock: [69-->] 90, UnitsOnOrder: 0, ReorderLevel: 5, Discontinued: 0, UnitsShipped: [0-->] -21 row: 0x10fa5b070 session: 0x10fa5b310 - 2022-03-22 20:11:03,467 - logic_logger - INF
....OrderDetail[1042] {Update - Cascading Order.ShippedDate (,...)} Id: 1042, OrderId: 10643, ProductId: 46, UnitPrice: 12.0000000000, Quantity: 2, Discount: 0.25, Amount: 24.0000000000, ShippedDate: None row: 0x10f991eb0 session: 0x10fa5b310 - 2022-03-22 20:11:03,468 - logic_logger - INF
....OrderDetail[1042] {Prune Formula: Amount [['UnitPrice', 'Quantity']]} Id: 1042, OrderId: 10643, ProductId: 46, UnitPrice: 12.0000000000, Quantity: 2, Discount: 0.25, Amount: 24.0000000000, ShippedDate: None row: 0x10f991eb0 session: 0x10fa5b310 - 2022-03-22 20:11:03,468 - logic_logger - INF
....OrderDetail[1042] {Formula ShippedDate} Id: 1042, OrderId: 10643, ProductId: 46, UnitPrice: 12.0000000000, Quantity: 2, Discount: 0.25, Amount: 24.0000000000, ShippedDate: [None-->] 2013-10-13 row: 0x10f991eb0 session: 0x10fa5b310 - 2022-03-22 20:11:03,469 - logic_logger - INF
......Product[46] {Update - Adjusting Product: UnitsShipped} Id: 46, ProductName: Spegesild, SupplierId: 21, CategoryId: 8, QuantityPerUnit: 4 - 450 g glasses, UnitPrice: 12.0000000000, UnitsInStock: 95, UnitsOnOrder: 0, ReorderLevel: 0, Discontinued: 0, UnitsShipped: [0-->] -2 row: 0x10fa27880 session: 0x10fa5b310 - 2022-03-22 20:11:03,470 - logic_logger - INF
......Product[46] {Formula UnitsInStock} Id: 46, ProductName: Spegesild, SupplierId: 21, CategoryId: 8, QuantityPerUnit: 4 - 450 g glasses, UnitPrice: 12.0000000000, UnitsInStock: [95-->] 97, UnitsOnOrder: 0, ReorderLevel: 0, Discontinued: 0, UnitsShipped: [0-->] -2 row: 0x10fa27880 session: 0x10fa5b310 - 2022-03-22 20:11:03,470 - logic_logger - INF
Logic Phase: COMMIT(session=0x10fa5b310) - 2022-03-22 20:11:03,471 - logic_logger - INF
..Order[10643] {Commit Event} Id: 10643, CustomerId: ALFKI, EmployeeId: 6, OrderDate: 2013-08-25, RequiredDate: 2013-10-13, ShippedDate: [None-->] 2013-10-13, ShipVia: 1, Freight: 29.4600000000, ShipName: Alfreds Futterkiste, ShipAddress: Obere Str. 57, ShipCity: Berlin, ShipRegion: Western Europe, ShipPostalCode: 12209, ShipCountry: Germany, AmountTotal: 1086.00, Country: None, City: None, Ready: True, OrderDetailCount: 3 row: 0x10fa27e20 session: 0x10fa5b310 - 2022-03-22 20:11:03,471 - logic_logger - INF
Scenario: Reset Shipped - adjust logic reuse
Scenario: Reset Shipped - adjust logic reuse
Given Shipped Order
When Order ShippedDate set to None
Then Logic adjusts Balance by -1086
Tests - and their logic - are transparent.. click to see Logic
Logic Doc for scenario: Reset Shipped - adjust logic reuse
Same logic as above.
Key Takeaway: Automatic Reuse (design one, solve many)
Rules Used in Scenario: Reset Shipped - adjust logic reuse
Customer
1. Derive Customer.UnpaidOrderCount as Count(<class 'database.models.Order'> Where <function declare_logic.<locals>.<lambda> at 0x10f2ca310>)
2. Derive Customer.OrderCount as Count(<class 'database.models.Order'> Where None)
3. Derive Customer.Balance as Sum(Order.AmountTotal Where <function declare_logic.<locals>.<lambda> at 0x10f1da940>)
Order
4. Derive Order.OrderDetailCount as Count(<class 'database.models.OrderDetail'> Where None)
5. Derive Order.AmountTotal as Sum(OrderDetail.Amount Where None)
6. RowEvent Order.congratulate_sales_rep()
OrderDetail
7. Derive OrderDetail.ShippedDate as Formula (2): row.Order.ShippedDate
Product
8. Derive Product.UnitsShipped as Sum(OrderDetail.Quantity Where <function declare_logic.<locals>.<lambda> at 0x10f2ca1f0>)
9. Derive Product.UnitsInStock as Formula (1): <function>
Logic Log in Scenario: Reset Shipped - adjust logic reuse
Logic Phase: ROW LOGIC(session=0x10fa27c70) (sqlalchemy before_flush) - 2022-03-22 20:11:03,556 - logic_logger - INF
..Order[10643] {Update - client} Id: 10643, CustomerId: ALFKI, EmployeeId: 6, OrderDate: 2013-08-25, RequiredDate: 2013-10-13, ShippedDate: [2013-10-13-->] None, ShipVia: 1, Freight: 29.4600000000, ShipName: Alfreds Futterkiste, ShipAddress: Obere Str. 57, ShipCity: Berlin, ShipRegion: Western Europe, ShipPostalCode: 12209, ShipCountry: Germany, AmountTotal: 1086.00, Country: None, City: None, Ready: True, OrderDetailCount: 3 row: 0x10f926c70 session: 0x10fa27c70 - 2022-03-22 20:11:03,556 - logic_logger - INF
....Customer[ALFKI] {Update - Adjusting Customer: Balance, UnpaidOrderCount} Id: ALFKI, CompanyName: Alfreds Futterkiste, ContactName: Maria Anders, ContactTitle: Sales Representative, Address: Obere Str. 57A, City: Berlin, Region: Western Europe, PostalCode: 12209, Country: Germany, Phone: 030-0074321, Fax: 030-0076545, Balance: [1016.0000000000-->] 2102.0000000000, CreditLimit: 2300.0000000000, OrderCount: 15, UnpaidOrderCount: [9-->] 10 row: 0x10fa2f3d0 session: 0x10fa27c70 - 2022-03-22 20:11:03,558 - logic_logger - INF
....OrderDetail[1040] {Update - Cascading Order.ShippedDate (,...)} Id: 1040, OrderId: 10643, ProductId: 28, UnitPrice: 45.6000000000, Quantity: 15, Discount: 0.25, Amount: 684.0000000000, ShippedDate: 2013-10-13 row: 0x10faf4850 session: 0x10fa27c70 - 2022-03-22 20:11:03,559 - logic_logger - INF
....OrderDetail[1040] {Prune Formula: Amount [['UnitPrice', 'Quantity']]} Id: 1040, OrderId: 10643, ProductId: 28, UnitPrice: 45.6000000000, Quantity: 15, Discount: 0.25, Amount: 684.0000000000, ShippedDate: 2013-10-13 row: 0x10faf4850 session: 0x10fa27c70 - 2022-03-22 20:11:03,560 - logic_logger - INF
....OrderDetail[1040] {Formula ShippedDate} Id: 1040, OrderId: 10643, ProductId: 28, UnitPrice: 45.6000000000, Quantity: 15, Discount: 0.25, Amount: 684.0000000000, ShippedDate: [2013-10-13-->] None row: 0x10faf4850 session: 0x10fa27c70 - 2022-03-22 20:11:03,560 - logic_logger - INF
......Product[28] {Update - Adjusting Product: UnitsShipped} Id: 28, ProductName: Rössle Sauerkraut, SupplierId: 12, CategoryId: 7, QuantityPerUnit: 25 - 825 g cans, UnitPrice: 45.6000000000, UnitsInStock: 41, UnitsOnOrder: 0, ReorderLevel: 0, Discontinued: 1, UnitsShipped: [-15-->] 0 row: 0x10fa1a2e0 session: 0x10fa27c70 - 2022-03-22 20:11:03,561 - logic_logger - INF
......Product[28] {Formula UnitsInStock} Id: 28, ProductName: Rössle Sauerkraut, SupplierId: 12, CategoryId: 7, QuantityPerUnit: 25 - 825 g cans, UnitPrice: 45.6000000000, UnitsInStock: [41-->] 26, UnitsOnOrder: 0, ReorderLevel: 0, Discontinued: 1, UnitsShipped: [-15-->] 0 row: 0x10fa1a2e0 session: 0x10fa27c70 - 2022-03-22 20:11:03,562 - logic_logger - INF
....OrderDetail[1041] {Update - Cascading Order.ShippedDate (,...)} Id: 1041, OrderId: 10643, ProductId: 39, UnitPrice: 18.0000000000, Quantity: 21, Discount: 0.25, Amount: 378.0000000000, ShippedDate: 2013-10-13 row: 0x10faf48b0 session: 0x10fa27c70 - 2022-03-22 20:11:03,562 - logic_logger - INF
....OrderDetail[1041] {Prune Formula: Amount [['UnitPrice', 'Quantity']]} Id: 1041, OrderId: 10643, ProductId: 39, UnitPrice: 18.0000000000, Quantity: 21, Discount: 0.25, Amount: 378.0000000000, ShippedDate: 2013-10-13 row: 0x10faf48b0 session: 0x10fa27c70 - 2022-03-22 20:11:03,563 - logic_logger - INF
....OrderDetail[1041] {Formula ShippedDate} Id: 1041, OrderId: 10643, ProductId: 39, UnitPrice: 18.0000000000, Quantity: 21, Discount: 0.25, Amount: 378.0000000000, ShippedDate: [2013-10-13-->] None row: 0x10faf48b0 session: 0x10fa27c70 - 2022-03-22 20:11:03,563 - logic_logger - INF
......Product[39] {Update - Adjusting Product: UnitsShipped} Id: 39, ProductName: Chartreuse verte, SupplierId: 18, CategoryId: 1, QuantityPerUnit: 750 cc per bottle, UnitPrice: 18.0000000000, UnitsInStock: 90, UnitsOnOrder: 0, ReorderLevel: 5, Discontinued: 0, UnitsShipped: [-21-->] 0 row: 0x10fa50250 session: 0x10fa27c70 - 2022-03-22 20:11:03,564 - logic_logger - INF
......Product[39] {Formula UnitsInStock} Id: 39, ProductName: Chartreuse verte, SupplierId: 18, CategoryId: 1, QuantityPerUnit: 750 cc per bottle, UnitPrice: 18.0000000000, UnitsInStock: [90-->] 69, UnitsOnOrder: 0, ReorderLevel: 5, Discontinued: 0, UnitsShipped: [-21-->] 0 row: 0x10fa50250 session: 0x10fa27c70 - 2022-03-22 20:11:03,564 - logic_logger - INF
....OrderDetail[1042] {Update - Cascading Order.ShippedDate (,...)} Id: 1042, OrderId: 10643, ProductId: 46, UnitPrice: 12.0000000000, Quantity: 2, Discount: 0.25, Amount: 24.0000000000, ShippedDate: 2013-10-13 row: 0x10faf47f0 session: 0x10fa27c70 - 2022-03-22 20:11:03,565 - logic_logger - INF
....OrderDetail[1042] {Prune Formula: Amount [['UnitPrice', 'Quantity']]} Id: 1042, OrderId: 10643, ProductId: 46, UnitPrice: 12.0000000000, Quantity: 2, Discount: 0.25, Amount: 24.0000000000, ShippedDate: 2013-10-13 row: 0x10faf47f0 session: 0x10fa27c70 - 2022-03-22 20:11:03,565 - logic_logger - INF
....OrderDetail[1042] {Formula ShippedDate} Id: 1042, OrderId: 10643, ProductId: 46, UnitPrice: 12.0000000000, Quantity: 2, Discount: 0.25, Amount: 24.0000000000, ShippedDate: [2013-10-13-->] None row: 0x10faf47f0 session: 0x10fa27c70 - 2022-03-22 20:11:03,566 - logic_logger - INF
......Product[46] {Update - Adjusting Product: UnitsShipped} Id: 46, ProductName: Spegesild, SupplierId: 21, CategoryId: 8, QuantityPerUnit: 4 - 450 g glasses, UnitPrice: 12.0000000000, UnitsInStock: 97, UnitsOnOrder: 0, ReorderLevel: 0, Discontinued: 0, UnitsShipped: [-2-->] 0 row: 0x10fa50490 session: 0x10fa27c70 - 2022-03-22 20:11:03,566 - logic_logger - INF
......Product[46] {Formula UnitsInStock} Id: 46, ProductName: Spegesild, SupplierId: 21, CategoryId: 8, QuantityPerUnit: 4 - 450 g glasses, UnitPrice: 12.0000000000, UnitsInStock: [97-->] 95, UnitsOnOrder: 0, ReorderLevel: 0, Discontinued: 0, UnitsShipped: [-2-->] 0 row: 0x10fa50490 session: 0x10fa27c70 - 2022-03-22 20:11:03,567 - logic_logger - INF
Logic Phase: COMMIT(session=0x10fa27c70) - 2022-03-22 20:11:03,568 - logic_logger - INF
..Order[10643] {Commit Event} Id: 10643, CustomerId: ALFKI, EmployeeId: 6, OrderDate: 2013-08-25, RequiredDate: 2013-10-13, ShippedDate: [2013-10-13-->] None, ShipVia: 1, Freight: 29.4600000000, ShipName: Alfreds Futterkiste, ShipAddress: Obere Str. 57, ShipCity: Berlin, ShipRegion: Western Europe, ShipPostalCode: 12209, ShipCountry: Germany, AmountTotal: 1086.00, Country: None, City: None, Ready: True, OrderDetailCount: 3 row: 0x10f926c70 session: 0x10fa27c70 - 2022-03-22 20:11:03,568 - logic_logger - INF
Feature: Salary Change
Scenario: Audit Salary Change
Scenario: Audit Salary Change
Given Employee 5 (Buchanan) - Salary 95k
When Patch Salary to 200k
Then Salary_audit row created
Tests - and their logic - are transparent.. click to see Logic
Logic Doc for scenario: Audit Salary Change
Observe the logic log to see that it creates audit rows:
- Discouraged: you can implement auditing with events. But auditing is a common pattern, and this can lead to repetitive, tedious code
- Preferred: approaches use extensible rules.
Generic event handlers can also reduce redundant code, illustrated in the time/date stamping handle_all
logic.
This is due to the copy_row
rule. Contrast this to the tedious audit_by_event
alternative:
![](https://github.com/valhuber/TDD/raw/main/images/salary_change.png?raw=true&raw=true)
Key Takeaway: use extensible own rule types to automate pattern you identify; events can result in tedious amounts of code.
Rules Used in Scenario: Audit Salary Change
Employee
1. RowEvent Employee.audit_by_event()
Logic Log in Scenario: Audit Salary Change
Logic Phase: ROW LOGIC(session=0x10faf4c10) (sqlalchemy before_flush) - 2022-03-22 20:11:03,619 - logic_logger - INF
..Employee[5] {Update - client} Id: 5, LastName: Buchanan, FirstName: Steven, Title: Sales Manager, TitleOfCourtesy: Mr., BirthDate: 1987-03-04, HireDate: 2025-10-17, Address: 14 Garrett Hill, City: London, Region: British Isles, PostalCode: SW1 8JR, Country: UK, HomePhone: (71) 555-4848, Extension: 3453, Photo: None, Notes: Steven Buchanan graduated from St. Andrews University, Scotland, with a BSC degree in 1976. Upon joining the company as a sales representative in 1992, he spent 6 months in an orientation program at the Seattle office and then returned to his permanent post in London. He was promoted to sales manager in March 1993. Mr. Buchanan has completed the courses 'Successful Telemarketing' and 'International Sales Management.' He is fluent in French., ReportsTo: 2, PhotoPath: http://accweb/emmployees/buchanan.bmp, IsCommissioned: 0, Salary: [95000.0000000000-->] 200000, WorksForDepartmentId: 3, OnLoanDepartmentId: None row: 0x10faf49a0 session: 0x10faf4c10 - 2022-03-22 20:11:03,619 - logic_logger - INF
..Employee[5] {BEGIN Copy to: EmployeeAudit} Id: 5, LastName: Buchanan, FirstName: Steven, Title: Sales Manager, TitleOfCourtesy: Mr., BirthDate: 1987-03-04, HireDate: 2025-10-17, Address: 14 Garrett Hill, City: London, Region: British Isles, PostalCode: SW1 8JR, Country: UK, HomePhone: (71) 555-4848, Extension: 3453, Photo: None, Notes: Steven Buchanan graduated from St. Andrews University, Scotland, with a BSC degree in 1976. Upon joining the company as a sales representative in 1992, he spent 6 months in an orientation program at the Seattle office and then returned to his permanent post in London. He was promoted to sales manager in March 1993. Mr. Buchanan has completed the courses 'Successful Telemarketing' and 'International Sales Management.' He is fluent in French., ReportsTo: 2, PhotoPath: http://accweb/emmployees/buchanan.bmp, IsCommissioned: 0, Salary: [95000.0000000000-->] 200000, WorksForDepartmentId: 3, OnLoanDepartmentId: None row: 0x10faf49a0 session: 0x10faf4c10 - 2022-03-22 20:11:03,621 - logic_logger - INF
....EmployeeAudit[None] {Insert - Copy EmployeeAudit} Id: None, Title: Sales Manager, Salary: 200000, LastName: Buchanan, FirstName: Steven, EmployeeId: None, CreatedOn: None row: 0x10faf4dc0 session: 0x10faf4c10 - 2022-03-22 20:11:03,622 - logic_logger - INF
....EmployeeAudit[None] {early_row_event_all_classes - handle_all sets 'Created_on} Id: None, Title: Sales Manager, Salary: 200000, LastName: Buchanan, FirstName: Steven, EmployeeId: None, CreatedOn: 2022-03-22 20:11:03.622180 row: 0x10faf4dc0 session: 0x10faf4c10 - 2022-03-22 20:11:03,622 - logic_logger - INF
Logic Phase: COMMIT(session=0x10faf4c10) - 2022-03-22 20:11:03,622 - logic_logger - INF
..Employee[5] {Commit Event} Id: 5, LastName: Buchanan, FirstName: Steven, Title: Sales Manager, TitleOfCourtesy: Mr., BirthDate: 1987-03-04, HireDate: 2025-10-17, Address: 14 Garrett Hill, City: London, Region: British Isles, PostalCode: SW1 8JR, Country: UK, HomePhone: (71) 555-4848, Extension: 3453, Photo: None, Notes: Steven Buchanan graduated from St. Andrews University, Scotland, with a BSC degree in 1976. Upon joining the company as a sales representative in 1992, he spent 6 months in an orientation program at the Seattle office and then returned to his permanent post in London. He was promoted to sales manager in March 1993. Mr. Buchanan has completed the courses 'Successful Telemarketing' and 'International Sales Management.' He is fluent in French., ReportsTo: 2, PhotoPath: http://accweb/emmployees/buchanan.bmp, IsCommissioned: 0, Salary: [95000.0000000000-->] 200000, WorksForDepartmentId: 3, OnLoanDepartmentId: None row: 0x10faf49a0 session: 0x10faf4c10 - 2022-03-22 20:11:03,623 - logic_logger - INF
Scenario: Raise Must be Meaningful
Scenario: Raise Must be Meaningful
Given Employee 5 (Buchanan) - Salary 95k
When Patch Salary to 96k
Then Reject - Raise too small
Tests - and their logic - are transparent.. click to see Logic
Logic Doc for scenario: Raise Must be Meaningful
Observe the use of old_row
Key Takeaway: State Transsition Logic enabled per
old_row
Rules Used in Scenario: Raise Must be Meaningful
Employee
1. Constraint Function: <function declare_logic.<locals>.raise_over_20_percent at 0x10f2caa60>
Logic Log in Scenario: Raise Must be Meaningful
Logic Phase: ROW LOGIC(session=0x10fb4f820) (sqlalchemy before_flush) - 2022-03-22 20:11:03,774 - logic_logger - INF
..Employee[5] {Update - client} Id: 5, LastName: Buchanan, FirstName: Steven, Title: Sales Manager, TitleOfCourtesy: Mr., BirthDate: 1987-03-04, HireDate: 2025-10-17, Address: 14 Garrett Hill, City: London, Region: British Isles, PostalCode: SW1 8JR, Country: UK, HomePhone: (71) 555-4848, Extension: 3453, Photo: None, Notes: Steven Buchanan graduated from St. Andrews University, Scotland, with a BSC degree in 1976. Upon joining the company as a sales representative in 1992, he spent 6 months in an orientation program at the Seattle office and then returned to his permanent post in London. He was promoted to sales manager in March 1993. Mr. Buchanan has completed the courses 'Successful Telemarketing' and 'International Sales Management.' He is fluent in French., ReportsTo: 2, PhotoPath: http://accweb/emmployees/buchanan.bmp, IsCommissioned: 0, Salary: [95000.0000000000-->] 96000, WorksForDepartmentId: 3, OnLoanDepartmentId: None row: 0x10fb4fa90 session: 0x10fb4f820 - 2022-03-22 20:11:03,775 - logic_logger - INF
..Employee[5] {Constraint Failure: Buchanan needs a more meaningful raise} Id: 5, LastName: Buchanan, FirstName: Steven, Title: Sales Manager, TitleOfCourtesy: Mr., BirthDate: 1987-03-04, HireDate: 2025-10-17, Address: 14 Garrett Hill, City: London, Region: British Isles, PostalCode: SW1 8JR, Country: UK, HomePhone: (71) 555-4848, Extension: 3453, Photo: None, Notes: Steven Buchanan graduated from St. Andrews University, Scotland, with a BSC degree in 1976. Upon joining the company as a sales representative in 1992, he spent 6 months in an orientation program at the Seattle office and then returned to his permanent post in London. He was promoted to sales manager in March 1993. Mr. Buchanan has completed the courses 'Successful Telemarketing' and 'International Sales Management.' He is fluent in French., ReportsTo: 2, PhotoPath: http://accweb/emmployees/buchanan.bmp, IsCommissioned: 0, Salary: [95000.0000000000-->] 96000, WorksForDepartmentId: 3, OnLoanDepartmentId: None row: 0x10fb4fa90 session: 0x10fb4f820 - 2022-03-22 20:11:03,775 - logic_logger - INF
4 features passed, 0 failed, 0 skipped
11 scenarios passed, 0 failed, 0 skipped
37 steps passed, 0 failed, 0 skipped, 0 undefined
Took 0m0.759s