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 { 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 ? (
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 />
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) */
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user