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