From aec793ddc7a4b047e754c130cc8670cfcdeb0b90 Mon Sep 17 00:00:00 2001 From: Jaanus Sellin Date: Fri, 17 Oct 2025 13:24:09 +0300 Subject: [PATCH] feat: calculate the esimtate invoice numbers (#10823) --- .../BillingInvoice/BillingInvoice.tsx | 76 +++++++++++++++++-- .../BillingInvoiceFooter.tsx | 27 +++++-- .../BillingInvoiceUsageRow.tsx | 47 ++++++++---- .../models/detailedInvoicesLineSchema.ts | 2 + .../detailedInvoicesSchemaInvoicesItem.ts | 2 + 5 files changed, 129 insertions(+), 25 deletions(-) diff --git a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoice.tsx b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoice.tsx index 75c8d5b396..31fc2aabf8 100644 --- a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoice.tsx +++ b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoice.tsx @@ -15,7 +15,10 @@ import { formatCurrency } from './formatCurrency.ts'; import { Badge } from 'component/common/Badge/Badge.tsx'; import { BillingInvoiceFooter } from './BillingInvoiceFooter/BillingInvoiceFooter.tsx'; import { StyledAmountCell, StyledSubgrid } from './BillingInvoice.styles.tsx'; -import type { DetailedInvoicesSchemaInvoicesItem } from 'openapi'; +import type { + DetailedInvoicesSchemaInvoicesItem, + DetailedInvoicesLineSchema, +} from 'openapi'; import { BillingInvoiceUsageRow } from './BillingInvoiceUsageRow/BillingInvoiceUsageRow.tsx'; import { BillingInvoiceMainRow } from './BillingInvoiceMainRow/BillingInvoiceMainRow.tsx'; @@ -64,7 +67,7 @@ const StyledInvoiceGrid = styled('div')(({ theme }) => ({ padding: theme.spacing(0, 2, 2), })); -const HeaderCell = styled(Typography)(({ theme }) => ({ +const HeaderCell = styled('div')(({ theme }) => ({ fontSize: theme.typography.body2.fontSize, fontWeight: theme.typography.fontWeightMedium, color: theme.palette.text.secondary, @@ -100,6 +103,49 @@ const CardActions = styled('div')(({ theme }) => ({ padding: theme.spacing(0, 2, 2), })); +const calculateEstimateTotals = ( + status: string, + subtotal: number, + taxAmount: number, + totalAmount: number, + taxPercentage: number | undefined, + mainLines: DetailedInvoicesLineSchema[], + usageLines: DetailedInvoicesLineSchema[], +) => { + if (status !== 'estimate') { + return { + subtotal: subtotal, + taxAmount: taxAmount, + totalAmount: totalAmount, + }; + } + + const mainLinesTotal = mainLines.reduce( + (sum, line) => sum + (line.totalAmount || 0), + 0, + ); + + const usageLinesTotal = usageLines.reduce((sum, line) => { + const overage = + line.consumption && line.limit + ? Math.max(0, line.consumption - line.limit) + : 0; + return sum + overage * (line.unitPrice || 0); + }, 0); + + const calculatedSubtotal = mainLinesTotal + usageLinesTotal; + const calculatedTaxAmount = taxPercentage + ? calculatedSubtotal * (taxPercentage / 100) + : 0; + const calculatedTotalAmount = calculatedSubtotal + calculatedTaxAmount; + + return { + subtotal: calculatedSubtotal, + taxAmount: calculatedTaxAmount, + totalAmount: calculatedTotalAmount, + }; +}; + type BillingInvoiceProps = DetailedInvoicesSchemaInvoicesItem & Pick, 'defaultExpanded'>; @@ -111,6 +157,7 @@ export const BillingInvoice = ({ totalAmount, subtotal, taxAmount, + taxPercentage, currency, mainLines, usageLines, @@ -130,6 +177,20 @@ export const BillingInvoice = ({ ? `, ${new Date(invoiceDate).getFullYear()}` : ''; + const { + subtotal: calculatedSubtotal, + taxAmount: calculatedTaxAmount, + totalAmount: calculatedTotalAmount, + } = calculateEstimateTotals( + status, + subtotal, + taxAmount, + totalAmount, + taxPercentage, + mainLines, + usageLines, + ); + return ( Paid ) : null} - {formatCurrency(totalAmount, currency)} + {formatCurrency(calculatedTotalAmount, currency)} @@ -208,6 +269,7 @@ export const BillingInvoice = ({ ))} @@ -217,10 +279,12 @@ export const BillingInvoice = ({ )} {invoiceURL || invoicePDF ? ( diff --git a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoiceFooter/BillingInvoiceFooter.tsx b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoiceFooter/BillingInvoiceFooter.tsx index 241b322eb5..edc1010451 100644 --- a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoiceFooter/BillingInvoiceFooter.tsx +++ b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoiceFooter/BillingInvoiceFooter.tsx @@ -29,10 +29,12 @@ const StyledTableFooterCell = styled('div', { ...(colSpan ? { gridColumn: `span ${colSpan}` } : {}), })); -const TaxRow: FC<{ value?: number; currency?: string }> = ({ - value, - currency, -}) => { +const TaxRow: FC<{ + value?: number; + percentage?: number; + currency?: string; + status?: string; +}> = ({ value, percentage, currency, status }) => { if (value === undefined) { return ( @@ -41,9 +43,13 @@ const TaxRow: FC<{ value?: number; currency?: string }> = ({ ); } + const isEstimate = status === 'estimate'; + const taxLabel = + isEstimate && percentage !== undefined ? `Tax (${percentage}%)` : 'Tax'; + return ( <> - Tax + {taxLabel} {formatCurrency(value, currency)} @@ -56,15 +62,19 @@ const TaxRow: FC<{ value?: number; currency?: string }> = ({ type BillingInvoiceFooterProps = { subTotal?: number; taxAmount?: number; + taxPercentage?: number; totalAmount: number; currency?: string; + status?: string; }; export const BillingInvoiceFooter = ({ subTotal, taxAmount, + taxPercentage, totalAmount, currency, + status, }: BillingInvoiceFooterProps) => { return ( @@ -77,7 +87,12 @@ export const BillingInvoiceFooter = ({ - + Total diff --git a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoiceUsageRow/BillingInvoiceUsageRow.tsx b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoiceUsageRow/BillingInvoiceUsageRow.tsx index 59a1039511..16f2f2008e 100644 --- a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoiceUsageRow/BillingInvoiceUsageRow.tsx +++ b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoiceUsageRow/BillingInvoiceUsageRow.tsx @@ -17,6 +17,7 @@ const StyledCellWithIndicator = styled('div')(({ theme }) => ({ type BillingInvoiceUsageRowProps = DetailedInvoicesLineSchema & { invoiceCurrency?: string; + invoiceStatus?: string; }; export const BillingInvoiceUsageRow = ({ @@ -24,36 +25,56 @@ export const BillingInvoiceUsageRow = ({ consumption, limit, description, - currency, totalAmount, + unitPrice, invoiceCurrency, + invoiceStatus, }: BillingInvoiceUsageRowProps) => { const percentage = limit && limit > 0 ? Math.min(100, Math.round(((consumption || 0) / limit) * 100)) : undefined; - const hasAmount = totalAmount && totalAmount > 0; + const isEstimate = invoiceStatus === 'estimate'; + const overage = + isEstimate && consumption && limit + ? Math.max(0, consumption - limit) + : quantity; + const includedAmount = + isEstimate && consumption && limit + ? Math.min(consumption, limit) + : consumption; + const calculatedAmount = + isEstimate && unitPrice && consumption && limit + ? Math.max(0, consumption - limit) * unitPrice + : totalAmount; + + const hasAmount = calculatedAmount && calculatedAmount > 0; + + const formatIncludedDisplay = () => { + if (includedAmount !== undefined && limit !== undefined) { + return `${formatLargeNumbers(includedAmount)}/${formatLargeNumbers(limit)}`; + } + if (includedAmount !== undefined) { + return formatLargeNumbers(includedAmount); + } + if (limit !== undefined) { + return formatLargeNumbers(limit); + } + return '–'; + }; return ( <> {description} -
- {consumption !== undefined && limit !== undefined - ? `${formatLargeNumbers(consumption)}/${formatLargeNumbers(limit)}` - : consumption !== undefined - ? formatLargeNumbers(consumption) - : limit !== undefined - ? formatLargeNumbers(limit) - : '–'} -
+
{formatIncludedDisplay()}
-
{quantity ? formatLargeNumbers(quantity) : ''}
+
{overage ? formatLargeNumbers(overage) : ''}
{hasAmount ? ( - {formatCurrency(totalAmount, invoiceCurrency)} + {formatCurrency(calculatedAmount, invoiceCurrency)} ) : (
diff --git a/frontend/src/openapi/models/detailedInvoicesLineSchema.ts b/frontend/src/openapi/models/detailedInvoicesLineSchema.ts index 8e5e4d45b9..88c384e6dc 100644 --- a/frontend/src/openapi/models/detailedInvoicesLineSchema.ts +++ b/frontend/src/openapi/models/detailedInvoicesLineSchema.ts @@ -23,4 +23,6 @@ export interface DetailedInvoicesLineSchema { startDate?: string; /** Total amount for this line item in minor currency units */ totalAmount: number; + /** Unit price for usage line items */ + unitPrice?: number; } diff --git a/frontend/src/openapi/models/detailedInvoicesSchemaInvoicesItem.ts b/frontend/src/openapi/models/detailedInvoicesSchemaInvoicesItem.ts index 6bcfa5895b..927026dec6 100644 --- a/frontend/src/openapi/models/detailedInvoicesSchemaInvoicesItem.ts +++ b/frontend/src/openapi/models/detailedInvoicesSchemaInvoicesItem.ts @@ -26,6 +26,8 @@ export type DetailedInvoicesSchemaInvoicesItem = { subtotal: number; /** Tax amount for the invoice */ taxAmount: number; + /** Tax percentage for the invoice */ + taxPercentage?: number; /** Total amount for the invoice */ totalAmount: number; /** Usage line items (traffic, consumption usage, overages) */