diff --git a/packages/api-v4/src/account/types.ts b/packages/api-v4/src/account/types.ts index 03e26466cbe..d908c8c87ef 100644 --- a/packages/api-v4/src/account/types.ts +++ b/packages/api-v4/src/account/types.ts @@ -118,7 +118,7 @@ export interface InvoiceItem { unit_price: null | string; tax: number; total: number; - region: Region['id']; + region: string | null; } export interface Payment { diff --git a/packages/manager/.changeset/pr-9597-upcoming-features-1693255450332.md b/packages/manager/.changeset/pr-9597-upcoming-features-1693255450332.md new file mode 100644 index 00000000000..a8d60c7df02 --- /dev/null +++ b/packages/manager/.changeset/pr-9597-upcoming-features-1693255450332.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +DC Specific Pricing Invoice Support ([#9597](https://github.com/linode/manager/pull/9597)) diff --git a/packages/manager/src/features/Billing/BillingPanels/BillingActivityPanel/BillingActivityPanel.tsx b/packages/manager/src/features/Billing/BillingPanels/BillingActivityPanel/BillingActivityPanel.tsx index 31a86fc9577..39c4863486a 100644 --- a/packages/manager/src/features/Billing/BillingPanels/BillingActivityPanel/BillingActivityPanel.tsx +++ b/packages/manager/src/features/Billing/BillingPanels/BillingActivityPanel/BillingActivityPanel.tsx @@ -247,7 +247,8 @@ export const BillingActivityPanel = (props: Props) => { account!, invoice, invoiceItems, - taxes + taxes, + flags ); if (result.status === 'error') { diff --git a/packages/manager/src/features/Billing/InvoiceDetail/InvoiceDetail.tsx b/packages/manager/src/features/Billing/InvoiceDetail/InvoiceDetail.tsx index 0970d8d5b59..da561351b9e 100644 --- a/packages/manager/src/features/Billing/InvoiceDetail/InvoiceDetail.tsx +++ b/packages/manager/src/features/Billing/InvoiceDetail/InvoiceDetail.tsx @@ -85,11 +85,23 @@ export const InvoiceDetail = () => { ) => { const taxes = flags[getShouldUseAkamaiBilling(invoice.date) ? 'taxes' : 'taxBanner']; - const result = await printInvoice(account, invoice, items, taxes); + const result = await printInvoice(account, invoice, items, taxes, flags); setPDFGenerationError(result.status === 'error' ? result.error : undefined); }; + const csvHeaders = [ + { key: 'label', label: 'Description' }, + { key: 'from', label: 'From' }, + { key: 'to', label: 'To' }, + { key: 'quantity', label: 'Quantity' }, + ...(flags.dcSpecificPricing ? [{ key: 'region', label: 'Region' }] : []), + { key: 'unit_price', label: 'Unit Price' }, + { key: 'amount', label: 'Amount (USD)' }, + { key: 'tax', label: 'Tax (USD)' }, + { key: 'total', label: 'Total (USD)' }, + ]; + const sxGrid = { alignItems: 'center', display: 'flex', @@ -226,14 +238,3 @@ export const InvoiceDetail = () => { }; export default InvoiceDetail; - -const csvHeaders = [ - { key: 'label', label: 'Description' }, - { key: 'from', label: 'From' }, - { key: 'to', label: 'To' }, - { key: 'quantity', label: 'Quantity' }, - { key: 'unit_price', label: 'Unit Price' }, - { key: 'amount', label: 'Amount (USD)' }, - { key: 'tax', label: 'Tax (USD)' }, - { key: 'total', label: 'Total (USD)' }, -]; diff --git a/packages/manager/src/features/Billing/InvoiceDetail/InvoiceTable.tsx b/packages/manager/src/features/Billing/InvoiceDetail/InvoiceTable.tsx index 0d3c9441703..45f913aa17d 100644 --- a/packages/manager/src/features/Billing/InvoiceDetail/InvoiceTable.tsx +++ b/packages/manager/src/features/Billing/InvoiceDetail/InvoiceTable.tsx @@ -17,6 +17,8 @@ import { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty'; import { TableRowError } from 'src/components/TableRowError/TableRowError'; import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading'; import { renderUnitPrice } from 'src/features/Billing/billingUtils'; +import { useFlags } from 'src/hooks/useFlags'; +import { getInvoiceRegion } from '../PdfGenerator/utils'; const useStyles = makeStyles()((theme: Theme) => ({ table: { @@ -38,6 +40,7 @@ interface Props { const InvoiceTable = (props: Props) => { const { classes } = useStyles(); + const flags = useFlags(); const { errors, items, loading } = props; @@ -45,16 +48,15 @@ const InvoiceTable = (props: Props) => { - Description - From - To - Quantity - - Unit Price - - Amount (USD) - Tax (USD) - Total (USD) + Description + From + To + Quantity + {flags.dcSpecificPricing && Region} + Unit Price + Amount (USD) + Tax (USD) + Total (USD) @@ -70,6 +72,7 @@ const renderDate = (v: null | string) => const renderQuantity = (v: null | number) => (v ? v : null); const RenderData = (props: { items: InvoiceItem[] }) => { + const flags = useFlags(); const { items } = props; const MIN_PAGE_SIZE = 25; @@ -85,36 +88,48 @@ const RenderData = (props: { items: InvoiceItem[] }) => { pageSize, }) => ( - {paginatedData.map( - ({ amount, from, label, quantity, tax, to, total, unit_price }) => ( - - - {label} - - - {renderDate(from)} - - - {renderDate(to)} - - - {renderQuantity(quantity)} - - - {unit_price !== 'None' && renderUnitPrice(unit_price)} - - - - - - - - - + {paginatedData.map((invoiceItem: InvoiceItem) => ( + + + {invoiceItem.label} + + + {renderDate(invoiceItem.from)} + + + {renderDate(invoiceItem.to)} + + + {renderQuantity(invoiceItem.quantity)} + + {flags.dcSpecificPricing && ( + + {getInvoiceRegion(invoiceItem)} - - ) - )} + )} + + {invoiceItem.unit_price !== 'None' && + renderUnitPrice(invoiceItem.unit_price)} + + + + + + + + + + + + ))} {count > MIN_PAGE_SIZE && ( { + const flags = useFlags(); const { errors, items, loading } = props; + const columns = flags.dcSpecificPricing ? 9 : 8; + if (loading) { - return ; + return ; } if (errors) { return ( - + ); } @@ -161,7 +182,7 @@ const MaybeRenderContent = (props: { return ; } - return ; + return ; }; export default InvoiceTable; diff --git a/packages/manager/src/features/Billing/PdfGenerator/PdfGenerator.ts b/packages/manager/src/features/Billing/PdfGenerator/PdfGenerator.ts index afd2ad02b7e..bdc166ca374 100644 --- a/packages/manager/src/features/Billing/PdfGenerator/PdfGenerator.ts +++ b/packages/manager/src/features/Billing/PdfGenerator/PdfGenerator.ts @@ -184,6 +184,7 @@ export const printInvoice = async ( invoice: Invoice, items: InvoiceItem[], taxes: FlagSet['taxBanner'] | FlagSet['taxes'], + flags: FlagSet, timezone?: string ): Promise => { try { @@ -264,7 +265,7 @@ export const printInvoice = async ( text: `Invoice: #${invoiceId}`, }); - createInvoiceItemsTable(doc, itemsChunk, timezone); + createInvoiceItemsTable(doc, itemsChunk, flags, timezone); createFooter(doc, baseFont, account.country, invoice.date); if (index < itemsChunks.length - 1) { doc.addPage(); diff --git a/packages/manager/src/features/Billing/PdfGenerator/utils.ts b/packages/manager/src/features/Billing/PdfGenerator/utils.ts index 03f1e1e3985..9b972d17b3d 100644 --- a/packages/manager/src/features/Billing/PdfGenerator/utils.ts +++ b/packages/manager/src/features/Billing/PdfGenerator/utils.ts @@ -12,6 +12,7 @@ import { ADDRESSES } from 'src/constants'; import { formatDate } from 'src/utilities/formatDate'; import { getShouldUseAkamaiBilling } from '../billingUtils'; +import { FlagSet } from 'src/featureFlags'; /** * Margin that has to be applied to every item added to the PDF. @@ -90,6 +91,7 @@ export const createPaymentsTotalsTable = (doc: JSPDF, payment: Payment) => { export const createInvoiceItemsTable = ( doc: JSPDF, items: InvoiceItem[], + flags: FlagSet, timezone?: string ) => { autoTable(doc, { @@ -116,6 +118,18 @@ export const createInvoiceItemsTable = ( content: item.quantity || '', styles: { fontSize: 8, halign: 'center', overflow: 'linebreak' }, }, + ...(flags.dcSpecificPricing + ? [ + { + content: getInvoiceRegion(item) ?? '', + styles: { + fontSize: 8, + halign: 'center', + overflow: 'linebreak', + }, + } as const, + ] + : []), { content: item.unit_price || '', styles: { fontSize: 8, halign: 'center', overflow: 'linebreak' }, @@ -146,6 +160,7 @@ export const createInvoiceItemsTable = ( 'From', 'To', 'Quantity', + ...(flags.dcSpecificPricing ? ['Region'] : []), 'Unit Price', 'Amount', 'Tax', @@ -279,6 +294,21 @@ const truncateLabel = (label: string) => { return label.length > 20 ? `${label.substr(0, 20)}...` : label; }; +export const getInvoiceRegion = (invoiceItem: InvoiceItem) => { + // If the invoice item is not regarding transfer, just return the region. + if (!invoiceItem.label.includes('Transfer Overage')) { + return invoiceItem.region; + } + + // If there is no region, this Transfer Overage item is for global transfer. + if (!invoiceItem.region) { + return 'Global'; + } + + // The Transfer Overage item is for a specific region's pool. + return invoiceItem.region; +}; + const formatDescription = (desc?: string) => { if (!desc) { return 'No Description'; diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index 6c74cca24e7..a423e3c0370 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -923,6 +923,26 @@ export const handlers = [ rest.get('*/kubeconfig', (req, res, ctx) => { return res(ctx.json({ kubeconfig: 'SSBhbSBhIHRlYXBvdA==' })); }), + rest.get('*invoices/555/items', (req, res, ctx) => { + return res( + ctx.json( + makeResourcePage([ + invoiceItemFactory.build({ + label: 'Linode', + region: 'br-gru', + }), + invoiceItemFactory.build({ + label: 'Outbound Transfer', + region: null, + }), + invoiceItemFactory.build({ + label: 'Outbound Transfer', + region: 'id-cgk', + }), + ]) + ) + ); + }), rest.get('*invoices/:invoiceId/items', (req, res, ctx) => { const items = invoiceItemFactory.buildList(10); return res(ctx.json(makeResourcePage(items, { page: 1, pages: 4 })));