Add end-to-end tests for user and team billing features

This commit introduces end-to-end tests for the user and team billing features. It also enhances existing billing configurations, logging, and error handling mechanisms. Refactoring has been done to simplify the code and make it more readable. Adjustments have also been made in the visual aspects of some components. The addition of these tests will help ensure the reliability of the billing features.
This commit is contained in:
giancarlo
2024-04-14 17:15:04 +08:00
parent 1321b31ae4
commit d078e0021c
20 changed files with 330 additions and 140 deletions

View File

@@ -356,7 +356,7 @@ export function getLineItemTypeById(
for (const product of config.products) {
for (const plan of product.plans) {
for (const lineItem of plan.lineItems) {
if (lineItem.type === id) {
if (lineItem.id === id) {
return lineItem.type;
}
}

View File

@@ -236,6 +236,7 @@ export function PlanPicker(
key={primaryLineItem.id}
>
<RadioGroupItem
data-test-plan={plan.id}
key={plan.id + selected}
id={plan.id}
value={plan.id}
@@ -346,7 +347,10 @@ export function PlanPicker(
/>
<div>
<Button disabled={props.pending ?? !form.formState.isValid}>
<Button
data-test="checkout-submit-button"
disabled={props.pending ?? !form.formState.isValid}
>
{props.pending ? (
t('processing')
) : (

View File

@@ -26,15 +26,14 @@ export class BillingEventHandlerService {
const client = this.clientProvider();
const logger = await getLogger();
const ctx = {
namespace: this.namespace,
subscriptionId,
};
// Handle the subscription deleted event
// here we delete the subscription from the database
logger.info(
{
namespace: this.namespace,
subscriptionId,
},
'Processing subscription deleted event',
);
logger.info(ctx, 'Processing subscription deleted event');
const { error } = await client
.from('subscriptions')
@@ -42,16 +41,18 @@ export class BillingEventHandlerService {
.match({ id: subscriptionId });
if (error) {
logger.error(
{
error,
...ctx,
},
`Failed to delete subscription`,
);
throw new Error('Failed to delete subscription');
}
logger.info(
{
namespace: this.namespace,
subscriptionId,
},
'Successfully deleted subscription',
);
logger.info(ctx, 'Successfully deleted subscription');
},
onSubscriptionUpdated: async (subscription) => {
const client = this.clientProvider();
@@ -65,7 +66,7 @@ export class BillingEventHandlerService {
customerId: subscription.target_customer_id,
};
logger.info(ctx, 'Processing subscription updated event');
logger.info(ctx, 'Processing subscription updated event ...');
// Handle the subscription updated event
// here we update the subscription in the database
@@ -139,15 +140,14 @@ export class BillingEventHandlerService {
const client = this.clientProvider();
const logger = await getLogger();
const ctx = {
namespace: this.namespace,
sessionId,
};
// Handle the payment succeeded event
// here we update the payment status in the database
logger.info(
{
namespace: this.namespace,
sessionId,
},
'Processing payment succeeded event',
);
logger.info(ctx, 'Processing payment succeeded event...');
const { error } = await client
.from('orders')
@@ -155,30 +155,31 @@ export class BillingEventHandlerService {
.match({ session_id: sessionId });
if (error) {
logger.error(
{
error,
...ctx,
},
'Failed to update payment status',
);
throw new Error('Failed to update payment status');
}
logger.info(
{
namespace: this.namespace,
sessionId,
},
'Successfully updated payment status',
);
logger.info(ctx, 'Successfully updated payment status');
},
onPaymentFailed: async (sessionId: string) => {
const client = this.clientProvider();
const logger = await getLogger();
const ctx = {
namespace: this.namespace,
sessionId,
};
// Handle the payment failed event
// here we update the payment status in the database
logger.info(
{
namespace: this.namespace,
sessionId,
},
'Processing payment failed event',
);
logger.info(ctx, 'Processing payment failed event');
const { error } = await client
.from('orders')
@@ -186,16 +187,18 @@ export class BillingEventHandlerService {
.match({ session_id: sessionId });
if (error) {
logger.error(
{
error,
...ctx,
},
'Failed to update payment status',
);
throw new Error('Failed to update payment status');
}
logger.info(
{
namespace: this.namespace,
sessionId,
},
'Successfully updated payment status',
);
logger.info(ctx, 'Successfully updated payment status');
},
});
}

View File

@@ -26,18 +26,19 @@ export async function createStripeCheckout(
const mode: Stripe.Checkout.SessionCreateParams.Mode =
params.plan.paymentType === 'recurring' ? 'subscription' : 'payment';
const isSubscription = mode === 'subscription';
// this should only be set if the mode is 'subscription'
const subscriptionData:
| Stripe.Checkout.SessionCreateParams.SubscriptionData
| undefined =
mode === 'subscription'
? {
trial_period_days: params.trialDays,
metadata: {
accountId: params.accountId,
},
}
: undefined;
| undefined = isSubscription
? {
trial_period_days: params.trialDays,
metadata: {
accountId: params.accountId,
},
}
: {};
const urls = getUrls({
returnUrl: params.returnUrl,
@@ -54,6 +55,10 @@ export async function createStripeCheckout(
customer_email: params.customerEmail,
};
const customerCreation = isSubscription
? ({} as Record<string, string>)
: { customer_creation: 'always' };
const lineItems = params.plan.lineItems.map((item) => {
if (item.type === 'metered') {
return {
@@ -81,7 +86,7 @@ export async function createStripeCheckout(
line_items: lineItems,
client_reference_id: clientReferenceId,
subscription_data: subscriptionData,
customer_creation: 'always',
...customerCreation,
...customerData,
...urls,
});

View File

@@ -282,17 +282,18 @@ export class StripeWebhookHandlerService
const lineItems = params.lineItems.map((item) => {
const quantity = item.quantity ?? 1;
const variantId = item.price?.id as string;
return {
id: item.id,
quantity,
subscription_id: params.id,
product_id: item.price?.product as string,
variant_id: item.price?.id,
variant_id: variantId,
price_amount: item.price?.unit_amount,
interval: item.price?.recurring?.interval as string,
interval_count: item.price?.recurring?.interval_count as number,
type: getLineItemTypeById(this.config, item.id),
type: getLineItemTypeById(this.config, variantId),
};
});

View File

@@ -47,30 +47,30 @@ export const UpdateInvitationDialog: React.FC<{
userRoleHierarchy,
account,
}) => {
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>
<Trans i18nKey={'teams:updateMemberRoleModalHeading'} />
</DialogTitle>
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>
<Trans i18nKey={'teams:updateMemberRoleModalHeading'} />
</DialogTitle>
<DialogDescription>
<Trans i18nKey={'teams:updateMemberRoleModalDescription'} />
</DialogDescription>
</DialogHeader>
<DialogDescription>
<Trans i18nKey={'teams:updateMemberRoleModalDescription'} />
</DialogDescription>
</DialogHeader>
<UpdateInvitationForm
account={account}
invitationId={invitationId}
userRole={userRole}
userRoleHierarchy={userRoleHierarchy}
setIsOpen={setIsOpen}
/>
</DialogContent>
</Dialog>
);
};
<UpdateInvitationForm
account={account}
invitationId={invitationId}
userRole={userRole}
userRoleHierarchy={userRoleHierarchy}
setIsOpen={setIsOpen}
/>
</DialogContent>
</Dialog>
);
};
function UpdateInvitationForm({
account,

View File

@@ -47,37 +47,37 @@ export const UpdateMemberRoleDialog: React.FC<{
userRole,
userRoleHierarchy,
}) => {
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>
<Trans i18nKey={'teams:updateMemberRoleModalHeading'} />
</DialogTitle>
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>
<Trans i18nKey={'teams:updateMemberRoleModalHeading'} />
</DialogTitle>
<DialogDescription>
<Trans i18nKey={'teams:updateMemberRoleModalDescription'} />
</DialogDescription>
</DialogHeader>
<DialogDescription>
<Trans i18nKey={'teams:updateMemberRoleModalDescription'} />
</DialogDescription>
</DialogHeader>
<RolesDataProvider
accountId={accountId}
maxRoleHierarchy={userRoleHierarchy}
>
{(data) => (
<UpdateMemberForm
setIsOpen={setIsOpen}
userId={userId}
accountId={accountId}
userRole={userRole}
roles={data}
/>
)}
</RolesDataProvider>
</DialogContent>
</Dialog>
);
};
<RolesDataProvider
accountId={accountId}
maxRoleHierarchy={userRoleHierarchy}
>
{(data) => (
<UpdateMemberForm
setIsOpen={setIsOpen}
userId={userId}
accountId={accountId}
userRole={userRole}
roles={data}
/>
)}
</RolesDataProvider>
</DialogContent>
</Dialog>
);
};
function UpdateMemberForm({
userId,

View File

@@ -26,7 +26,9 @@ export function GlobalLoader({
</If>
<If condition={displaySpinner}>
<div className={'flex flex-1 flex-col items-center justify-center py-48'}>
<div
className={'flex flex-1 flex-col items-center justify-center py-48'}
>
<LoadingOverlay displayLogo={displayLogo} fullPage={fullPage}>
{Text}
</LoadingOverlay>