diff --git a/packages/twenty-front/src/generated-admin/graphql.ts b/packages/twenty-front/src/generated-admin/graphql.ts index d5d5b45116..49276c6d83 100644 --- a/packages/twenty-front/src/generated-admin/graphql.ts +++ b/packages/twenty-front/src/generated-admin/graphql.ts @@ -95,6 +95,7 @@ export type AdminPanelRecentUser = { export type AdminPanelTopWorkspace = { __typename?: 'AdminPanelTopWorkspace'; id: Scalars['UUID']; + logo?: Maybe; name: Scalars['String']; subdomain: Scalars['String']; totalUsers: Scalars['Int']; @@ -107,6 +108,40 @@ export type AdminPanelWorkerQueueHealth = { status: AdminPanelHealthServiceStatus; }; +export type AdminPanelWorkspaceBilling = { + __typename?: 'AdminPanelWorkspaceBilling'; + creditBalance?: Maybe; + stripeCustomerId?: Maybe; + subscription?: Maybe; +}; + +export type AdminPanelWorkspaceSubscription = { + __typename?: 'AdminPanelWorkspaceSubscription'; + cancelAt?: Maybe; + cancelAtPeriodEnd: Scalars['Boolean']; + canceledAt?: Maybe; + currency: Scalars['String']; + currentPeriodEnd: Scalars['DateTime']; + currentPeriodStart: Scalars['DateTime']; + interval?: Maybe; + items: Array; + planKey?: Maybe; + status: SubscriptionStatus; + stripeSubscriptionId: Scalars['String']; + trialEnd?: Maybe; + trialStart?: Maybe; +}; + +export type AdminPanelWorkspaceSubscriptionItem = { + __typename?: 'AdminPanelWorkspaceSubscriptionItem'; + includedCredits?: Maybe; + productKey?: Maybe; + productName: Scalars['String']; + quantity?: Maybe; + stripePriceId: Scalars['String']; + unitAmount?: Maybe; +}; + export type AdminWorkspaceChatThread = { __typename?: 'AdminWorkspaceChatThread'; conversationSize: Scalars['Int']; @@ -447,6 +482,7 @@ export type Query = { getSystemHealthStatus: SystemHealth; userLookupAdminPanel: UserLookup; versionInfo: VersionInfo; + workspaceBillingAdminPanel?: Maybe; workspaceLookupAdminPanel: UserLookup; }; @@ -516,6 +552,11 @@ export type QueryUserLookupAdminPanelArgs = { }; +export type QueryWorkspaceBillingAdminPanelArgs = { + workspaceId: Scalars['UUID']; +}; + + export type QueryWorkspaceLookupAdminPanelArgs = { workspaceId: Scalars['UUID']; }; @@ -588,6 +629,22 @@ export type RetryJobsResponse = { retriedCount: Scalars['Int']; }; +export enum SubscriptionInterval { + Month = 'Month', + Year = 'Year' +} + +export enum SubscriptionStatus { + Active = 'Active', + Canceled = 'Canceled', + Incomplete = 'Incomplete', + IncompleteExpired = 'IncompleteExpired', + PastDue = 'PastDue', + Paused = 'Paused', + Trialing = 'Trialing', + Unpaid = 'Unpaid' +} + export type SystemHealth = { __typename?: 'SystemHealth'; services: Array; @@ -832,7 +889,7 @@ export type AdminPanelTopWorkspacesQueryVariables = Exact<{ }>; -export type AdminPanelTopWorkspacesQuery = { __typename?: 'Query', adminPanelTopWorkspaces: Array<{ __typename?: 'AdminPanelTopWorkspace', id: string, name: string, totalUsers: number, subdomain: string }> }; +export type AdminPanelTopWorkspacesQuery = { __typename?: 'Query', adminPanelTopWorkspaces: Array<{ __typename?: 'AdminPanelTopWorkspace', id: string, name: string, totalUsers: number, subdomain: string, logo?: string | null }> }; export type FindOneAdminApplicationRegistrationQueryVariables = Exact<{ id: Scalars['String']; @@ -860,6 +917,13 @@ export type GetVersionInfoQueryVariables = Exact<{ [key: string]: never; }>; export type GetVersionInfoQuery = { __typename?: 'Query', versionInfo: { __typename?: 'VersionInfo', currentVersion?: string | null, latestVersion: string } }; +export type WorkspaceBillingAdminPanelQueryVariables = Exact<{ + workspaceId: Scalars['UUID']; +}>; + + +export type WorkspaceBillingAdminPanelQuery = { __typename?: 'Query', workspaceBillingAdminPanel?: { __typename?: 'AdminPanelWorkspaceBilling', stripeCustomerId?: string | null, creditBalance?: number | null, subscription?: { __typename?: 'AdminPanelWorkspaceSubscription', stripeSubscriptionId: string, status: SubscriptionStatus, interval?: SubscriptionInterval | null, currency: string, planKey?: string | null, currentPeriodStart: string, currentPeriodEnd: string, trialStart?: string | null, trialEnd?: string | null, cancelAt?: string | null, canceledAt?: string | null, cancelAtPeriodEnd: boolean, items: Array<{ __typename?: 'AdminPanelWorkspaceSubscriptionItem', productName: string, productKey?: string | null, stripePriceId: string, quantity?: number | null, unitAmount?: number | null, includedCredits?: number | null }> } | null } | null }; + export type UserLookupAdminPanelQueryVariables = Exact<{ userIdentifier: Scalars['String']; }>; @@ -965,11 +1029,12 @@ export const GetConfigVariablesGroupedDocument = {"kind":"Document","definitions export const GetDatabaseConfigVariableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetDatabaseConfigVariable"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"key"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getDatabaseConfigVariable"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"key"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"isSensitive"}},{"kind":"Field","name":{"kind":"Name","value":"isEnvOnly"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"options"}},{"kind":"Field","name":{"kind":"Name","value":"source"}}]}}]}}]} as unknown as DocumentNode; export const UpdateWorkspaceFeatureFlagDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateWorkspaceFeatureFlag"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"featureFlag"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"value"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateWorkspaceFeatureFlag"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}},{"kind":"Argument","name":{"kind":"Name","value":"featureFlag"},"value":{"kind":"Variable","name":{"kind":"Name","value":"featureFlag"}}},{"kind":"Argument","name":{"kind":"Name","value":"value"},"value":{"kind":"Variable","name":{"kind":"Name","value":"value"}}}]}]}}]} as unknown as DocumentNode; export const AdminPanelRecentUsersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AdminPanelRecentUsers"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"searchTerm"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"adminPanelRecentUsers"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"searchTerm"},"value":{"kind":"Variable","name":{"kind":"Name","value":"searchTerm"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceName"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}}]}}]}}]} as unknown as DocumentNode; -export const AdminPanelTopWorkspacesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AdminPanelTopWorkspaces"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"searchTerm"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"adminPanelTopWorkspaces"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"searchTerm"},"value":{"kind":"Variable","name":{"kind":"Name","value":"searchTerm"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"totalUsers"}},{"kind":"Field","name":{"kind":"Name","value":"subdomain"}}]}}]}}]} as unknown as DocumentNode; +export const AdminPanelTopWorkspacesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AdminPanelTopWorkspaces"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"searchTerm"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"adminPanelTopWorkspaces"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"searchTerm"},"value":{"kind":"Variable","name":{"kind":"Name","value":"searchTerm"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"totalUsers"}},{"kind":"Field","name":{"kind":"Name","value":"subdomain"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}}]}}]}}]} as unknown as DocumentNode; export const FindOneAdminApplicationRegistrationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindOneAdminApplicationRegistration"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findOneAdminApplicationRegistration"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ApplicationRegistrationFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ApplicationRegistrationFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ApplicationRegistration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthClientId"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthRedirectUris"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthScopes"}},{"kind":"Field","name":{"kind":"Name","value":"sourceType"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePackage"}},{"kind":"Field","name":{"kind":"Name","value":"latestAvailableVersion"}},{"kind":"Field","name":{"kind":"Name","value":"isListed"}},{"kind":"Field","name":{"kind":"Name","value":"isFeatured"}},{"kind":"Field","name":{"kind":"Name","value":"isPreInstalled"}},{"kind":"Field","name":{"kind":"Name","value":"ownerWorkspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; export const GetAdminChatThreadMessagesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAdminChatThreadMessages"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getAdminChatThreadMessages"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"threadId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"thread"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"totalInputTokens"}},{"kind":"Field","name":{"kind":"Name","value":"totalOutputTokens"}},{"kind":"Field","name":{"kind":"Name","value":"conversationSize"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"messages"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"parts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"textContent"}},{"kind":"Field","name":{"kind":"Name","value":"toolName"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]}}]} as unknown as DocumentNode; export const GetAdminWorkspaceChatThreadsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAdminWorkspaceChatThreads"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getAdminWorkspaceChatThreads"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"totalInputTokens"}},{"kind":"Field","name":{"kind":"Name","value":"totalOutputTokens"}},{"kind":"Field","name":{"kind":"Name","value":"conversationSize"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode; export const GetVersionInfoDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetVersionInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"versionInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"currentVersion"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}}]}}]}}]} as unknown as DocumentNode; +export const WorkspaceBillingAdminPanelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"WorkspaceBillingAdminPanel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceBillingAdminPanel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stripeCustomerId"}},{"kind":"Field","name":{"kind":"Name","value":"creditBalance"}},{"kind":"Field","name":{"kind":"Name","value":"subscription"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stripeSubscriptionId"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"interval"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}},{"kind":"Field","name":{"kind":"Name","value":"planKey"}},{"kind":"Field","name":{"kind":"Name","value":"currentPeriodStart"}},{"kind":"Field","name":{"kind":"Name","value":"currentPeriodEnd"}},{"kind":"Field","name":{"kind":"Name","value":"trialStart"}},{"kind":"Field","name":{"kind":"Name","value":"trialEnd"}},{"kind":"Field","name":{"kind":"Name","value":"cancelAt"}},{"kind":"Field","name":{"kind":"Name","value":"canceledAt"}},{"kind":"Field","name":{"kind":"Name","value":"cancelAtPeriodEnd"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"productName"}},{"kind":"Field","name":{"kind":"Name","value":"productKey"}},{"kind":"Field","name":{"kind":"Name","value":"stripePriceId"}},{"kind":"Field","name":{"kind":"Name","value":"quantity"}},{"kind":"Field","name":{"kind":"Name","value":"unitAmount"}},{"kind":"Field","name":{"kind":"Name","value":"includedCredits"}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const UserLookupAdminPanelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserLookupAdminPanel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"userIdentifier"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userLookupAdminPanel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"userIdentifier"},"value":{"kind":"Variable","name":{"kind":"Name","value":"userIdentifier"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"UserInfoFragment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"totalUsers"}},{"kind":"Field","name":{"kind":"Name","value":"activationStatus"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"allowImpersonation"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceUrls"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"customUrl"}},{"kind":"Field","name":{"kind":"Name","value":"subdomainUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"users"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}}]}},{"kind":"Field","name":{"kind":"Name","value":"featureFlags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UserInfoFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"UserInfo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]} as unknown as DocumentNode; export const WorkspaceLookupAdminPanelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"WorkspaceLookupAdminPanel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceLookupAdminPanel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"UserInfoFragment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"allowImpersonation"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"totalUsers"}},{"kind":"Field","name":{"kind":"Name","value":"activationStatus"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceUrls"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"customUrl"}},{"kind":"Field","name":{"kind":"Name","value":"subdomainUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"users"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}}]}},{"kind":"Field","name":{"kind":"Name","value":"featureFlags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UserInfoFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"UserInfo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]} as unknown as DocumentNode; export const DeleteJobsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteJobs"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"queueName"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"jobIds"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteJobs"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"queueName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"queueName"}}},{"kind":"Argument","name":{"kind":"Name","value":"jobIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"jobIds"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deletedCount"}},{"kind":"Field","name":{"kind":"Name","value":"results"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"jobId"}},{"kind":"Field","name":{"kind":"Name","value":"success"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}}]}}]}}]} as unknown as DocumentNode; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminGeneral.tsx b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminGeneral.tsx index 4a2c0b879a..530db389f5 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminGeneral.tsx +++ b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminGeneral.tsx @@ -4,6 +4,7 @@ import { SettingsSectionSkeletonLoader } from '@/settings/components/SettingsSec import { SettingsAdminVersionContainer } from '@/settings/admin-panel/components/SettingsAdminVersionContainer'; import { ADMIN_PANEL_RECENT_USERS } from '@/settings/admin-panel/graphql/queries/adminPanelRecentUsers'; import { ADMIN_PANEL_TOP_WORKSPACES } from '@/settings/admin-panel/graphql/queries/adminPanelTopWorkspaces'; +import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo'; import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput'; import { Table } from '@/ui/layout/table/components/Table'; import { TableBody } from '@/ui/layout/table/components/TableBody'; @@ -14,21 +15,37 @@ import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomState import { useQuery } from '@apollo/client/react'; import { styled } from '@linaria/react'; import { t } from '@lingui/core/macro'; +import { isNonEmptyString } from '@sniptt/guards'; import { useState } from 'react'; import { useDebounce } from 'use-debounce'; import { SettingsPath } from 'twenty-shared/types'; -import { getSettingsPath } from 'twenty-shared/utils'; +import { getImageAbsoluteURI, getSettingsPath } from 'twenty-shared/utils'; +import { AvatarOrIcon } from 'twenty-ui/components'; import { currentUserState } from '@/auth/states/currentUserState'; import { H2Title } from 'twenty-ui/display'; import { Section } from 'twenty-ui/layout'; import { themeCssVariables } from 'twenty-ui/theme-constants'; +import { REACT_APP_SERVER_BASE_URL } from '~/config'; const StyledEmptyState = styled.div` color: ${themeCssVariables.font.color.tertiary}; padding: ${themeCssVariables.spacing[4]} 0; `; +const StyledWorkspaceCell = styled.div` + align-items: center; + display: flex; + gap: ${themeCssVariables.spacing[2]}; + min-width: 0; +`; + +const StyledWorkspaceName = styled.span` + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + export const SettingsAdminGeneral = () => { const apolloAdminClient = useApolloAdminClient(); const [userSearchTerm, setUserSearchTerm] = useState(''); @@ -64,6 +81,7 @@ export const SettingsAdminGeneral = () => { name: string; totalUsers: number; subdomain: string; + logo: string | null; }[]; }>(ADMIN_PANEL_TOP_WORKSPACES, { client: apolloAdminClient, @@ -176,7 +194,23 @@ export const SettingsAdminGeneral = () => { )} > - {workspace.name || '\u2014'} + + + + {workspace.name || '\u2014'} + + {workspace.totalUsers} diff --git a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminWorkspaceBillingContent.tsx b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminWorkspaceBillingContent.tsx new file mode 100644 index 0000000000..37e3ad7f9b --- /dev/null +++ b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminWorkspaceBillingContent.tsx @@ -0,0 +1,371 @@ +import { useQuery } from '@apollo/client/react'; +import { styled } from '@linaria/react'; +import { useLingui } from '@lingui/react/macro'; +import { isDefined } from 'twenty-shared/utils'; +import { Tag } from 'twenty-ui/components'; +import { + H2Title, + IconBox, + IconCalendarEvent, + IconCalendarRepeat, + IconCircleX, + IconCoins, + IconCreditCard, + IconExternalLink, + IconId, + IconStatusChange, + IconTag, + IconUsers, +} from 'twenty-ui/display'; +import { Section } from 'twenty-ui/layout'; +import { type ThemeColor } from 'twenty-ui/theme'; +import { themeCssVariables } from 'twenty-ui/theme-constants'; + +import { useApolloAdminClient } from '@/settings/admin-panel/apollo/hooks/useApolloAdminClient'; +import { GET_WORKSPACE_BILLING_ADMIN_PANEL } from '@/settings/admin-panel/graphql/queries/getWorkspaceBillingAdminPanel'; +import { SettingsTableCard } from '@/settings/components/SettingsTableCard'; +import { PlansTags } from '@/settings/billing/components/internal/PlansTags'; +import { SettingsSectionSkeletonLoader } from '@/settings/components/SettingsSectionSkeletonLoader'; +import { useNumberFormat } from '@/localization/hooks/useNumberFormat'; +import { beautifyExactDate } from '~/utils/date-utils'; +import { BillingPlanKey } from '~/generated-metadata/graphql'; +import { + SubscriptionInterval, + SubscriptionStatus, + type WorkspaceBillingAdminPanelQuery, +} from '~/generated-admin/graphql'; + +const STRIPE_DASHBOARD_BASE_URL = 'https://dashboard.stripe.com'; +const BASE_PRODUCT_KEY = 'BASE_PRODUCT'; +const METERED_PRODUCT_KEY = 'WORKFLOW_NODE_EXECUTION'; +const EM_DASH = '\u2014'; + +type SettingsAdminWorkspaceBillingContentProps = { + workspaceId: string; +}; + +const StyledContainer = styled.div` + display: flex; + flex-direction: column; + gap: ${themeCssVariables.spacing[3]}; + margin-top: ${themeCssVariables.spacing[6]}; +`; + +const StyledExternalLink = styled.a` + align-items: center; + color: inherit; + display: inline-flex; + gap: ${themeCssVariables.spacing[1]}; + text-decoration: none; + + &:hover { + color: ${themeCssVariables.font.color.primary}; + } +`; + +const StyledMono = styled.span` + font-family: ${themeCssVariables.code.font.family}; +`; + +const StyledItemValue = styled.div` + align-items: center; + display: flex; + gap: ${themeCssVariables.spacing[2]}; +`; + +const STATUS_COLORS: Record = { + [SubscriptionStatus.Active]: 'green', + [SubscriptionStatus.Trialing]: 'blue', + [SubscriptionStatus.PastDue]: 'orange', + [SubscriptionStatus.Canceled]: 'red', + [SubscriptionStatus.Unpaid]: 'red', + [SubscriptionStatus.Paused]: 'gray', + [SubscriptionStatus.Incomplete]: 'gray', + [SubscriptionStatus.IncompleteExpired]: 'gray', +}; + +const STATUS_LABELS: Record = { + [SubscriptionStatus.Active]: 'Active', + [SubscriptionStatus.Trialing]: 'Trialing', + [SubscriptionStatus.PastDue]: 'Past Due', + [SubscriptionStatus.Canceled]: 'Canceled', + [SubscriptionStatus.Unpaid]: 'Unpaid', + [SubscriptionStatus.Paused]: 'Paused', + [SubscriptionStatus.Incomplete]: 'Incomplete', + [SubscriptionStatus.IncompleteExpired]: 'Incomplete Expired', +}; + +const formatCurrency = (amountMinor: number, currency: string): string => { + const normalizedCurrency = currency.toUpperCase(); + + try { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: normalizedCurrency, + }).format(amountMinor / 100); + } catch { + return `${(amountMinor / 100).toFixed(2)} ${normalizedCurrency}`; + } +}; + +const toBillingPlanKey = (planKey: string): BillingPlanKey | null => + planKey === BillingPlanKey.PRO + ? BillingPlanKey.PRO + : planKey === BillingPlanKey.ENTERPRISE + ? BillingPlanKey.ENTERPRISE + : null; + +const StripeLink = ({ + path, + id, +}: { + path: 'customers' | 'subscriptions'; + id: string; +}) => ( + + {id} + + +); + +export const SettingsAdminWorkspaceBillingContent = ({ + workspaceId, +}: SettingsAdminWorkspaceBillingContentProps) => { + const { t } = useLingui(); + const { formatNumber } = useNumberFormat(); + const apolloAdminClient = useApolloAdminClient(); + + const { data, loading } = useQuery( + GET_WORKSPACE_BILLING_ADMIN_PANEL, + { + client: apolloAdminClient, + variables: { workspaceId }, + skip: !workspaceId, + }, + ); + + if (loading) { + return ( + + + + ); + } + + const billing = data?.workspaceBillingAdminPanel ?? null; + + if (!billing) { + return ( + +
+ +
+
+ ); + } + + const { stripeCustomerId, creditBalance, subscription } = billing; + + const customerItems = [ + { + Icon: IconId, + label: t`Stripe customer`, + value: isDefined(stripeCustomerId) ? ( + + ) : ( + EM_DASH + ), + }, + { + Icon: IconCoins, + label: t`Credit balance`, + value: isDefined(creditBalance) + ? `${formatNumber(creditBalance, { abbreviate: true, decimals: 2 })} ${t`credits`}` + : EM_DASH, + }, + ]; + + const intervalLabel = + subscription?.interval === SubscriptionInterval.Month + ? t`Monthly` + : subscription?.interval === SubscriptionInterval.Year + ? t`Yearly` + : null; + + const formatPeriod = (start: string, end: string): string => + `${beautifyExactDate(start)} → ${beautifyExactDate(end)}`; + + const planKey = isDefined(subscription?.planKey) + ? toBillingPlanKey(subscription.planKey) + : null; + const isTrialing = subscription?.status === SubscriptionStatus.Trialing; + + const formatItemValue = ( + item: NonNullable['items'][number], + ): string => { + const parts: string[] = []; + + if (isDefined(item.quantity)) { + parts.push(`${formatNumber(item.quantity)} ${t`seats`}`); + } + if (isDefined(item.includedCredits)) { + parts.push( + `${formatNumber(item.includedCredits, { abbreviate: true, decimals: 2 })} ${t`credits/period`}`, + ); + } + if (isDefined(item.unitAmount) && isDefined(subscription)) { + parts.push(formatCurrency(item.unitAmount, subscription.currency)); + } + + return parts.length > 0 ? parts.join(' · ') : EM_DASH; + }; + + const subscriptionItems = subscription + ? [ + { + Icon: IconCreditCard, + label: t`Stripe subscription`, + value: ( + + ), + }, + { + Icon: IconStatusChange, + label: t`Status`, + value: ( + + ), + }, + ...(isDefined(planKey) + ? [ + { + Icon: IconTag, + label: t`Plan`, + value: , + }, + ] + : []), + ...(isDefined(intervalLabel) + ? [ + { + Icon: IconCalendarEvent, + label: t`Billing interval`, + value: intervalLabel, + }, + ] + : []), + { + Icon: IconCalendarRepeat, + label: t`Current period`, + value: formatPeriod( + subscription.currentPeriodStart, + subscription.currentPeriodEnd, + ), + }, + ...(isDefined(subscription.trialStart) && + isDefined(subscription.trialEnd) + ? [ + { + Icon: IconCalendarRepeat, + label: t`Trial period`, + value: formatPeriod( + subscription.trialStart, + subscription.trialEnd, + ), + }, + ] + : []), + ...(subscription.cancelAtPeriodEnd + ? [ + { + Icon: IconCircleX, + label: t`Cancels at period end`, + value: t`Yes`, + }, + ] + : []), + ...(isDefined(subscription.cancelAt) + ? [ + { + Icon: IconCircleX, + label: t`Cancels at`, + value: beautifyExactDate(subscription.cancelAt), + }, + ] + : []), + ...(isDefined(subscription.canceledAt) + ? [ + { + Icon: IconCircleX, + label: t`Canceled at`, + value: beautifyExactDate(subscription.canceledAt), + }, + ] + : []), + ...subscription.items.map((item) => ({ + Icon: + item.productKey === BASE_PRODUCT_KEY + ? IconUsers + : item.productKey === METERED_PRODUCT_KEY + ? IconCoins + : IconBox, + label: item.productName || t`Unnamed product`, + value: ( + + {formatItemValue(item)} + {isDefined(item.productKey) && ( + + )} + + ), + })), + ] + : []; + + return ( + +
+ + +
+ +
+ + {subscription && ( + + )} +
+
+ ); +}; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/graphql/queries/adminPanelTopWorkspaces.ts b/packages/twenty-front/src/modules/settings/admin-panel/graphql/queries/adminPanelTopWorkspaces.ts index 76ca5c9270..77ce422432 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/graphql/queries/adminPanelTopWorkspaces.ts +++ b/packages/twenty-front/src/modules/settings/admin-panel/graphql/queries/adminPanelTopWorkspaces.ts @@ -7,6 +7,7 @@ export const ADMIN_PANEL_TOP_WORKSPACES = gql` name totalUsers subdomain + logo } } `; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/graphql/queries/getWorkspaceBillingAdminPanel.ts b/packages/twenty-front/src/modules/settings/admin-panel/graphql/queries/getWorkspaceBillingAdminPanel.ts new file mode 100644 index 0000000000..0064f48803 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/admin-panel/graphql/queries/getWorkspaceBillingAdminPanel.ts @@ -0,0 +1,32 @@ +import { gql } from '@apollo/client'; + +export const GET_WORKSPACE_BILLING_ADMIN_PANEL = gql` + query WorkspaceBillingAdminPanel($workspaceId: UUID!) { + workspaceBillingAdminPanel(workspaceId: $workspaceId) { + stripeCustomerId + creditBalance + subscription { + stripeSubscriptionId + status + interval + currency + planKey + currentPeriodStart + currentPeriodEnd + trialStart + trialEnd + cancelAt + canceledAt + cancelAtPeriodEnd + items { + productName + productKey + stripePriceId + quantity + unitAmount + includedCredits + } + } + } + } +`; diff --git a/packages/twenty-front/src/modules/settings/billing/components/internal/SettingsBillingLabelValueItem.tsx b/packages/twenty-front/src/modules/settings/billing/components/internal/SettingsBillingLabelValueItem.tsx index c2a33571e6..bb2cca5161 100644 --- a/packages/twenty-front/src/modules/settings/billing/components/internal/SettingsBillingLabelValueItem.tsx +++ b/packages/twenty-front/src/modules/settings/billing/components/internal/SettingsBillingLabelValueItem.tsx @@ -1,10 +1,11 @@ import { styled } from '@linaria/react'; +import { type ReactNode } from 'react'; import { AppTooltip, IconInfoCircle, TooltipDelay } from 'twenty-ui/display'; import { themeCssVariables } from 'twenty-ui/theme-constants'; type SettingsBillingLabelValueItemProps = { label: string; - value: string; + value: ReactNode; isValueInPrimaryColor?: boolean; tooltipText?: string; tooltipId?: string; diff --git a/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminWorkspaceDetail.tsx b/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminWorkspaceDetail.tsx index 2162e67f9e..80e7919b73 100644 --- a/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminWorkspaceDetail.tsx +++ b/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminWorkspaceDetail.tsx @@ -7,11 +7,13 @@ import { getSettingsPath, isDefined } from 'twenty-shared/utils'; import { currentUserState } from '@/auth/states/currentUserState'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; +import { billingState } from '@/client-config/states/billingState'; import { canManageFeatureFlagsState } from '@/client-config/states/canManageFeatureFlagsState'; import { AI_ADMIN_PATH } from '@/settings/admin-panel/ai/constants/AiAdminPath'; import { useApolloAdminClient } from '@/settings/admin-panel/apollo/hooks/useApolloAdminClient'; -import { SettingsSectionSkeletonLoader } from '@/settings/components/SettingsSectionSkeletonLoader'; +import { SettingsAdminWorkspaceBillingContent } from '@/settings/admin-panel/components/SettingsAdminWorkspaceBillingContent'; import { SettingsAdminWorkspaceContent } from '@/settings/admin-panel/components/SettingsAdminWorkspaceContent'; +import { SettingsSectionSkeletonLoader } from '@/settings/components/SettingsSectionSkeletonLoader'; import { GET_ADMIN_WORKSPACE_CHAT_THREADS } from '@/settings/admin-panel/graphql/queries/getAdminWorkspaceChatThreads'; import { WORKSPACE_LOOKUP_ADMIN_PANEL } from '@/settings/admin-panel/graphql/queries/workspaceLookupAdminPanel'; import { useFeatureFlagState } from '@/settings/admin-panel/hooks/useFeatureFlagState'; @@ -31,6 +33,7 @@ import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/use import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { H2Title, + IconCreditCard, IconEyeShare, IconFlag, IconMessage, @@ -51,6 +54,7 @@ const WORKSPACE_DETAIL_TABS_ID = 'settings-admin-workspace-detail-tabs'; const WORKSPACE_DETAIL_TAB_IDS = { INFO: 'info', + BILLING: 'billing', MEMBERS: 'members', FEATURE_FLAGS: 'feature-flags', CHATS: 'chats', @@ -67,6 +71,8 @@ export const SettingsAdminWorkspaceDetail = () => { const currentUser = useAtomStateValue(currentUserState); const currentWorkspace = useAtomStateValue(currentWorkspaceState); + const billing = useAtomStateValue(billingState); + const isBillingEnabled = billing?.isBillingEnabled ?? false; const canManageFeatureFlags = useAtomStateValue(canManageFeatureFlagsState); const { enqueueErrorSnackBar } = useSnackBar(); const { updateFeatureFlagState } = useFeatureFlagState(); @@ -135,6 +141,15 @@ export const SettingsAdminWorkspaceDetail = () => { title: t`Info`, Icon: IconSettings2, }, + ...(isBillingEnabled + ? [ + { + id: WORKSPACE_DETAIL_TAB_IDS.BILLING, + title: t`Billing`, + Icon: IconCreditCard, + }, + ] + : []), ...(currentUser?.canImpersonate ? [ { @@ -197,6 +212,12 @@ export const SettingsAdminWorkspaceDetail = () => { )} + {effectiveTabId === WORKSPACE_DETAIL_TAB_IDS.BILLING && + isBillingEnabled && + workspaceId && ( + + )} + {effectiveTabId === WORKSPACE_DETAIL_TAB_IDS.MEMBERS && workspace && (
diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.module.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.module.ts index 90ec477220..8e20852628 100644 --- a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.module.ts +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.module.ts @@ -5,6 +5,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { AdminPanelHealthService } from 'src/engine/core-modules/admin-panel/admin-panel-health.service'; import { AdminPanelQueueService } from 'src/engine/core-modules/admin-panel/admin-panel-queue.service'; import { AdminPanelResolver } from 'src/engine/core-modules/admin-panel/admin-panel.resolver'; +import { AdminPanelBillingService } from 'src/engine/core-modules/admin-panel/services/admin-panel-billing.service'; import { AdminPanelChatService } from 'src/engine/core-modules/admin-panel/services/admin-panel-chat.service'; import { AdminPanelConfigService } from 'src/engine/core-modules/admin-panel/services/admin-panel-config.service'; import { AdminPanelStatisticsService } from 'src/engine/core-modules/admin-panel/services/admin-panel-statistics.service'; @@ -14,6 +15,9 @@ import { MaintenanceModeService } from 'src/engine/core-modules/admin-panel/main import { ApplicationRegistrationModule } from 'src/engine/core-modules/application/application-registration/application-registration.module'; import { AuditModule } from 'src/engine/core-modules/audit/audit.module'; import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; +import { BillingModule } from 'src/engine/core-modules/billing/billing.module'; +import { BillingCustomerEntity } from 'src/engine/core-modules/billing/entities/billing-customer.entity'; +import { BillingPriceEntity } from 'src/engine/core-modules/billing/entities/billing-price.entity'; import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { FileModule } from 'src/engine/core-modules/file/file.module'; @@ -47,8 +51,11 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi FeatureFlagEntity, AgentChatThreadEntity, AgentMessageEntity, + BillingCustomerEntity, + BillingPriceEntity, ]), AuthModule, + BillingModule, FileModule, WorkspaceDomainsModule, RedisClientModule, @@ -69,6 +76,7 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi AdminPanelResolver, AdminPanelUserLookupService, AdminPanelStatisticsService, + AdminPanelBillingService, AdminPanelChatService, AdminPanelConfigService, AdminPanelVersionService, diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts index 0d47aa24a6..7aa7b9b9c3 100644 --- a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts @@ -11,6 +11,7 @@ import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/ import { AdminPanelHealthService } from 'src/engine/core-modules/admin-panel/admin-panel-health.service'; import { AdminPanelQueueService } from 'src/engine/core-modules/admin-panel/admin-panel-queue.service'; +import { AdminPanelBillingService } from 'src/engine/core-modules/admin-panel/services/admin-panel-billing.service'; import { AdminPanelChatService } from 'src/engine/core-modules/admin-panel/services/admin-panel-chat.service'; import { AdminPanelConfigService } from 'src/engine/core-modules/admin-panel/services/admin-panel-config.service'; import { AdminPanelStatisticsService } from 'src/engine/core-modules/admin-panel/services/admin-panel-statistics.service'; @@ -19,6 +20,7 @@ import { AdminPanelVersionService } from 'src/engine/core-modules/admin-panel/se import { MaintenanceModeService } from 'src/engine/core-modules/admin-panel/maintenance-mode.service'; import { AdminPanelRecentUserDTO } from 'src/engine/core-modules/admin-panel/dtos/admin-panel-recent-user.dto'; import { AdminPanelTopWorkspaceDTO } from 'src/engine/core-modules/admin-panel/dtos/admin-panel-top-workspace.dto'; +import { AdminPanelWorkspaceBillingDTO } from 'src/engine/core-modules/admin-panel/dtos/admin-panel-workspace-billing.dto'; import { AdminWorkspaceChatThreadDTO } from 'src/engine/core-modules/admin-panel/dtos/admin-workspace-chat-thread.dto'; import { AdminChatThreadMessagesDTO } from 'src/engine/core-modules/admin-panel/dtos/admin-chat-thread-messages.dto'; import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity'; @@ -89,6 +91,7 @@ export class AdminPanelResolver { constructor( private readonly adminUserLookupService: AdminPanelUserLookupService, private readonly adminStatisticsService: AdminPanelStatisticsService, + private readonly adminBillingService: AdminPanelBillingService, private readonly adminChatService: AdminPanelChatService, private readonly adminConfigService: AdminPanelConfigService, private readonly adminVersionService: AdminPanelVersionService, @@ -667,6 +670,14 @@ export class AdminPanelResolver { return this.adminUserLookupService.workspaceLookup(workspaceId); } + @UseGuards(ServerLevelImpersonateGuard) + @Query(() => AdminPanelWorkspaceBillingDTO, { nullable: true }) + async workspaceBillingAdminPanel( + @Args('workspaceId', { type: () => UUIDScalarType }) workspaceId: string, + ): Promise { + return this.adminBillingService.getWorkspaceBilling(workspaceId); + } + @UseGuards(ServerLevelImpersonateGuard) @Query(() => [AdminWorkspaceChatThreadDTO]) async getAdminWorkspaceChatThreads( diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/admin-panel-top-workspace.dto.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/admin-panel-top-workspace.dto.ts index 33f9ff5c57..d09a86baca 100644 --- a/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/admin-panel-top-workspace.dto.ts +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/admin-panel-top-workspace.dto.ts @@ -15,4 +15,7 @@ export class AdminPanelTopWorkspaceDTO { @Field(() => String) subdomain: string; + + @Field(() => String, { nullable: true }) + logo: string | null; } diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/admin-panel-workspace-billing.dto.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/admin-panel-workspace-billing.dto.ts new file mode 100644 index 0000000000..cf963a3fdf --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/admin-panel-workspace-billing.dto.ts @@ -0,0 +1,15 @@ +import { Field, Float, ObjectType } from '@nestjs/graphql'; + +import { AdminPanelWorkspaceSubscriptionDTO } from 'src/engine/core-modules/admin-panel/dtos/admin-panel-workspace-subscription.dto'; + +@ObjectType('AdminPanelWorkspaceBilling') +export class AdminPanelWorkspaceBillingDTO { + @Field(() => String, { nullable: true }) + stripeCustomerId: string | null; + + @Field(() => Float, { nullable: true }) + creditBalance: number | null; + + @Field(() => AdminPanelWorkspaceSubscriptionDTO, { nullable: true }) + subscription: AdminPanelWorkspaceSubscriptionDTO | null; +} diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/admin-panel-workspace-subscription-item.dto.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/admin-panel-workspace-subscription-item.dto.ts new file mode 100644 index 0000000000..686a541465 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/admin-panel-workspace-subscription-item.dto.ts @@ -0,0 +1,22 @@ +import { Field, Float, ObjectType } from '@nestjs/graphql'; + +@ObjectType('AdminPanelWorkspaceSubscriptionItem') +export class AdminPanelWorkspaceSubscriptionItemDTO { + @Field(() => String) + productName: string; + + @Field(() => String, { nullable: true }) + productKey: string | null; + + @Field(() => String) + stripePriceId: string; + + @Field(() => Float, { nullable: true }) + quantity: number | null; + + @Field(() => Float, { nullable: true }) + unitAmount: number | null; + + @Field(() => Float, { nullable: true }) + includedCredits: number | null; +} diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/admin-panel-workspace-subscription.dto.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/admin-panel-workspace-subscription.dto.ts new file mode 100644 index 0000000000..2eeeed9dda --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/admin-panel-workspace-subscription.dto.ts @@ -0,0 +1,47 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +import { AdminPanelWorkspaceSubscriptionItemDTO } from 'src/engine/core-modules/admin-panel/dtos/admin-panel-workspace-subscription-item.dto'; +import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum'; +import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum'; + +@ObjectType('AdminPanelWorkspaceSubscription') +export class AdminPanelWorkspaceSubscriptionDTO { + @Field(() => String) + stripeSubscriptionId: string; + + @Field(() => SubscriptionStatus) + status: SubscriptionStatus; + + @Field(() => SubscriptionInterval, { nullable: true }) + interval: SubscriptionInterval | null; + + @Field(() => String) + currency: string; + + @Field(() => String, { nullable: true }) + planKey: string | null; + + @Field(() => Date) + currentPeriodStart: Date; + + @Field(() => Date) + currentPeriodEnd: Date; + + @Field(() => Date, { nullable: true }) + trialStart: Date | null; + + @Field(() => Date, { nullable: true }) + trialEnd: Date | null; + + @Field(() => Date, { nullable: true }) + cancelAt: Date | null; + + @Field(() => Date, { nullable: true }) + canceledAt: Date | null; + + @Field(() => Boolean) + cancelAtPeriodEnd: boolean; + + @Field(() => [AdminPanelWorkspaceSubscriptionItemDTO]) + items: AdminPanelWorkspaceSubscriptionItemDTO[]; +} diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/services/admin-panel-billing.service.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/services/admin-panel-billing.service.ts new file mode 100644 index 0000000000..b89d1e09d6 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/services/admin-panel-billing.service.ts @@ -0,0 +1,114 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { In, type Repository } from 'typeorm'; + +import { AdminPanelWorkspaceBillingDTO } from 'src/engine/core-modules/admin-panel/dtos/admin-panel-workspace-billing.dto'; +import { BillingCustomerEntity } from 'src/engine/core-modules/billing/entities/billing-customer.entity'; +import { BillingPriceEntity } from 'src/engine/core-modules/billing/entities/billing-price.entity'; +import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-plan-key.enum'; +import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; +import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; + +const CREDIT_BALANCE_MICRO_UNIT = 1_000_000; + +const KNOWN_PLAN_KEYS: ReadonlySet = new Set( + Object.values(BillingPlanKey), +); + +@Injectable() +export class AdminPanelBillingService { + constructor( + @InjectRepository(BillingCustomerEntity) + private readonly billingCustomerRepository: Repository, + @InjectRepository(BillingPriceEntity) + private readonly billingPriceRepository: Repository, + private readonly billingSubscriptionService: BillingSubscriptionService, + private readonly twentyConfigService: TwentyConfigService, + ) {} + + async getWorkspaceBilling( + workspaceId: string, + ): Promise { + if (!this.twentyConfigService.get('IS_BILLING_ENABLED')) { + return null; + } + + const [customer, subscription] = await Promise.all([ + this.billingCustomerRepository.findOne({ where: { workspaceId } }), + this.billingSubscriptionService.getCurrentBillingSubscription({ + workspaceId, + }), + ]); + + if (!customer && !subscription) { + return null; + } + + const stripeCustomerId = + customer?.stripeCustomerId ?? subscription?.stripeCustomerId ?? null; + const creditBalance = customer + ? customer.creditBalanceMicro / CREDIT_BALANCE_MICRO_UNIT + : null; + + if (!subscription) { + return { + stripeCustomerId, + creditBalance, + subscription: null, + }; + } + + const items = subscription.billingSubscriptionItems ?? []; + const priceIds = items.map((item) => item.stripePriceId); + const prices = priceIds.length + ? await this.billingPriceRepository.find({ + where: { stripePriceId: In(priceIds) }, + }) + : []; + const priceByStripeId = new Map( + prices.map((price) => [price.stripePriceId, price]), + ); + + const planValue = subscription.metadata?.plan; + const planKey = + typeof planValue === 'string' && KNOWN_PLAN_KEYS.has(planValue) + ? planValue + : null; + + return { + stripeCustomerId, + creditBalance, + subscription: { + stripeSubscriptionId: subscription.stripeSubscriptionId, + status: subscription.status, + interval: subscription.interval ?? null, + currency: subscription.currency, + planKey, + currentPeriodStart: subscription.currentPeriodStart, + currentPeriodEnd: subscription.currentPeriodEnd, + trialStart: subscription.trialStart, + trialEnd: subscription.trialEnd, + cancelAt: subscription.cancelAt, + canceledAt: subscription.canceledAt, + cancelAtPeriodEnd: subscription.cancelAtPeriodEnd, + items: items.map((item) => { + const price = priceByStripeId.get(item.stripePriceId); + const firstTier = price?.tiers?.[0]; + const productKey = item.billingProduct?.metadata?.productKey; + + return { + productName: item.billingProduct?.name ?? '', + productKey: typeof productKey === 'string' ? productKey : null, + stripePriceId: item.stripePriceId, + quantity: item.quantity != null ? Number(item.quantity) : null, + unitAmount: + price?.unitAmount != null ? Number(price.unitAmount) : null, + includedCredits: + typeof firstTier?.up_to === 'number' ? firstTier.up_to : null, + }; + }), + }, + }; + } +} diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/services/admin-panel-statistics.service.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/services/admin-panel-statistics.service.ts index cfb7e3b19c..5fdc482b72 100644 --- a/packages/twenty-server/src/engine/core-modules/admin-panel/services/admin-panel-statistics.service.ts +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/services/admin-panel-statistics.service.ts @@ -80,7 +80,7 @@ export class AdminPanelStatisticsService { } const results = await this.workspaceRepository.manager.query( - `SELECT w.id, w."displayName" AS name, w.subdomain, COUNT(uw.id)::int AS "totalUsers" + `SELECT w.id, w."displayName" AS name, w.subdomain, w.logo, COUNT(uw.id)::int AS "totalUsers" FROM core.workspace w LEFT JOIN core."userWorkspace" uw ON uw."workspaceId" = w.id AND uw."deletedAt" IS NULL WHERE ${whereClause} @@ -95,11 +95,13 @@ export class AdminPanelStatisticsService { id: string; name: string; subdomain: string; + logo: string | null; totalUsers: number; }) => ({ id: row.id, name: row.name ?? '', subdomain: row.subdomain ?? '', + logo: row.logo ?? null, totalUsers: row.totalUsers, }), );