1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-10-27 11:02:16 +01:00

feat: calculate the esimtate invoice numbers (#10823)

This commit is contained in:
Jaanus Sellin 2025-10-17 13:24:09 +03:00 committed by GitHub
parent 91ecf2fabd
commit aec793ddc7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 129 additions and 25 deletions

View File

@ -15,7 +15,10 @@ import { formatCurrency } from './formatCurrency.ts';
import { Badge } from 'component/common/Badge/Badge.tsx'; import { Badge } from 'component/common/Badge/Badge.tsx';
import { BillingInvoiceFooter } from './BillingInvoiceFooter/BillingInvoiceFooter.tsx'; import { BillingInvoiceFooter } from './BillingInvoiceFooter/BillingInvoiceFooter.tsx';
import { StyledAmountCell, StyledSubgrid } from './BillingInvoice.styles.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 { BillingInvoiceUsageRow } from './BillingInvoiceUsageRow/BillingInvoiceUsageRow.tsx';
import { BillingInvoiceMainRow } from './BillingInvoiceMainRow/BillingInvoiceMainRow.tsx'; import { BillingInvoiceMainRow } from './BillingInvoiceMainRow/BillingInvoiceMainRow.tsx';
@ -64,7 +67,7 @@ const StyledInvoiceGrid = styled('div')(({ theme }) => ({
padding: theme.spacing(0, 2, 2), padding: theme.spacing(0, 2, 2),
})); }));
const HeaderCell = styled(Typography)(({ theme }) => ({ const HeaderCell = styled('div')(({ theme }) => ({
fontSize: theme.typography.body2.fontSize, fontSize: theme.typography.body2.fontSize,
fontWeight: theme.typography.fontWeightMedium, fontWeight: theme.typography.fontWeightMedium,
color: theme.palette.text.secondary, color: theme.palette.text.secondary,
@ -100,6 +103,49 @@ const CardActions = styled('div')(({ theme }) => ({
padding: theme.spacing(0, 2, 2), 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 & type BillingInvoiceProps = DetailedInvoicesSchemaInvoicesItem &
Pick<ComponentProps<typeof Accordion>, 'defaultExpanded'>; Pick<ComponentProps<typeof Accordion>, 'defaultExpanded'>;
@ -111,6 +157,7 @@ export const BillingInvoice = ({
totalAmount, totalAmount,
subtotal, subtotal,
taxAmount, taxAmount,
taxPercentage,
currency, currency,
mainLines, mainLines,
usageLines, usageLines,
@ -130,6 +177,20 @@ export const BillingInvoice = ({
? `, ${new Date(invoiceDate).getFullYear()}` ? `, ${new Date(invoiceDate).getFullYear()}`
: ''; : '';
const {
subtotal: calculatedSubtotal,
taxAmount: calculatedTaxAmount,
totalAmount: calculatedTotalAmount,
} = calculateEstimateTotals(
status,
subtotal,
taxAmount,
totalAmount,
taxPercentage,
mainLines,
usageLines,
);
return ( return (
<StyledAccordion defaultExpanded={Boolean(defaultExpanded)}> <StyledAccordion defaultExpanded={Boolean(defaultExpanded)}>
<HeaderRoot <HeaderRoot
@ -160,7 +221,7 @@ export const BillingInvoice = ({
<Badge color='success'>Paid</Badge> <Badge color='success'>Paid</Badge>
) : null} ) : null}
<Typography variant='body1' sx={{ fontWeight: 700 }}> <Typography variant='body1' sx={{ fontWeight: 700 }}>
{formatCurrency(totalAmount, currency)} {formatCurrency(calculatedTotalAmount, currency)}
</Typography> </Typography>
</HeaderRight> </HeaderRight>
</HeaderRoot> </HeaderRoot>
@ -208,6 +269,7 @@ export const BillingInvoice = ({
<BillingInvoiceUsageRow <BillingInvoiceUsageRow
{...line} {...line}
invoiceCurrency={currency} invoiceCurrency={currency}
invoiceStatus={status}
/> />
</StyledTableRow> </StyledTableRow>
))} ))}
@ -217,10 +279,12 @@ export const BillingInvoice = ({
)} )}
<BillingInvoiceFooter <BillingInvoiceFooter
subTotal={subtotal} subTotal={calculatedSubtotal}
taxAmount={taxAmount} taxAmount={calculatedTaxAmount}
totalAmount={totalAmount} taxPercentage={taxPercentage}
totalAmount={calculatedTotalAmount}
currency={currency} currency={currency}
status={status}
/> />
</StyledInvoiceGrid> </StyledInvoiceGrid>
{invoiceURL || invoicePDF ? ( {invoiceURL || invoicePDF ? (

View File

@ -29,10 +29,12 @@ const StyledTableFooterCell = styled('div', {
...(colSpan ? { gridColumn: `span ${colSpan}` } : {}), ...(colSpan ? { gridColumn: `span ${colSpan}` } : {}),
})); }));
const TaxRow: FC<{ value?: number; currency?: string }> = ({ const TaxRow: FC<{
value, value?: number;
currency, percentage?: number;
}) => { currency?: string;
status?: string;
}> = ({ value, percentage, currency, status }) => {
if (value === undefined) { if (value === undefined) {
return ( return (
<StyledTableFooterCell colSpan={2}> <StyledTableFooterCell colSpan={2}>
@ -41,9 +43,13 @@ const TaxRow: FC<{ value?: number; currency?: string }> = ({
); );
} }
const isEstimate = status === 'estimate';
const taxLabel =
isEstimate && percentage !== undefined ? `Tax (${percentage}%)` : 'Tax';
return ( return (
<> <>
<StyledTableFooterCell>Tax</StyledTableFooterCell> <StyledTableFooterCell>{taxLabel}</StyledTableFooterCell>
<StyledTableFooterCell> <StyledTableFooterCell>
<StyledAmountCell> <StyledAmountCell>
{formatCurrency(value, currency)} {formatCurrency(value, currency)}
@ -56,15 +62,19 @@ const TaxRow: FC<{ value?: number; currency?: string }> = ({
type BillingInvoiceFooterProps = { type BillingInvoiceFooterProps = {
subTotal?: number; subTotal?: number;
taxAmount?: number; taxAmount?: number;
taxPercentage?: number;
totalAmount: number; totalAmount: number;
currency?: string; currency?: string;
status?: string;
}; };
export const BillingInvoiceFooter = ({ export const BillingInvoiceFooter = ({
subTotal, subTotal,
taxAmount, taxAmount,
taxPercentage,
totalAmount, totalAmount,
currency, currency,
status,
}: BillingInvoiceFooterProps) => { }: BillingInvoiceFooterProps) => {
return ( return (
<StyledTableFooter> <StyledTableFooter>
@ -77,7 +87,12 @@ export const BillingInvoiceFooter = ({
</StyledTableFooterCell> </StyledTableFooterCell>
</StyledTableFooterRow> </StyledTableFooterRow>
<StyledTableFooterRow> <StyledTableFooterRow>
<TaxRow value={taxAmount} currency={currency} /> <TaxRow
value={taxAmount}
percentage={taxPercentage}
currency={currency}
status={status}
/>
</StyledTableFooterRow> </StyledTableFooterRow>
<StyledTableFooterRow last> <StyledTableFooterRow last>
<StyledTableFooterCell>Total</StyledTableFooterCell> <StyledTableFooterCell>Total</StyledTableFooterCell>

View File

@ -17,6 +17,7 @@ const StyledCellWithIndicator = styled('div')(({ theme }) => ({
type BillingInvoiceUsageRowProps = DetailedInvoicesLineSchema & { type BillingInvoiceUsageRowProps = DetailedInvoicesLineSchema & {
invoiceCurrency?: string; invoiceCurrency?: string;
invoiceStatus?: string;
}; };
export const BillingInvoiceUsageRow = ({ export const BillingInvoiceUsageRow = ({
@ -24,36 +25,56 @@ export const BillingInvoiceUsageRow = ({
consumption, consumption,
limit, limit,
description, description,
currency,
totalAmount, totalAmount,
unitPrice,
invoiceCurrency, invoiceCurrency,
invoiceStatus,
}: BillingInvoiceUsageRowProps) => { }: BillingInvoiceUsageRowProps) => {
const percentage = const percentage =
limit && limit > 0 limit && limit > 0
? Math.min(100, Math.round(((consumption || 0) / limit) * 100)) ? Math.min(100, Math.round(((consumption || 0) / limit) * 100))
: undefined; : 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 ( return (
<> <>
<StyledDescriptionCell>{description}</StyledDescriptionCell> <StyledDescriptionCell>{description}</StyledDescriptionCell>
<StyledCellWithIndicator> <StyledCellWithIndicator>
<ConsumptionIndicator percentage={percentage || 0} /> <ConsumptionIndicator percentage={percentage || 0} />
<div> <div>{formatIncludedDisplay()}</div>
{consumption !== undefined && limit !== undefined
? `${formatLargeNumbers(consumption)}/${formatLargeNumbers(limit)}`
: consumption !== undefined
? formatLargeNumbers(consumption)
: limit !== undefined
? formatLargeNumbers(limit)
: ''}
</div>
</StyledCellWithIndicator> </StyledCellWithIndicator>
<div>{quantity ? formatLargeNumbers(quantity) : ''}</div> <div>{overage ? formatLargeNumbers(overage) : ''}</div>
{hasAmount ? ( {hasAmount ? (
<StyledAmountCell> <StyledAmountCell>
{formatCurrency(totalAmount, invoiceCurrency)} {formatCurrency(calculatedAmount, invoiceCurrency)}
</StyledAmountCell> </StyledAmountCell>
) : ( ) : (
<div /> <div />

View File

@ -23,4 +23,6 @@ export interface DetailedInvoicesLineSchema {
startDate?: string; startDate?: string;
/** Total amount for this line item in minor currency units */ /** Total amount for this line item in minor currency units */
totalAmount: number; totalAmount: number;
/** Unit price for usage line items */
unitPrice?: number;
} }

View File

@ -26,6 +26,8 @@ export type DetailedInvoicesSchemaInvoicesItem = {
subtotal: number; subtotal: number;
/** Tax amount for the invoice */ /** Tax amount for the invoice */
taxAmount: number; taxAmount: number;
/** Tax percentage for the invoice */
taxPercentage?: number;
/** Total amount for the invoice */ /** Total amount for the invoice */
totalAmount: number; totalAmount: number;
/** Usage line items (traffic, consumption usage, overages) */ /** Usage line items (traffic, consumption usage, overages) */