1
0
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:
Jaanus Sellin 2025-10-17 14:11:14 +03:00 committed by GitHub
parent aec793ddc7
commit 3f9a726db6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 350 additions and 53 deletions

View File

@ -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'>;

View File

@ -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;

View File

@ -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,
});
});
});
});

View File

@ -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,
};
};