Repository: softwareone-platform/ffc-finops-operations
Pull request #183: Fix the datasource expense's datasource_id to correctly identify entitlements (MPT-9810)
Author: login: arturbalabanov
Created: 2025-05-08T10:48:29Z | Merged: 2025-05-12T15:17:09Z



Diff:
diff --git a/app/commands/generate_monthly_charges.py b/app/commands/generate_monthly_charges.py
index 0cd1d0f9..2b542c77 100644
--- a/app/commands/generate_monthly_charges.py
+++ b/app/commands/generate_monthly_charges.py
@@ -27,7 +27,7 @@
     OrganizationHandler,
 )
 from app.db.models import Account, ChargesFile, DatasourceExpense, Organization
-from app.enums import AccountType, ChargesFileStatus, EntitlementStatus
+from app.enums import AccountType, ChargesFileStatus
 
 logger = logging.getLogger(__name__)
 
@@ -143,10 +143,7 @@ def add_datasource_expense(self, datasource_expense: DatasourceExpense) -> None:
 
         if self.account.type == AccountType.AFFILIATE:
             for entitlement in datasource_expense.entitlements:
-                if (
-                    entitlement.owner != self.account
-                    or entitlement.status != EntitlementStatus.ACTIVE
-                ):
+                if entitlement.owner != self.account:
                     continue
 
                 self.append_row(entry)
@@ -161,10 +158,7 @@ def add_datasource_expense(self, datasource_expense: DatasourceExpense) -> None:
         if self.account.type == AccountType.OPERATIONS:
             self.append_row(entry)
 
-            for entitlement in datasource_expense.entitlements:
-                if entitlement.status != EntitlementStatus.ACTIVE:
-                    continue
-
+            for _entitlement in datasource_expense.entitlements:
                 self.append_row(entry.contra_entry())
 
                 # If multiple entitlements match the same datasource expense, we need to
diff --git a/app/commands/update_current_month_datasource_expenses.py b/app/commands/update_current_month_datasource_expenses.py
index 55d4b26c..f0e8b530 100644
--- a/app/commands/update_current_month_datasource_expenses.py
+++ b/app/commands/update_current_month_datasource_expenses.py
@@ -12,6 +12,7 @@
 from app.db.base import session_factory
 from app.db.handlers import DatasourceExpenseHandler, OrganizationHandler
 from app.db.models import DatasourceExpense, Organization
+from app.enums import DatasourceType
 from app.notifications import send_exception, send_info
 
 logger = logging.getLogger(__name__)
@@ -96,19 +97,31 @@ async def store_datasource_expenses(
         ds_count += len(datasources)
         for datasource in datasources:
             existing_datasource_expense, created = await datasource_expense_handler.get_or_create(
-                datasource_id=datasource["id"],
+                datasource_id=datasource["account_id"],
                 organization_id=organization_id,
                 year=year,
                 month=month,
                 defaults={
                     "month_expenses": datasource["details"]["cost"],
                     "datasource_name": datasource["name"],
+                    "linked_datasource_id": datasource["id"],
+                    "linked_datasource_type": datasource["type"],
                 },
+                extra_conditions=[
+                    DatasourceExpense.linked_datasource_type.in_(
+                        [DatasourceType.UNKNOWN, datasource["type"]]
+                    ),
+                ],
             )
             if not created:
                 await datasource_expense_handler.update(
                     existing_datasource_expense,
-                    {"month_expenses": datasource["details"]["cost"]},
+                    {
+                        "month_expenses": datasource["details"]["cost"],
+                        "datasource_name": datasource["name"],
+                        "linked_datasource_id": datasource["id"],
+                        "linked_datasource_type": datasource["type"],
+                    },
                 )
     await send_info(
         "Datasource Expenses Update Success",
diff --git a/app/db/models.py b/app/db/models.py
index 81dfaf9d..dfdcb3f9 100644
--- a/app/db/models.py
+++ b/app/db/models.py
@@ -293,6 +293,10 @@ class DatasourceExpense(Base, HumanReadablePKMixin, TimestampMixin):
     PK_NUM_LENGTH = 12
 
     datasource_id: Mapped[str] = mapped_column(String(255), index=True)
+    linked_datasource_id: Mapped[str] = mapped_column(String(255))
+    linked_datasource_type: Mapped[DatasourceType] = mapped_column(
+        Enum(DatasourceType, values_callable=lambda obj: [e.value for e in obj]),
+    )
     datasource_name: Mapped[str] = mapped_column(String(255), nullable=False)
     organization_id: Mapped[str] = mapped_column(ForeignKey("organizations.id"))
 
@@ -306,8 +310,15 @@ class DatasourceExpense(Base, HumanReadablePKMixin, TimestampMixin):
 
     entitlements: Mapped[list[Entitlement]] = relationship(
         "Entitlement",
-        primaryjoin=lambda: DatasourceExpense.datasource_id == Entitlement.datasource_id,
-        foreign_keys=lambda: [DatasourceExpense.datasource_id],
+        primaryjoin=lambda: (
+            (DatasourceExpense.datasource_id == Entitlement.datasource_id)
+            & (DatasourceExpense.linked_datasource_type == Entitlement.linked_datasource_type)
+            & (Entitlement.status == EntitlementStatus.ACTIVE)
+        ),
+        foreign_keys=lambda: [
+            DatasourceExpense.datasource_id,
+            DatasourceExpense.linked_datasource_type,
+        ],
         viewonly=True,
         uselist=True,
         lazy="selectin",
@@ -317,6 +328,7 @@ class DatasourceExpense(Base, HumanReadablePKMixin, TimestampMixin):
         Index("ix_datasource_expenses_year_and_month", year, month),
         UniqueConstraint(
             datasource_id,
+            linked_datasource_type,
             organization_id,
             year,
             month,
diff --git a/migrations/versions/46d8a57dabef_create_datasourceexpense_linked_.py b/migrations/versions/46d8a57dabef_create_datasourceexpense_linked_.py
new file mode 100644
index 00000000..40cba4de
--- /dev/null
+++ b/migrations/versions/46d8a57dabef_create_datasourceexpense_linked_.py
@@ -0,0 +1,53 @@
+"""Create DatasourceExpense.linked_datasource_id and linked_datasource_type
+
+Revision ID: 46d8a57dabef
+Revises: 7cd884694384
+Create Date: 2025-05-12 13:54:33.330961
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+import sqlalchemy_utils
+
+
+# revision identifiers, used by Alembic.
+revision: str = '0fcca0cf1151'
+down_revision: Union[str, None] = '7cd884694384'
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+    # Drop the existing unique constraint as it doesn't include the datasource type
+    op.drop_constraint('uq_datasource_expenses_per_month', 'datasource_expenses', type_='unique')
+    
+    # Add the new columns with nullable=True as there are existing rows
+    op.add_column('datasource_expenses', sa.Column('linked_datasource_id', sa.String(length=255), nullable=True))
+    op.add_column('datasource_expenses', sa.Column('linked_datasource_type', sa.Enum('aws_cnr', 'azure_cnr', 'azure_tenant', 'gcp_cnr', 'unknown', name='datasourcetype'), nullable=True))
+    
+    # Set the default values for existing rows to an empty string or 'unknown' for datasource_type
+    op.execute(sa.text("UPDATE datasource_expenses SET linked_datasource_id = ''"))
+    op.execute(sa.text("UPDATE datasource_expenses SET linked_datasource_type = 'unknown'"))
+
+    # Remove the nullable constraints now that all existing rows have a value
+    op.alter_column('datasource_expenses', 'linked_datasource_id', existing_type=sa.String(length=255), nullable=False)
+    op.alter_column('datasource_expenses', 'linked_datasource_type', existing_type=sa.Enum('aws_cnr', 'azure_cnr', 'azure_tenant', 'gcp_cnr', 'unknown', name='datasourcetype'), nullable=False)
+    
+    # Re-add the constraint (now also including the linked datasource type) once all the new columns
+    # populated and non-nullab le
+    op.create_unique_constraint('uq_datasource_expenses_per_month', 'datasource_expenses', ['datasource_id', 'linked_datasource_type', 'organization_id', 'year', 'month'])
+    
+
+
+def downgrade() -> None:
+    # Drop the constraint as it has a reference to the linked datasource type which is being removed
+    op.drop_constraint('uq_datasource_expenses_per_month', 'datasource_expenses', type_='unique')
+    
+    # Drop the new columns
+    op.drop_column('datasource_expenses', 'linked_datasource_type')
+    op.drop_column('datasource_expenses', 'linked_datasource_id')
+    
+    # Re-add the original constraint (without the linked datasource type)
+    op.create_unique_constraint('uq_datasource_expenses_per_month', 'datasource_expenses', ['datasource_id', 'organization_id', 'year', 'month'])
diff --git a/tests/commands/snapshots/test_generate_monthtly_charges/test_export_to_zip/charges.csv b/tests/commands/snapshots/test_generate_monthtly_charges/test_export_to_zip/charges.csv
index 81dfbc4f..4cb8c8de 100644
--- a/tests/commands/snapshots/test_generate_monthtly_charges/test_export_to_zip/charges.csv
+++ b/tests/commands/snapshots/test_generate_monthtly_charges/test_export_to_zip/charges.csv
@@ -1,5 +1,5 @@
 Entry ID,Subscription Search Criteria,Subscription Search Value,Item Search Criteria,Item Search Value,Usage Start Time,Usage End Time,Quantity,Purchase Price,Total Purchase Price,External Reference,Vendor Description 1,Vendor Description 2,Vendor Reference
-1,subscription.externalIds.vendor,FORG-7330-1645-9608,item.externalIds.vendor,FIN-0001-P1M,1-Mar-2025,31-Mar-2025,1,0.56,0.56,06cb0fb3-9a1d-4644-815e-f6d13b8faa18,AWS Account,a65ed389-b74d-4fb1-b2e7-06298fadc1a6,
-2,subscription.externalIds.vendor,FORG-7330-1645-9608,item.externalIds.vendor,FIN-0001-P1M,1-Mar-2025,31-Mar-2025,1,-0.56,-0.56,06cb0fb3-9a1d-4644-815e-f6d13b8faa18,AWS Account,a65ed389-b74d-4fb1-b2e7-06298fadc1a6,
-3,subscription.externalIds.vendor,FORG-7330-1645-9608,item.externalIds.vendor,FIN-0001-P1M,1-Mar-2025,31-Mar-2025,1,0.65,0.65,06cb0fb3-9a1d-4644-815e-f6d13b8faa18,GCP Account,386ecbe0-6b65-46a4-8b81-48f6b38a088c,
-4,subscription.externalIds.vendor,FORG-7330-1645-9608,item.externalIds.vendor,FIN-0001-P1M,1-Mar-2025,31-Mar-2025,1,-0.65,-0.65,06cb0fb3-9a1d-4644-815e-f6d13b8faa18,GCP Account,386ecbe0-6b65-46a4-8b81-48f6b38a088c,
+1,subscription.externalIds.vendor,FORG-7330-1645-9608,item.externalIds.vendor,FIN-0001-P1M,1-Mar-2025,31-Mar-2025,1,0.56,0.56,06cb0fb3-9a1d-4644-815e-f6d13b8faa18,AWS Account,11111111111,
+2,subscription.externalIds.vendor,FORG-7330-1645-9608,item.externalIds.vendor,FIN-0001-P1M,1-Mar-2025,31-Mar-2025,1,-0.56,-0.56,06cb0fb3-9a1d-4644-815e-f6d13b8faa18,AWS Account,11111111111,
+3,subscription.externalIds.vendor,FORG-7330-1645-9608,item.externalIds.vendor,FIN-0001-P1M,1-Mar-2025,31-Mar-2025,1,0.65,0.65,06cb0fb3-9a1d-4644-815e-f6d13b8faa18,GCP Account,22222222222,
+4,subscription.externalIds.vendor,FORG-7330-1645-9608,item.externalIds.vendor,FIN-0001-P1M,1-Mar-2025,31-Mar-2025,1,-0.65,-0.65,06cb0fb3-9a1d-4644-815e-f6d13b8faa18,GCP Account,22222222222,
diff --git a/tests/commands/snapshots/test_generate_monthtly_charges/test_generate_charges_file_dataframe_affiliate_multiple_organizations_and_currencies/EUR/charges.csv b/tests/commands/snapshots/test_generate_monthtly_charges/test_generate_charges_file_dataframe_affiliate_multiple_organizations_and_currencies/EUR/charges.csv
index 4f90bad9..947591ed 100644
--- a/tests/commands/snapshots/test_generate_monthtly_charges/test_generate_charges_file_dataframe_affiliate_multiple_organizations_and_currencies/EUR/charges.csv
+++ b/tests/commands/snapshots/test_generate_monthtly_charges/test_generate_charges_file_dataframe_affiliate_multiple_organizations_and_currencies/EUR/charges.csv
@@ -1,2 +1,2 @@
 Entry ID,Subscription Search Criteria,Subscription Search Value,Item Search Criteria,Item Search Value,Usage Start Time,Usage End Time,Quantity,Purchase Price,Total Purchase Price,External Reference,Vendor Description 1,Vendor Description 2,Vendor Reference
-1,subscription.externalIds.vendor,FORG-8987-7789-2913,item.externalIds.vendor,FIN-0001-P1M,1-Mar-2025,31-Mar-2025,1,0.65,0.65,f50bea63-371e-4d7b-a7cd-813047229389,GCP Account,d8f56413-5be6-428e-98c2-67976142ea7d,
+1,subscription.externalIds.vendor,FORG-8987-7789-2913,item.externalIds.vendor,FIN-0001-P1M,1-Mar-2025,31-Mar-2025,1,0.65,0.65,f50bea63-371e-4d7b-a7cd-813047229389,GCP Account,22222222222,
diff --git a/tests/commands/snapshots/test_generate_monthtly_charges/test_generate_charges_file_dataframe_affiliate_multiple_organizations_and_currencies/GBP/charges.csv b/tests/commands/snapshots/test_generate_monthtly_charges/test_generate_charges_file_dataframe_affiliate_multiple_organizations_and_currencies/GBP/charges.csv
index 3231ce60..a3c8fa17 100644
--- a/tests/commands/snapshots/test_generate_monthtly_charges/test_generate_charges_file_dataframe_affiliate_multiple_organizations_and_currencies/GBP/charges.csv
+++ b/tests/commands/snapshots/test_generate_monthtly_charges/test_generate_charges_file_dataframe_affiliate_multiple_organizations_and_currencies/GBP/charges.csv
@@ -1,2 +1,2 @@
 Entry ID,Subscription Search Criteria,Subscription Search Value,Item Search Criteria,Item Search Value,Usage Start Time,Usage End Time,Quantity,Purchase Price,Total Purchase Price,External Reference,Vendor Description 1,Vendor Description 2,Vendor Reference
-1,subscription.externalIds.vendor,FORG-1368-8412-9667,item.externalIds.vendor,FIN-0001-P1M,1-Mar-2025,31-Mar-2025,1,0.34,0.34,1ff49b78-8946-4e85-b59c-de66bacfb3d0,Azure Account,a0ee89ae-d453-4d32-8b0d-bb418d5288f1,
+1,subscription.externalIds.vendor,FORG-1368-8412-9667,item.externalIds.vendor,FIN-0001-P1M,1-Mar-2025,31-Mar-2025,1,0.34,0.34,1ff49b78-8946-4e85-b59c-de66bacfb3d0,Azure Account,44444444444,
diff --git a/tests/commands/snapshots/test_generate_monthtly_charges/test_generate_charges_file_dataframe_affiliate_multiple_organizations_and_currencies/USD/charges.csv b/tests/commands/snapshots/test_generate_monthtly_charges/test_generate_charges_file_dataframe_affiliate_multiple_organizations_and_currencies/USD/charges.csv
index a1281ccd..7e6229ce 100644
--- a/tests/commands/snapshots/test_generate_monthtly_charges/test_generate_charges_file_dataframe_affiliate_multiple_organizations_and_currencies/USD/charges.csv
+++ b/tests/commands/snapshots/test_generate_monthtly_charges/test_generate_charges_file_dataframe_affiliate_multiple_organizations_and_currencies/USD/charges.csv
@@ -1,2 +1,2 @@
 Entry ID,Subscription Search Criteria,Subscription Search Value,Item Search Criteria,Item Search Value,Usage Start Time,Usage End Time,Quantity,Purchase Price,Total Purchase Price,External Reference,Vendor Description 1,Vendor Description 2,Vendor Reference
-1,subscription.externalIds.vendor,FORG-9068-1803-9384,item.externalIds.vendor,FIN-0001-P1M,1-Mar-2025,31-Mar-2025,1,0.60,0.60,9a1de644-815e-46d1-bb8f-aa1837f8a88b,AWS Account,b74d0fb1-32e7-4629-8fad-c1a606cb0fb3,
+1,subscription.externalIds.vendor,FORG-9068-1803-9384,item.externalIds.vendor,FIN-0001-P1M,1-Mar-2025,31-Mar-2025,1,0.60,0.60,9a1de644-815e-46d1-bb8f-aa1837f8a88b,AWS Account,99999999999,
diff --git a/tests/commands/snapshots/test_generate_monthtly_charges/test_generate_charges_file_dataframe_affiliate_same_currency/charges.csv b/tests/commands/snapshots/test_generate_monthtly_charges/test_generate_charges_file_dataframe_affiliate_same_currency/charges.csv
index a1281ccd..7e6229ce 100644
--- a/tests/commands/snapshots/test_generate_monthtly_charges/test_generate_charges_file_dataframe_affiliate_same_currency/charges.csv
+++ b/tests/commands/snapshots/test_generate_monthtly_charges/test_generate_charges_file_dataframe_affiliate_same_currency/charges.csv
@@ -1,2 +1,2 @@
 Entry ID,Subscription Search Criteria,Subscription Search Value,Item Search Criteria,Item Search Value,Usage Start Time,Usage End Time,Quantity,Purchase Price,Total Purchase Price,External Reference,Vendor Description 1,Vendor Description 2,Vendor Reference
-1,subscription.externalIds.vendor,FORG-9068-1803-9384,item.externalIds.vendor,FIN-0001-P1M,1-Mar-2025,31-Mar-2025,1,0.60,0.60,9a1de644-815e-46d1-bb8f-aa1837f8a88b,AWS Account,b74d0fb1-32e7-4629-8fad-c1a606cb0fb3,
+1,subscription.externalIds.vendor,FORG-9068-1803-9384,item.externalIds.vendor,FIN-0001-P1M,1-Mar-2025,31-Mar-2025,1,0.60,0.60,9a1de644-815e-46d1-bb8f-aa1837f8a88b,AWS Account,99999999999,
diff --git a/tests/commands/snapshots/test_generate_monthtly_charges/test_generate_charges_file_dataframe_operations_same_currency/charges.csv b/tests/commands/snapshots/test_generate_monthtly_charges/test_generate_charges_file_dataframe_operations_same_currency/charges.csv
index ab697fc6..6523d2ef 100644
--- a/tests/commands/snapshots/test_generate_monthtly_charges/test_generate_charges_file_dataframe_operations_same_currency/charges.csv
+++ b/tests/commands/snapshots/test_generate_monthtly_charges/test_generate_charges_file_dataframe_operations_same_currency/charges.csv
@@ -1,5 +1,5 @@
 Entry ID,Subscription Search Criteria,Subscription Search Value,Item Search Criteria,Item Search Value,Usage Start Time,Usage End Time,Quantity,Purchase Price,Total Purchase Price,External Reference,Vendor Description 1,Vendor Description 2,Vendor Reference
-1,subscription.externalIds.vendor,FORG-9068-1803-9384,item.externalIds.vendor,FIN-0001-P1M,1-Mar-2025,31-Mar-2025,1,0.60,0.60,9a1de644-815e-46d1-bb8f-aa1837f8a88b,AWS Account,b74d0fb1-32e7-4629-8fad-c1a606cb0fb3,
-2,subscription.externalIds.vendor,FORG-9068-1803-9384,item.externalIds.vendor,FIN-0001-P1M,1-Mar-2025,31-Mar-2025,1,-0.60,-0.60,9a1de644-815e-46d1-bb8f-aa1837f8a88b,AWS Account,b74d0fb1-32e7-4629-8fad-c1a606cb0fb3,
-3,subscription.externalIds.vendor,FORG-9068-1803-9384,item.externalIds.vendor,FIN-0001-P1M,1-Mar-2025,31-Mar-2025,1,0.70,0.70,9a1de644-815e-46d1-bb8f-aa1837f8a88b,GCP Account,6b65a6a4-8b81-48f6-b38a-088ca65ed389,
-4,subscription.externalIds.vendor,FORG-9068-1803-9384,item.externalIds.vendor,FIN-0001-P1M,1-Mar-2025,31-Mar-2025,1,0.40,0.40,9a1de644-815e-46d1-bb8f-aa1837f8a88b,Azure Account,47378190-96da-4dac-b2ff-5d2a386ecbe0,
+1,subscription.externalIds.vendor,FORG-9068-1803-9384,item.externalIds.vendor,FIN-0001-P1M,1-Mar-2025,31-Mar-2025,1,0.60,0.60,9a1de644-815e-46d1-bb8f-aa1837f8a88b,AWS Account,99999999999,
+2,subscription.externalIds.vendor,FORG-9068-1803-9384,item.externalIds.vendor,FIN-0001-P1M,1-Mar-2025,31-Mar-2025,1,-0.60,-0.60,9a1de644-815e-46d1-bb8f-aa1837f8a88b,AWS Account,99999999999,
+3,subscription.externalIds.vendor,FORG-9068-1803-9384,item.externalIds.vendor,FIN-0001-P1M,1-Mar-2025,31-Mar-2025,1,0.70,0.70,9a1de644-815e-46d1-bb8f-aa1837f8a88b,GCP Account,0,
+4,subscription.externalIds.vendor,FORG-9068-1803-9384,item.externalIds.vendor,FIN-0001-P1M,1-Mar-2025,31-Mar-2025,1,0.40,0.40,9a1de644-815e-46d1-bb8f-aa1837f8a88b,Azure Account,10101010101,
diff --git a/tests/commands/test_generate_monthtly_charges.py b/tests/commands/test_generate_monthtly_charges.py
index cf1c402d..7011277a 100644
--- a/tests/commands/test_generate_monthtly_charges.py
+++ b/tests/commands/test_generate_monthtly_charges.py
@@ -43,6 +43,7 @@
     AccountStatus,
     AccountType,
     ChargesFileStatus,
+    DatasourceType,
     EntitlementStatus,
     OrganizationStatus,
 )
@@ -86,7 +87,9 @@ async def usd_org_billed_in_eur_expenses(
         linked_organization_id=faker.uuid4(str),
     )
     aws_acc_feb_expenses = await datasource_expense_factory(  # noqa: F841
-        datasource_id=faker.uuid4(str),
+        datasource_id="11111111111",
+        linked_datasource_id=faker.uuid4(str),
+        linked_datasource_type=DatasourceType.AWS_CNR,
         datasource_name="AWS Account",
         organization=org,
         month=2,
@@ -97,6 +100,8 @@ async def usd_org_billed_in_eur_expenses(
     )
     aws_acc_mar_expenses = await datasource_expense_factory(  # noqa: F841
         datasource_id=aws_acc_feb_expenses.datasource_id,
+        linked_datasource_id=aws_acc_feb_expenses.linked_datasource_id,
+        linked_datasource_type=aws_acc_feb_expenses.linked_datasource_type,
         datasource_name=aws_acc_feb_expenses.datasource_name,
         organization=org,
         month=3,
@@ -106,7 +111,9 @@ async def usd_org_billed_in_eur_expenses(
         updated_at=datetime(2025, 3, 31, 10, 0, 0, tzinfo=UTC),
     )
     gcp_acc_mar_expenses = await datasource_expense_factory(
-        datasource_id=faker.uuid4(str),
+        datasource_id="22222222222",
+        linked_datasource_id=faker.uuid4(str),
+        linked_datasource_type=DatasourceType.GCP_CNR,
         datasource_name="GCP Account",
         organization=org,
         month=3,
@@ -120,6 +127,8 @@ async def usd_org_billed_in_eur_expenses(
         name="free GCP account",
         affiliate_external_id="ACC-11111",
         datasource_id=gcp_acc_mar_expenses.datasource_id,
+        linked_datasource_id=gcp_acc_mar_expenses.linked_datasource_id,
+        linked_datasource_type=gcp_acc_mar_expenses.linked_datasource_type,
         status=EntitlementStatus.ACTIVE,
         owner=affiliate_account,
     )
@@ -129,6 +138,10 @@ async def usd_org_billed_in_eur_expenses(
         name="free AWS account",
         affiliate_external_id="ACC-22222",
         datasource_id=aws_acc_mar_expenses.datasource_id,
+        # Intentionally set to None to verify that the relationship
+        # with the datasource expense is still happening
+        linked_datasource_id=None,
+        linked_datasource_type=DatasourceType.AWS_CNR,
         status=EntitlementStatus.ACTIVE,
         owner=another_affiliate_account,
     )
@@ -160,7 +173,9 @@ async def eur_org_billed_in_gbp_expenses(
     )
 
     aws_acc_mar_expenses = await datasource_expense_factory(  # noqa: F841
-        datasource_id=faker.uuid4(str),
+        datasource_id="33333333333",
+        linked_datasource_id=faker.uuid4(str),
+        linked_datasource_type=DatasourceType.AWS_CNR,
         datasource_name="AWS Account",
         organization=org,
         month=3,
@@ -171,7 +186,9 @@ async def eur_org_billed_in_gbp_expenses(
     )
 
     azure_acc_mar_expenses = await datasource_expense_factory(  # noqa: F841
-        datasource_id=faker.uuid4(str),
+        datasource_id="44444444444",
+        linked_datasource_id=faker.uuid4(str),
+        linked_datasource_type=DatasourceType.AZURE_CNR,
         datasource_name="Azure Account",
         organization=org,
         month=3,
@@ -185,6 +202,8 @@ async def eur_org_billed_in_gbp_expenses(
         name="Free AWS account",
         affiliate_external_id="ACC-33333",
         datasource_id=aws_acc_mar_expenses.datasource_id,
+        linked_datasource_id=aws_acc_mar_expenses.linked_datasource_id,
+        linked_datasource_type=aws_acc_mar_expenses.linked_datasource_type,
         status=EntitlementStatus.TERMINATED,  # should be ignored
         owner=affiliate_account,
     )
@@ -193,6 +212,8 @@ async def eur_org_billed_in_gbp_expenses(
         name="Free Azure account",
         affiliate_external_id="ACC-44444",
         datasource_id=azure_acc_mar_expenses.datasource_id,
+        linked_datasource_id=azure_acc_mar_expenses.linked_datasource_id,
+        linked_datasource_type=azure_acc_mar_expenses.linked_datasource_type,
         status=EntitlementStatus.ACTIVE,
         owner=affiliate_account,
     )
@@ -222,7 +243,9 @@ async def gbp_org_billed_in_eur_expenses(
     )
 
     aws_acc_mar_expenses = await datasource_expense_factory(  # noqa: F841
-        datasource_id=faker.uuid4(str),
+        datasource_id="55555555555",
+        linked_datasource_id=faker.uuid4(str),
+        linked_datasource_type=DatasourceType.AWS_CNR,
         datasource_name="AWS Account",
         organization=org,
         month=3,
@@ -233,7 +256,9 @@ async def gbp_org_billed_in_eur_expenses(
     )
 
     azure_acc_mar_expenses = await datasource_expense_factory(  # noqa: F841
-        datasource_id=faker.uuid4(str),
+        datasource_id="66666666666",
+        linked_datasource_id=faker.uuid4(str),
+        linked_datasource_type=DatasourceType.AZURE_CNR,
         datasource_name="Azure Account",
         organization=org,
         month=3,
@@ -247,6 +272,8 @@ async def gbp_org_billed_in_eur_expenses(
         name="Free AWS account",
         affiliate_external_id="ACC-111111",
         datasource_id=aws_acc_mar_expenses.datasource_id,
+        linked_datasource_id=aws_acc_mar_expenses.linked_datasource_id,
+        linked_datasource_type=aws_acc_mar_expenses.linked_datasource_type,
         status=EntitlementStatus.TERMINATED,  # should be ignored
         owner=affiliate_account,
     )
@@ -255,6 +282,8 @@ async def gbp_org_billed_in_eur_expenses(
         name="Free Azure account",
         affiliate_external_id="ACC-888888",
         datasource_id=azure_acc_mar_expenses.datasource_id,
+        linked_datasource_id=azure_acc_mar_expenses.linked_datasource_id,
+        linked_datasource_type=azure_acc_mar_expenses.linked_datasource_type,
         status=EntitlementStatus.ACTIVE,
         owner=affiliate_account,
     )
@@ -284,7 +313,9 @@ async def eur_org_billed_in_eur_expenses(
     )
 
     aws_acc_mar_expenses = await datasource_expense_factory(  # noqa: F841
-        datasource_id=faker.uuid4(str),
+        datasource_id="77777777777",
+        linked_datasource_id=faker.uuid4(str),
+        linked_datasource_type=DatasourceType.AWS_CNR,
         datasource_name="AWS Account",
         organization=org,
         month=3,
@@ -295,7 +326,9 @@ async def eur_org_billed_in_eur_expenses(
     )
 
     azure_acc_mar_expenses = await datasource_expense_factory(  # noqa: F841
-        datasource_id=faker.uuid4(str),
+        datasource_id="88888888888",
+        linked_datasource_id=faker.uuid4(str),
+        linked_datasource_type=DatasourceType.AZURE_CNR,
         datasource_name="Azure Account",
         organization=org,
         month=3,
@@ -309,6 +342,8 @@ async def eur_org_billed_in_eur_expenses(
         name="Free AWS account",
         affiliate_external_id="ACC-99999",
         datasource_id=aws_acc_mar_expenses.datasource_id,
+        linked_datasource_id=aws_acc_mar_expenses.linked_datasource_id,
+        linked_datasource_type=aws_acc_mar_expenses.linked_datasource_type,
         status=EntitlementStatus.TERMINATED,  # should be ignored
         owner=affiliate_account,
     )
@@ -317,6 +352,8 @@ async def eur_org_billed_in_eur_expenses(
         name="Free Azure account",
         affiliate_external_id="ACC-88888",
         datasource_id=azure_acc_mar_expenses.datasource_id,
+        linked_datasource_id=azure_acc_mar_expenses.linked_datasource_id,
+        linked_datasource_type=azure_acc_mar_expenses.linked_datasource_type,
         status=EntitlementStatus.ACTIVE,
         owner=affiliate_account,
     )
@@ -345,7 +382,9 @@ async def usd_org_billed_in_usd_expenses(
         linked_organization_id=faker.uuid4(str),
     )
     aws_acc_mar_expenses = await datasource_expense_factory(  # noqa: F841
-        datasource_id=faker.uuid4(str),
+        datasource_id="99999999999",
+        linked_datasource_id=faker.uuid4(str),
+        linked_datasource_type=DatasourceType.AWS_CNR,
         datasource_name="AWS Account",
         organization=org,
         month=3,
@@ -355,7 +394,9 @@ async def usd_org_billed_in_usd_expenses(
         updated_at=datetime(2025, 3, 31, 10, 0, 0, tzinfo=UTC),
     )
     gcp_acc_mar_expenses = await datasource_expense_factory(
-        datasource_id=faker.uuid4(str),
+        datasource_id="00000000000",
+        linked_datasource_id=faker.uuid4(str),
+        linked_datasource_type=DatasourceType.GCP_CNR,
         datasource_name="GCP Account",
         organization=org,
         month=3,
@@ -365,7 +406,9 @@ async def usd_org_billed_in_usd_expenses(
         updated_at=datetime(2025, 3, 31, 10, 0, 0, tzinfo=UTC),
     )
     azure_acc_mar_expenses = await datasource_expense_factory(  # noqa: F841
-        datasource_id=faker.uuid4(str),
+        datasource_id="10101010101",
+        linked_datasource_id=faker.uuid4(str),
+        linked_datasource_type=DatasourceType.AZURE_CNR,
         datasource_name="Azure Account",
         organization=org,
         month=3,
@@ -379,6 +422,8 @@ async def usd_org_billed_in_usd_expenses(
         name="free AWS account",
         affiliate_external_id="ACC-55555",
         datasource_id=aws_acc_mar_expenses.datasource_id,
+        linked_datasource_id=aws_acc_mar_expenses.linked_datasource_id,
+        linked_datasource_type=aws_acc_mar_expenses.linked_datasource_type,
         status=EntitlementStatus.ACTIVE,
         owner=affiliate_account,
     )
@@ -388,6 +433,8 @@ async def usd_org_billed_in_usd_expenses(
         name="free GCP account",
         affiliate_external_id="ACC-66666",
         datasource_id=gcp_acc_mar_expenses.datasource_id,
+        linked_datasource_id=gcp_acc_mar_expenses.linked_datasource_id,
+        linked_datasource_type=gcp_acc_mar_expenses.linked_datasource_type,
         status=EntitlementStatus.TERMINATED,
         owner=affiliate_account,
     )
diff --git a/tests/commands/test_update_current_month_datasource_expenses.py b/tests/commands/test_update_current_month_datasource_expenses.py
index 140bffd9..09de89d4 100644
--- a/tests/commands/test_update_current_month_datasource_expenses.py
+++ b/tests/commands/test_update_current_month_datasource_expenses.py
@@ -16,7 +16,7 @@
 from app.conf import Settings
 from app.db.handlers import DatasourceExpenseHandler
 from app.db.models import DatasourceExpense, Organization
-from app.enums import OrganizationStatus
+from app.enums import DatasourceType, OrganizationStatus
 from tests.fixtures.mock_api_clients import MockOptscaleClient
 from tests.types import ModelFactory
 
@@ -51,8 +51,18 @@ async def test_create_new_datasource_expenses_single_organization(
     mock_optscale_client.mock_fetch_datasources_for_organization(
         organization,
         [
-            {"id": datasource_id1, "name": "First cloud account", "details": {"cost": 123.45}},
-            {"id": datasource_id2, "name": "Second cloud account", "details": {"cost": 567.89}},
+            {
+                "id": datasource_id1,
+                "name": "First cloud account",
+                "details": {"cost": 123.45},
+                "account_id": "123456",
+            },
+            {
+                "id": datasource_id2,
+                "name": "Second cloud account",
+                "details": {"cost": 567.89},
+                "account_id": "654321",
+            },
         ],
     )
 
@@ -65,10 +75,14 @@ async def test_create_new_datasource_expenses_single_organization(
     assert len(new_datasource_expenses) == 2
 
     ds_exp1 = next(
-        ds_exp for ds_exp in new_datasource_expenses if ds_exp.datasource_id == datasource_id1
+        ds_exp
+        for ds_exp in new_datasource_expenses
+        if ds_exp.linked_datasource_id == datasource_id1
     )
     ds_exp2 = next(
-        ds_exp for ds_exp in new_datasource_expenses if ds_exp.datasource_id == datasource_id2
+        ds_exp
+        for ds_exp in new_datasource_expenses
+        if ds_exp.linked_datasource_id == datasource_id2
     )
 
     assert ds_exp1.organization_id == organization.id
@@ -76,12 +90,14 @@ async def test_create_new_datasource_expenses_single_organization(
     assert ds_exp1.month == 3
     assert ds_exp1.month_expenses == Decimal("123.45")
     assert ds_exp1.datasource_name == "First cloud account"
+    assert ds_exp1.datasource_id == "123456"
 
     assert ds_exp2.organization_id == organization.id
     assert ds_exp2.year == 2025
     assert ds_exp2.month == 3
     assert ds_exp2.month_expenses == Decimal("567.89")
     assert ds_exp2.datasource_name == "Second cloud account"
+    assert ds_exp2.datasource_id == "654321"
 
 
 @time_machine.travel("2025-03-20T10:00:00Z", tick=False)
@@ -104,8 +120,10 @@ async def test_datasource_expenses_are_updated_for_current_month(
 
     existing_datasource_expense1 = await datasource_expense_factory(
         organization=organization,
-        datasource_id=datasource_id1,
+        linked_datasource_id=datasource_id1,
+        linked_datasource_type=DatasourceType.AWS_CNR,
         datasource_name="First cloud account",
+        datasource_id="11111111",
         year=2025,
         month=3,  # NOTE: This is for the current month, so it should be updated
         month_expenses=Decimal("123.45"),
@@ -113,8 +131,10 @@ async def test_datasource_expenses_are_updated_for_current_month(
 
     existing_datasource_expense2 = await datasource_expense_factory(
         organization=organization,
-        datasource_id=datasource_id2,
+        linked_datasource_id=datasource_id2,
+        linked_datasource_type=DatasourceType.AZURE_CNR,
         datasource_name="Second cloud account",
+        datasource_id="22222222",
         year=2025,
         month=2,  # NOTE: this is for the previous month, so it should NOT be updated
         month_expenses=Decimal("567.89"),
@@ -123,8 +143,20 @@ async def test_datasource_expenses_are_updated_for_current_month(
     mock_optscale_client.mock_fetch_datasources_for_organization(
         organization,
         [
-            {"id": datasource_id1, "name": "First cloud account", "details": {"cost": 234.56}},
-            {"id": datasource_id2, "name": "Second cloud account", "details": {"cost": 678.90}},
+            {
+                "id": datasource_id1,
+                "account_id": "11111111",
+                "type": "aws_cnr",
+                "name": "First cloud account",
+                "details": {"cost": 234.56},
+            },
+            {
+                "id": datasource_id2,
+                "account_id": "22222222",
+                "type": "azure_cnr",
+                "name": "Second cloud account",
+                "details": {"cost": 678.90},
+            },
         ],
     )
 
@@ -137,18 +169,20 @@ async def test_datasource_expenses_are_updated_for_current_month(
     assert len(new_datasource_expenses) == 3
 
     ds_exp1 = next(
-        ds_exp for ds_exp in new_datasource_expenses if ds_exp.datasource_id == datasource_id1
+        ds_exp
+        for ds_exp in new_datasource_expenses
+        if ds_exp.linked_datasource_id == datasource_id1
     )
     ds_exp2_current_month = next(
         ds_exp
         for ds_exp in new_datasource_expenses
-        if (ds_exp.datasource_id == datasource_id2 and ds_exp.month == 2)
+        if (ds_exp.linked_datasource_id == datasource_id2 and ds_exp.month == 2)
     )
 
     ds_exp2_this_month = next(
         ds_exp
         for ds_exp in new_datasource_expenses
-        if (ds_exp.datasource_id == datasource_id2 and ds_exp.month == 3)
+        if (ds_exp.linked_datasource_id == datasource_id2 and ds_exp.month == 3)
     )
 
     await db_session.refresh(existing_datasource_expense1)
@@ -329,7 +363,9 @@ async def test_multiple_datasources_are_handled_correctly(
 
     await datasource_expense_factory(
         organization=organization1,
-        datasource_id=org_1_datasource_id1,
+        linked_datasource_id=org_1_datasource_id1,
+        linked_datasource_type=DatasourceType.AWS_CNR,
+        datasource_id="11111111",
         year=2025,
         month=2,
         month_expenses=Decimal("123.45"),
@@ -337,7 +373,9 @@ async def test_multiple_datasources_are_handled_correctly(
 
     await datasource_expense_factory(
         organization=organization1,
-        datasource_id=org_1_datasource_id1,
+        linked_datasource_id=org_1_datasource_id1,
+        linked_datasource_type=DatasourceType.AWS_CNR,
+        datasource_id="11111111",
         year=2025,
         month=3,
         month_expenses=Decimal("234.56"),
@@ -345,7 +383,9 @@ async def test_multiple_datasources_are_handled_correctly(
 
     await datasource_expense_factory(
         organization=organization1,
-        datasource_id=org_1_datasource_id2,
+        linked_datasource_id=org_1_datasource_id2,
+        datasource_id="12222222",
+        linked_datasource_type=DatasourceType.AWS_CNR,
         year=2025,
         month=3,
         month_expenses=Decimal("567.89"),
@@ -353,7 +393,9 @@ async def test_multiple_datasources_are_handled_correctly(
 
     await datasource_expense_factory(
         organization=organization2,
-        datasource_id=org_2_datasource_id1,
+        linked_datasource_id=org_2_datasource_id1,
+        datasource_id="21111111",
+        linked_datasource_type=DatasourceType.AWS_CNR,
         year=2025,
         month=3,
         month_expenses=Decimal("999.88"),
@@ -365,24 +407,51 @@ async def test_multiple_datasources_are_handled_correctly(
     mock_optscale_client.mock_fetch_datasources_for_organization(
         organization1,
         [
-            {"id": org_1_datasource_id1, "details": {"cost": 789.01}},
-            {"id": org_1_datasource_id2, "details": {"cost": 678.90}},
-            {"id": org_1_datasource_id3, "type": "azure_tenant"},
-            {"id": org_1_datasource_id4, "type": "gcp_tenant"},
+            {
+                "id": org_1_datasource_id1,
+                "account_id": "11111111",
+                "type": "aws_cnr",
+                "details": {"cost": 789.01},
+            },
+            {
+                "id": org_1_datasource_id2,
+                "account_id": "12222222",
+                "type": "aws_cnr",
+                "details": {"cost": 678.90},
+            },
+            {"id": org_1_datasource_id3, "account_id": "13333333", "type": "azure_tenant"},
+            {"id": org_1_datasource_id4, "account_id": "14444444", "type": "gcp_tenant"},
         ],
     )
 
     mock_optscale_client.mock_fetch_datasources_for_organization(
         organization2,
         [
-            {"id": org_2_datasource_id1, "details": {"cost": 234.56}},
-            {"id": org_2_datasource_id2, "details": {"cost": 654.32}},
+            {
+                "id": org_2_datasource_id1,
+                "account_id": "21111111",
+                "type": "aws_cnr",
+                "details": {"cost": 234.56},
+            },
+            {
+                "id": org_2_datasource_id2,
+                "account_id": "22222222",
+                "type": "aws_cnr",
+                "details": {"cost": 654.32},
+            },
         ],
     )
 
     mock_optscale_client.mock_fetch_datasources_for_organization(
         organization3,
-        [{"id": org_3_datasource_id1, "details": {"cost": 777.88}}],
+        [
+            {
+                "id": org_3_datasource_id1,
+                "account_id": "31111111",
+                "type": "azure_cnr",
+                "details": {"cost": 777.88},
+            }
+        ],
     )
 
     mock_optscale_client.mock_fetch_datasources_for_organization(organization4, status_code=404)
@@ -400,6 +469,7 @@ async def test_multiple_datasources_are_handled_correctly(
     expenses_data = {
         (
             ds_exp.organization_id,
+            ds_exp.linked_datasource_id,
             ds_exp.datasource_id,
             ds_exp.year,
             ds_exp.month,
@@ -409,12 +479,12 @@ async def test_multiple_datasources_are_handled_correctly(
     }
 
     assert expenses_data == {
-        (organization1.id, org_1_datasource_id1, 2025, 2, Decimal("123.4500")),
-        (organization1.id, org_1_datasource_id1, 2025, 3, Decimal("234.5600")),
-        (organization1.id, org_1_datasource_id2, 2025, 3, Decimal("567.8900")),
-        (organization2.id, org_2_datasource_id1, 2025, 3, Decimal("999.8800")),
-        (organization2.id, org_2_datasource_id2, 2025, 3, Decimal("654.3200")),
-        (organization3.id, org_3_datasource_id1, 2025, 3, Decimal("777.8800")),
+        (organization1.id, org_1_datasource_id1, "11111111", 2025, 2, Decimal("123.4500")),
+        (organization1.id, org_1_datasource_id1, "11111111", 2025, 3, Decimal("234.5600")),
+        (organization1.id, org_1_datasource_id2, "12222222", 2025, 3, Decimal("567.8900")),
+        (organization2.id, org_2_datasource_id1, "21111111", 2025, 3, Decimal("999.8800")),
+        (organization2.id, org_2_datasource_id2, "22222222", 2025, 3, Decimal("654.3200")),
+        (organization3.id, org_3_datasource_id1, "31111111", 2025, 3, Decimal("777.8800")),
     }
 
 
diff --git a/tests/conftest.py b/tests/conftest.py
index 18ff5e4b..f637569b 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -39,6 +39,7 @@
     AccountType,
     AccountUserStatus,
     ChargesFileStatus,
+    DatasourceType,
     EntitlementStatus,
     OrganizationStatus,
     SystemStatus,
@@ -205,6 +206,8 @@ async def _entitlement(
         name: str | None = None,
         affiliate_external_id: str | None = None,
         datasource_id: str | None = None,
+        linked_datasource_id: str | None = None,
+        linked_datasource_type: DatasourceType | None = None,
         created_by: Actor | None = None,
         updated_by: Actor | None = None,
         owner: Account | None = None,
@@ -214,6 +217,8 @@ async def _entitlement(
             name=name or "AWS",
             affiliate_external_id=affiliate_external_id or "ACC-1234-5678",
             datasource_id=datasource_id or faker.uuid4(),
+            linked_datasource_id=linked_datasource_id,
+            linked_datasource_type=linked_datasource_type,
             created_by=created_by,
             updated_by=updated_by,
             status=status or EntitlementStatus.NEW,
@@ -445,6 +450,8 @@ async def _datasource_expense(
         month: int = 3,
         month_expenses: float = 123.45,
         datasource_id: str | None = None,
+        linked_datasource_id: str | None = None,
+        linked_datasource_type: DatasourceType | None = None,
         datasource_name: str | None = None,
         created_at: datetime | None = None,
         updated_at: datetime | None = None,
@@ -454,6 +461,8 @@ async def _datasource_expense(
         datasource_expense = DatasourceExpense(
             organization=organization,
             datasource_id=datasource_id or faker.uuid4(),
+            linked_datasource_id=linked_datasource_id or faker.uuid4(),
+            linked_datasource_type=linked_datasource_type or DatasourceType.AWS_CNR,
             datasource_name=datasource_name or "Datasource Name",
             year=year,
             month=month,


Reviews:
user: login: ffaraone
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-05-08T17:46:28Z
user: login: ffaraone
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-05-08T17:48:31Z
user: login: ffaraone
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-12T15:00:35Z
user: login: arturbalabanov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T15:13:42Z

Review comments:
diff_hunk: @@ -93,13 +93,14 @@ async def store_datasource_expenses(
     for organization_id, datasources in datasources_per_organization_id.items():
         for datasource in datasources:
             existing_datasource_expense, created = await datasource_expense_handler.get_or_create(
-                datasource_id=datasource["id"],
+                linked_datasource_id=datasource["id"],
path: app/commands/update_current_month_datasource_expenses.py
user: login: ffaraone
body: as a filter for get you should use:

```suggestion
                datasource_id=datasource["account_id"],
```
created_at: 2025-05-08T17:44:31Z
author_association: COLLABORATOR
diff_hunk: @@ -93,13 +93,14 @@ async def store_datasource_expenses(
     for organization_id, datasources in datasources_per_organization_id.items():
         for datasource in datasources:
             existing_datasource_expense, created = await datasource_expense_handler.get_or_create(
-                datasource_id=datasource["id"],
+                linked_datasource_id=datasource["id"],
                 organization_id=organization_id,
                 year=year,
                 month=month,
                 defaults={
                     "month_expenses": datasource["details"]["cost"],
                     "datasource_name": datasource["name"],
+                    "datasource_id": datasource["account_id"],
path: app/commands/update_current_month_datasource_expenses.py
user: login: ffaraone
body: you should add the `linked_datasource_id` to default and, in any case the it should be updated together with the `month_expenses` field.
created_at: 2025-05-08T17:46:20Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,31 @@
+"""Create DatasourceExpense.linked_datasource_id
+
+Revision ID: 0fcca0cf1151
+Revises: 7cd884694384
+Create Date: 2025-05-07 12:30:00.321343
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+import sqlalchemy_utils
+
+
+# revision identifiers, used by Alembic.
+revision: str = '0fcca0cf1151'
+down_revision: Union[str, None] = '7cd884694384'
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+    # Add a new column with "" as the default value (so that the migration can be run on a live database)
+    op.add_column('datasource_expenses', sa.Column('linked_datasource_id', sa.String(length=255), server_default="", nullable=False))
path: migrations/versions/0fcca0cf1151_create_datasourceexpense_linked_.py
user: login: ffaraone
body: This cannot be `nullable=False` because there's data in the table. You can:
1) set initially as `nullable=True`
2) set all the existing records to a blank string
3) set initially as `nullable=False`
created_at: 2025-05-08T17:48:26Z
author_association: COLLABORATOR
diff_hunk: @@ -306,8 +310,14 @@ class DatasourceExpense(Base, HumanReadablePKMixin, TimestampMixin):
 
     entitlements: Mapped[list[Entitlement]] = relationship(
         "Entitlement",
-        primaryjoin=lambda: DatasourceExpense.datasource_id == Entitlement.datasource_id,
-        foreign_keys=lambda: [DatasourceExpense.datasource_id],
+        primaryjoin=lambda: (
+            (DatasourceExpense.datasource_id == Entitlement.datasource_id)
path: app/db/models.py
user: login: ffaraone
body: Wdyt to add here the condition that the status of the Entitlement must be ACTIVE ?
created_at: 2025-05-12T14:59:12Z
author_association: COLLABORATOR
diff_hunk: @@ -306,8 +310,14 @@ class DatasourceExpense(Base, HumanReadablePKMixin, TimestampMixin):
 
     entitlements: Mapped[list[Entitlement]] = relationship(
         "Entitlement",
-        primaryjoin=lambda: DatasourceExpense.datasource_id == Entitlement.datasource_id,
-        foreign_keys=lambda: [DatasourceExpense.datasource_id],
+        primaryjoin=lambda: (
+            (DatasourceExpense.datasource_id == Entitlement.datasource_id)
path: app/db/models.py
user: login: arturbalabanov
body: That's a good idea, added that :)
created_at: 2025-05-12T15:13:42Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-05-12T15:16:14Z
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=softwareone-platform_ffc-finops-operations&pullRequest=183) **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=softwareone-platform_ffc-finops-operations&pullRequest=183&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=softwareone-platform_ffc-finops-operations&pullRequest=183&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=softwareone-platform_ffc-finops-operations&pullRequest=183&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [100.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=softwareone-platform_ffc-finops-operations&pullRequest=183&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=softwareone-platform_ffc-finops-operations&pullRequest=183&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=softwareone-platform_ffc-finops-operations&pullRequest=183)


author_association: NONE