diff --git a/apps/web/app/(dashboard)/home/(user)/billing/_lib/server/server-actions.ts b/apps/web/app/(dashboard)/home/(user)/billing/_lib/server/server-actions.ts index f3a692bc5..f45232c4f 100644 --- a/apps/web/app/(dashboard)/home/(user)/billing/_lib/server/server-actions.ts +++ b/apps/web/app/(dashboard)/home/(user)/billing/_lib/server/server-actions.ts @@ -6,7 +6,7 @@ import { enhanceAction } from '@kit/next/actions'; import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client'; import { PersonalAccountCheckoutSchema } from '../schema/personal-account-checkout.schema'; -import { UserBillingService } from './user-billing.service'; +import { createUserBillingService } from './user-billing.service'; /** * @name createPersonalAccountCheckoutSession @@ -14,7 +14,8 @@ import { UserBillingService } from './user-billing.service'; */ export const createPersonalAccountCheckoutSession = enhanceAction( async function (data) { - const service = new UserBillingService(getSupabaseServerActionClient()); + const client = getSupabaseServerActionClient(); + const service = createUserBillingService(client); return await service.createCheckoutSession(data); }, @@ -24,10 +25,14 @@ export const createPersonalAccountCheckoutSession = enhanceAction( ); /** + * @name createPersonalAccountBillingPortalSession * @description Creates a billing Portal session for a personal account */ export async function createPersonalAccountBillingPortalSession() { - const service = new UserBillingService(getSupabaseServerActionClient()); + const client = getSupabaseServerActionClient(); + const service = createUserBillingService(client); + + // get url to billing portal const url = await service.createBillingPortalSession(); return redirect(url); diff --git a/apps/web/app/(dashboard)/home/(user)/billing/_lib/server/user-billing.service.ts b/apps/web/app/(dashboard)/home/(user)/billing/_lib/server/user-billing.service.ts index ec52af453..527f43d22 100644 --- a/apps/web/app/(dashboard)/home/(user)/billing/_lib/server/user-billing.service.ts +++ b/apps/web/app/(dashboard)/home/(user)/billing/_lib/server/user-billing.service.ts @@ -17,7 +17,15 @@ import { Database } from '~/lib/database.types'; import { PersonalAccountCheckoutSchema } from '../schema/personal-account-checkout.schema'; -export class UserBillingService { +export function createUserBillingService(client: SupabaseClient) { + return new UserBillingService(client); +} + +/** + * @name UserBillingService + * @description Service for managing billing for personal accounts. + */ +class UserBillingService { private readonly namespace = 'billing.personal-account'; constructor(private readonly client: SupabaseClient) {} diff --git a/apps/web/app/(dashboard)/home/[account]/billing/_lib/server/server-actions.ts b/apps/web/app/(dashboard)/home/[account]/billing/_lib/server/server-actions.ts index d762a22d2..27bc130fb 100644 --- a/apps/web/app/(dashboard)/home/[account]/billing/_lib/server/server-actions.ts +++ b/apps/web/app/(dashboard)/home/[account]/billing/_lib/server/server-actions.ts @@ -11,7 +11,7 @@ import { TeamBillingPortalSchema, TeamCheckoutSchema, } from '../schema/team-billing.schema'; -import { TeamBillingService } from './team-billing.service'; +import { createTeamBillingService } from './team-billing.service'; /** * @name createTeamAccountCheckoutSession @@ -21,7 +21,9 @@ export async function createTeamAccountCheckoutSession( params: z.infer, ) { const data = TeamCheckoutSchema.parse(params); - const service = new TeamBillingService(getSupabaseServerActionClient()); + + const client = getSupabaseServerActionClient(); + const service = createTeamBillingService(client); return service.createCheckout(data); } @@ -33,7 +35,9 @@ export async function createTeamAccountCheckoutSession( */ export async function createBillingPortalSession(formData: FormData) { const params = TeamBillingPortalSchema.parse(Object.fromEntries(formData)); - const service = new TeamBillingService(getSupabaseServerActionClient()); + + const client = getSupabaseServerActionClient(); + const service = createTeamBillingService(client); // get url to billing portal const url = await service.createBillingPortalSession(params); diff --git a/apps/web/app/(dashboard)/home/[account]/billing/_lib/server/team-billing.service.ts b/apps/web/app/(dashboard)/home/[account]/billing/_lib/server/team-billing.service.ts index 3f80b66ff..68d389bc8 100644 --- a/apps/web/app/(dashboard)/home/[account]/billing/_lib/server/team-billing.service.ts +++ b/apps/web/app/(dashboard)/home/[account]/billing/_lib/server/team-billing.service.ts @@ -18,7 +18,15 @@ import { Database } from '~/lib/database.types'; import { TeamCheckoutSchema } from '../schema/team-billing.schema'; -export class TeamBillingService { +export function createTeamBillingService(client: SupabaseClient) { + return new TeamBillingService(client); +} + +/** + * @name TeamBillingService + * @description Service for managing billing for team accounts. + */ +class TeamBillingService { private readonly namespace = 'billing.team-account'; constructor(private readonly client: SupabaseClient) {} diff --git a/apps/web/app/(dashboard)/home/[account]/members/_lib/server/members-page.loader.ts b/apps/web/app/(dashboard)/home/[account]/members/_lib/server/members-page.loader.ts new file mode 100644 index 000000000..f9f471956 --- /dev/null +++ b/apps/web/app/(dashboard)/home/[account]/members/_lib/server/members-page.loader.ts @@ -0,0 +1,91 @@ +import 'server-only'; + +import { SupabaseClient } from '@supabase/supabase-js'; + +import { Database } from '~/lib/database.types'; + +import { loadTeamWorkspace } from '../../../_lib/server/team-account-workspace.loader'; + +/** + * Load data for the members page + * @param client + * @param slug + */ +export async function loadMembersPageData( + client: SupabaseClient, + slug: string, +) { + return Promise.all([ + loadTeamWorkspace(slug), + loadAccountMembers(client, slug), + loadInvitations(client, slug), + loadUser(client), + canAddMember, + ]); +} + +/** + * @name canAddMember + * @description Check if the current user can add a member to the account + * + * This needs additional logic to determine if the user can add a member to the account + * Please implement the logic and return a boolean value + * + * The same check needs to be added when creating an invitation + * + */ +async function canAddMember() { + return Promise.resolve(true); +} + +async function loadUser(client: SupabaseClient) { + const { data, error } = await client.auth.getUser(); + + if (error) { + throw error; + } + + return data.user; +} + +/** + * Load account members + * @param client + * @param account + */ +async function loadAccountMembers( + client: SupabaseClient, + account: string, +) { + const { data, error } = await client.rpc('get_account_members', { + account_slug: account, + }); + + if (error) { + console.error(error); + throw error; + } + + return data ?? []; +} + +/** + * Load account invitations + * @param client + * @param account + */ +async function loadInvitations( + client: SupabaseClient, + account: string, +) { + const { data, error } = await client.rpc('get_account_invitations', { + account_slug: account, + }); + + if (error) { + console.error(error); + throw error; + } + + return data ?? []; +} diff --git a/apps/web/app/(dashboard)/home/[account]/members/page.tsx b/apps/web/app/(dashboard)/home/[account]/members/page.tsx index b3d0ce107..ee398a1c6 100644 --- a/apps/web/app/(dashboard)/home/[account]/members/page.tsx +++ b/apps/web/app/(dashboard)/home/[account]/members/page.tsx @@ -1,5 +1,3 @@ -import { SupabaseClient } from '@supabase/supabase-js'; - import { PlusCircle } from 'lucide-react'; import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; @@ -20,12 +18,11 @@ import { If } from '@kit/ui/if'; import { PageBody } from '@kit/ui/page'; import { Trans } from '@kit/ui/trans'; -import { Database } from '~/lib/database.types'; import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; import { withI18n } from '~/lib/i18n/with-i18n'; import { AccountLayoutHeader } from '../_components/account-layout-header'; -import { loadTeamWorkspace } from '../_lib/server/team-account-workspace.loader'; +import { loadMembersPageData } from './_lib/server/members-page.loader'; interface Params { params: { @@ -33,57 +30,6 @@ interface Params { }; } -async function loadUser(client: SupabaseClient) { - const { data, error } = await client.auth.getUser(); - - if (error) { - throw error; - } - - return data.user; -} - -async function loadAccountMembers( - client: SupabaseClient, - account: string, -) { - const { data, error } = await client.rpc('get_account_members', { - account_slug: account, - }); - - if (error) { - console.error(error); - throw error; - } - - return data ?? []; -} - -async function loadInvitations( - client: SupabaseClient, - account: string, -) { - const { data, error } = await client.rpc('get_account_invitations', { - account_slug: account, - }); - - if (error) { - console.error(error); - throw error; - } - - return data ?? []; -} - -async function loadData(client: SupabaseClient, slug: string) { - return Promise.all([ - loadTeamWorkspace(slug), - loadAccountMembers(client, slug), - loadInvitations(client, slug), - loadUser(client), - ]); -} - export const generateMetadata = async () => { const i18n = await createI18nServerInstance(); const title = i18n.t('teams:members.pageTitle'); @@ -96,10 +42,8 @@ export const generateMetadata = async () => { async function TeamAccountMembersPage({ params }: Params) { const client = getSupabaseServerComponentClient(); - const [{ account }, members, invitations, user] = await loadData( - client, - params.account, - ); + const [{ account }, members, invitations, user, canAddMember] = + await loadMembersPageData(client, params.account); const canManageRoles = account.permissions.includes('roles.manage'); const canManageInvitations = account.permissions.includes('invites.manage'); @@ -131,7 +75,7 @@ async function TeamAccountMembersPage({ params }: Params) { - + , +) { + return new BillingGatewayService(provider); +} + /** * @description The billing gateway service to interact with the billing provider of choice (e.g. Stripe) * @class BillingGatewayService @@ -23,7 +29,7 @@ import { BillingGatewayFactoryService } from './billing-gateway-factory.service' * const provider = 'stripe'; * const billingGatewayService = new BillingGatewayService(provider); */ -export class BillingGatewayService { +class BillingGatewayService { constructor( private readonly provider: z.infer, ) {} diff --git a/packages/billing/gateway/src/server/services/billing-webhooks/billing-webhooks.service.ts b/packages/billing/gateway/src/server/services/billing-webhooks/billing-webhooks.service.ts index 76cadf49e..587b14aaa 100644 --- a/packages/billing/gateway/src/server/services/billing-webhooks/billing-webhooks.service.ts +++ b/packages/billing/gateway/src/server/services/billing-webhooks/billing-webhooks.service.ts @@ -2,13 +2,26 @@ import 'server-only'; import { Database } from '@kit/supabase/database'; -import { BillingGatewayService } from '../billing-gateway/billing-gateway.service'; +import { createBillingGatewayService } from '../billing-gateway/billing-gateway.service'; type Subscription = Database['public']['Tables']['subscriptions']['Row']; -export class BillingWebhooksService { +export function createBillingWebhooksService() { + return new BillingWebhooksService(); +} + +/** + * @name BillingWebhooksService + * @description Service for handling billing webhooks. + */ +class BillingWebhooksService { + /** + * @name handleSubscriptionDeletedWebhook + * @description Handles the webhook for when a subscription is deleted. + * @param subscription + */ async handleSubscriptionDeletedWebhook(subscription: Subscription) { - const gateway = new BillingGatewayService(subscription.billing_provider); + const gateway = createBillingGatewayService(subscription.billing_provider); await gateway.cancelSubscription({ subscriptionId: subscription.id, diff --git a/packages/database-webhooks/src/server/services/database-webhook-router.service.ts b/packages/database-webhooks/src/server/services/database-webhook-router.service.ts index a0a210bbe..dc583751a 100644 --- a/packages/database-webhooks/src/server/services/database-webhook-router.service.ts +++ b/packages/database-webhooks/src/server/services/database-webhook-router.service.ts @@ -1,5 +1,6 @@ import { SupabaseClient } from '@supabase/supabase-js'; +import { createBillingWebhooksService } from '@kit/billing-gateway'; import { getLogger } from '@kit/shared/logger'; import { Database } from '@kit/supabase/database'; @@ -42,11 +43,11 @@ export class DatabaseWebhookRouterService { } private async handleInvitationsWebhook(body: RecordChange<'invitations'>) { - const { AccountInvitationsWebhookService } = await import( + const { createAccountInvitationsWebhookService } = await import( '@kit/team-accounts/webhooks' ); - const service = new AccountInvitationsWebhookService(this.adminClient); + const service = createAccountInvitationsWebhookService(this.adminClient); return service.handleInvitationWebhook(body.record); } @@ -54,22 +55,25 @@ export class DatabaseWebhookRouterService { private async handleSubscriptionsWebhook( body: RecordChange<'subscriptions'>, ) { - const { BillingWebhooksService } = await import('@kit/billing-gateway'); - const service = new BillingWebhooksService(); - if (body.type === 'DELETE' && body.old_record) { + const { createBillingWebhooksService } = await import( + '@kit/billing-gateway' + ); + + const service = createBillingWebhooksService(); + return service.handleSubscriptionDeletedWebhook(body.old_record); } } private async handleAccountsWebhook(body: RecordChange<'accounts'>) { - const { AccountWebhooksService } = await import( - '@kit/team-accounts/webhooks' - ); - - const service = new AccountWebhooksService(); - if (body.type === 'DELETE' && body.old_record) { + const { createAccountWebhooksService } = await import( + '@kit/team-accounts/webhooks' + ); + + const service = createAccountWebhooksService(); + return service.handleAccountDeletedWebhook(body.old_record); } } diff --git a/packages/features/accounts/src/server/personal-accounts-server-actions.ts b/packages/features/accounts/src/server/personal-accounts-server-actions.ts index 81dded878..e9d30f55b 100644 --- a/packages/features/accounts/src/server/personal-accounts-server-actions.ts +++ b/packages/features/accounts/src/server/personal-accounts-server-actions.ts @@ -10,7 +10,7 @@ import { requireUser } from '@kit/supabase/require-user'; import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client'; import { DeletePersonalAccountSchema } from '../schema/delete-personal-account.schema'; -import { DeletePersonalAccountService } from './services/delete-personal-account.service'; +import { createDeletePersonalAccountService } from './services/delete-personal-account.service'; const emailSettings = getEmailSettingsFromEnvironment(); @@ -53,7 +53,7 @@ export async function deletePersonalAccountAction(formData: FormData) { const userEmail = auth.data.email ?? null; // create a new instance of the personal accounts service - const service = new DeletePersonalAccountService(); + const service = createDeletePersonalAccountService(); // delete the user's account and cancel all subscriptions await service.deletePersonalAccount({ diff --git a/packages/features/accounts/src/server/services/delete-personal-account.service.ts b/packages/features/accounts/src/server/services/delete-personal-account.service.ts index 8e8ca9b8c..17046d0de 100644 --- a/packages/features/accounts/src/server/services/delete-personal-account.service.ts +++ b/packages/features/accounts/src/server/services/delete-personal-account.service.ts @@ -5,6 +5,10 @@ import { SupabaseClient } from '@supabase/supabase-js'; import { getLogger } from '@kit/shared/logger'; import { Database } from '@kit/supabase/database'; +export function createDeletePersonalAccountService() { + return new DeletePersonalAccountService(); +} + /** * @name DeletePersonalAccountService * @description Service for managing accounts in the application @@ -13,7 +17,7 @@ import { Database } from '@kit/supabase/database'; * const client = getSupabaseClient(); * const accountsService = new DeletePersonalAccountService(); */ -export class DeletePersonalAccountService { +class DeletePersonalAccountService { private namespace = 'accounts.delete'; /** diff --git a/packages/features/admin/src/lib/server/admin-server-actions.ts b/packages/features/admin/src/lib/server/admin-server-actions.ts index 79683d9e8..348535c94 100644 --- a/packages/features/admin/src/lib/server/admin-server-actions.ts +++ b/packages/features/admin/src/lib/server/admin-server-actions.ts @@ -13,8 +13,8 @@ import { ImpersonateUserSchema, ReactivateUserSchema, } from './schema/admin-actions.schema'; -import { AdminAccountsService } from './services/admin-accounts.service'; -import { AdminAuthUserService } from './services/admin-auth-user.service'; +import { createAdminAccountsService } from './services/admin-accounts.service'; +import { createAdminAuthUserService } from './services/admin-auth-user.service'; import { adminAction } from './utils/admin-action'; /** @@ -126,13 +126,13 @@ function getAdminAuthService() { const client = getSupabaseServerActionClient(); const adminClient = getSupabaseServerActionClient({ admin: true }); - return new AdminAuthUserService(client, adminClient); + return createAdminAuthUserService(client, adminClient); } function getAdminAccountsService() { const adminClient = getSupabaseServerActionClient({ admin: true }); - return new AdminAccountsService(adminClient); + return createAdminAccountsService(adminClient); } function revalidateAdmin() { diff --git a/packages/features/admin/src/lib/server/loaders/admin-dashboard.loader.ts b/packages/features/admin/src/lib/server/loaders/admin-dashboard.loader.ts index 55d2b0510..2363b1950 100644 --- a/packages/features/admin/src/lib/server/loaders/admin-dashboard.loader.ts +++ b/packages/features/admin/src/lib/server/loaders/admin-dashboard.loader.ts @@ -4,7 +4,7 @@ import { cache } from 'react'; import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; -import { AdminDashboardService } from '../services/admin-dashboard.service'; +import { createAdminDashboardService } from '../services/admin-dashboard.service'; /** * @name loadAdminDashboard @@ -13,7 +13,7 @@ import { AdminDashboardService } from '../services/admin-dashboard.service'; */ export const loadAdminDashboard = cache(() => { const client = getSupabaseServerComponentClient({ admin: true }); - const service = new AdminDashboardService(client); + const service = createAdminDashboardService(client); return service.getDashboardData(); }); diff --git a/packages/features/admin/src/lib/server/services/admin-accounts.service.ts b/packages/features/admin/src/lib/server/services/admin-accounts.service.ts index 9dc8ab6aa..35815e0dd 100644 --- a/packages/features/admin/src/lib/server/services/admin-accounts.service.ts +++ b/packages/features/admin/src/lib/server/services/admin-accounts.service.ts @@ -4,7 +4,11 @@ import { SupabaseClient } from '@supabase/supabase-js'; import { Database } from '@kit/supabase/database'; -export class AdminAccountsService { +export function createAdminAccountsService(client: SupabaseClient) { + return new AdminAccountsService(client); +} + +class AdminAccountsService { constructor(private adminClient: SupabaseClient) {} async deleteAccount(accountId: string) { diff --git a/packages/features/admin/src/lib/server/services/admin-auth-user.service.ts b/packages/features/admin/src/lib/server/services/admin-auth-user.service.ts index 70e2b5beb..1ccc511e8 100644 --- a/packages/features/admin/src/lib/server/services/admin-auth-user.service.ts +++ b/packages/features/admin/src/lib/server/services/admin-auth-user.service.ts @@ -4,12 +4,19 @@ import { SupabaseClient } from '@supabase/supabase-js'; import { Database } from '@kit/supabase/database'; +export function createAdminAuthUserService( + client: SupabaseClient, + adminClient: SupabaseClient, +) { + return new AdminAuthUserService(client, adminClient); +} + /** * @name AdminAuthUserService * @description Service for performing admin actions on users in the system. * This service only interacts with the Supabase Auth Admin API. */ -export class AdminAuthUserService { +class AdminAuthUserService { constructor( private readonly client: SupabaseClient, private readonly adminClient: SupabaseClient, diff --git a/packages/features/admin/src/lib/server/services/admin-dashboard.service.ts b/packages/features/admin/src/lib/server/services/admin-dashboard.service.ts index 0ceaf7987..0531a17c6 100644 --- a/packages/features/admin/src/lib/server/services/admin-dashboard.service.ts +++ b/packages/features/admin/src/lib/server/services/admin-dashboard.service.ts @@ -2,9 +2,17 @@ import { SupabaseClient } from '@supabase/supabase-js'; import { Database } from '@kit/supabase/database'; +export function createAdminDashboardService(client: SupabaseClient) { + return new AdminDashboardService(client); +} + export class AdminDashboardService { constructor(private readonly client: SupabaseClient) {} + /** + * Get the dashboard data for the admin dashboard + * @param count + */ async getDashboardData( { count }: { count: 'exact' | 'estimated' | 'planned' } = { count: 'estimated', diff --git a/packages/features/team-accounts/src/server/actions/create-team-account-server-actions.ts b/packages/features/team-accounts/src/server/actions/create-team-account-server-actions.ts index 4ba268b1e..ad2a32785 100644 --- a/packages/features/team-accounts/src/server/actions/create-team-account-server-actions.ts +++ b/packages/features/team-accounts/src/server/actions/create-team-account-server-actions.ts @@ -8,7 +8,7 @@ import { requireUser } from '@kit/supabase/require-user'; import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client'; import { CreateTeamSchema } from '../../schema/create-team.schema'; -import { CreateTeamAccountService } from '../services/create-team-account.service'; +import { createCreateTeamAccountService } from '../services/create-team-account.service'; const TEAM_ACCOUNTS_HOME_PATH = z .string({ @@ -23,7 +23,6 @@ export async function createOrganizationAccountAction( const { name: accountName } = CreateTeamSchema.parse(params); const client = getSupabaseServerActionClient(); - const service = new CreateTeamAccountService(client); const auth = await requireUser(client); if (auth.error) { @@ -31,6 +30,7 @@ export async function createOrganizationAccountAction( } const userId = auth.data.id; + const service = createCreateTeamAccountService(client); const { data, error } = await service.createNewOrganizationAccount({ name: accountName, diff --git a/packages/features/team-accounts/src/server/actions/delete-team-account-server-actions.ts b/packages/features/team-accounts/src/server/actions/delete-team-account-server-actions.ts index 9436d3973..a3bbafcef 100644 --- a/packages/features/team-accounts/src/server/actions/delete-team-account-server-actions.ts +++ b/packages/features/team-accounts/src/server/actions/delete-team-account-server-actions.ts @@ -9,7 +9,7 @@ import { requireUser } from '@kit/supabase/require-user'; import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client'; import { DeleteTeamAccountSchema } from '../../schema/delete-team-account.schema'; -import { DeleteTeamAccountService } from '../services/delete-team-account.service'; +import { createDeleteTeamAccountService } from '../services/delete-team-account.service'; export async function deleteTeamAccountAction(formData: FormData) { const params = DeleteTeamAccountSchema.parse( @@ -27,7 +27,7 @@ export async function deleteTeamAccountAction(formData: FormData) { await assertUserPermissionsToDeleteTeamAccount(client, params.accountId); // Get the Supabase client and create a new service instance. - const service = new DeleteTeamAccountService(); + const service = createDeleteTeamAccountService(); // Delete the team account and all associated data. await service.deleteTeamAccount( diff --git a/packages/features/team-accounts/src/server/actions/leave-team-account-server-actions.ts b/packages/features/team-accounts/src/server/actions/leave-team-account-server-actions.ts index ab788e85e..bdaaad484 100644 --- a/packages/features/team-accounts/src/server/actions/leave-team-account-server-actions.ts +++ b/packages/features/team-accounts/src/server/actions/leave-team-account-server-actions.ts @@ -7,7 +7,7 @@ import { requireUser } from '@kit/supabase/require-user'; import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client'; import { LeaveTeamAccountSchema } from '../../schema/leave-team-account.schema'; -import { LeaveTeamAccountService } from '../services/leave-team-account.service'; +import { createLeaveTeamAccountService } from '../services/leave-team-account.service'; export async function leaveTeamAccountAction(formData: FormData) { const body = Object.fromEntries(formData.entries()); @@ -20,7 +20,7 @@ export async function leaveTeamAccountAction(formData: FormData) { throw new Error('Authentication required'); } - const service = new LeaveTeamAccountService( + const service = createLeaveTeamAccountService( getSupabaseServerActionClient({ admin: true }), ); diff --git a/packages/features/team-accounts/src/server/actions/team-invitations-server-actions.ts b/packages/features/team-accounts/src/server/actions/team-invitations-server-actions.ts index c4fbcc646..9f1a317aa 100644 --- a/packages/features/team-accounts/src/server/actions/team-invitations-server-actions.ts +++ b/packages/features/team-accounts/src/server/actions/team-invitations-server-actions.ts @@ -16,8 +16,8 @@ import { DeleteInvitationSchema } from '../../schema/delete-invitation.schema'; import { InviteMembersSchema } from '../../schema/invite-members.schema'; import { RenewInvitationSchema } from '../../schema/renew-invitation.schema'; import { UpdateInvitationSchema } from '../../schema/update-invitation.schema'; -import { AccountInvitationsService } from '../services/account-invitations.service'; -import { AccountPerSeatBillingService } from '../services/account-per-seat-billing.service'; +import { createAccountInvitationsService } from '../services/account-invitations.service'; +import { createAccountPerSeatBillingService } from '../services/account-per-seat-billing.service'; /** * Creates invitations for inviting members. @@ -34,8 +34,10 @@ export async function createInvitationsAction(params: { invitations: params.invitations, }); - const service = new AccountInvitationsService(client); + // Create the service + const service = createAccountInvitationsService(client); + // send invitations await service.sendInvitations({ invitations, accountSlug: params.accountSlug, @@ -43,7 +45,9 @@ export async function createInvitationsAction(params: { revalidateMemberPage(); - return { success: true }; + return { + success: true, + }; } /** @@ -66,8 +70,9 @@ export async function deleteInvitationAction( throw new Error(`Authentication required`); } - const service = new AccountInvitationsService(client); + const service = createAccountInvitationsService(client); + // Delete the invitation await service.deleteInvitation(invitation); revalidateMemberPage(); @@ -83,7 +88,7 @@ export async function updateInvitationAction( await assertSession(client); - const service = new AccountInvitationsService(client); + const service = createAccountInvitationsService(client); await service.updateInvitation(invitation); @@ -99,10 +104,12 @@ export async function acceptInvitationAction(data: FormData) { Object.fromEntries(data), ); - const accountPerSeatBillingService = new AccountPerSeatBillingService(client); + // Ensure the user is authenticated const user = await assertSession(client); - const service = new AccountInvitationsService(client); + // create the services + const perSeatBillingService = createAccountPerSeatBillingService(client); + const service = createAccountInvitationsService(client); // Accept the invitation const accountId = await service.acceptInvitationToTeam( @@ -113,7 +120,13 @@ export async function acceptInvitationAction(data: FormData) { }, ); - await accountPerSeatBillingService.increaseSeats(accountId); + // If the account ID is not present, throw an error + if (!accountId) { + throw new Error('Failed to accept invitation'); + } + + // Increase the seats for the account + await perSeatBillingService.increaseSeats(accountId); return redirect(nextPath); } @@ -126,13 +139,16 @@ export async function renewInvitationAction( await assertSession(client); - const service = new AccountInvitationsService(client); + const service = createAccountInvitationsService(client); + // Renew the invitation await service.renewInvitation(invitationId); revalidateMemberPage(); - return { success: true }; + return { + success: true, + }; } async function assertSession(client: SupabaseClient) { diff --git a/packages/features/team-accounts/src/server/actions/team-members-server-actions.ts b/packages/features/team-accounts/src/server/actions/team-members-server-actions.ts index d65454b7c..ba13e8faa 100644 --- a/packages/features/team-accounts/src/server/actions/team-members-server-actions.ts +++ b/packages/features/team-accounts/src/server/actions/team-members-server-actions.ts @@ -12,7 +12,7 @@ import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-clie import { RemoveMemberSchema } from '../../schema/remove-member.schema'; import { TransferOwnershipConfirmationSchema } from '../../schema/transfer-ownership-confirmation.schema'; import { UpdateMemberRoleSchema } from '../../schema/update-member-role.schema'; -import { AccountMembersService } from '../services/account-members.service'; +import { createAccountMembersService } from '../services/account-members.service'; export async function removeMemberFromAccountAction( params: z.infer, @@ -26,7 +26,7 @@ export async function removeMemberFromAccountAction( const { accountId, userId } = RemoveMemberSchema.parse(params); - const service = new AccountMembersService(client); + const service = createAccountMembersService(client); await service.removeMemberFromAccount({ accountId, @@ -46,7 +46,7 @@ export async function updateMemberRoleAction( await assertSession(client); - const service = new AccountMembersService(client); + const service = createAccountMembersService(client); const adminClient = getSupabaseServerActionClient({ admin: true }); // update the role of the member @@ -87,7 +87,7 @@ export async function transferOwnershipAction( ); } - const service = new AccountMembersService(client); + const service = createAccountMembersService(client); // at this point, the user is authenticated and is the owner of the account // so we proceed with the transfer of ownership with admin privileges @@ -105,7 +105,9 @@ export async function transferOwnershipAction( // revalidate all pages that depend on the account revalidatePath('/home/[account]', 'layout'); - return { success: true }; + return { + success: true, + }; } async function assertSession(client: SupabaseClient) { diff --git a/packages/features/team-accounts/src/server/services/account-invitations.service.ts b/packages/features/team-accounts/src/server/services/account-invitations.service.ts index 6ab580c7a..26b79f986 100644 --- a/packages/features/team-accounts/src/server/services/account-invitations.service.ts +++ b/packages/features/team-accounts/src/server/services/account-invitations.service.ts @@ -12,7 +12,17 @@ import { DeleteInvitationSchema } from '../../schema/delete-invitation.schema'; import { InviteMembersSchema } from '../../schema/invite-members.schema'; import { UpdateInvitationSchema } from '../../schema/update-invitation.schema'; -export class AccountInvitationsService { +export function createAccountInvitationsService( + client: SupabaseClient, +) { + return new AccountInvitationsService(client); +} + +/** + * @name AccountInvitationsService + * @description Service for managing account invitations. + */ +class AccountInvitationsService { private readonly namespace = 'invitations'; constructor(private readonly client: SupabaseClient) {} @@ -50,6 +60,11 @@ export class AccountInvitationsService { return data; } + /** + * @name updateInvitation + * @param params + * @description Updates an invitation in the database. + */ async updateInvitation(params: z.infer) { const logger = await getLogger(); diff --git a/packages/features/team-accounts/src/server/services/account-members.service.ts b/packages/features/team-accounts/src/server/services/account-members.service.ts index 954f14ff1..81f2759bb 100644 --- a/packages/features/team-accounts/src/server/services/account-members.service.ts +++ b/packages/features/team-accounts/src/server/services/account-members.service.ts @@ -10,13 +10,22 @@ import { Database } from '@kit/supabase/database'; import { RemoveMemberSchema } from '../../schema/remove-member.schema'; import { TransferOwnershipConfirmationSchema } from '../../schema/transfer-ownership-confirmation.schema'; import { UpdateMemberRoleSchema } from '../../schema/update-member-role.schema'; -import { AccountPerSeatBillingService } from './account-per-seat-billing.service'; +import { createAccountPerSeatBillingService } from './account-per-seat-billing.service'; -export class AccountMembersService { +export function createAccountMembersService(client: SupabaseClient) { + return new AccountMembersService(client); +} + +class AccountMembersService { private readonly namespace = 'account-members'; constructor(private readonly client: SupabaseClient) {} + /** + * @name removeMemberFromAccount + * @description Removes a member from an account. + * @param params + */ async removeMemberFromAccount(params: z.infer) { const logger = await getLogger(); @@ -52,13 +61,19 @@ export class AccountMembersService { `Successfully removed member from account. Verifying seat count...`, ); - const service = new AccountPerSeatBillingService(this.client); + const service = createAccountPerSeatBillingService(this.client); await service.decreaseSeats(params.accountId); return data; } + /** + * @name updateMemberRole + * @description Updates the role of a member in an account. + * @param params + * @param adminClient + */ async updateMemberRole( params: z.infer, adminClient: SupabaseClient, @@ -123,6 +138,12 @@ export class AccountMembersService { return data; } + /** + * @name transferOwnership + * @description Transfers ownership of an account to another user. + * @param params + * @param adminClient + */ async transferOwnership( params: z.infer, adminClient: SupabaseClient, diff --git a/packages/features/team-accounts/src/server/services/account-per-seat-billing.service.ts b/packages/features/team-accounts/src/server/services/account-per-seat-billing.service.ts index 28bebcc80..08f9eb053 100644 --- a/packages/features/team-accounts/src/server/services/account-per-seat-billing.service.ts +++ b/packages/features/team-accounts/src/server/services/account-per-seat-billing.service.ts @@ -2,15 +2,30 @@ import 'server-only'; import { SupabaseClient } from '@supabase/supabase-js'; -import { BillingGatewayService } from '@kit/billing-gateway'; +import { createBillingGatewayService } from '@kit/billing-gateway'; import { getLogger } from '@kit/shared/logger'; import { Database } from '@kit/supabase/database'; -export class AccountPerSeatBillingService { +export function createAccountPerSeatBillingService( + client: SupabaseClient, +) { + return new AccountPerSeatBillingService(client); +} + +/** + * @name AccountPerSeatBillingService + * @description Service for managing per-seat billing for accounts. + */ +class AccountPerSeatBillingService { private readonly namespace = 'accounts.per-seat-billing'; constructor(private readonly client: SupabaseClient) {} + /** + * @name getPerSeatSubscriptionItem + * @description Retrieves the per-seat subscription item for an account. + * @param accountId + */ async getPerSeatSubscriptionItem(accountId: string) { const logger = await getLogger(); const ctx = { accountId, name: this.namespace }; @@ -66,6 +81,11 @@ export class AccountPerSeatBillingService { return data; } + /** + * @name increaseSeats + * @description Increases the number of seats for an account. + * @param accountId + */ async increaseSeats(accountId: string) { const logger = await getLogger(); const subscription = await this.getPerSeatSubscriptionItem(accountId); @@ -82,7 +102,7 @@ export class AccountPerSeatBillingService { return; } - const billingGateway = new BillingGatewayService(subscription.provider); + const billingGateway = createBillingGatewayService(subscription.provider); const ctx = { name: this.namespace, @@ -133,6 +153,11 @@ export class AccountPerSeatBillingService { await Promise.all(promises); } + /** + * @name decreaseSeats + * @description Decreases the number of seats for an account. + * @param accountId + */ async decreaseSeats(accountId: string) { const logger = await getLogger(); const subscription = await this.getPerSeatSubscriptionItem(accountId); @@ -157,7 +182,7 @@ export class AccountPerSeatBillingService { logger.info(ctx, `Decreasing seats for account ${accountId}...`); - const billingGateway = new BillingGatewayService(subscription.provider); + const billingGateway = createBillingGatewayService(subscription.provider); const promises = subscriptionItems.map(async (item) => { try { diff --git a/packages/features/team-accounts/src/server/services/create-team-account.service.ts b/packages/features/team-accounts/src/server/services/create-team-account.service.ts index acf9c8ced..04d337160 100644 --- a/packages/features/team-accounts/src/server/services/create-team-account.service.ts +++ b/packages/features/team-accounts/src/server/services/create-team-account.service.ts @@ -5,7 +5,13 @@ import { SupabaseClient } from '@supabase/supabase-js'; import { getLogger } from '@kit/shared/logger'; import { Database } from '@kit/supabase/database'; -export class CreateTeamAccountService { +export function createCreateTeamAccountService( + client: SupabaseClient, +) { + return new CreateTeamAccountService(client); +} + +class CreateTeamAccountService { private readonly namespace = 'accounts.create-team-account'; constructor(private readonly client: SupabaseClient) {} diff --git a/packages/features/team-accounts/src/server/services/delete-team-account.service.ts b/packages/features/team-accounts/src/server/services/delete-team-account.service.ts index 0506c415c..75e0b3b91 100644 --- a/packages/features/team-accounts/src/server/services/delete-team-account.service.ts +++ b/packages/features/team-accounts/src/server/services/delete-team-account.service.ts @@ -5,7 +5,11 @@ import { SupabaseClient } from '@supabase/supabase-js'; import { getLogger } from '@kit/shared/logger'; import { Database } from '@kit/supabase/database'; -export class DeleteTeamAccountService { +export function createDeleteTeamAccountService() { + return new DeleteTeamAccountService(); +} + +class DeleteTeamAccountService { private readonly namespace = 'accounts.delete-team-account'; /** diff --git a/packages/features/team-accounts/src/server/services/leave-team-account.service.ts b/packages/features/team-accounts/src/server/services/leave-team-account.service.ts index b81983e01..e039a2dad 100644 --- a/packages/features/team-accounts/src/server/services/leave-team-account.service.ts +++ b/packages/features/team-accounts/src/server/services/leave-team-account.service.ts @@ -12,11 +12,26 @@ const Schema = z.object({ userId: z.string().uuid(), }); -export class LeaveTeamAccountService { +export function createLeaveTeamAccountService( + client: SupabaseClient, +) { + return new LeaveTeamAccountService(client); +} + +/** + * @name LeaveTeamAccountService + * @description Service for leaving a team account. + */ +class LeaveTeamAccountService { private readonly namespace = 'leave-team-account'; constructor(private readonly adminClient: SupabaseClient) {} + /** + * @name leaveTeamAccount + * @description Leave a team account + * @param params + */ async leaveTeamAccount(params: z.infer) { const logger = await getLogger(); diff --git a/packages/features/team-accounts/src/server/services/webhooks/account-invitations-webhook.service.ts b/packages/features/team-accounts/src/server/services/webhooks/account-invitations-webhook.service.ts index 1d830a4f6..bd638cf5b 100644 --- a/packages/features/team-accounts/src/server/services/webhooks/account-invitations-webhook.service.ts +++ b/packages/features/team-accounts/src/server/services/webhooks/account-invitations-webhook.service.ts @@ -26,7 +26,13 @@ const env = z emailSender, }); -export class AccountInvitationsWebhookService { +export function createAccountInvitationsWebhookService( + client: SupabaseClient, +) { + return new AccountInvitationsWebhookService(client); +} + +class AccountInvitationsWebhookService { private namespace = 'accounts.invitations.webhook'; constructor(private readonly adminClient: SupabaseClient) {} diff --git a/packages/features/team-accounts/src/server/services/webhooks/account-webhooks.service.ts b/packages/features/team-accounts/src/server/services/webhooks/account-webhooks.service.ts index 95678dfb7..cc24cd8a2 100644 --- a/packages/features/team-accounts/src/server/services/webhooks/account-webhooks.service.ts +++ b/packages/features/team-accounts/src/server/services/webhooks/account-webhooks.service.ts @@ -5,7 +5,11 @@ import { Database } from '@kit/supabase/database'; type Account = Database['public']['Tables']['accounts']['Row']; -export class AccountWebhooksService { +export function createAccountWebhooksService() { + return new AccountWebhooksService(); +} + +class AccountWebhooksService { private readonly namespace = 'accounts.webhooks'; async handleAccountDeletedWebhook(account: Account) {