Skip to content
Merged
18 changes: 16 additions & 2 deletions static/gsApp/types/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,18 @@ type SentryTaxIds = TaxNumberName & {
};
};

export type Charge = {
amount: number;
amountRefunded: number;
cardLast4: string | null;
dateCreated: string;
failureCode: string | null;
id: string;
isPaid: boolean;
isRefunded: boolean;
stripeID: string | null;
};

export type InvoiceBase = StructuredAddress & {
amount: number;
amountBilled: number | null;
Expand All @@ -568,7 +580,7 @@ export type InvoiceBase = StructuredAddress & {
};

export type Invoice = InvoiceBase & {
charges: any[];
charges: Charge[];
customer:
| Subscription
| {
Expand Down Expand Up @@ -643,6 +655,8 @@ export enum InvoiceItemType {
RESERVED_PROFILE_DURATION = 'reserved_profile_duration',
RESERVED_SEER_AUTOFIX = 'reserved_seer_autofix',
RESERVED_SEER_SCANNER = 'reserved_seer_scanner',
RESERVED_SEER_BUDGET = 'reserved_seer_budget',
RESERVED_LOG_BYTES = 'reserved_log_bytes',
}

export enum InvoiceStatus {
Expand Down Expand Up @@ -712,7 +726,7 @@ export type PreviewData = {
paymentSecret?: string;
};

type PreviewInvoiceItem = BaseInvoiceItem & {
export type PreviewInvoiceItem = BaseInvoiceItem & {
period_end: string;
period_start: string;
};
Expand Down
9 changes: 9 additions & 0 deletions static/gsApp/views/amCheckout/cart.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ describe('Cart', () => {
hasCompleteBillingDetails
organization={organization}
subscription={subscription}
onSuccess={jest.fn()}
/>
);

Expand Down Expand Up @@ -174,6 +175,7 @@ describe('Cart', () => {
hasCompleteBillingDetails
organization={organization}
subscription={subscription}
onSuccess={jest.fn()}
/>
);

Expand Down Expand Up @@ -225,6 +227,7 @@ describe('Cart', () => {
hasCompleteBillingDetails
organization={organization}
subscription={subscription}
onSuccess={jest.fn()}
/>
);

Expand Down Expand Up @@ -264,6 +267,7 @@ describe('Cart', () => {
hasCompleteBillingDetails
organization={organization}
subscription={subscription}
onSuccess={jest.fn()}
/>
);

Expand All @@ -283,6 +287,7 @@ describe('Cart', () => {
hasCompleteBillingDetails={false}
organization={organization}
subscription={subscription}
onSuccess={jest.fn()}
/>
);
expect(await screen.findByRole('button', {name: 'Confirm and pay'})).toBeDisabled();
Expand Down Expand Up @@ -312,6 +317,7 @@ describe('Cart', () => {
hasCompleteBillingDetails
organization={partnerOrg}
subscription={partnerSub}
onSuccess={jest.fn()}
/>
);

Expand Down Expand Up @@ -351,6 +357,7 @@ describe('Cart', () => {
hasCompleteBillingDetails
organization={organization}
subscription={partnerSub}
onSuccess={jest.fn()}
/>
);

Expand Down Expand Up @@ -391,6 +398,7 @@ describe('Cart', () => {
hasCompleteBillingDetails
organization={organization}
subscription={paidSub}
onSuccess={jest.fn()}
/>
);

Expand Down Expand Up @@ -421,6 +429,7 @@ describe('Cart', () => {
hasCompleteBillingDetails
organization={organization}
subscription={paidSub}
onSuccess={jest.fn()}
/>
);

Expand Down
70 changes: 47 additions & 23 deletions static/gsApp/views/amCheckout/cart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
hasPartnerMigrationFeature,
} from 'getsentry/utils/billing';
import {getPlanCategoryName, getSingularCategoryName} from 'getsentry/utils/dataCategory';
import type {State as CheckoutState} from 'getsentry/views/amCheckout/';
import CartDiff from 'getsentry/views/amCheckout/cartDiff';
import type {CheckoutFormData, SelectableProduct} from 'getsentry/views/amCheckout/types';
import * as utils from 'getsentry/views/amCheckout/utils';
Expand All @@ -48,6 +49,11 @@ interface CartProps {
activePlan: Plan;
formData: CheckoutFormData;
hasCompleteBillingDetails: boolean;
onSuccess: ({
invoice,
nextQueryParams,
isSubmitted,
}: Pick<CheckoutState, 'invoice' | 'nextQueryParams' | 'isSubmitted'>) => void;
organization: Organization;
subscription: Subscription;
referrer?: string;
Expand Down Expand Up @@ -417,34 +423,49 @@ function TotalSummary({
return subtext;
};

const fees = utils.getFees({invoiceItems: previewData?.invoiceItems ?? []});
const credits = utils.getCredits({invoiceItems: previewData?.invoiceItems ?? []});
const creditApplied = utils.getCreditApplied({
creditApplied: previewData?.creditApplied ?? 0,
invoiceItems: previewData?.invoiceItems ?? [],
});

return (
<SummarySection>
{!previewDataLoading && (
{!previewDataLoading && isDueToday && (
<Fragment>
{isDueToday &&
previewData?.invoiceItems
.filter(item => item.type === InvoiceItemType.SALES_TAX)
.map(item => {
const formattedPrice = utils.displayPrice({cents: item.amount});
return (
<Item key={item.type} data-test-id={`summary-item-${item.type}`}>
<ItemFlex>
<div>{item.description}</div>
<div>{formattedPrice}</div>
</ItemFlex>
</Item>
);
})}
{fees.map(item => {
const formattedPrice = utils.displayPrice({cents: item.amount});
return (
<Item key={item.type} data-test-id={`summary-item-${item.type}`}>
<ItemFlex>
<div>{item.description}</div>
<div>{formattedPrice}</div>
</ItemFlex>
</Item>
);
})}
{!!creditApplied && (
<Item data-test-id="summary-item-credit_applied">
<ItemFlex>
<div>{t('Credit applied')}</div>
<Credit>{utils.displayPrice({cents: -creditApplied})}</Credit>
</ItemFlex>
</Item>
)}
{credits.map(item => {
const formattedPrice = utils.displayPrice({cents: item.amount});
return (
<Item key={item.type} data-test-id={`summary-item-${item.type}`}>
<ItemFlex>
<div>{item.description}</div>
<div>{formattedPrice}</div>
</ItemFlex>
</Item>
);
})}
</Fragment>
)}
{!previewDataLoading && isDueToday && !!previewData?.creditApplied && (
<Item data-test-id="summary-item-credit_applied">
<ItemFlex>
<div>{t('Credit applied')}</div>
<Credit>{utils.displayPrice({cents: -previewData.creditApplied})}</Credit>
</ItemFlex>
</Item>
)}
<Item data-test-id="summary-item-due-today">
<ItemFlex>
<DueToday>{t('Due today')}</DueToday>
Expand Down Expand Up @@ -511,6 +532,7 @@ function Cart({
organization,
referrer,
hasCompleteBillingDetails,
onSuccess,
}: CartProps) {
const [previewState, setPreviewState] = useState<CartPreviewState>(NULL_PREVIEW_STATE);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
Expand Down Expand Up @@ -610,6 +632,8 @@ function Cart({
onHandleCardAction: handleCardAction,
onFetchPreviewData: fetchPreview,
referrer,
previewData: previewState.previewData ?? undefined,
onSuccess,
});

const handleConfirmAndPay = (applyNow?: boolean) => {
Expand Down
66 changes: 66 additions & 0 deletions static/gsApp/views/amCheckout/checkoutSuccess.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {InvoiceFixture} from 'getsentry-test/fixtures/invoice';
import {PlanDetailsLookupFixture} from 'getsentry-test/fixtures/planDetailsLookup';
import {render, screen} from 'sentry-test/reactTestingLibrary';
import {resetMockDate, setMockDate} from 'sentry-test/utils';

import {PreviewDataFixture} from 'getsentry/__fixtures__/previewData';
import CheckoutSuccess from 'getsentry/views/amCheckout/checkoutSuccess';

describe('CheckoutSuccess', () => {
const bizPlan = PlanDetailsLookupFixture('am3_business')!;
const mockDate = new Date('2025-01-01');

beforeEach(() => {
setMockDate(mockDate);
});

afterEach(() => {
resetMockDate();
});

it('renders for immediate charges', async () => {
render(
<CheckoutSuccess
invoice={InvoiceFixture()}
basePlan={bizPlan}
nextQueryParams={[]}
/>
);

expect(
await screen.findByText('Pleasure doing business with you')
).toBeInTheDocument();
expect(screen.getByTestId('receipt')).toBeInTheDocument();
expect(screen.queryByTestId('scheduled-changes')).not.toBeInTheDocument();
});

it('renders for scheduled changes', async () => {
render(
<CheckoutSuccess
basePlan={bizPlan}
nextQueryParams={[]}
previewData={PreviewDataFixture({})}
/>
);

expect(await screen.findByText('Consider it done (soon)')).toBeInTheDocument();
expect(screen.getByTestId('scheduled-changes')).toBeInTheDocument();
expect(screen.queryByTestId('receipt')).not.toBeInTheDocument();
});

it('renders for no immediate charges nor scheduled changes', async () => {
render(
<CheckoutSuccess
basePlan={bizPlan}
nextQueryParams={[]}
previewData={PreviewDataFixture({
effectiveAt: mockDate.toISOString(),
})}
/>
);

expect(await screen.findByText('Consider it done')).toBeInTheDocument();
expect(screen.queryByTestId('scheduled-changes')).not.toBeInTheDocument();
expect(screen.queryByTestId('receipt')).not.toBeInTheDocument();
});
});
Loading
Loading