Repository: folio-org/mod-fqm-manager
Pull request #701: [MODFQMMGR-724|752] Create composite entity type for order-invoice reports in Lists app/FQM
Author: login: Saba-Zedginidze-EPAM
Created: 2025-04-06T22:33:55Z | Merged: 2025-05-16T20:23:13Z

## Purpose
[[MODFQMMGR-724] POC: Add ability to search order related data from Finance, Invoice, Organization domains](https://folio-org.atlassian.net/browse/MODFQMMGR-724)
[[MODFQMMGR-752] Validation for Order-Invoice queries in the Lists app/FQM](https://folio-org.atlassian.net/browse/MODFQMMGR-752)
[[UXPROD-5202] Create composite entity type for order-invoice reports in Lists app/FQM](https://folio-org.atlassian.net/browse/UXPROD-5202)

Spike created to document the outcome: https://folio-org.atlassian.net/wiki/spaces/FOLIJET/pages/994934786/Spike+MODFQMMGR-752+Create+composite+entity+type+for+order-invoice+reports+in+Lists+app+FQM

## Approach
- Add new composite entity for Order Invoice Analysis
- Add auxiliary entity type to fetch fund data from the fund distribution array
- Add function alias for jsonb_array_elements

## Pre-Merge Checklist
If you are adding entity type(s), have you:
- [x] Added the JSON5 definition to the `src/main/resources/entity-types` directory?
- [x] Ensured that GETing the entity type at `/entity-types/{id}` works as expected?
- [x] Added translations for all fields, per the [translation guidelines](/translations/README.md)? (Check this by ensuring `GET /entity-types/{id}` does not have `mod-fqm-manager.entityType.` in the response)
- [ ] Added views to liquibase, as applicable?
- [ ] Added required interfaces to the module descriptor?
- [x] Checked that querying fields works correctly and all SQL is valid?

If you are changing/removing entity type(s), have you:
- [ ] Added migration code for any changes?


Diff:
diff --git a/src/main/resources/db/changelog/migrations/s-migrations/changelog-s-migrations.xml b/src/main/resources/db/changelog/migrations/s-migrations/changelog-s-migrations.xml
index 6fd4d7c10..0b8f8fd4a 100644
--- a/src/main/resources/db/changelog/migrations/s-migrations/changelog-s-migrations.xml
+++ b/src/main/resources/db/changelog/migrations/s-migrations/changelog-s-migrations.xml
@@ -8,4 +8,16 @@
       ALTER TABLE query_details ADD CONSTRAINT query_details_status_check CHECK (status IN ('IN_PROGRESS', 'SUCCESS', 'FAILED', 'CANCELLED', 'MAX_SIZE_EXCEEDED'));
     </sql>
   </changeSet>
+
+  <changeSet id="MODFQMMGR-752" author="saba_zedginidze@epam.com" runOnChange="true">
+    <createProcedure>
+      CREATE OR REPLACE FUNCTION ${tenant_id}_mod_fqm_manager.jsonb_array_elements(input jsonb)
+      RETURNS SETOF jsonb
+      AS $$
+      BEGIN
+        RETURN QUERY SELECT * FROM jsonb_array_elements(input);
+      END;
+      $$ LANGUAGE 'plpgsql';
+    </createProcedure>
+  </changeSet>
 </databaseChangeLog>
diff --git a/src/main/resources/entity-types/invoice/composite_invoice_line_fund_distribution.json5 b/src/main/resources/entity-types/invoice/composite_invoice_line_fund_distribution.json5
new file mode 100644
index 000000000..f671b4900
--- /dev/null
+++ b/src/main/resources/entity-types/invoice/composite_invoice_line_fund_distribution.json5
@@ -0,0 +1,104 @@
+// NOTE: This auxiliary entity is tightly coupled to the main 'composite_order_invoice_analytics' entity and is not independently reusable.
+// It exists solely to normalize the 'fundDistributions' array in invoice line records by transforming each element into its own row.
+// The entity includes 'invoice_line' as a source and applies a CROSS JOIN LATERAL with jsonb_array_elements,
+// allowing fund distribution elements to be extracted cleanly and used for joins (e.g., to fund and expense class).
+// While this design means the auxiliary entity is dependent on the structure of the main entity and can't function on its own,
+// it provides a clean and maintainable solution to the array normalization problem within the overall entity model.
+{
+  id: '4999bd5f-3898-4b44-b283-e37bc9261ad8',
+  name: 'composite_invoice_line_fund_distribution',
+  private: true,
+  sources: [
+    {
+      type: 'entity-type',
+      alias: 'invoice_line',
+      targetId: '1373f063-8b86-48cc-832d-68b4d0dd44fc', // simple_invoice_line
+      useIdColumns: true,
+    },
+    {
+      type: 'db',
+      alias: 'fund_distribution',
+      target: 'jsonb_array_elements("invoice_line_fund_distribution.invoice_line.invoice_lines".jsonb -> \'fundDistributions\')',
+      join: {
+        type: 'LEFT JOIN LATERAL',
+        joinTo: 'invoice_line.invoice_lines',
+        condition: 'TRUE',
+      }
+    },
+  ],
+  columns: [
+    {
+      name: 'invoice_id',
+      sourceAlias: 'invoice_line.invoice_lines',
+      dataType: {
+        dataType: 'rangedUUIDType',
+      },
+      isIdColumn: true,
+      valueGetter: ":sourceAlias.invoiceid",
+      filterValueGetter: "lower(:sourceAlias.invoiceid)",
+      valueFunction: 'lower(:value)',
+      joinsTo: [
+        {
+          targetId: '4d626ce1-1880-48d2-9d4c-81667fdc5dbb', // simple_invoice
+          targetField: 'id',
+          type: 'equality-simple',
+          direction: 'left',
+        },
+      ],
+    },
+    {
+      name: 'po_line_id',
+      sourceAlias: 'invoice_line.invoice_lines',
+      dataType: {
+        dataType: 'rangedUUIDType',
+      },
+      isIdColumn: true,
+      valueGetter: "(:sourceAlias.jsonb->>'poLineId')",
+      filterValueGetter: "lower(:sourceAlias.jsonb->>'poLineId')",
+      valueFunction: 'lower(:value)',
+      joinsTo: [
+        {
+          targetId: '58148257-bfb0-4687-8c42-d2833d772f3e', // purchase_order_line
+          targetField: 'id',
+          type: 'equality-cast-uuid',
+          direction: 'left',
+        },
+      ],
+    },
+    {
+      name: 'fund_id',
+      dataType: {
+        dataType: 'rangedUUIDType',
+      },
+      isIdColumn: true,
+      valueGetter: "(:fund_distribution.jsonb->>'fundId')",
+      filterValueGetter: "lower(:fund_distribution.jsonb->>'fundId')",
+      valueFunction: 'lower(:value)',
+      joinsTo: [
+        {
+          targetId: '4c63c7f5-1a28-4394-b401-98c02c31492d', // simple_fund
+          targetField: 'id',
+          type: 'equality-cast-uuid',
+          direction: 'left',
+        },
+      ],
+    },
+    {
+      name: 'expense_class_id',
+      dataType: {
+        dataType: 'rangedUUIDType',
+      },
+      valueGetter: "(:fund_distribution.jsonb->>'expenseClassId')",
+      filterValueGetter: "lower(:fund_distribution.jsonb->>'expenseClassId')",
+      valueFunction: 'lower(:value)',
+      joinsTo: [
+        {
+          targetId: '6e11cd39-a34e-4afc-90db-957dd45d139e', // simple_expense_class
+          targetField: 'id',
+          type: 'equality-cast-uuid',
+          direction: 'left',
+        },
+      ],
+    },
+  ],
+}
diff --git a/src/main/resources/entity-types/invoice/simple_invoice.json5 b/src/main/resources/entity-types/invoice/simple_invoice.json5
index 24d1373c7..5ab0d25b3 100644
--- a/src/main/resources/entity-types/invoice/simple_invoice.json5
+++ b/src/main/resources/entity-types/invoice/simple_invoice.json5
@@ -469,6 +469,14 @@
       essential: true,
       hidden: true,
       valueGetter: ":invoice.jsonb->>'vendorId'",
+      joinsTo: [
+        {
+          targetId: 'b5ffa2e9-8080-471a-8003-a8c5a1274503', // simple_organization
+          targetField: 'id',
+          type: 'equality-cast-uuid',
+          direction: 'left',
+        },
+      ]
     },
     {
       name: 'vendor_name',
@@ -511,6 +519,14 @@
       essential: true,
       hidden: true,
       valueGetter: ":invoice.jsonb->>'fiscalYearId'",
+      joinsTo: [
+        {
+          targetId: 'e61f988a-1d47-4b16-b8be-220a1c38ca8a', // simple_fiscal_year
+          targetField: 'id',
+          type: 'equality-cast-uuid',
+          direction: 'left',
+        },
+      ]
     },
     {
       name: 'fiscal_year',
diff --git a/src/main/resources/entity-types/orders/composite_order_invoice_analytics.json5 b/src/main/resources/entity-types/orders/composite_order_invoice_analytics.json5
new file mode 100644
index 000000000..63e003960
--- /dev/null
+++ b/src/main/resources/entity-types/orders/composite_order_invoice_analytics.json5
@@ -0,0 +1,94 @@
+{
+  id: 'f3ccbf49-8e3e-4f5c-a60e-04ad80543a4a',
+  name: 'composite_order_invoice_analytics',
+  description: "Compound entity type that allows concurrent querying of PO/PO Line, Invoice/Invoice Line, Organization, \
+                Fund, Expense class, Fiscal Year to generate certain types of acquisitions-related reports",
+  private: false,
+  sources: [
+    {
+      alias: 'po',
+      type: 'entity-type',
+      targetId: '48de4796-11a2-44e9-abb7-01e84af87234', // simple_purchase_order
+      useIdColumns: true,
+      essentialOnly: true,
+      order: 10,
+    },
+    {
+      alias: 'pol',
+      type: 'entity-type',
+      targetId: '58148257-bfb0-4687-8c42-d2833d772f3e', // simple_purchase_order_line
+      targetField: 'purchase_order_id',
+      sourceField: 'po.id',
+      overrideJoinDirection: "left",
+      useIdColumns: true,
+      essentialOnly: true,
+      order: 20,
+    },
+    {
+      alias: 'invoice_line_fund_distribution',
+      type: 'entity-type',
+      targetId: '4999bd5f-3898-4b44-b283-e37bc9261ad8', // composite_invoice_line_fund_distribution
+      targetField: 'po_line_id',
+      sourceField: 'pol.id',
+      overrideJoinDirection: "left",
+      essentialOnly: true,
+      order: 30,
+    },
+    {
+      alias: 'invoice',
+      type: 'entity-type',
+      targetId: '4d626ce1-1880-48d2-9d4c-81667fdc5dbb', // simple_invoice
+      targetField: 'id',
+      sourceField: 'invoice_line_fund_distribution.invoice_id',
+      essentialOnly: true,
+      order: 40,
+    },
+    {
+      alias: 'organization',
+      type: 'entity-type',
+      targetId: 'b5ffa2e9-8080-471a-8003-a8c5a1274503', // simple_organization
+      targetField: 'id',
+      sourceField: 'invoice.vendor_id',
+      essentialOnly: true,
+      order: 50
+    },
+    {
+      alias: 'fiscal_year',
+      type: 'entity-type',
+      targetId: 'e61f988a-1d47-4b16-b8be-220a1c38ca8a', // simple_fiscal_year
+      targetField: 'id',
+      sourceField: 'invoice.fiscal_year_id',
+      essentialOnly: true,
+      order: 60
+    },
+    {
+      alias: 'fund',
+      type: 'entity-type',
+      targetId: '4c63c7f5-1a28-4394-b401-98c02c31492d', // simple_fund
+      useIdColumns: true,
+      essentialOnly: true,
+      targetField: 'id',
+      sourceField: 'invoice_line_fund_distribution.fund_id',
+      order: 70,
+    },
+    {
+      alias: 'fund_type',
+      type: 'entity-type',
+      targetId: 'b7e43005-3e3a-4633-82b9-a96fcd7d8c47', // simple_fund_type
+      essentialOnly: true,
+      targetField: 'id',
+      sourceField: 'fund.fund_type_id',
+      order: 80
+    },
+    {
+      alias: 'expense_class',
+      type: 'entity-type',
+      targetId: '6e11cd39-a34e-4afc-90db-957dd45d139e', // simple_expense_class
+      useIdColumns: true,
+      essentialOnly: true,
+      targetField: 'id',
+      sourceField: 'invoice_line_fund_distribution.expense_class_id',
+      order: 90
+    },
+  ]
+}
diff --git a/translations/mod-fqm-manager/en.json b/translations/mod-fqm-manager/en.json
index 3603a34d1..c62df8d6f 100644
--- a/translations/mod-fqm-manager/en.json
+++ b/translations/mod-fqm-manager/en.json
@@ -1638,5 +1638,16 @@
   "entityType.composite_transaction.source_fiscal_year": "Source fiscal year",
   "entityType.composite_transaction.expense_class": "Expense class",
   "entityType.composite_transaction.source_invoice": "Source invoice",
-  "entityType.composite_transaction.source_invoice_line": "Source invoice line"
+  "entityType.composite_transaction.source_invoice_line": "Source invoice line",
+  "entityType.composite_order_invoice_analytics": "Order — Invoice Analysis",
+  "entityType.composite_order_invoice_analytics.po": "PO",
+  "entityType.composite_order_invoice_analytics.pol": "PO line",
+  "entityType.composite_order_invoice_analytics.invoice_line_fund_distribution": "Invoice",
+  "entityType.composite_invoice_line_fund_distribution.invoice_line": "Line",
+  "entityType.composite_order_invoice_analytics.invoice": "Invoice",
+  "entityType.composite_order_invoice_analytics.organization": "Organization",
+  "entityType.composite_order_invoice_analytics.fiscal_year": "Fiscal year",
+  "entityType.composite_order_invoice_analytics.fund": "Fund",
+  "entityType.composite_order_invoice_analytics.fund_type": "Fund type",
+  "entityType.composite_order_invoice_analytics.expense_class": "Expense class"
 }


Reviews:
user: login: SerhiiNosko
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T11:02:15Z
user: login: SerhiiNosko
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T11:02:52Z
user: login: Saba-Zedginidze-EPAM
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T11:22:52Z
user: login: SerhiiNosko
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T15:06:57Z
user: login: BKadirkhodjaev
state: APPROVED
author_association: NONE
submitted_at: 2025-05-15T03:54:57Z
user: login: mweaver-ebsco
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-15T12:38:29Z
user: login: Saba-Zedginidze-EPAM
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T13:28:39Z
user: login: mweaver-ebsco
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-15T14:46:03Z
user: login: ncovercash
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T15:00:03Z
user: login: mweaver-ebsco
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-15T15:09:52Z
user: login: Saba-Zedginidze-EPAM
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T16:59:26Z
user: login: Saba-Zedginidze-EPAM
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T09:54:37Z
user: login: Saba-Zedginidze-EPAM
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T09:55:19Z
user: login: mweaver-ebsco
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-16T14:51:35Z

Review comments:
diff_hunk: @@ -0,0 +1,22 @@
+DROP VIEW IF EXISTS drv_fund_data;
+
+CREATE VIEW drv_fund_data AS
+SELECT
+  F.jsonb->>'id' as "fund_id",
+	F.jsonb->>'code' as "fund_code",
+	L.jsonb->>'id' as "ledger_id",
+	L.jsonb->>'code' as "ledger_code",
+	FY.jsonb->>'id' as "fiscal_year_id",
+	FY.jsonb->>'code' as "fiscal_year_code",
+	jsonb_agg(B.jsonb->'name') as "budget_names",
+FROM  ${tenant_id}_mod_fqm_manager.src_finance_fund F
+JOIN  ${tenant_id}_mod_fqm_manager.src_finance_budget B ON B.jsonb->>'fundId' = F.id::text
path: src/main/resources/db/changelog/bootstrap/sql/create-view-drv-fund-data.sql
user: login: SerhiiNosko
body: B.fundId would be more performant because it is foreign key with index
created_at: 2025-05-13T11:02:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,22 @@
+DROP VIEW IF EXISTS drv_fund_data;
+
+CREATE VIEW drv_fund_data AS
+SELECT
+  F.jsonb->>'id' as "fund_id",
+	F.jsonb->>'code' as "fund_code",
+	L.jsonb->>'id' as "ledger_id",
+	L.jsonb->>'code' as "ledger_code",
+	FY.jsonb->>'id' as "fiscal_year_id",
+	FY.jsonb->>'code' as "fiscal_year_code",
+	jsonb_agg(B.jsonb->'name') as "budget_names",
+FROM  ${tenant_id}_mod_fqm_manager.src_finance_fund F
+JOIN  ${tenant_id}_mod_fqm_manager.src_finance_budget B ON B.jsonb->>'fundId' = F.id::text
+JOIN  ${tenant_id}_mod_fqm_manager.src_finance_ledger L ON L.id = F.ledgerId
+JOIN  ${tenant_id}_mod_fqm_manager.src_finance_fiscal_year FY ON B.jsonb->>'fiscalYearId' = FY.id::text
path: src/main/resources/db/changelog/bootstrap/sql/create-view-drv-fund-data.sql
user: login: SerhiiNosko
body: B.fiscalYearId should be used because it has index
created_at: 2025-05-13T11:02:52Z
author_association: CONTRIBUTOR
path: src/main/resources/db/changelog/migrations/s-migrations/changelog-s-migrations.xml
user: login: Saba-Zedginidze-EPAM
body: Needed to add alias for the jsonb_array_elements to the schema as it is getting prefixed by the schema name during query generation
created_at: 2025-05-14T11:22:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,98 @@
+// Auxiliary entity created for Order Invoice Analysis composite entity
+{
+  id: '4999bd5f-3898-4b44-b283-e37bc9261ad8',
+  name: 'composite_invoice_line_fund_distribution',
+  private: true,
+  sources: [
+    {
+      type: 'entity-type',
+      alias: 'invoice_line',
+      targetId: '1373f063-8b86-48cc-832d-68b4d0dd44fc', // simple_invoice_line
+      useIdColumns: true,
+    },
+    {
+      type: 'db',
+      alias: 'fund_distribution',
+      target: 'jsonb_array_elements("invoice_line_fund_distribution.invoice_line.invoice_lines".jsonb -> \'fundDistributions\')',
path: src/main/resources/entity-types/invoice/composite_invoice_line_fund_distribution.json5
user: login: mweaver-ebsco
body: Is this target right? I'd expect "invoice_line_fund_distribution" to show up somewhere in this composite in order for the "invoice_line_fund_distribution.invoice_line.invoice_lines" reference to work
created_at: 2025-05-15T12:38:29Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,98 @@
+// Auxiliary entity created for Order Invoice Analysis composite entity
+{
+  id: '4999bd5f-3898-4b44-b283-e37bc9261ad8',
+  name: 'composite_invoice_line_fund_distribution',
+  private: true,
+  sources: [
+    {
+      type: 'entity-type',
+      alias: 'invoice_line',
+      targetId: '1373f063-8b86-48cc-832d-68b4d0dd44fc', // simple_invoice_line
+      useIdColumns: true,
+    },
+    {
+      type: 'db',
+      alias: 'fund_distribution',
+      target: 'jsonb_array_elements("invoice_line_fund_distribution.invoice_line.invoice_lines".jsonb -> \'fundDistributions\')',
path: src/main/resources/entity-types/invoice/composite_invoice_line_fund_distribution.json5
user: login: Saba-Zedginidze-EPAM
body: This auxiliary entity (`composite_invoice_line_fund_distribution`) was created to normalize the fundDistributions array within invoice_line by transforming each array element into a separate row using jsonb_array_elements.

Attempting to perform this normalization directly in the main composite entity (`composite_order_invoice_analytics`) is not viable due to several limitations:
- Alias resolution: You cannot refer to invoice_line.invoice_lines in the target expression unless invoice_line is a source in the main entity. However, adding it as a source introduces redundancy.
- Improper table context: Even if normalization was performed in the main entity, the result would not be treated as a proper table-like source. This means you wouldn't be able to use fields from within the normalized array (e.g., fundId, expenseClassId) in sourceField or join definitions, because they wouldn't resolve properly.
- Column extraction restrictions: Additionally, composite entities do not support referencing their own columns within the same scope. So even if normalization succeeded, extracting and joining on the desired fields from the array would still not be possible within the same entity.

To solve this, normalization is encapsulated in an auxiliary entity. It includes invoice_line as a source and uses a` CROSS JOIN LATERAL` with `jsonb_array_elements` to turn each fund distribution entry into a separate row. This allows proper column extraction and joining with related entities like fund and expense_class.
While this makes the auxiliary entity dependent on the context of the main one (as seen in the target path), it avoids duplicate sources in the main entity and enables a clean and functional setup. The auxiliary entity isn’t independently reusable, but it serves as a critical piece in achieving the necessary normalization and joins.


created_at: 2025-05-15T13:28:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,98 @@
+// Auxiliary entity created for Order Invoice Analysis composite entity
+{
+  id: '4999bd5f-3898-4b44-b283-e37bc9261ad8',
+  name: 'composite_invoice_line_fund_distribution',
+  private: true,
+  sources: [
+    {
+      type: 'entity-type',
+      alias: 'invoice_line',
+      targetId: '1373f063-8b86-48cc-832d-68b4d0dd44fc', // simple_invoice_line
+      useIdColumns: true,
+    },
+    {
+      type: 'db',
+      alias: 'fund_distribution',
+      target: 'jsonb_array_elements("invoice_line_fund_distribution.invoice_line.invoice_lines".jsonb -> \'fundDistributions\')',
path: src/main/resources/entity-types/invoice/composite_invoice_line_fund_distribution.json5
user: login: mweaver-ebsco
body: Fair enough. If it doesn't make things too crazy, I'd work hard to combine these ETs, or at least do them in a way that makes the auxiliary one functional on its own, but if that's a bug burden, it's not a big deal. In that case, I'd add a bigger comment at the top of the auxiliary ET to explain that it isn't not independently reusable, since I think it might be the only ET like that.

Also worth noting: Ultimately, FQM flattens composite entity types into a single simple entity type before it does anything with it, so it actually does end up in the same ET in the end, in some form. So it should definitely be possible to include it in the main composite. Put another way: any composite entity type can be rewritten as a simple entity type (in fact, before we introduced composites, all entity types were like that), so if something is split between multiple entity types and are combined using a composite, then they can be combined into a single entity type. For example, redundant sources are okay, as long as you give them distinct aliases.

> composite entities do not support referencing their own columns within the same scope

What do you mean? Composites can definitely have their own columns, and those columns can references DB sources within the composite and sources from child ETs.
created_at: 2025-05-15T14:46:03Z
author_association: COLLABORATOR
diff_hunk: @@ -1638,5 +1638,16 @@
   "entityType.composite_transaction.source_fiscal_year": "Source fiscal year",
   "entityType.composite_transaction.expense_class": "Expense class",
   "entityType.composite_transaction.source_invoice": "Source invoice",
-  "entityType.composite_transaction.source_invoice_line": "Source invoice line"
+  "entityType.composite_transaction.source_invoice_line": "Source invoice line",
+  "entityType.composite_order_invoice_analytics": "Order - Invoice Analysis",
+  "entityType.composite_order_invoice_analytics.po": "PO",
+  "entityType.composite_order_invoice_analytics.pol": "PO Line",
+  "entityType.composite_order_invoice_analytics.invoice_line_fund_distribution": "Invoice",
+  "entityType.composite_invoice_line_fund_distribution.invoice_line": "Line",
+  "entityType.composite_order_invoice_analytics.invoice": "Invoice",
+  "entityType.composite_order_invoice_analytics.organization": "Organization",
+  "entityType.composite_order_invoice_analytics.fiscal_year": "Fiscal Year",
+  "entityType.composite_order_invoice_analytics.fund": "Fund",
+  "entityType.composite_order_invoice_analytics.fund_type": "Fund Type",
+  "entityType.composite_order_invoice_analytics.expense_class": "Expense Class"
path: translations/mod-fqm-manager/en.json
user: login: ncovercash
body: Please use `Sentence case` for labels and EM dashes `—` for separators

```suggestion
  "entityType.composite_order_invoice_analytics": "Order — Invoice analysis",
  "entityType.composite_order_invoice_analytics.po": "PO",
  "entityType.composite_order_invoice_analytics.pol": "PO line",
  "entityType.composite_order_invoice_analytics.invoice_line_fund_distribution": "Invoice",
  "entityType.composite_invoice_line_fund_distribution.invoice_line": "Line",
  "entityType.composite_order_invoice_analytics.invoice": "Invoice",
  "entityType.composite_order_invoice_analytics.organization": "Organization",
  "entityType.composite_order_invoice_analytics.fiscal_year": "Fiscal year",
  "entityType.composite_order_invoice_analytics.fund": "Fund",
  "entityType.composite_order_invoice_analytics.fund_type": "Fund type",
  "entityType.composite_order_invoice_analytics.expense_class": "Expense class"
```
created_at: 2025-05-15T15:00:03Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,92 @@
+{
+  id: 'f3ccbf49-8e3e-4f5c-a60e-04ad80543a4a',
+  name: 'composite_order_invoice_analytics',
path: src/main/resources/entity-types/orders/composite_order_invoice_analytics.json5
user: login: mweaver-ebsco
body: Could you add a `description` property to these new entity types? It's a brand-new property that doesn't do anything yet, so there aren't any examples of it out there yet. I would have been surprised if it were already here 😛. It will eventually be user-facing. With the growing number of entity types, it's often hard to see what each one is and what it provides, so we want to start documenting that to make things easier for users.
created_at: 2025-05-15T15:09:52Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,98 @@
+// Auxiliary entity created for Order Invoice Analysis composite entity
+{
+  id: '4999bd5f-3898-4b44-b283-e37bc9261ad8',
+  name: 'composite_invoice_line_fund_distribution',
+  private: true,
+  sources: [
+    {
+      type: 'entity-type',
+      alias: 'invoice_line',
+      targetId: '1373f063-8b86-48cc-832d-68b4d0dd44fc', // simple_invoice_line
+      useIdColumns: true,
+    },
+    {
+      type: 'db',
+      alias: 'fund_distribution',
+      target: 'jsonb_array_elements("invoice_line_fund_distribution.invoice_line.invoice_lines".jsonb -> \'fundDistributions\')',
path: src/main/resources/entity-types/invoice/composite_invoice_line_fund_distribution.json5
user: login: Saba-Zedginidze-EPAM
body: I have been experimenting for quite some time to be able to use columns of the same entity in other sources' `sourceField` values, however due to [this code](https://github.com/folio-org/mod-fqm-manager/blob/cfea4c5235bdc2d0a3d5dcaa6c7e2601335f3ab2/src/main/java/org/folio/fqm/utils/flattening/SourceUtils.java#L229) every column that is used as a `sourceField` will have some prefix applied to it.
If we directly `sourceField: "fund_id"` then the actual source field will be replresented as `.fund_id`, for which we will receive an error:
`Failure reason: Column .fund_id could not be found`.
Otherwise, we can hardcode column's name to be `composite_order_invoice_analytics.fund_id`, which would resolve the previous issue, but will result in this: 
`"Cannot invoke \"org.folio.querytool.domain.dto.EntityTypeSource.getAlias()\" because \"source\" is null"`
After further investigation, I found that [here](https://github.com/folio-org/mod-fqm-manager/blob/cfea4c5235bdc2d0a3d5dcaa6c7e2601335f3ab2/src/main/java/org/folio/fqm/utils/flattening/FromClauseUtils.java#L291), during sources ordering, the passed source is null, which I tried to fix with an additional null check, but this would later mess up the sources ordering process and result into an incorrectly generated query.

Frankly, I could not yet figure out correct way to use columns in this scenario without modifying Java code, but introcuding an additional source (auxiliary entity) with its own columns resolved it. If you have some suggestion or idea to better accomodate this problem, would gladly appreciate.

I will add detailed comments to the auxiliary entity as well.
created_at: 2025-05-15T16:59:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1638,5 +1638,16 @@
   "entityType.composite_transaction.source_fiscal_year": "Source fiscal year",
   "entityType.composite_transaction.expense_class": "Expense class",
   "entityType.composite_transaction.source_invoice": "Source invoice",
-  "entityType.composite_transaction.source_invoice_line": "Source invoice line"
+  "entityType.composite_transaction.source_invoice_line": "Source invoice line",
+  "entityType.composite_order_invoice_analytics": "Order - Invoice Analysis",
+  "entityType.composite_order_invoice_analytics.po": "PO",
+  "entityType.composite_order_invoice_analytics.pol": "PO Line",
+  "entityType.composite_order_invoice_analytics.invoice_line_fund_distribution": "Invoice",
+  "entityType.composite_invoice_line_fund_distribution.invoice_line": "Line",
+  "entityType.composite_order_invoice_analytics.invoice": "Invoice",
+  "entityType.composite_order_invoice_analytics.organization": "Organization",
+  "entityType.composite_order_invoice_analytics.fiscal_year": "Fiscal Year",
+  "entityType.composite_order_invoice_analytics.fund": "Fund",
+  "entityType.composite_order_invoice_analytics.fund_type": "Fund Type",
+  "entityType.composite_order_invoice_analytics.expense_class": "Expense Class"
path: translations/mod-fqm-manager/en.json
user: login: Saba-Zedginidze-EPAM
body: Thanks, fixed the names
created_at: 2025-05-16T09:54:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,92 @@
+{
+  id: 'f3ccbf49-8e3e-4f5c-a60e-04ad80543a4a',
+  name: 'composite_order_invoice_analytics',
path: src/main/resources/entity-types/orders/composite_order_invoice_analytics.json5
user: login: Saba-Zedginidze-EPAM
body: Added description field with the purpose of this entity as value
created_at: 2025-05-16T09:55:18Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-05-16T20:10:03Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=org.folio%3Amod-fqm-manager&pullRequest=701) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=org.folio%3Amod-fqm-manager&pullRequest=701&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=org.folio%3Amod-fqm-manager&pullRequest=701&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=org.folio%3Amod-fqm-manager&pullRequest=701&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=org.folio%3Amod-fqm-manager&pullRequest=701&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=org.folio%3Amod-fqm-manager&pullRequest=701&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=org.folio%3Amod-fqm-manager&pullRequest=701)


author_association: NONE