Spike: [MODFQMMGR-752] Create composite entity type for order-invoice reports in Lists app/FQM
Order - Invoice Analysis
Usage
Compound entity is built around purchase orders. It consists of the following entities from different domains:
Purchase Order
Purchase Order Line
Invoice
Invoice Line
Fiscal Year¹
Organization¹
Fund²
Fund Type
Expense class²
1 - associated entities are derived from Invoice
2 - associated entities are derived from Invoice Line's fund distributions
In order to create queries based on different requirements, any fields of these entities can be used to filter the results. To build a query, 3 necessary parameters need to be provided: Field
, Operator
and Value
. Field
can be chosen from any of the mentioned entities. Operator
selection usually depends on the field type and determines how the value would be filtered. Value
is the input that the filter will be applied with for the provided field.
For example, below are 4 simple query definitions with their descriptions:
1) Provide a list of all invoices and invoice lines associated with ongoing orders
2) Provide a list of what was paid for ongoing orders for each vendor last fiscal year
3) Provide a report on expense classes used for ongoing orders last year for a certain vendor
4) Provide a report on a certain fund’s expenditure over time by vendor
In order to alter the output list columns Actions
dropdown can be used and different fields be selected that are desired to be returned by the query
Notes
Currently the analysis entity contains
Fiscal Year
as a separate entity with all its fields and an additional field ofInvoice
entity namedFiscal Year
. The additional field would act the same as theName
field ofFiscal Year
entity, so they are interchangeable.Fund and Expense class related data is dependent on the fund distributions of invoice lines, not purchase order lines
Documentation
This document provides guidance on how the composite_order_invoice_analytics
entity is constructed. It serves both as a technical explanation of this entity and a reference for creating future composite entities in a similar fashion.
Composite entities are built by combining data from multiple sources, which may include database tables/views or simple entity types (which themselves map to a table/view). The final goal is to expose a normalized and analytical view over complex, nested structures like invoices and purchase orders.
Data Source Types
There are two primary types of sources used to build composite entities:
entity-type
: Refers to a pre-defined simple or composite entity. Simple entity types usually wrap a direct table/view and provide standardized access to fields. As for composite entity types, they can be a combination of several other types.db
: Refers to a raw SQL-based data source, typically used in simple entity types
Each source can have a defined order
, defines the order in which fields show up in the final entity type that the API exposes. Default value for this field is usually MAX_INT/2
Entity Construction Steps
1. Start from Purchase Order (po
)
The entry point for this entity is the Purchase Order, as it forms the foundation for all subsequent data. Without a purchase order, associated invoice lines, fund distributions, and related data would not be meaningful.
{
alias: 'po',
targetId: '48de4796-11a2-44e9-abb7-01e84af87234',
type: 'entity-type',
useIdColumns: true,
order: 10
}
The useIdColumns: true
setting ensures that each row is treated distinctly when computing uniqueness.
2. Join to Purchase Order Line (pol
)
Purchase Order Lines are joined using:
sourceField
:po.id
targetField
:purchase_order_id
This is a left join, ensuring all PO lines are included even if they don’t have associated downstream data.
{
alias: 'pol',
targetId: '58148257-bfb0-4687-8c42-d2833d772f3e',
sourceField: 'po.id',
targetField: 'purchase_order_id',
overrideJoinDirection: "left",
useIdColumns: true,
order: 20
}
3. Invoice Line Fund Distribution Join
The invoice_line_fund_distribution
alias is not a standalone entity but a composite that exists to flatten the fundDistributions
array found in invoice line JSON. It serves the purpose of exposing individual rows for:
fund_id
expense_class_id
invoice_id
po_line_id
(for joining)
These fields are then used to join with other simple entities like fund
, expense_class
, and invoice
.
Implementation Details:
A CROSS JOIN LATERAL
is used together with jsonb_array_elements
to extract individual fundDistribution
objects from the invoice line JSON array.
{
alias: 'fund_distribution',
type: 'db',
target: 'jsonb_array_elements("invoice_line_fund_distribution.invoice_line.invoice_lines".jsonb -> \'fundDistributions\')',
join: {
type: 'CROSS JOIN LATERAL',
joinTo: 'invoice_line.invoice_lines'
}
}
The resulting structure allows us to create a normalized view over array-based data and extract individual fields from each distribution.
Additional Joins
After extracting the fund and expense class references from the fund distribution array, we proceed to join additional related entities:
Invoice (
invoice_line_fund_distribution.invoice_id → invoice.id
)Organization (
invoice.vendor_id → organization.id
)Fiscal Year (
invoice.fiscal_year_id → fiscal_year.id
)Fund (
invoice_line_fund_distribution.fund_id → fund.id
)Fund Type (
fund.fund_type_id → fund_type.id
)Expense Class (
invoice_line_fund_distribution.expense_class_id → expense_class.id
)
Each of these joins uses the standard sourceField
to targetField
pattern with left
join semantics and an order
value to maintain evaluation sequence.
Row Differentiation and useIdColumns
To avoid ambiguity in the result set, any source that contributes to row-level uniqueness must have useIdColumns: true
.
For example, if a PO has multiple lines and each line has multiple fund distributions, then uniqueness depends not only on the PO but also on the PO Line and Fund. Omitting useIdColumns
on these levels would cause duplicate detection issues.
Summary Table
Alias | Target Entity | Join Condition | Order | useIdColumns |
---|---|---|---|---|
po | simple_purchase_order | – | 10 | true |
pol | simple_purchase_order_line | pol.purchase_order_id = po.id | 20 | true |
invoice_line_fund_distribution | composite_invoice_line_fund_distribution | po_line_id = pol.id | 30 | true |
invoice | simple_invoice | invoice.id = invoice_line_fund_distribution.invoice_id | 40 | false |
organization | simple_organization | organization.id = invoice.vendor_id | 50 | false |
fiscal_year | simple_fiscal_year | fiscal_year.id = invoice.fiscal_year_id | 60 | false |
fund | simple_fund | fund.id = invoice_line_fund_distribution.fund_id | 70 | true |
fund_type | simple_fund_type | fund_type.id = fund.fund_type_id | 80 | false |
expense_class | simple_expense_class | expense_class.id = invoice_line_fund_distribution.expense_class_id | 90 | true |
Best Practices for Future Composite Entities
Start with the primary/root entity to ensure all dependent data can be meaningfully joined.
Use
sourceField
andtargetField
consistently to define join conditions.Respect loading order by using ascending
order
values to resolve dependencies before use.Use
useIdColumns: true
for all sources that contribute to uniqueness of the final dataset.Handle JSON arrays with
CROSS JOIN LATERAL
when normalization is required.Avoid redundant sources. If a source is already transitively available (e.g., invoice line via fund distribution), do not include it again.
Use
essentialOnly: true
to fetch only essential data from a given entity (not to include all fields).
Scripts used to set up the test data: