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:
parent
91ecf2fabd
commit
aec793ddc7
@ -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<ComponentProps<typeof Accordion>, '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 (
|
||||
<StyledAccordion defaultExpanded={Boolean(defaultExpanded)}>
|
||||
<HeaderRoot
|
||||
@ -160,7 +221,7 @@ export const BillingInvoice = ({
|
||||
<Badge color='success'>Paid</Badge>
|
||||
) : null}
|
||||
<Typography variant='body1' sx={{ fontWeight: 700 }}>
|
||||
{formatCurrency(totalAmount, currency)}
|
||||
{formatCurrency(calculatedTotalAmount, currency)}
|
||||
</Typography>
|
||||
</HeaderRight>
|
||||
</HeaderRoot>
|
||||
@ -208,6 +269,7 @@ export const BillingInvoice = ({
|
||||
<BillingInvoiceUsageRow
|
||||
{...line}
|
||||
invoiceCurrency={currency}
|
||||
invoiceStatus={status}
|
||||
/>
|
||||
</StyledTableRow>
|
||||
))}
|
||||
@ -217,10 +279,12 @@ export const BillingInvoice = ({
|
||||
)}
|
||||
|
||||
<BillingInvoiceFooter
|
||||
subTotal={subtotal}
|
||||
taxAmount={taxAmount}
|
||||
totalAmount={totalAmount}
|
||||
subTotal={calculatedSubtotal}
|
||||
taxAmount={calculatedTaxAmount}
|
||||
taxPercentage={taxPercentage}
|
||||
totalAmount={calculatedTotalAmount}
|
||||
currency={currency}
|
||||
status={status}
|
||||
/>
|
||||
</StyledInvoiceGrid>
|
||||
{invoiceURL || invoicePDF ? (
|
||||
|
||||
@ -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 (
|
||||
<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 (
|
||||
<>
|
||||
<StyledTableFooterCell>Tax</StyledTableFooterCell>
|
||||
<StyledTableFooterCell>{taxLabel}</StyledTableFooterCell>
|
||||
<StyledTableFooterCell>
|
||||
<StyledAmountCell>
|
||||
{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 (
|
||||
<StyledTableFooter>
|
||||
@ -77,7 +87,12 @@ export const BillingInvoiceFooter = ({
|
||||
</StyledTableFooterCell>
|
||||
</StyledTableFooterRow>
|
||||
<StyledTableFooterRow>
|
||||
<TaxRow value={taxAmount} currency={currency} />
|
||||
<TaxRow
|
||||
value={taxAmount}
|
||||
percentage={taxPercentage}
|
||||
currency={currency}
|
||||
status={status}
|
||||
/>
|
||||
</StyledTableFooterRow>
|
||||
<StyledTableFooterRow last>
|
||||
<StyledTableFooterCell>Total</StyledTableFooterCell>
|
||||
|
||||
@ -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 (
|
||||
<>
|
||||
<StyledDescriptionCell>{description}</StyledDescriptionCell>
|
||||
<StyledCellWithIndicator>
|
||||
<ConsumptionIndicator percentage={percentage || 0} />
|
||||
<div>
|
||||
{consumption !== undefined && limit !== undefined
|
||||
? `${formatLargeNumbers(consumption)}/${formatLargeNumbers(limit)}`
|
||||
: consumption !== undefined
|
||||
? formatLargeNumbers(consumption)
|
||||
: limit !== undefined
|
||||
? formatLargeNumbers(limit)
|
||||
: '–'}
|
||||
</div>
|
||||
<div>{formatIncludedDisplay()}</div>
|
||||
</StyledCellWithIndicator>
|
||||
<div>{quantity ? formatLargeNumbers(quantity) : ''}</div>
|
||||
<div>{overage ? formatLargeNumbers(overage) : ''}</div>
|
||||
{hasAmount ? (
|
||||
<StyledAmountCell>
|
||||
{formatCurrency(totalAmount, invoiceCurrency)}
|
||||
{formatCurrency(calculatedAmount, invoiceCurrency)}
|
||||
</StyledAmountCell>
|
||||
) : (
|
||||
<div />
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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) */
|
||||
|
||||
Loading…
Reference in New Issue
Block a user