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 { 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 {
|
import type { DetailedInvoicesSchemaInvoicesItem } from 'openapi';
|
||||||
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';
|
||||||
|
import { calculateEstimateTotals } from './calculateEstimateTotals.ts';
|
||||||
|
|
||||||
const StyledAccordion = styled(Accordion)(({ theme }) => ({
|
const StyledAccordion = styled(Accordion)(({ theme }) => ({
|
||||||
background: theme.palette.background.paper,
|
background: theme.palette.background.paper,
|
||||||
@ -103,49 +101,6 @@ 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'>;
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,25 @@ import {
|
|||||||
StyledDescriptionCell,
|
StyledDescriptionCell,
|
||||||
} from '../BillingInvoice.styles.tsx';
|
} 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 }) => ({
|
const StyledCellWithIndicator = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@ -36,17 +55,18 @@ export const BillingInvoiceUsageRow = ({
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const isEstimate = invoiceStatus === 'estimate';
|
const isEstimate = invoiceStatus === 'estimate';
|
||||||
|
const hasValidData = hasValidUsageData(consumption, limit);
|
||||||
const overage =
|
const overage =
|
||||||
isEstimate && consumption && limit
|
isEstimate && hasValidData
|
||||||
? Math.max(0, consumption - limit)
|
? calculateOverage(consumption, limit)
|
||||||
: quantity;
|
: quantity;
|
||||||
const includedAmount =
|
const includedAmount =
|
||||||
isEstimate && consumption && limit
|
isEstimate && hasValidData
|
||||||
? Math.min(consumption, limit)
|
? calculateIncludedAmount(consumption, limit)
|
||||||
: consumption;
|
: consumption;
|
||||||
const calculatedAmount =
|
const calculatedAmount =
|
||||||
isEstimate && unitPrice && consumption && limit
|
isEstimate && unitPrice && hasValidData
|
||||||
? Math.max(0, consumption - limit) * unitPrice
|
? calculateOverage(consumption, limit) * unitPrice
|
||||||
: totalAmount;
|
: totalAmount;
|
||||||
|
|
||||||
const hasAmount = calculatedAmount && calculatedAmount > 0;
|
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