mirror of
https://github.com/Unleash/unleash.git
synced 2025-11-10 01:19:53 +01:00
refactor: move invoice logic into separate file/function (#10824)
Extract calculateEstimateTotals and some utility functions.
This commit is contained in:
parent
aec793ddc7
commit
3f9a726db6
@ -15,12 +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,
|
||||
DetailedInvoicesLineSchema,
|
||||
} from 'openapi';
|
||||
import type { DetailedInvoicesSchemaInvoicesItem } from 'openapi';
|
||||
import { BillingInvoiceUsageRow } from './BillingInvoiceUsageRow/BillingInvoiceUsageRow.tsx';
|
||||
import { BillingInvoiceMainRow } from './BillingInvoiceMainRow/BillingInvoiceMainRow.tsx';
|
||||
import { calculateEstimateTotals } from './calculateEstimateTotals.ts';
|
||||
|
||||
const StyledAccordion = styled(Accordion)(({ theme }) => ({
|
||||
background: theme.palette.background.paper,
|
||||
@ -103,49 +101,6 @@ 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'>;
|
||||
|
||||
|
||||
@ -8,6 +8,25 @@ import {
|
||||
StyledDescriptionCell,
|
||||
} from '../BillingInvoice.styles.tsx';
|
||||
|
||||
const hasValidUsageData = (consumption?: number, limit?: number): boolean => {
|
||||
return Boolean(consumption && limit);
|
||||
};
|
||||
|
||||
const calculateOverage = (consumption?: number, limit?: number): number => {
|
||||
return hasValidUsageData(consumption, limit)
|
||||
? Math.max(0, consumption! - limit!)
|
||||
: 0;
|
||||
};
|
||||
|
||||
const calculateIncludedAmount = (
|
||||
consumption?: number,
|
||||
limit?: number,
|
||||
): number | undefined => {
|
||||
return hasValidUsageData(consumption, limit)
|
||||
? Math.min(consumption!, limit!)
|
||||
: consumption;
|
||||
};
|
||||
|
||||
const StyledCellWithIndicator = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
@ -36,17 +55,18 @@ export const BillingInvoiceUsageRow = ({
|
||||
: undefined;
|
||||
|
||||
const isEstimate = invoiceStatus === 'estimate';
|
||||
const hasValidData = hasValidUsageData(consumption, limit);
|
||||
const overage =
|
||||
isEstimate && consumption && limit
|
||||
? Math.max(0, consumption - limit)
|
||||
isEstimate && hasValidData
|
||||
? calculateOverage(consumption, limit)
|
||||
: quantity;
|
||||
const includedAmount =
|
||||
isEstimate && consumption && limit
|
||||
? Math.min(consumption, limit)
|
||||
isEstimate && hasValidData
|
||||
? calculateIncludedAmount(consumption, limit)
|
||||
: consumption;
|
||||
const calculatedAmount =
|
||||
isEstimate && unitPrice && consumption && limit
|
||||
? Math.max(0, consumption - limit) * unitPrice
|
||||
isEstimate && unitPrice && hasValidData
|
||||
? calculateOverage(consumption, limit) * unitPrice
|
||||
: totalAmount;
|
||||
|
||||
const hasAmount = calculatedAmount && calculatedAmount > 0;
|
||||
|
||||
@ -0,0 +1,278 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { calculateEstimateTotals } from './calculateEstimateTotals.ts';
|
||||
import type { DetailedInvoicesLineSchema } from 'openapi';
|
||||
|
||||
describe('calculateEstimateTotals', () => {
|
||||
const createMainLine = (
|
||||
totalAmount: number,
|
||||
): DetailedInvoicesLineSchema => ({
|
||||
description: 'Test main line',
|
||||
currency: 'USD',
|
||||
lookupKey: 'test-key',
|
||||
quantity: 1,
|
||||
totalAmount,
|
||||
});
|
||||
|
||||
const createUsageLine = (
|
||||
consumption: number,
|
||||
limit: number,
|
||||
unitPrice: number,
|
||||
totalAmount: number = 0,
|
||||
): DetailedInvoicesLineSchema => ({
|
||||
description: 'Test usage line',
|
||||
currency: 'USD',
|
||||
lookupKey: 'test-usage-key',
|
||||
quantity: 1,
|
||||
totalAmount,
|
||||
consumption,
|
||||
limit,
|
||||
unitPrice,
|
||||
});
|
||||
|
||||
describe('non-estimate invoices', () => {
|
||||
it('returns original values for non-estimate status', () => {
|
||||
const result = calculateEstimateTotals(
|
||||
'invoiced',
|
||||
1000,
|
||||
150,
|
||||
1150,
|
||||
15,
|
||||
[createMainLine(1000)],
|
||||
[createUsageLine(100, 50, 2)],
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
subtotal: 1000,
|
||||
taxAmount: 150,
|
||||
totalAmount: 1150,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns original values for paid status', () => {
|
||||
const result = calculateEstimateTotals(
|
||||
'paid',
|
||||
2000,
|
||||
300,
|
||||
2300,
|
||||
15,
|
||||
[createMainLine(2000)],
|
||||
[],
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
subtotal: 2000,
|
||||
taxAmount: 300,
|
||||
totalAmount: 2300,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('estimate invoices', () => {
|
||||
it('calculates totals correctly with main lines only', () => {
|
||||
const mainLines = [createMainLine(1000), createMainLine(500)];
|
||||
const result = calculateEstimateTotals(
|
||||
'estimate',
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
15,
|
||||
mainLines,
|
||||
[],
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
subtotal: 1500,
|
||||
taxAmount: 225,
|
||||
totalAmount: 1725,
|
||||
});
|
||||
});
|
||||
|
||||
it('calculates totals correctly with usage lines only', () => {
|
||||
const usageLines = [
|
||||
createUsageLine(100, 50, 2),
|
||||
createUsageLine(200, 150, 1.5),
|
||||
];
|
||||
const result = calculateEstimateTotals(
|
||||
'estimate',
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
20,
|
||||
[],
|
||||
usageLines,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
subtotal: 175,
|
||||
taxAmount: 35,
|
||||
totalAmount: 210,
|
||||
});
|
||||
});
|
||||
|
||||
it('calculates totals correctly with both main and usage lines', () => {
|
||||
const mainLines = [createMainLine(1000)];
|
||||
const usageLines = [createUsageLine(100, 50, 2)];
|
||||
const result = calculateEstimateTotals(
|
||||
'estimate',
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
mainLines,
|
||||
usageLines,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
subtotal: 1100,
|
||||
taxAmount: 110,
|
||||
totalAmount: 1210,
|
||||
});
|
||||
});
|
||||
|
||||
it('handles usage lines with no overage', () => {
|
||||
const usageLines = [
|
||||
createUsageLine(30, 50, 2),
|
||||
createUsageLine(100, 100, 1.5),
|
||||
];
|
||||
const result = calculateEstimateTotals(
|
||||
'estimate',
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
15,
|
||||
[],
|
||||
usageLines,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
subtotal: 0,
|
||||
taxAmount: 0,
|
||||
totalAmount: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('handles usage lines with missing consumption or limit', () => {
|
||||
const usageLines = [
|
||||
createUsageLine(100, 50, 2),
|
||||
{ ...createUsageLine(0, 0, 0), consumption: undefined },
|
||||
{ ...createUsageLine(0, 0, 0), limit: undefined },
|
||||
];
|
||||
const result = calculateEstimateTotals(
|
||||
'estimate',
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
[],
|
||||
usageLines,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
subtotal: 100,
|
||||
taxAmount: 10,
|
||||
totalAmount: 110,
|
||||
});
|
||||
});
|
||||
|
||||
it('handles zero tax percentage', () => {
|
||||
const mainLines = [createMainLine(1000)];
|
||||
const result = calculateEstimateTotals(
|
||||
'estimate',
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
mainLines,
|
||||
[],
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
subtotal: 1000,
|
||||
taxAmount: 0,
|
||||
totalAmount: 1000,
|
||||
});
|
||||
});
|
||||
|
||||
it('handles undefined tax percentage', () => {
|
||||
const mainLines = [createMainLine(1000)];
|
||||
const result = calculateEstimateTotals(
|
||||
'estimate',
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
undefined,
|
||||
mainLines,
|
||||
[],
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
subtotal: 1000,
|
||||
taxAmount: 0,
|
||||
totalAmount: 1000,
|
||||
});
|
||||
});
|
||||
|
||||
it('handles empty arrays', () => {
|
||||
const result = calculateEstimateTotals(
|
||||
'estimate',
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
15,
|
||||
[],
|
||||
[],
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
subtotal: 0,
|
||||
taxAmount: 0,
|
||||
totalAmount: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('handles main lines with zero totalAmount', () => {
|
||||
const mainLines = [
|
||||
createMainLine(1000),
|
||||
createMainLine(0),
|
||||
createMainLine(500),
|
||||
];
|
||||
const result = calculateEstimateTotals(
|
||||
'estimate',
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
mainLines,
|
||||
[],
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
subtotal: 1500,
|
||||
taxAmount: 150,
|
||||
totalAmount: 1650,
|
||||
});
|
||||
});
|
||||
|
||||
it('handles usage lines with zero unitPrice', () => {
|
||||
const usageLines = [
|
||||
createUsageLine(100, 50, 2),
|
||||
createUsageLine(200, 100, 0),
|
||||
];
|
||||
const result = calculateEstimateTotals(
|
||||
'estimate',
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
[],
|
||||
usageLines,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
subtotal: 100,
|
||||
taxAmount: 10,
|
||||
totalAmount: 110,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,44 @@
|
||||
import type { DetailedInvoicesLineSchema } from 'openapi';
|
||||
|
||||
export 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,
|
||||
};
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user