import {Fragment} from 'react'; import styled from '@emotion/styled'; import {DateTime} from 'sentry/components/dateTime'; import LoadingError from 'sentry/components/loadingError'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import Panel from 'sentry/components/panels/panel'; import PanelBody from 'sentry/components/panels/panelBody'; import {IconSentry} from 'sentry/icons'; import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {RouteComponentProps} from 'sentry/types/legacyReactRouter'; import type {Organization} from 'sentry/types/organization'; import {keepPreviousData, useApiQuery} from 'sentry/utils/queryClient'; import withOrganization from 'sentry/utils/withOrganization'; import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader'; import type {BillingDetails, Invoice} from 'getsentry/types'; import {InvoiceItemType, InvoiceStatus} from 'getsentry/types'; import {getTaxFieldInfo} from 'getsentry/utils/salesTax'; import {displayPriceWithCents} from '../amCheckout/utils'; import InvoiceDetailsActions from './actions'; type Props = RouteComponentProps<{invoiceGuid: string}, unknown> & { organization: Organization; }; function InvoiceDetails({organization, params}: Props) { const { data: billingDetails, isPending: isBillingDetailsLoading, isError: isBillingDetailsError, refetch: billingDetailsRefetch, } = useApiQuery([`/customers/${organization.slug}/billing-details/`], { staleTime: 0, placeholderData: keepPreviousData, }); const { data: invoice, isPending: isInvoiceLoading, isError: isInvoiceError, refetch: invoiceRefetch, } = useApiQuery( [`/customers/${organization.slug}/invoices/${params.invoiceGuid}/`], {staleTime: Infinity} ); if (isBillingDetailsError || isInvoiceError) { return ( { billingDetailsRefetch(); invoiceRefetch(); }} /> ); } return ( {t('Invoice Details')} {isInvoiceLoading || isBillingDetailsLoading ? ( ) : (
{invoice.sender.name} {invoice.sender.address.map((line, idx) => (
{line}
))}
{invoice.sentryTaxIds && (
{invoice.sentryTaxIds.taxIdName}: {invoice.sentryTaxIds.taxId}
)} {invoice.sentryTaxIds?.region && (
{invoice.sentryTaxIds.region.taxIdName}:{' '} {invoice.sentryTaxIds.region.taxId}
)}
{invoice && ( )}

)}
); } type AttributeProps = { invoice: Invoice; billingDetails?: BillingDetails; }; function InvoiceAttributes({invoice, billingDetails}: AttributeProps) { let paymentStatus: InvoiceStatus = InvoiceStatus.CLOSED; if (invoice.isPaid) { paymentStatus = InvoiceStatus.PAID; } else if (!invoice.isClosed) { paymentStatus = InvoiceStatus.AWAITING_PAYMENT; } const contactInfo = invoice?.displayAddress || billingDetails?.displayAddress; const companyName = billingDetails?.companyName; const billingEmail = billingDetails?.billingEmail; const taxNumber = invoice?.taxNumber || billingDetails?.taxNumber; const countryCode = invoice?.countryCode || billingDetails?.countryCode; const taxNumberName = `${getTaxFieldInfo(countryCode).label}:`; return (
{t('Account:')}
{invoice.customer?.name &&
{invoice.customer.name}
} {billingEmail}
{companyName || contactInfo ? (
{t('Details:')}
{!!companyName &&
{companyName}
} {!!contactInfo &&
{contactInfo}
}
) : null} {!!taxNumber && (
{taxNumberName}
{taxNumber}
)}
{t('Invoice ID:')}
{invoice.id}
{t('Status:')}
{paymentStatus.toUpperCase()}
{t('Date:')}
); } type ContentsProps = { invoice: Invoice; billingDetails?: BillingDetails; }; function InvoiceDetailsContents({billingDetails, invoice}: ContentsProps) { // If an Invoice has 'isReverseCharge: true', it should be noted in // the last row of the table with "VAT" in the left column and "Reverse Charge" // on the right underneath the totals and (if included) refunds return ( {t('Item')} {t('Price')} {t('Total')} {displayPriceWithCents({cents: invoice.amountBilled ?? 0})} USD {invoice.isRefunded && ( {t('Refunds')} {displayPriceWithCents({cents: invoice.amountRefunded})} USD )} {invoice.isReverseCharge && ( {invoice.defaultTaxName} {t('Reverse Charge')} )} {invoice.items.map((item, i) => { if (item.type === InvoiceItemType.SUBSCRIPTION) { return ( {tct('[description] Plan', {description: item.description})} {tct('[start] to [end]', { start: , end: , })} {displayPriceWithCents({cents: item.amount})} USD ); } return ( {item.description} {displayPriceWithCents({cents: item.amount})} USD ); })} ); } export default withOrganization(InvoiceDetails); const SenderName = styled('h3')` display: flex; align-items: center; gap: ${space(0.5)}; `; const SenderContainer = styled('div')` display: grid; grid-template-columns: auto auto; gap: ${space(2)}; padding-left: ${space(1)}; /* Use a vertical layout on smaller viewports */ @media (max-width: ${p => p.theme.breakpoints.small}) { grid-template-columns: auto; grid-template-rows: auto auto; } `; const AttributeGroup = styled('div')` display: grid; grid-template-columns: 1fr 1fr; gap: ${space(2)}; /* Use a vertical layout on smaller viewports */ @media (max-width: ${p => p.theme.breakpoints.small}) { grid-template-columns: auto; grid-template-rows: auto auto; } `; const Attributes = styled('dl')` overflow: hidden; dt { font-weight: bold; margin: 0 0 ${space(0.25)} ${space(1)}; } dd { background: ${p => p.theme.backgroundSecondary}; padding: ${space(1)}; margin-bottom: ${space(2)}; } `; const StyledAddress = styled('address')` margin-bottom: 0px; line-height: 1.5; font-style: normal; `; const InvoiceItems = styled('table')` width: 100%; tr th, tr td { border-top: 1px solid ${p => p.theme.innerBorder}; padding: ${space(2)} ${space(1)}; } thead tr:first-child th, thead tr:first-child td { border-top: none; } th:last-child, td:last-child { font-variant-numeric: tabular-nums; text-align: right; } td small { display: block; margin-top: ${space(0.5)}; } `; const RefundRow = styled('tr')` td, th { background: ${p => p.theme.alert.warning.backgroundLight}; } `;