Refactor and improve billing module

The billing module has been refined and enhanced to include deeper validation and detailing of billing plans and products. The checkout session creation process was revised to handle more complex scenarios, incorporating better parsing and validation. Additional validations were added for the plan and product schemas, improving product details extraction, and rearranging of module exports was made for better organization. The code refactor allows easier future modifications and upgrades for recurring and one-time payments with nuanced product configurations.
This commit is contained in:
giancarlo
2024-03-27 21:06:34 +08:00
parent 7579ee9a2c
commit c3a4a05b22
22 changed files with 578 additions and 225 deletions

View File

@@ -15,6 +15,7 @@
"./hooks/*": "./src/hooks/*.ts"
},
"devDependencies": {
"@kit/billing-gateway": "*",
"@kit/eslint-config": "0.2.0",
"@kit/prettier-config": "0.1.0",
"@kit/shared": "*",

View File

@@ -1,6 +1,6 @@
'use server';
import { redirect } from 'next/navigation';
import { RedirectType, redirect } from 'next/navigation';
import { Logger } from '@kit/shared/logger';
import { requireAuth } from '@kit/supabase/require-auth';
@@ -33,28 +33,13 @@ export async function deletePersonalAccountAction(formData: FormData) {
`Deleting personal account...`,
);
const deleteAccountResponse = await service.deletePersonalAccount(
await service.deletePersonalAccount(
getSupabaseServerActionClient({ admin: true }),
{
userId,
},
);
//
// also delete any associated data and subscriptions
if (deleteAccountResponse.error) {
Logger.error(
{
error: deleteAccountResponse.error,
name: 'accounts',
},
`Error deleting personal account`,
);
throw new Error('Error deleting personal account');
}
Logger.info(
{
userId,
@@ -65,5 +50,5 @@ export async function deletePersonalAccountAction(formData: FormData) {
await client.auth.signOut();
redirect('/');
redirect('/', RedirectType.replace);
}

View File

@@ -1,5 +1,7 @@
import { SupabaseClient } from '@supabase/supabase-js';
import { BillingGatewayService } from '@kit/billing-gateway';
import { Logger } from '@kit/shared/logger';
import { Database } from '@kit/supabase/database';
/**
@@ -11,12 +13,95 @@ import { Database } from '@kit/supabase/database';
* const accountsService = new AccountsService(client);
*/
export class PersonalAccountsService {
private namespace = 'account';
constructor(private readonly client: SupabaseClient<Database>) {}
/**
* @name deletePersonalAccount
* Delete personal account of a user.
* This will delete the user from the authentication provider and cancel all subscriptions.
*/
async deletePersonalAccount(
adminClient: SupabaseClient<Database>,
params: { userId: string },
) {
return adminClient.auth.admin.deleteUser(params.userId);
Logger.info(
{ userId: params.userId, name: this.namespace },
'User requested deletion. Processing...',
);
try {
await adminClient.auth.admin.deleteUser(params.userId);
} catch (error) {
Logger.error(
{
userId: params.userId,
error,
name: this.namespace,
},
'Error deleting user',
);
throw new Error('Error deleting user');
}
try {
await this.cancelAllUserSubscriptions(params.userId);
} catch (error) {
Logger.error({
userId: params.userId,
error,
name: this.namespace,
});
}
}
private async cancelAllUserSubscriptions(userId: string) {
Logger.info(
{
userId,
name: this.namespace,
},
'Cancelling all subscriptions for user...',
);
const { data: subscriptions } = await this.client
.from('subscriptions')
.select('*')
.eq('account_id', userId);
const cancellationRequests = [];
Logger.info(
{
userId,
subscriptions: subscriptions?.length ?? 0,
name: this.namespace,
},
'Cancelling subscriptions...',
);
for (const subscription of subscriptions ?? []) {
const gateway = new BillingGatewayService(subscription.billing_provider);
cancellationRequests.push(
gateway.cancelSubscription({
subscriptionId: subscription.id,
invoiceNow: true,
}),
);
}
await Promise.all(cancellationRequests);
Logger.info(
{
userId,
subscriptions: subscriptions?.length ?? 0,
name: this.namespace,
},
'Subscriptions cancelled successfully',
);
}
}