Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,4 @@ data:
EXT_PENDING_ORDERS_INFORMATION_REPORT_PAGE_ID: {{ .Values.global.PendingOrdersInformationReportPageId | quote }}
EXT_CONFLUENCE_BASE_URL: {{ .Values.global.ConfluenceBaseUrl | quote }}
EXT_CONFLUENCE_USER: {{ .Values.global.ConfluenceUser | quote }}
EXT_PLS_CHARGE_PERCENTAGE: {{ .Values.global.PlSChargePercentage | quote }}
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,4 @@ data:
EXT_PENDING_ORDERS_INFORMATION_REPORT_PAGE_ID: {{ .Values.global.PendingOrdersInformationReportPageId | quote }}
EXT_CONFLUENCE_BASE_URL: {{ .Values.global.ConfluenceBaseUrl | quote }}
EXT_CONFLUENCE_USER: {{ .Values.global.ConfluenceUser | quote }}
EXT_PLS_CHARGE_PERCENTAGE: {{ .Values.global.PlSChargePercentage | quote }}
1 change: 1 addition & 0 deletions helm/swo-extension-aws/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ global:
ConfluenceUser: default
ConfluenceToken: ZGVmYXVsdA==
ConfluenceBaseUrl: default
PlSChargePercentage: "5.0"

cronjobs:
order-querying:
Expand Down
9 changes: 9 additions & 0 deletions swo_aws_extension/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

from django.conf import settings

DEFAULT_PLS_CHARGE_PERCENTAGE = 5.0


class Config:
"""AWS extension configuration."""
Expand Down Expand Up @@ -196,6 +198,13 @@ def mpt_portal_base_url(self) -> str:
"""Get the base URL for the MPT Portal."""
return settings.MPT_PORTAL_BASE_URL

@property
def pls_charge_percentage(self) -> float:
"""Get the PLS charge percentage (defaults to 5.0)."""
return float(
settings.EXTENSION_CONFIG.get("PLS_CHARGE_PERCENTAGE", DEFAULT_PLS_CHARGE_PERCENTAGE),
)

def _patch_path(self, file_path):
"""Fixes relative paths to be from the project root."""
path = Path(file_path)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
from mpt_extension_sdk.runtime.tracer import dynamic_trace_span

from swo_aws_extension.constants import SupportTypesEnum
from swo_aws_extension.flows.jobs.billing_journal.generators.invoice import InvoiceGenerator
from swo_aws_extension.flows.jobs.billing_journal.generators.journal_line import (
JournalLineGenerator,
)
from swo_aws_extension.flows.jobs.billing_journal.generators.pls_charge_manager import (
PlSChargeManager,
)
from swo_aws_extension.flows.jobs.billing_journal.generators.usage import (
BaseOrganizationUsageGenerator,
)
Expand All @@ -14,6 +18,7 @@
)
from swo_aws_extension.flows.jobs.billing_journal.models.usage import OrganizationUsageResult
from swo_aws_extension.logger import get_logger
from swo_aws_extension.parameters import get_support_type
from swo_aws_extension.utils.decorators import with_log_context

logger = get_logger(__name__)
Expand All @@ -30,6 +35,7 @@ def __init__(
invoice_generator: InvoiceGenerator,
) -> None:
self._authorization_currency = authorization_currency
self._pls_charge_percentage = context.pls_charge_percentage
self._config = context.config
self._mpt_client = context.mpt_client
self._billing_period = context.billing_period
Expand Down Expand Up @@ -77,18 +83,22 @@ def run(self, agreement: dict) -> list[JournalLine]:
)

return self._generate_lines_for_accounts(
agreement,
usage_result,
journal_details,
invoice_result.invoice,
)

def _generate_lines_for_accounts(
self,
agreement: dict,
usage_result: OrganizationUsageResult,
journal_details: JournalDetails,
organization_invoice,
) -> list[JournalLine]:
line_generator = JournalLineGenerator()
is_pls = get_support_type(agreement) == SupportTypesEnum.AWS_RESOLD_SUPPORT
line_generator = JournalLineGenerator(is_pls=is_pls)

all_lines: list[JournalLine] = []
for account_id, account_usage in usage_result.usage_by_account.items():
all_lines.extend(
Expand All @@ -99,4 +109,16 @@ def _generate_lines_for_accounts(
organization_invoice,
)
)

# If PLS is active, calculate and add the PLS charge line.
if is_pls:
all_lines.extend(
PlSChargeManager().process(
self._pls_charge_percentage,
usage_result,
journal_details,
organization_invoice,
)
)

return all_lines
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from decimal import Decimal

from swo_aws_extension.constants import DEC_ZERO
from swo_aws_extension.flows.jobs.billing_journal.models.invoice import InvoiceEntity


def resolve_service_amount(
amount: Decimal,
invoice_entity: InvoiceEntity | None,
) -> Decimal:
"""Calculate the service amount converted to the local payment currency."""
if not invoice_entity:
return amount

if invoice_entity.payment_currency_code == invoice_entity.base_currency_code:
return amount
if invoice_entity.exchange_rate <= DEC_ZERO:
return amount

return round(amount * invoice_entity.exchange_rate, 6)
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from swo_aws_extension.constants import AWSRecordTypeEnum
from swo_aws_extension.flows.jobs.billing_journal.generators.line_processors.base import (
LineProcessor,
JournalLineProcessor,
)
from swo_aws_extension.flows.jobs.billing_journal.generators.line_processors.credit import (
CreditLineProcessor,
CreditJournalLineProcessor,
)
from swo_aws_extension.flows.jobs.billing_journal.generators.line_processors.marketplace import (
MarketplaceJournalLineProcessor,
)
from swo_aws_extension.flows.jobs.billing_journal.models.context import LineProcessorContext
from swo_aws_extension.flows.jobs.billing_journal.models.invoice import OrganizationInvoice
Expand All @@ -17,25 +20,31 @@
logger = get_logger(__name__)


def _build_processor_registry() -> dict[str, LineProcessor]:
default = LineProcessor()
credit = CreditLineProcessor()
def _build_processor_registry(*, is_pls: bool = False) -> dict[str, JournalLineProcessor]:
default = JournalLineProcessor()
credit = CreditJournalLineProcessor()
marketplace = MarketplaceJournalLineProcessor()

return {
registry = {
AWSRecordTypeEnum.USAGE: default,
AWSRecordTypeEnum.SUPPORT: default,
AWSRecordTypeEnum.RECURRING: default,
AWSRecordTypeEnum.SAVING_PLAN_RECURRING_FEE: default,
AWSRecordTypeEnum.CREDIT: credit,
"MARKETPLACE": default,
"MARKETPLACE": marketplace,
}

if is_pls:
registry.pop(AWSRecordTypeEnum.SUPPORT, None)

return registry


class JournalLineGenerator:
"""Generates journal lines from account usage data using line processors."""

def __init__(self) -> None:
self._processors = _build_processor_registry()
def __init__(self, *, is_pls: bool = False) -> None:
self._processors = _build_processor_registry(is_pls=is_pls)

def generate(
self,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from decimal import Decimal

from swo_aws_extension.constants import DEC_ZERO
from swo_aws_extension.flows.jobs.billing_journal.generators.currency import (
resolve_service_amount,
)
from swo_aws_extension.flows.jobs.billing_journal.models.context import LineProcessorContext
from swo_aws_extension.flows.jobs.billing_journal.models.invoice import OrganizationInvoice
from swo_aws_extension.flows.jobs.billing_journal.models.journal_line import (
InvoiceDetails,
JournalLine,
Expand All @@ -12,7 +12,7 @@
ITEM_SKU = "AWS Usage"


class LineProcessor:
class JournalLineProcessor:
"""Base class for all line processors."""

def __init__(
Expand Down Expand Up @@ -47,7 +47,8 @@ def _build_line(
context: LineProcessorContext,
) -> JournalLine:
service_name = f"{self._prefix_name}{metric.service_name}{self._suffix_name}"
service_amount = self._resolve_service_amount(metric, context.organization_invoice)
invoice_entity = context.organization_invoice.entities.get(metric.invoice_entity or "")
service_amount = resolve_service_amount(metric.amount, invoice_entity)
invoice_details = InvoiceDetails(
item_sku=ITEM_SKU,
service_name=service_name,
Expand All @@ -57,20 +58,3 @@ def _build_line(
invoice_id=metric.invoice_id or "invoice_id",
)
return JournalLine.build(ITEM_SKU, context.journal_details, invoice_details)

def _resolve_service_amount(
self,
metric: ServiceMetric,
organization_invoice: OrganizationInvoice,
) -> Decimal:
invoice_entity_name = metric.invoice_entity or ""
invoice_entity = organization_invoice.entities.get(invoice_entity_name)
if not invoice_entity:
return metric.amount

if invoice_entity.payment_currency_code == invoice_entity.base_currency_code:
return metric.amount
if invoice_entity.exchange_rate <= DEC_ZERO:
return metric.amount

return round(metric.amount * invoice_entity.exchange_rate, 6)
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from swo_aws_extension.constants import DEC_ZERO, AWSRecordTypeEnum
from swo_aws_extension.flows.jobs.billing_journal.generators.line_processors.base import (
LineProcessor,
JournalLineProcessor,
)
from swo_aws_extension.flows.jobs.billing_journal.models.context import LineProcessorContext
from swo_aws_extension.flows.jobs.billing_journal.models.journal_line import JournalLine
Expand All @@ -16,7 +16,7 @@
)


class CreditLineProcessor(LineProcessor):
class CreditJournalLineProcessor(JournalLineProcessor):
"""Generates journal lines for Credit metrics.

Always generates a credit line with the CREDIT prefix. When the principal invoice amount is
Expand All @@ -25,7 +25,7 @@ class CreditLineProcessor(LineProcessor):

def __init__(self) -> None:
super().__init__(prefix_name=CREDIT_PREFIX)
self._spp_processor = LineProcessor(
self._spp_processor = JournalLineProcessor(
prefix_name=SPP_PREFIX,
suffix_name=SPP_SUFFIX,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from typing import override

from swo_aws_extension.flows.jobs.billing_journal.generators.line_processors.base import (
JournalLineProcessor,
)
from swo_aws_extension.flows.jobs.billing_journal.models.context import LineProcessorContext
from swo_aws_extension.flows.jobs.billing_journal.models.journal_line import JournalLine
from swo_aws_extension.flows.jobs.billing_journal.models.usage import ServiceMetric

TAX_SERVICE_NAME = "Tax"


class MarketplaceJournalLineProcessor(JournalLineProcessor):
"""Generates journal lines for Marketplace metrics, excluding Tax entries."""

@override
def process(
self,
metric: ServiceMetric,
context: LineProcessorContext,
) -> list[JournalLine]:
"""Process a marketplace metric, skipping Tax services.

Args:
metric: The service metric to process.
context: Shared context for the current account.

Returns:
List of journal lines (empty if metric is Tax or should be skipped).
"""
if metric.service_name == TAX_SERVICE_NAME:
return []
return super().process(metric, context)
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from decimal import Decimal

from swo_aws_extension.constants import (
DEC_ZERO,
AWSRecordTypeEnum,
)
from swo_aws_extension.flows.jobs.billing_journal.generators.currency import (
resolve_service_amount,
)
from swo_aws_extension.flows.jobs.billing_journal.models.invoice import OrganizationInvoice
from swo_aws_extension.flows.jobs.billing_journal.models.journal_line import (
InvoiceDetails,
JournalDetails,
JournalLine,
)
from swo_aws_extension.flows.jobs.billing_journal.models.usage import (
OrganizationUsageResult,
)
from swo_aws_extension.logger import get_logger

ITEM_SKU = "AWS Usage"
logger = get_logger(__name__)


class PlSChargeManager:
"""Manager to calculate and generate SWO Enterprise Support for AWS (PLS) charges."""

def __init__(self) -> None:
self._service_name = "SWO Enterprise support for AWS"

def process(
self,
charge_percentage: Decimal,
usage_result: OrganizationUsageResult,
journal_details: JournalDetails,
organization_invoice: OrganizationInvoice,
) -> list[JournalLine]:
"""Process PLS charge and return journal lines.

Args:
charge_percentage: The PLS percentage scalar to compute the charge.
usage_result: The global organization usage result.
journal_details: Shared journal details containing the MPA ID.
organization_invoice: The organization invoice.

Returns:
List containing the PLS Charge journal line, if applicable.
"""
principal_amount = organization_invoice.principal_invoice_amount
if principal_amount is None or principal_amount == DEC_ZERO:
return []

if charge_percentage <= DEC_ZERO:
return []

base_amount = self._calculate_base_amount(usage_result, organization_invoice)
if base_amount <= DEC_ZERO:
return []

charge_amount = self._calculate_charge_amount(base_amount, charge_percentage)

invoice_details = InvoiceDetails(
item_sku=ITEM_SKU,
service_name=self._service_name,
amount=charge_amount,
account_id=journal_details.mpa_id,
invoice_entity="",
invoice_id="invoice_id",
)
return [JournalLine.build(ITEM_SKU, journal_details, invoice_details)]

def _calculate_base_amount(
self,
usage_result: OrganizationUsageResult,
organization_invoice: OrganizationInvoice,
) -> Decimal:
total = DEC_ZERO
for account_usage in usage_result.usage_by_account.values():
usage_metrics = list(account_usage.get_metrics_by_record_type(AWSRecordTypeEnum.USAGE))
total += sum(
resolve_service_amount(
metric.amount,
organization_invoice.entities.get(metric.invoice_entity or ""),
)
for metric in usage_metrics
)
return total

def _calculate_charge_amount(self, base_amount: Decimal, percentage: Decimal) -> Decimal:
charge = base_amount * (percentage / Decimal(100))
return round(charge, 6)
Loading
Loading