gergo/web 2038 billing graphql api (#3379)
* feat(gatekeeper): add gatekeeper module feature flag * feat(gatekeeper): add workspace pricing table domain * feat(gatekeeper): add checkout session creation * feat(gatekeeper): verify stripe signature * wip(gatekeeper): checkout callbacks * feat(gatekeeper): add unlimited and academia plan types * refactor(envHelper): getStringFromEnv helper * chore(gatekeeper): add future todos * feat(gatekeeper): add productId to the subscription domain * feat(gatekeeper): add in memory repositories * feat(gatekeeper): add more errors * feat(gatekeeper): complete checkout session service * feat(gatekeeper): add stripe client implementation * feat(gatekeeper): add checkout session completion webhook callback path * feat(gendo): fix not needing env vars if gendo module is not enabled * feat(gatekeeper): require a license for billing * chore(gatekeeper): cleanup before testing * feat(gatekeeper): subscriptionData parsing model * ci: add billing integration and gatekeeper modules to test config * test(gatekeeper): add checkout service tests * feat(gatekeeper): make completeCheckout callback idempotent properly * feat(gatekeeper): move to knex based repositories * test(gatekeeper): billing repository tests * feat(gatekeeper): add yearly billing cycle toggle * feat(ci): add stripe integration context to test job * feat(billingPage): conditionally render the checkout CTAs * fix(gatekeeper): remove flaky test condition * feat(helm): add billing integration feature flag * WIP billing gql api * feat(gatekeeper): cancel checkout session api * feat(gatekeeper): handle existing checkout sessions, when trying to create a new one * feat(gatekeeper): add workspace plans gql api * feat(gatekeeper): handle cancelation and subscription updates * fix(gatekeeper): scope initialization * fix(gatekeeper): eliminate stripe client import sideeffect * fix(gatekeeper): eliminate stripe client import sideeffect 2 * fix(mainConstants): fitler gatekeeper scopes with feature flag
This commit is contained in:
Родитель
d4241b04f5
Коммит
af3857a209
|
@ -428,6 +428,11 @@ export type BasicGitRepositoryMetadata = {
|
|||
url: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export enum BillingInterval {
|
||||
Monthly = 'monthly',
|
||||
Yearly = 'yearly'
|
||||
}
|
||||
|
||||
export type BlobMetadata = {
|
||||
__typename?: 'BlobMetadata';
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
|
@ -505,6 +510,28 @@ export type BranchUpdateInput = {
|
|||
streamId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type CancelCheckoutSessionInput = {
|
||||
sessionId: Scalars['ID']['input'];
|
||||
workspaceId: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
export type CheckoutSession = {
|
||||
__typename?: 'CheckoutSession';
|
||||
billingInterval: BillingInterval;
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
id: Scalars['ID']['output'];
|
||||
paymentStatus: SessionPaymentStatus;
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
url: Scalars['String']['output'];
|
||||
workspacePlan: PaidWorkspacePlans;
|
||||
};
|
||||
|
||||
export type CheckoutSessionInput = {
|
||||
billingInterval: BillingInterval;
|
||||
workspaceId: Scalars['ID']['input'];
|
||||
workspacePlan: PaidWorkspacePlans;
|
||||
};
|
||||
|
||||
export type Comment = {
|
||||
__typename?: 'Comment';
|
||||
archived: Scalars['Boolean']['output'];
|
||||
|
@ -1709,6 +1736,12 @@ export type ObjectCreateInput = {
|
|||
streamId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export enum PaidWorkspacePlans {
|
||||
Business = 'business',
|
||||
Pro = 'pro',
|
||||
Team = 'team'
|
||||
}
|
||||
|
||||
export type PasswordStrengthCheckFeedback = {
|
||||
__typename?: 'PasswordStrengthCheckFeedback';
|
||||
suggestions: Array<Scalars['String']['output']>;
|
||||
|
@ -2852,6 +2885,11 @@ export type ServerWorkspacesInfo = {
|
|||
workspacesEnabled: Scalars['Boolean']['output'];
|
||||
};
|
||||
|
||||
export enum SessionPaymentStatus {
|
||||
Paid = 'paid',
|
||||
Unpaid = 'unpaid'
|
||||
}
|
||||
|
||||
export type SetPrimaryUserEmailInput = {
|
||||
id: Scalars['ID']['input'];
|
||||
};
|
||||
|
@ -3889,6 +3927,7 @@ export type Workspace = {
|
|||
/** Billing data for Workspaces beta */
|
||||
billing?: Maybe<WorkspaceBilling>;
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
customerPortalUrl?: Maybe<Scalars['String']['output']>;
|
||||
/** Selected fallback when `logo` not set */
|
||||
defaultLogoIndex: Scalars['Int']['output'];
|
||||
/** The default role workspace members will receive for workspace projects. */
|
||||
|
@ -3906,10 +3945,12 @@ export type Workspace = {
|
|||
/** Logo image as base64-encoded string */
|
||||
logo?: Maybe<Scalars['String']['output']>;
|
||||
name: Scalars['String']['output'];
|
||||
plan?: Maybe<WorkspacePlan>;
|
||||
projects: ProjectCollection;
|
||||
/** Active user's role for this workspace. `null` if request is not authenticated, or the workspace is not explicitly shared with you. */
|
||||
role?: Maybe<Scalars['String']['output']>;
|
||||
slug: Scalars['String']['output'];
|
||||
subscription?: Maybe<WorkspaceSubscription>;
|
||||
team: WorkspaceCollaboratorCollection;
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
};
|
||||
|
@ -3939,6 +3980,22 @@ export type WorkspaceBilling = {
|
|||
versionsCount: WorkspaceVersionsCount;
|
||||
};
|
||||
|
||||
export type WorkspaceBillingMutations = {
|
||||
__typename?: 'WorkspaceBillingMutations';
|
||||
cancelCheckoutSession: Scalars['Boolean']['output'];
|
||||
createCheckoutSession: CheckoutSession;
|
||||
};
|
||||
|
||||
|
||||
export type WorkspaceBillingMutationsCancelCheckoutSessionArgs = {
|
||||
input: CancelCheckoutSessionInput;
|
||||
};
|
||||
|
||||
|
||||
export type WorkspaceBillingMutationsCreateCheckoutSessionArgs = {
|
||||
input: CheckoutSessionInput;
|
||||
};
|
||||
|
||||
/** Overridden by `WorkspaceCollaboratorGraphQLReturn` */
|
||||
export type WorkspaceCollaborator = {
|
||||
__typename?: 'WorkspaceCollaborator';
|
||||
|
@ -4080,6 +4137,7 @@ export type WorkspaceInviteUseInput = {
|
|||
export type WorkspaceMutations = {
|
||||
__typename?: 'WorkspaceMutations';
|
||||
addDomain: Workspace;
|
||||
billing: WorkspaceBillingMutations;
|
||||
create: Workspace;
|
||||
delete: Scalars['Boolean']['output'];
|
||||
deleteDomain: Workspace;
|
||||
|
@ -4131,6 +4189,27 @@ export type WorkspaceMutationsUpdateRoleArgs = {
|
|||
input: WorkspaceRoleUpdateInput;
|
||||
};
|
||||
|
||||
export type WorkspacePlan = {
|
||||
__typename?: 'WorkspacePlan';
|
||||
name: WorkspacePlans;
|
||||
status: WorkspacePlanStatuses;
|
||||
};
|
||||
|
||||
export enum WorkspacePlanStatuses {
|
||||
Canceled = 'canceled',
|
||||
PaymentFailed = 'paymentFailed',
|
||||
Trial = 'trial',
|
||||
Valid = 'valid'
|
||||
}
|
||||
|
||||
export enum WorkspacePlans {
|
||||
Academia = 'academia',
|
||||
Business = 'business',
|
||||
Pro = 'pro',
|
||||
Team = 'team',
|
||||
Unlimited = 'unlimited'
|
||||
}
|
||||
|
||||
export type WorkspaceProjectInviteCreateInput = {
|
||||
/** Either this or userId must be filled */
|
||||
email?: InputMaybe<Scalars['String']['input']>;
|
||||
|
@ -4184,6 +4263,14 @@ export type WorkspaceRoleUpdateInput = {
|
|||
workspaceId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type WorkspaceSubscription = {
|
||||
__typename?: 'WorkspaceSubscription';
|
||||
billingInterval: BillingInterval;
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
currentBillingCycleEnd: Scalars['DateTime']['output'];
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
};
|
||||
|
||||
export type WorkspaceTeamFilter = {
|
||||
/** Limit team members to provided role(s) */
|
||||
roles?: InputMaybe<Array<Scalars['String']['input']>>;
|
||||
|
@ -6142,6 +6229,7 @@ export type AllObjectTypes = {
|
|||
BlobMetadataCollection: BlobMetadataCollection,
|
||||
Branch: Branch,
|
||||
BranchCollection: BranchCollection,
|
||||
CheckoutSession: CheckoutSession,
|
||||
Comment: Comment,
|
||||
CommentActivityMessage: CommentActivityMessage,
|
||||
CommentCollection: CommentCollection,
|
||||
|
@ -6236,6 +6324,7 @@ export type AllObjectTypes = {
|
|||
WebhookEventCollection: WebhookEventCollection,
|
||||
Workspace: Workspace,
|
||||
WorkspaceBilling: WorkspaceBilling,
|
||||
WorkspaceBillingMutations: WorkspaceBillingMutations,
|
||||
WorkspaceCollaborator: WorkspaceCollaborator,
|
||||
WorkspaceCollaboratorCollection: WorkspaceCollaboratorCollection,
|
||||
WorkspaceCollection: WorkspaceCollection,
|
||||
|
@ -6245,7 +6334,9 @@ export type AllObjectTypes = {
|
|||
WorkspaceDomain: WorkspaceDomain,
|
||||
WorkspaceInviteMutations: WorkspaceInviteMutations,
|
||||
WorkspaceMutations: WorkspaceMutations,
|
||||
WorkspacePlan: WorkspacePlan,
|
||||
WorkspaceProjectMutations: WorkspaceProjectMutations,
|
||||
WorkspaceSubscription: WorkspaceSubscription,
|
||||
WorkspaceVersionsCount: WorkspaceVersionsCount,
|
||||
}
|
||||
export type ActiveUserMutationsFieldArgs = {
|
||||
|
@ -6463,6 +6554,15 @@ export type BranchCollectionFieldArgs = {
|
|||
items: {},
|
||||
totalCount: {},
|
||||
}
|
||||
export type CheckoutSessionFieldArgs = {
|
||||
billingInterval: {},
|
||||
createdAt: {},
|
||||
id: {},
|
||||
paymentStatus: {},
|
||||
updatedAt: {},
|
||||
url: {},
|
||||
workspacePlan: {},
|
||||
}
|
||||
export type CommentFieldArgs = {
|
||||
archived: {},
|
||||
author: {},
|
||||
|
@ -7300,6 +7400,7 @@ export type WebhookEventCollectionFieldArgs = {
|
|||
export type WorkspaceFieldArgs = {
|
||||
billing: {},
|
||||
createdAt: {},
|
||||
customerPortalUrl: {},
|
||||
defaultLogoIndex: {},
|
||||
defaultProjectRole: {},
|
||||
description: {},
|
||||
|
@ -7310,9 +7411,11 @@ export type WorkspaceFieldArgs = {
|
|||
invitedTeam: WorkspaceInvitedTeamArgs,
|
||||
logo: {},
|
||||
name: {},
|
||||
plan: {},
|
||||
projects: WorkspaceProjectsArgs,
|
||||
role: {},
|
||||
slug: {},
|
||||
subscription: {},
|
||||
team: WorkspaceTeamArgs,
|
||||
updatedAt: {},
|
||||
}
|
||||
|
@ -7320,6 +7423,10 @@ export type WorkspaceBillingFieldArgs = {
|
|||
cost: {},
|
||||
versionsCount: {},
|
||||
}
|
||||
export type WorkspaceBillingMutationsFieldArgs = {
|
||||
cancelCheckoutSession: WorkspaceBillingMutationsCancelCheckoutSessionArgs,
|
||||
createCheckoutSession: WorkspaceBillingMutationsCreateCheckoutSessionArgs,
|
||||
}
|
||||
export type WorkspaceCollaboratorFieldArgs = {
|
||||
id: {},
|
||||
projectRoles: {},
|
||||
|
@ -7366,6 +7473,7 @@ export type WorkspaceInviteMutationsFieldArgs = {
|
|||
}
|
||||
export type WorkspaceMutationsFieldArgs = {
|
||||
addDomain: WorkspaceMutationsAddDomainArgs,
|
||||
billing: {},
|
||||
create: WorkspaceMutationsCreateArgs,
|
||||
delete: WorkspaceMutationsDeleteArgs,
|
||||
deleteDomain: WorkspaceMutationsDeleteDomainArgs,
|
||||
|
@ -7376,10 +7484,20 @@ export type WorkspaceMutationsFieldArgs = {
|
|||
update: WorkspaceMutationsUpdateArgs,
|
||||
updateRole: WorkspaceMutationsUpdateRoleArgs,
|
||||
}
|
||||
export type WorkspacePlanFieldArgs = {
|
||||
name: {},
|
||||
status: {},
|
||||
}
|
||||
export type WorkspaceProjectMutationsFieldArgs = {
|
||||
moveToWorkspace: WorkspaceProjectMutationsMoveToWorkspaceArgs,
|
||||
updateRole: WorkspaceProjectMutationsUpdateRoleArgs,
|
||||
}
|
||||
export type WorkspaceSubscriptionFieldArgs = {
|
||||
billingInterval: {},
|
||||
createdAt: {},
|
||||
currentBillingCycleEnd: {},
|
||||
updatedAt: {},
|
||||
}
|
||||
export type WorkspaceVersionsCountFieldArgs = {
|
||||
current: {},
|
||||
max: {},
|
||||
|
@ -7416,6 +7534,7 @@ export type AllObjectFieldArgTypes = {
|
|||
BlobMetadataCollection: BlobMetadataCollectionFieldArgs,
|
||||
Branch: BranchFieldArgs,
|
||||
BranchCollection: BranchCollectionFieldArgs,
|
||||
CheckoutSession: CheckoutSessionFieldArgs,
|
||||
Comment: CommentFieldArgs,
|
||||
CommentActivityMessage: CommentActivityMessageFieldArgs,
|
||||
CommentCollection: CommentCollectionFieldArgs,
|
||||
|
@ -7510,6 +7629,7 @@ export type AllObjectFieldArgTypes = {
|
|||
WebhookEventCollection: WebhookEventCollectionFieldArgs,
|
||||
Workspace: WorkspaceFieldArgs,
|
||||
WorkspaceBilling: WorkspaceBillingFieldArgs,
|
||||
WorkspaceBillingMutations: WorkspaceBillingMutationsFieldArgs,
|
||||
WorkspaceCollaborator: WorkspaceCollaboratorFieldArgs,
|
||||
WorkspaceCollaboratorCollection: WorkspaceCollaboratorCollectionFieldArgs,
|
||||
WorkspaceCollection: WorkspaceCollectionFieldArgs,
|
||||
|
@ -7519,7 +7639,9 @@ export type AllObjectFieldArgTypes = {
|
|||
WorkspaceDomain: WorkspaceDomainFieldArgs,
|
||||
WorkspaceInviteMutations: WorkspaceInviteMutationsFieldArgs,
|
||||
WorkspaceMutations: WorkspaceMutationsFieldArgs,
|
||||
WorkspacePlan: WorkspacePlanFieldArgs,
|
||||
WorkspaceProjectMutations: WorkspaceProjectMutationsFieldArgs,
|
||||
WorkspaceSubscription: WorkspaceSubscriptionFieldArgs,
|
||||
WorkspaceVersionsCount: WorkspaceVersionsCountFieldArgs,
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,85 @@
|
|||
extend type Query {
|
||||
workspacePricingPlans: JSONObject!
|
||||
}
|
||||
|
||||
extend type WorkspaceMutations {
|
||||
billing: WorkspaceBillingMutations! @hasScope(scope: "workspace:billing")
|
||||
}
|
||||
|
||||
enum PaidWorkspacePlans {
|
||||
team
|
||||
pro
|
||||
business
|
||||
}
|
||||
|
||||
enum BillingInterval {
|
||||
monthly
|
||||
yearly
|
||||
}
|
||||
|
||||
enum SessionPaymentStatus {
|
||||
paid
|
||||
unpaid
|
||||
}
|
||||
|
||||
input CheckoutSessionInput {
|
||||
workspaceId: ID!
|
||||
workspacePlan: PaidWorkspacePlans!
|
||||
billingInterval: BillingInterval!
|
||||
}
|
||||
|
||||
type CheckoutSession {
|
||||
id: ID!
|
||||
url: String!
|
||||
workspacePlan: PaidWorkspacePlans!
|
||||
paymentStatus: SessionPaymentStatus!
|
||||
billingInterval: BillingInterval!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
input CancelCheckoutSessionInput {
|
||||
sessionId: ID!
|
||||
workspaceId: ID!
|
||||
}
|
||||
|
||||
type WorkspaceBillingMutations {
|
||||
createCheckoutSession(input: CheckoutSessionInput!): CheckoutSession!
|
||||
cancelCheckoutSession(input: CancelCheckoutSessionInput!): Boolean!
|
||||
}
|
||||
|
||||
enum WorkspacePlans {
|
||||
team
|
||||
pro
|
||||
business
|
||||
unlimited
|
||||
academia
|
||||
}
|
||||
|
||||
enum WorkspacePlanStatuses {
|
||||
valid
|
||||
paymentFailed
|
||||
canceled
|
||||
trial
|
||||
}
|
||||
|
||||
type WorkspacePlan {
|
||||
name: WorkspacePlans!
|
||||
status: WorkspacePlanStatuses!
|
||||
}
|
||||
|
||||
type WorkspaceSubscription {
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
currentBillingCycleEnd: DateTime!
|
||||
billingInterval: BillingInterval!
|
||||
}
|
||||
|
||||
extend type Workspace {
|
||||
# for now, this is nullable, cause existing workspaces have not been migrated to plans
|
||||
# this doesn't need a special token scope
|
||||
plan: WorkspacePlan
|
||||
subscription: WorkspaceSubscription @hasScope(scope: "workspace:billing")
|
||||
# this can only be created if there is an active subscription
|
||||
customerPortalUrl: String @hasScope(scope: "workspace:billing")
|
||||
}
|
||||
|
|
|
@ -61,6 +61,7 @@ generates:
|
|||
WorkspaceMutations: '@/modules/workspacesCore/helpers/graphTypes#WorkspaceMutationsGraphQLReturn'
|
||||
WorkspaceInviteMutations: '@/modules/workspacesCore/helpers/graphTypes#WorkspaceInviteMutationsGraphQLReturn'
|
||||
WorkspaceProjectMutations: '@/modules/workspacesCore/helpers/graphTypes#WorkspaceProjectMutationsGraphQLReturn'
|
||||
WorkspaceBillingMutations: '@/modules/gatekeeper/helpers/graphTypes#WorkspaceBillingMutationsGraphQLReturn'
|
||||
PendingWorkspaceCollaborator: '@/modules/workspacesCore/helpers/graphTypes#PendingWorkspaceCollaboratorGraphQLReturn'
|
||||
WorkspaceCollaborator: '@/modules/workspacesCore/helpers/graphTypes#WorkspaceCollaboratorGraphQLReturn'
|
||||
Webhook: '@/modules/webhooks/helpers/graphTypes#WebhookGraphQLReturn'
|
||||
|
|
|
@ -6,6 +6,7 @@ import { PendingStreamCollaboratorGraphQLReturn } from '@/modules/serverinvites/
|
|||
import { FileUploadGraphQLReturn } from '@/modules/fileuploads/helpers/types';
|
||||
import { AutomateFunctionGraphQLReturn, AutomateFunctionReleaseGraphQLReturn, AutomationGraphQLReturn, AutomationRevisionGraphQLReturn, AutomationRevisionFunctionGraphQLReturn, AutomateRunGraphQLReturn, AutomationRunTriggerGraphQLReturn, AutomationRevisionTriggerDefinitionGraphQLReturn, AutomateFunctionRunGraphQLReturn, TriggeredAutomationsStatusGraphQLReturn, ProjectAutomationMutationsGraphQLReturn, ProjectTriggeredAutomationsStatusUpdatedMessageGraphQLReturn, ProjectAutomationsUpdatedMessageGraphQLReturn, UserAutomateInfoGraphQLReturn } from '@/modules/automate/helpers/graphTypes';
|
||||
import { WorkspaceGraphQLReturn, WorkspaceBillingGraphQLReturn, WorkspaceMutationsGraphQLReturn, WorkspaceInviteMutationsGraphQLReturn, WorkspaceProjectMutationsGraphQLReturn, PendingWorkspaceCollaboratorGraphQLReturn, WorkspaceCollaboratorGraphQLReturn, ProjectRoleGraphQLReturn } from '@/modules/workspacesCore/helpers/graphTypes';
|
||||
import { WorkspaceBillingMutationsGraphQLReturn } from '@/modules/gatekeeper/helpers/graphTypes';
|
||||
import { WebhookGraphQLReturn } from '@/modules/webhooks/helpers/graphTypes';
|
||||
import { SmartTextEditorValueSchema } from '@/modules/core/services/richTextEditorService';
|
||||
import { BlobStorageItem } from '@/modules/blobstorage/domain/types';
|
||||
|
@ -442,6 +443,11 @@ export type BasicGitRepositoryMetadata = {
|
|||
url: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export enum BillingInterval {
|
||||
Monthly = 'monthly',
|
||||
Yearly = 'yearly'
|
||||
}
|
||||
|
||||
export type BlobMetadata = {
|
||||
__typename?: 'BlobMetadata';
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
|
@ -519,6 +525,28 @@ export type BranchUpdateInput = {
|
|||
streamId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type CancelCheckoutSessionInput = {
|
||||
sessionId: Scalars['ID']['input'];
|
||||
workspaceId: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
export type CheckoutSession = {
|
||||
__typename?: 'CheckoutSession';
|
||||
billingInterval: BillingInterval;
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
id: Scalars['ID']['output'];
|
||||
paymentStatus: SessionPaymentStatus;
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
url: Scalars['String']['output'];
|
||||
workspacePlan: PaidWorkspacePlans;
|
||||
};
|
||||
|
||||
export type CheckoutSessionInput = {
|
||||
billingInterval: BillingInterval;
|
||||
workspaceId: Scalars['ID']['input'];
|
||||
workspacePlan: PaidWorkspacePlans;
|
||||
};
|
||||
|
||||
export type Comment = {
|
||||
__typename?: 'Comment';
|
||||
archived: Scalars['Boolean']['output'];
|
||||
|
@ -1728,6 +1756,12 @@ export type ObjectCreateInput = {
|
|||
streamId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export enum PaidWorkspacePlans {
|
||||
Business = 'business',
|
||||
Pro = 'pro',
|
||||
Team = 'team'
|
||||
}
|
||||
|
||||
export type PasswordStrengthCheckFeedback = {
|
||||
__typename?: 'PasswordStrengthCheckFeedback';
|
||||
suggestions: Array<Scalars['String']['output']>;
|
||||
|
@ -2871,6 +2905,11 @@ export type ServerWorkspacesInfo = {
|
|||
workspacesEnabled: Scalars['Boolean']['output'];
|
||||
};
|
||||
|
||||
export enum SessionPaymentStatus {
|
||||
Paid = 'paid',
|
||||
Unpaid = 'unpaid'
|
||||
}
|
||||
|
||||
export type SetPrimaryUserEmailInput = {
|
||||
id: Scalars['ID']['input'];
|
||||
};
|
||||
|
@ -3908,6 +3947,7 @@ export type Workspace = {
|
|||
/** Billing data for Workspaces beta */
|
||||
billing?: Maybe<WorkspaceBilling>;
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
customerPortalUrl?: Maybe<Scalars['String']['output']>;
|
||||
/** Selected fallback when `logo` not set */
|
||||
defaultLogoIndex: Scalars['Int']['output'];
|
||||
/** The default role workspace members will receive for workspace projects. */
|
||||
|
@ -3925,10 +3965,12 @@ export type Workspace = {
|
|||
/** Logo image as base64-encoded string */
|
||||
logo?: Maybe<Scalars['String']['output']>;
|
||||
name: Scalars['String']['output'];
|
||||
plan?: Maybe<WorkspacePlan>;
|
||||
projects: ProjectCollection;
|
||||
/** Active user's role for this workspace. `null` if request is not authenticated, or the workspace is not explicitly shared with you. */
|
||||
role?: Maybe<Scalars['String']['output']>;
|
||||
slug: Scalars['String']['output'];
|
||||
subscription?: Maybe<WorkspaceSubscription>;
|
||||
team: WorkspaceCollaboratorCollection;
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
};
|
||||
|
@ -3958,6 +4000,22 @@ export type WorkspaceBilling = {
|
|||
versionsCount: WorkspaceVersionsCount;
|
||||
};
|
||||
|
||||
export type WorkspaceBillingMutations = {
|
||||
__typename?: 'WorkspaceBillingMutations';
|
||||
cancelCheckoutSession: Scalars['Boolean']['output'];
|
||||
createCheckoutSession: CheckoutSession;
|
||||
};
|
||||
|
||||
|
||||
export type WorkspaceBillingMutationsCancelCheckoutSessionArgs = {
|
||||
input: CancelCheckoutSessionInput;
|
||||
};
|
||||
|
||||
|
||||
export type WorkspaceBillingMutationsCreateCheckoutSessionArgs = {
|
||||
input: CheckoutSessionInput;
|
||||
};
|
||||
|
||||
/** Overridden by `WorkspaceCollaboratorGraphQLReturn` */
|
||||
export type WorkspaceCollaborator = {
|
||||
__typename?: 'WorkspaceCollaborator';
|
||||
|
@ -4099,6 +4157,7 @@ export type WorkspaceInviteUseInput = {
|
|||
export type WorkspaceMutations = {
|
||||
__typename?: 'WorkspaceMutations';
|
||||
addDomain: Workspace;
|
||||
billing: WorkspaceBillingMutations;
|
||||
create: Workspace;
|
||||
delete: Scalars['Boolean']['output'];
|
||||
deleteDomain: Workspace;
|
||||
|
@ -4150,6 +4209,27 @@ export type WorkspaceMutationsUpdateRoleArgs = {
|
|||
input: WorkspaceRoleUpdateInput;
|
||||
};
|
||||
|
||||
export type WorkspacePlan = {
|
||||
__typename?: 'WorkspacePlan';
|
||||
name: WorkspacePlans;
|
||||
status: WorkspacePlanStatuses;
|
||||
};
|
||||
|
||||
export enum WorkspacePlanStatuses {
|
||||
Canceled = 'canceled',
|
||||
PaymentFailed = 'paymentFailed',
|
||||
Trial = 'trial',
|
||||
Valid = 'valid'
|
||||
}
|
||||
|
||||
export enum WorkspacePlans {
|
||||
Academia = 'academia',
|
||||
Business = 'business',
|
||||
Pro = 'pro',
|
||||
Team = 'team',
|
||||
Unlimited = 'unlimited'
|
||||
}
|
||||
|
||||
export type WorkspaceProjectInviteCreateInput = {
|
||||
/** Either this or userId must be filled */
|
||||
email?: InputMaybe<Scalars['String']['input']>;
|
||||
|
@ -4203,6 +4283,14 @@ export type WorkspaceRoleUpdateInput = {
|
|||
workspaceId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type WorkspaceSubscription = {
|
||||
__typename?: 'WorkspaceSubscription';
|
||||
billingInterval: BillingInterval;
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
currentBillingCycleEnd: Scalars['DateTime']['output'];
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
};
|
||||
|
||||
export type WorkspaceTeamFilter = {
|
||||
/** Limit team members to provided role(s) */
|
||||
roles?: InputMaybe<Array<Scalars['String']['input']>>;
|
||||
|
@ -4345,6 +4433,7 @@ export type ResolversTypes = {
|
|||
AvatarUser: ResolverTypeWrapper<AvatarUser>;
|
||||
BasicGitRepositoryMetadata: ResolverTypeWrapper<BasicGitRepositoryMetadata>;
|
||||
BigInt: ResolverTypeWrapper<Scalars['BigInt']['output']>;
|
||||
BillingInterval: BillingInterval;
|
||||
BlobMetadata: ResolverTypeWrapper<BlobStorageItem>;
|
||||
BlobMetadataCollection: ResolverTypeWrapper<Omit<BlobMetadataCollection, 'items'> & { items?: Maybe<Array<ResolversTypes['BlobMetadata']>> }>;
|
||||
Boolean: ResolverTypeWrapper<Scalars['Boolean']['output']>;
|
||||
|
@ -4353,6 +4442,9 @@ export type ResolversTypes = {
|
|||
BranchCreateInput: BranchCreateInput;
|
||||
BranchDeleteInput: BranchDeleteInput;
|
||||
BranchUpdateInput: BranchUpdateInput;
|
||||
CancelCheckoutSessionInput: CancelCheckoutSessionInput;
|
||||
CheckoutSession: ResolverTypeWrapper<CheckoutSession>;
|
||||
CheckoutSessionInput: CheckoutSessionInput;
|
||||
Comment: ResolverTypeWrapper<CommentGraphQLReturn>;
|
||||
CommentActivityMessage: ResolverTypeWrapper<Omit<CommentActivityMessage, 'comment'> & { comment: ResolversTypes['Comment'] }>;
|
||||
CommentCollection: ResolverTypeWrapper<Omit<CommentCollection, 'items'> & { items: Array<ResolversTypes['Comment']> }>;
|
||||
|
@ -4412,6 +4504,7 @@ export type ResolversTypes = {
|
|||
Object: ResolverTypeWrapper<ObjectGraphQLReturn>;
|
||||
ObjectCollection: ResolverTypeWrapper<Omit<ObjectCollection, 'objects'> & { objects: Array<ResolversTypes['Object']> }>;
|
||||
ObjectCreateInput: ObjectCreateInput;
|
||||
PaidWorkspacePlans: PaidWorkspacePlans;
|
||||
PasswordStrengthCheckFeedback: ResolverTypeWrapper<PasswordStrengthCheckFeedback>;
|
||||
PasswordStrengthCheckResults: ResolverTypeWrapper<PasswordStrengthCheckResults>;
|
||||
PendingStreamCollaborator: ResolverTypeWrapper<PendingStreamCollaboratorGraphQLReturn>;
|
||||
|
@ -4480,6 +4573,7 @@ export type ResolversTypes = {
|
|||
ServerStatistics: ResolverTypeWrapper<GraphQLEmptyReturn>;
|
||||
ServerStats: ResolverTypeWrapper<GraphQLEmptyReturn>;
|
||||
ServerWorkspacesInfo: ResolverTypeWrapper<GraphQLEmptyReturn>;
|
||||
SessionPaymentStatus: SessionPaymentStatus;
|
||||
SetPrimaryUserEmailInput: SetPrimaryUserEmailInput;
|
||||
SmartTextEditorValue: ResolverTypeWrapper<SmartTextEditorValueSchema>;
|
||||
SortDirection: SortDirection;
|
||||
|
@ -4537,6 +4631,7 @@ export type ResolversTypes = {
|
|||
WebhookUpdateInput: WebhookUpdateInput;
|
||||
Workspace: ResolverTypeWrapper<WorkspaceGraphQLReturn>;
|
||||
WorkspaceBilling: ResolverTypeWrapper<WorkspaceBillingGraphQLReturn>;
|
||||
WorkspaceBillingMutations: ResolverTypeWrapper<WorkspaceBillingMutationsGraphQLReturn>;
|
||||
WorkspaceCollaborator: ResolverTypeWrapper<WorkspaceCollaboratorGraphQLReturn>;
|
||||
WorkspaceCollaboratorCollection: ResolverTypeWrapper<Omit<WorkspaceCollaboratorCollection, 'items'> & { items: Array<ResolversTypes['WorkspaceCollaborator']> }>;
|
||||
WorkspaceCollection: ResolverTypeWrapper<Omit<WorkspaceCollection, 'items'> & { items: Array<ResolversTypes['Workspace']> }>;
|
||||
|
@ -4552,12 +4647,16 @@ export type ResolversTypes = {
|
|||
WorkspaceInviteResendInput: WorkspaceInviteResendInput;
|
||||
WorkspaceInviteUseInput: WorkspaceInviteUseInput;
|
||||
WorkspaceMutations: ResolverTypeWrapper<WorkspaceMutationsGraphQLReturn>;
|
||||
WorkspacePlan: ResolverTypeWrapper<WorkspacePlan>;
|
||||
WorkspacePlanStatuses: WorkspacePlanStatuses;
|
||||
WorkspacePlans: WorkspacePlans;
|
||||
WorkspaceProjectInviteCreateInput: WorkspaceProjectInviteCreateInput;
|
||||
WorkspaceProjectMutations: ResolverTypeWrapper<WorkspaceProjectMutationsGraphQLReturn>;
|
||||
WorkspaceProjectsFilter: WorkspaceProjectsFilter;
|
||||
WorkspaceRole: WorkspaceRole;
|
||||
WorkspaceRoleDeleteInput: WorkspaceRoleDeleteInput;
|
||||
WorkspaceRoleUpdateInput: WorkspaceRoleUpdateInput;
|
||||
WorkspaceSubscription: ResolverTypeWrapper<WorkspaceSubscription>;
|
||||
WorkspaceTeamFilter: WorkspaceTeamFilter;
|
||||
WorkspaceUpdateInput: WorkspaceUpdateInput;
|
||||
WorkspaceVersionsCount: ResolverTypeWrapper<WorkspaceVersionsCount>;
|
||||
|
@ -4613,6 +4712,9 @@ export type ResolversParentTypes = {
|
|||
BranchCreateInput: BranchCreateInput;
|
||||
BranchDeleteInput: BranchDeleteInput;
|
||||
BranchUpdateInput: BranchUpdateInput;
|
||||
CancelCheckoutSessionInput: CancelCheckoutSessionInput;
|
||||
CheckoutSession: CheckoutSession;
|
||||
CheckoutSessionInput: CheckoutSessionInput;
|
||||
Comment: CommentGraphQLReturn;
|
||||
CommentActivityMessage: Omit<CommentActivityMessage, 'comment'> & { comment: ResolversParentTypes['Comment'] };
|
||||
CommentCollection: Omit<CommentCollection, 'items'> & { items: Array<ResolversParentTypes['Comment']> };
|
||||
|
@ -4778,6 +4880,7 @@ export type ResolversParentTypes = {
|
|||
WebhookUpdateInput: WebhookUpdateInput;
|
||||
Workspace: WorkspaceGraphQLReturn;
|
||||
WorkspaceBilling: WorkspaceBillingGraphQLReturn;
|
||||
WorkspaceBillingMutations: WorkspaceBillingMutationsGraphQLReturn;
|
||||
WorkspaceCollaborator: WorkspaceCollaboratorGraphQLReturn;
|
||||
WorkspaceCollaboratorCollection: Omit<WorkspaceCollaboratorCollection, 'items'> & { items: Array<ResolversParentTypes['WorkspaceCollaborator']> };
|
||||
WorkspaceCollection: Omit<WorkspaceCollection, 'items'> & { items: Array<ResolversParentTypes['Workspace']> };
|
||||
|
@ -4793,11 +4896,13 @@ export type ResolversParentTypes = {
|
|||
WorkspaceInviteResendInput: WorkspaceInviteResendInput;
|
||||
WorkspaceInviteUseInput: WorkspaceInviteUseInput;
|
||||
WorkspaceMutations: WorkspaceMutationsGraphQLReturn;
|
||||
WorkspacePlan: WorkspacePlan;
|
||||
WorkspaceProjectInviteCreateInput: WorkspaceProjectInviteCreateInput;
|
||||
WorkspaceProjectMutations: WorkspaceProjectMutationsGraphQLReturn;
|
||||
WorkspaceProjectsFilter: WorkspaceProjectsFilter;
|
||||
WorkspaceRoleDeleteInput: WorkspaceRoleDeleteInput;
|
||||
WorkspaceRoleUpdateInput: WorkspaceRoleUpdateInput;
|
||||
WorkspaceSubscription: WorkspaceSubscription;
|
||||
WorkspaceTeamFilter: WorkspaceTeamFilter;
|
||||
WorkspaceUpdateInput: WorkspaceUpdateInput;
|
||||
WorkspaceVersionsCount: WorkspaceVersionsCount;
|
||||
|
@ -5126,6 +5231,17 @@ export type BranchCollectionResolvers<ContextType = GraphQLContext, ParentType e
|
|||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type CheckoutSessionResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['CheckoutSession'] = ResolversParentTypes['CheckoutSession']> = {
|
||||
billingInterval?: Resolver<ResolversTypes['BillingInterval'], ParentType, ContextType>;
|
||||
createdAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
|
||||
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
|
||||
paymentStatus?: Resolver<ResolversTypes['SessionPaymentStatus'], ParentType, ContextType>;
|
||||
updatedAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
|
||||
url?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
workspacePlan?: Resolver<ResolversTypes['PaidWorkspacePlans'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type CommentResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['Comment'] = ResolversParentTypes['Comment']> = {
|
||||
archived?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
|
||||
author?: Resolver<ResolversTypes['LimitedUser'], ParentType, ContextType>;
|
||||
|
@ -6152,6 +6268,7 @@ export type WebhookEventCollectionResolvers<ContextType = GraphQLContext, Parent
|
|||
export type WorkspaceResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['Workspace'] = ResolversParentTypes['Workspace']> = {
|
||||
billing?: Resolver<Maybe<ResolversTypes['WorkspaceBilling']>, ParentType, ContextType>;
|
||||
createdAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
|
||||
customerPortalUrl?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
defaultLogoIndex?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
|
||||
defaultProjectRole?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
description?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
|
@ -6162,9 +6279,11 @@ export type WorkspaceResolvers<ContextType = GraphQLContext, ParentType extends
|
|||
invitedTeam?: Resolver<Maybe<Array<ResolversTypes['PendingWorkspaceCollaborator']>>, ParentType, ContextType, Partial<WorkspaceInvitedTeamArgs>>;
|
||||
logo?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
plan?: Resolver<Maybe<ResolversTypes['WorkspacePlan']>, ParentType, ContextType>;
|
||||
projects?: Resolver<ResolversTypes['ProjectCollection'], ParentType, ContextType, RequireFields<WorkspaceProjectsArgs, 'limit'>>;
|
||||
role?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
slug?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
subscription?: Resolver<Maybe<ResolversTypes['WorkspaceSubscription']>, ParentType, ContextType>;
|
||||
team?: Resolver<ResolversTypes['WorkspaceCollaboratorCollection'], ParentType, ContextType, RequireFields<WorkspaceTeamArgs, 'limit'>>;
|
||||
updatedAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
|
@ -6176,6 +6295,12 @@ export type WorkspaceBillingResolvers<ContextType = GraphQLContext, ParentType e
|
|||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type WorkspaceBillingMutationsResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['WorkspaceBillingMutations'] = ResolversParentTypes['WorkspaceBillingMutations']> = {
|
||||
cancelCheckoutSession?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<WorkspaceBillingMutationsCancelCheckoutSessionArgs, 'input'>>;
|
||||
createCheckoutSession?: Resolver<ResolversTypes['CheckoutSession'], ParentType, ContextType, RequireFields<WorkspaceBillingMutationsCreateCheckoutSessionArgs, 'input'>>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type WorkspaceCollaboratorResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['WorkspaceCollaborator'] = ResolversParentTypes['WorkspaceCollaborator']> = {
|
||||
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
|
||||
projectRoles?: Resolver<Array<ResolversTypes['ProjectRole']>, ParentType, ContextType>;
|
||||
|
@ -6238,6 +6363,7 @@ export type WorkspaceInviteMutationsResolvers<ContextType = GraphQLContext, Pare
|
|||
|
||||
export type WorkspaceMutationsResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['WorkspaceMutations'] = ResolversParentTypes['WorkspaceMutations']> = {
|
||||
addDomain?: Resolver<ResolversTypes['Workspace'], ParentType, ContextType, RequireFields<WorkspaceMutationsAddDomainArgs, 'input'>>;
|
||||
billing?: Resolver<ResolversTypes['WorkspaceBillingMutations'], ParentType, ContextType>;
|
||||
create?: Resolver<ResolversTypes['Workspace'], ParentType, ContextType, RequireFields<WorkspaceMutationsCreateArgs, 'input'>>;
|
||||
delete?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<WorkspaceMutationsDeleteArgs, 'workspaceId'>>;
|
||||
deleteDomain?: Resolver<ResolversTypes['Workspace'], ParentType, ContextType, RequireFields<WorkspaceMutationsDeleteDomainArgs, 'input'>>;
|
||||
|
@ -6250,12 +6376,26 @@ export type WorkspaceMutationsResolvers<ContextType = GraphQLContext, ParentType
|
|||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type WorkspacePlanResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['WorkspacePlan'] = ResolversParentTypes['WorkspacePlan']> = {
|
||||
name?: Resolver<ResolversTypes['WorkspacePlans'], ParentType, ContextType>;
|
||||
status?: Resolver<ResolversTypes['WorkspacePlanStatuses'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type WorkspaceProjectMutationsResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['WorkspaceProjectMutations'] = ResolversParentTypes['WorkspaceProjectMutations']> = {
|
||||
moveToWorkspace?: Resolver<ResolversTypes['Project'], ParentType, ContextType, RequireFields<WorkspaceProjectMutationsMoveToWorkspaceArgs, 'projectId' | 'workspaceId'>>;
|
||||
updateRole?: Resolver<ResolversTypes['Project'], ParentType, ContextType, RequireFields<WorkspaceProjectMutationsUpdateRoleArgs, 'input'>>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type WorkspaceSubscriptionResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['WorkspaceSubscription'] = ResolversParentTypes['WorkspaceSubscription']> = {
|
||||
billingInterval?: Resolver<ResolversTypes['BillingInterval'], ParentType, ContextType>;
|
||||
createdAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
|
||||
currentBillingCycleEnd?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
|
||||
updatedAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type WorkspaceVersionsCountResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['WorkspaceVersionsCount'] = ResolversParentTypes['WorkspaceVersionsCount']> = {
|
||||
current?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
|
||||
max?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
|
||||
|
@ -6297,6 +6437,7 @@ export type Resolvers<ContextType = GraphQLContext> = {
|
|||
BlobMetadataCollection?: BlobMetadataCollectionResolvers<ContextType>;
|
||||
Branch?: BranchResolvers<ContextType>;
|
||||
BranchCollection?: BranchCollectionResolvers<ContextType>;
|
||||
CheckoutSession?: CheckoutSessionResolvers<ContextType>;
|
||||
Comment?: CommentResolvers<ContextType>;
|
||||
CommentActivityMessage?: CommentActivityMessageResolvers<ContextType>;
|
||||
CommentCollection?: CommentCollectionResolvers<ContextType>;
|
||||
|
@ -6393,6 +6534,7 @@ export type Resolvers<ContextType = GraphQLContext> = {
|
|||
WebhookEventCollection?: WebhookEventCollectionResolvers<ContextType>;
|
||||
Workspace?: WorkspaceResolvers<ContextType>;
|
||||
WorkspaceBilling?: WorkspaceBillingResolvers<ContextType>;
|
||||
WorkspaceBillingMutations?: WorkspaceBillingMutationsResolvers<ContextType>;
|
||||
WorkspaceCollaborator?: WorkspaceCollaboratorResolvers<ContextType>;
|
||||
WorkspaceCollaboratorCollection?: WorkspaceCollaboratorCollectionResolvers<ContextType>;
|
||||
WorkspaceCollection?: WorkspaceCollectionResolvers<ContextType>;
|
||||
|
@ -6402,7 +6544,9 @@ export type Resolvers<ContextType = GraphQLContext> = {
|
|||
WorkspaceDomain?: WorkspaceDomainResolvers<ContextType>;
|
||||
WorkspaceInviteMutations?: WorkspaceInviteMutationsResolvers<ContextType>;
|
||||
WorkspaceMutations?: WorkspaceMutationsResolvers<ContextType>;
|
||||
WorkspacePlan?: WorkspacePlanResolvers<ContextType>;
|
||||
WorkspaceProjectMutations?: WorkspaceProjectMutationsResolvers<ContextType>;
|
||||
WorkspaceSubscription?: WorkspaceSubscriptionResolvers<ContextType>;
|
||||
WorkspaceVersionsCount?: WorkspaceVersionsCountResolvers<ContextType>;
|
||||
};
|
||||
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import { Roles, Scopes, AllScopes as BaseAllScopes } from '@speckle/shared'
|
||||
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
|
||||
|
||||
const { FF_AUTOMATE_MODULE_ENABLED, FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
|
||||
const {
|
||||
FF_AUTOMATE_MODULE_ENABLED,
|
||||
FF_WORKSPACES_MODULE_ENABLED,
|
||||
FF_GATEKEEPER_MODULE_ENABLED
|
||||
} = getFeatureFlags()
|
||||
|
||||
const buildAllScopes = () => {
|
||||
let base = BaseAllScopes
|
||||
|
@ -33,6 +37,11 @@ const buildAllScopes = () => {
|
|||
)
|
||||
}
|
||||
|
||||
if (!FF_GATEKEEPER_MODULE_ENABLED) {
|
||||
base = base.filter(
|
||||
(s: string) => !([Scopes.Gatekeeper.WorkspaceBilling] as string[]).includes(s)
|
||||
)
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
|
|
|
@ -426,6 +426,11 @@ export type BasicGitRepositoryMetadata = {
|
|||
url: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export enum BillingInterval {
|
||||
Monthly = 'monthly',
|
||||
Yearly = 'yearly'
|
||||
}
|
||||
|
||||
export type BlobMetadata = {
|
||||
__typename?: 'BlobMetadata';
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
|
@ -503,6 +508,28 @@ export type BranchUpdateInput = {
|
|||
streamId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type CancelCheckoutSessionInput = {
|
||||
sessionId: Scalars['ID']['input'];
|
||||
workspaceId: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
export type CheckoutSession = {
|
||||
__typename?: 'CheckoutSession';
|
||||
billingInterval: BillingInterval;
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
id: Scalars['ID']['output'];
|
||||
paymentStatus: SessionPaymentStatus;
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
url: Scalars['String']['output'];
|
||||
workspacePlan: PaidWorkspacePlans;
|
||||
};
|
||||
|
||||
export type CheckoutSessionInput = {
|
||||
billingInterval: BillingInterval;
|
||||
workspaceId: Scalars['ID']['input'];
|
||||
workspacePlan: PaidWorkspacePlans;
|
||||
};
|
||||
|
||||
export type Comment = {
|
||||
__typename?: 'Comment';
|
||||
archived: Scalars['Boolean']['output'];
|
||||
|
@ -1712,6 +1739,12 @@ export type ObjectCreateInput = {
|
|||
streamId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export enum PaidWorkspacePlans {
|
||||
Business = 'business',
|
||||
Pro = 'pro',
|
||||
Team = 'team'
|
||||
}
|
||||
|
||||
export type PasswordStrengthCheckFeedback = {
|
||||
__typename?: 'PasswordStrengthCheckFeedback';
|
||||
suggestions: Array<Scalars['String']['output']>;
|
||||
|
@ -2855,6 +2888,11 @@ export type ServerWorkspacesInfo = {
|
|||
workspacesEnabled: Scalars['Boolean']['output'];
|
||||
};
|
||||
|
||||
export enum SessionPaymentStatus {
|
||||
Paid = 'paid',
|
||||
Unpaid = 'unpaid'
|
||||
}
|
||||
|
||||
export type SetPrimaryUserEmailInput = {
|
||||
id: Scalars['ID']['input'];
|
||||
};
|
||||
|
@ -3892,6 +3930,7 @@ export type Workspace = {
|
|||
/** Billing data for Workspaces beta */
|
||||
billing?: Maybe<WorkspaceBilling>;
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
customerPortalUrl?: Maybe<Scalars['String']['output']>;
|
||||
/** Selected fallback when `logo` not set */
|
||||
defaultLogoIndex: Scalars['Int']['output'];
|
||||
/** The default role workspace members will receive for workspace projects. */
|
||||
|
@ -3909,10 +3948,12 @@ export type Workspace = {
|
|||
/** Logo image as base64-encoded string */
|
||||
logo?: Maybe<Scalars['String']['output']>;
|
||||
name: Scalars['String']['output'];
|
||||
plan?: Maybe<WorkspacePlan>;
|
||||
projects: ProjectCollection;
|
||||
/** Active user's role for this workspace. `null` if request is not authenticated, or the workspace is not explicitly shared with you. */
|
||||
role?: Maybe<Scalars['String']['output']>;
|
||||
slug: Scalars['String']['output'];
|
||||
subscription?: Maybe<WorkspaceSubscription>;
|
||||
team: WorkspaceCollaboratorCollection;
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
};
|
||||
|
@ -3942,6 +3983,22 @@ export type WorkspaceBilling = {
|
|||
versionsCount: WorkspaceVersionsCount;
|
||||
};
|
||||
|
||||
export type WorkspaceBillingMutations = {
|
||||
__typename?: 'WorkspaceBillingMutations';
|
||||
cancelCheckoutSession: Scalars['Boolean']['output'];
|
||||
createCheckoutSession: CheckoutSession;
|
||||
};
|
||||
|
||||
|
||||
export type WorkspaceBillingMutationsCancelCheckoutSessionArgs = {
|
||||
input: CancelCheckoutSessionInput;
|
||||
};
|
||||
|
||||
|
||||
export type WorkspaceBillingMutationsCreateCheckoutSessionArgs = {
|
||||
input: CheckoutSessionInput;
|
||||
};
|
||||
|
||||
/** Overridden by `WorkspaceCollaboratorGraphQLReturn` */
|
||||
export type WorkspaceCollaborator = {
|
||||
__typename?: 'WorkspaceCollaborator';
|
||||
|
@ -4083,6 +4140,7 @@ export type WorkspaceInviteUseInput = {
|
|||
export type WorkspaceMutations = {
|
||||
__typename?: 'WorkspaceMutations';
|
||||
addDomain: Workspace;
|
||||
billing: WorkspaceBillingMutations;
|
||||
create: Workspace;
|
||||
delete: Scalars['Boolean']['output'];
|
||||
deleteDomain: Workspace;
|
||||
|
@ -4134,6 +4192,27 @@ export type WorkspaceMutationsUpdateRoleArgs = {
|
|||
input: WorkspaceRoleUpdateInput;
|
||||
};
|
||||
|
||||
export type WorkspacePlan = {
|
||||
__typename?: 'WorkspacePlan';
|
||||
name: WorkspacePlans;
|
||||
status: WorkspacePlanStatuses;
|
||||
};
|
||||
|
||||
export enum WorkspacePlanStatuses {
|
||||
Canceled = 'canceled',
|
||||
PaymentFailed = 'paymentFailed',
|
||||
Trial = 'trial',
|
||||
Valid = 'valid'
|
||||
}
|
||||
|
||||
export enum WorkspacePlans {
|
||||
Academia = 'academia',
|
||||
Business = 'business',
|
||||
Pro = 'pro',
|
||||
Team = 'team',
|
||||
Unlimited = 'unlimited'
|
||||
}
|
||||
|
||||
export type WorkspaceProjectInviteCreateInput = {
|
||||
/** Either this or userId must be filled */
|
||||
email?: InputMaybe<Scalars['String']['input']>;
|
||||
|
@ -4187,6 +4266,14 @@ export type WorkspaceRoleUpdateInput = {
|
|||
workspaceId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type WorkspaceSubscription = {
|
||||
__typename?: 'WorkspaceSubscription';
|
||||
billingInterval: BillingInterval;
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
currentBillingCycleEnd: Scalars['DateTime']['output'];
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
};
|
||||
|
||||
export type WorkspaceTeamFilter = {
|
||||
/** Limit team members to provided role(s) */
|
||||
roles?: InputMaybe<Array<Scalars['String']['input']>>;
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import {
|
||||
CreateCheckoutSession,
|
||||
GetSubscriptionData,
|
||||
SubscriptionData,
|
||||
WorkspaceSubscription
|
||||
} from '@/modules/gatekeeper/domain/billing'
|
||||
import {
|
||||
|
@ -15,6 +16,19 @@ type GetWorkspacePlanPrice = (args: {
|
|||
billingInterval: WorkspacePlanBillingIntervals
|
||||
}) => string
|
||||
|
||||
const getResultUrl = ({
|
||||
frontendOrigin,
|
||||
workspaceId,
|
||||
workspaceSlug
|
||||
}: {
|
||||
frontendOrigin: string
|
||||
workspaceSlug: string
|
||||
workspaceId: string
|
||||
}) =>
|
||||
new URL(
|
||||
`${frontendOrigin}/workspaces/${workspaceSlug}?workspace=${workspaceId}&settings=workspace/billing`
|
||||
)
|
||||
|
||||
export const createCheckoutSessionFactory =
|
||||
({
|
||||
stripe,
|
||||
|
@ -34,10 +48,7 @@ export const createCheckoutSessionFactory =
|
|||
workspaceId
|
||||
}) => {
|
||||
//?settings=workspace/security&
|
||||
const resultUrl = new URL(
|
||||
`${frontendOrigin}/workspaces/${workspaceSlug}?workspace=${workspaceId}&settings=workspace/billing`
|
||||
)
|
||||
|
||||
const resultUrl = getResultUrl({ frontendOrigin, workspaceId, workspaceSlug })
|
||||
const price = getWorkspacePlanPrice({ billingInterval, workspacePlan })
|
||||
const costLineItems: Stripe.Checkout.SessionCreateParams.LineItem[] = [
|
||||
{ price, quantity: seatCount }
|
||||
|
@ -57,8 +68,7 @@ export const createCheckoutSessionFactory =
|
|||
line_items: costLineItems,
|
||||
|
||||
success_url: `${resultUrl.toString()}&payment_status=success&session_id={CHECKOUT_SESSION_ID}`,
|
||||
|
||||
cancel_url: `${resultUrl.toString()}&payment_status=cancelled&session_id={CHECKOUT_SESSION_ID}`
|
||||
cancel_url: `${resultUrl.toString()}&payment_status=canceled&session_id={CHECKOUT_SESSION_ID}`
|
||||
})
|
||||
|
||||
if (!session.url) throw new Error('Failed to create an active checkout session')
|
||||
|
@ -74,6 +84,36 @@ export const createCheckoutSessionFactory =
|
|||
}
|
||||
}
|
||||
|
||||
export const createCustomerPortalUrlFactory =
|
||||
({
|
||||
stripe,
|
||||
frontendOrigin
|
||||
}: // getWorkspacePlanPrice
|
||||
{
|
||||
stripe: Stripe
|
||||
frontendOrigin: string
|
||||
// getWorkspacePlanPrice: GetWorkspacePlanPrice
|
||||
}) =>
|
||||
async ({
|
||||
workspaceId,
|
||||
workspaceSlug,
|
||||
customerId
|
||||
}: {
|
||||
customerId: string
|
||||
workspaceId: string
|
||||
workspaceSlug: string
|
||||
}): Promise<string> => {
|
||||
const session = await stripe.billingPortal.sessions.create({
|
||||
customer: customerId,
|
||||
return_url: getResultUrl({
|
||||
frontendOrigin,
|
||||
workspaceId,
|
||||
workspaceSlug
|
||||
}).toString()
|
||||
})
|
||||
return session.url
|
||||
}
|
||||
|
||||
export const getSubscriptionDataFactory =
|
||||
({
|
||||
stripe
|
||||
|
@ -84,33 +124,42 @@ export const getSubscriptionDataFactory =
|
|||
}): GetSubscriptionData =>
|
||||
async ({ subscriptionId }) => {
|
||||
const stripeSubscription = await stripe.subscriptions.retrieve(subscriptionId)
|
||||
|
||||
return {
|
||||
customerId:
|
||||
typeof stripeSubscription.customer === 'string'
|
||||
? stripeSubscription.customer
|
||||
: stripeSubscription.customer.id,
|
||||
subscriptionId,
|
||||
products: stripeSubscription.items.data.map((subscriptionItem) => {
|
||||
const productId =
|
||||
typeof subscriptionItem.price.product === 'string'
|
||||
? subscriptionItem.price.product
|
||||
: subscriptionItem.price.product.id
|
||||
const quantity = subscriptionItem.quantity
|
||||
if (!quantity)
|
||||
throw new Error(
|
||||
'invalid subscription, we do not support products without quantities'
|
||||
)
|
||||
return {
|
||||
priceId: subscriptionItem.price.id,
|
||||
productId,
|
||||
quantity,
|
||||
subscriptionItemId: subscriptionItem.id
|
||||
}
|
||||
})
|
||||
}
|
||||
return parseSubscriptionData(stripeSubscription)
|
||||
}
|
||||
|
||||
export const parseSubscriptionData = (
|
||||
stripeSubscription: Stripe.Subscription
|
||||
): SubscriptionData => {
|
||||
return {
|
||||
customerId:
|
||||
typeof stripeSubscription.customer === 'string'
|
||||
? stripeSubscription.customer
|
||||
: stripeSubscription.customer.id,
|
||||
subscriptionId: stripeSubscription.id,
|
||||
status: stripeSubscription.status,
|
||||
cancelAt: stripeSubscription.cancel_at
|
||||
? new Date(stripeSubscription.cancel_at)
|
||||
: null,
|
||||
products: stripeSubscription.items.data.map((subscriptionItem) => {
|
||||
const productId =
|
||||
typeof subscriptionItem.price.product === 'string'
|
||||
? subscriptionItem.price.product
|
||||
: subscriptionItem.price.product.id
|
||||
const quantity = subscriptionItem.quantity
|
||||
if (!quantity)
|
||||
throw new Error(
|
||||
'invalid subscription, we do not support products without quantities'
|
||||
)
|
||||
return {
|
||||
priceId: subscriptionItem.price.id,
|
||||
productId,
|
||||
quantity,
|
||||
subscriptionItemId: subscriptionItem.id
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// this should be a reconcile subscriptions, we keep an accurate state in the DB
|
||||
// on each change, we're reconciling that state to stripe
|
||||
export const reconcileWorkspaceSubscriptionFactory =
|
||||
|
|
|
@ -13,9 +13,10 @@ export type PaidWorkspacePlanStatuses =
|
|||
| UnpaidWorkspacePlanStatuses
|
||||
// | 'paymentNeeded' // unsure if this is needed
|
||||
| 'paymentFailed'
|
||||
| 'cancelled'
|
||||
| 'cancelationScheduled'
|
||||
| 'canceled'
|
||||
|
||||
export type TrialWorkspacePlanStatuses = 'trial'
|
||||
export type TrialWorkspacePlanStatuses = 'trial' | 'expired'
|
||||
|
||||
type BaseWorkspacePlan = {
|
||||
workspaceId: string
|
||||
|
@ -108,10 +109,20 @@ export type WorkspaceSubscription = {
|
|||
billingInterval: WorkspacePlanBillingIntervals
|
||||
subscriptionData: SubscriptionData
|
||||
}
|
||||
|
||||
export const subscriptionData = z.object({
|
||||
subscriptionId: z.string().min(1),
|
||||
customerId: z.string().min(1),
|
||||
cancelAt: z.date().nullable(),
|
||||
status: z.union([
|
||||
z.literal('incomplete'),
|
||||
z.literal('incomplete_expired'),
|
||||
z.literal('trialing'),
|
||||
z.literal('active'),
|
||||
z.literal('past_due'),
|
||||
z.literal('canceled'),
|
||||
z.literal('unpaid'),
|
||||
z.literal('paused')
|
||||
]),
|
||||
products: z
|
||||
.object({
|
||||
// we're going to use the productId to match with our
|
||||
|
@ -126,10 +137,18 @@ export const subscriptionData = z.object({
|
|||
// this abstracts the stripe sub data
|
||||
export type SubscriptionData = z.infer<typeof subscriptionData>
|
||||
|
||||
export type SaveWorkspaceSubscription = (args: {
|
||||
export type UpsertWorkspaceSubscription = (args: {
|
||||
workspaceSubscription: WorkspaceSubscription
|
||||
}) => Promise<void>
|
||||
|
||||
export type GetWorkspaceSubscription = (args: {
|
||||
workspaceId: string
|
||||
}) => Promise<WorkspaceSubscription | null>
|
||||
|
||||
export type GetWorkspaceSubscriptionBySubscriptionId = (args: {
|
||||
subscriptionId: string
|
||||
}) => Promise<WorkspaceSubscription | null>
|
||||
|
||||
export type GetSubscriptionData = (args: {
|
||||
subscriptionId: string
|
||||
}) => Promise<SubscriptionData>
|
||||
|
|
|
@ -6,6 +6,12 @@ export class WorkspacePlanNotFoundError extends BaseError {
|
|||
static statusCode = 500
|
||||
}
|
||||
|
||||
export class WorkspacePlanMismatchError extends BaseError {
|
||||
static defaultMessage = 'Workspace plan is not matching the expected state'
|
||||
static code = 'WORKSPACE_PLAN_MISMATCH'
|
||||
static statusCode = 500
|
||||
}
|
||||
|
||||
export class WorkspaceCheckoutSessionInProgressError extends BaseError {
|
||||
static defaultMessage = 'Workspace already has a checkout session in progress'
|
||||
static code = 'WORKSPACE_CHECKOUT_SESSION_IN_PROGRESS_ERROR'
|
||||
|
@ -23,3 +29,9 @@ export class CheckoutSessionNotFoundError extends BaseError {
|
|||
static code = 'CHECKOUT_SESSION_NOT_FOUND'
|
||||
static statusCode = 404
|
||||
}
|
||||
|
||||
export class WorkspaceSubscriptionNotFoundError extends BaseError {
|
||||
static defaultMessage = 'Workspace subscription not found'
|
||||
static code = 'WORKSPACE_SUBSCRIPTION_NOT_FOUND'
|
||||
static statusCode = 404
|
||||
}
|
||||
|
|
|
@ -1,6 +1,27 @@
|
|||
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
|
||||
import { getFeatureFlags, getFrontendOrigin } from '@/modules/shared/helpers/envHelper'
|
||||
import { Resolvers } from '@/modules/core/graph/generated/graphql'
|
||||
import { pricingTable } from '@/modules/gatekeeper/domain/workspacePricing'
|
||||
import { authorizeResolver } from '@/modules/shared'
|
||||
import { Roles } from '@speckle/shared'
|
||||
import {
|
||||
countWorkspaceRoleWithOptionalProjectRoleFactory,
|
||||
getWorkspaceFactory
|
||||
} from '@/modules/workspaces/repositories/workspaces'
|
||||
import { WorkspaceNotFoundError } from '@/modules/workspaces/errors/workspace'
|
||||
import { db } from '@/db/knex'
|
||||
import {
|
||||
createCheckoutSessionFactory,
|
||||
createCustomerPortalUrlFactory
|
||||
} from '@/modules/gatekeeper/clients/stripe'
|
||||
import { getWorkspacePlanPrice, getStripeClient } from '@/modules/gatekeeper/stripe'
|
||||
import { startCheckoutSessionFactory } from '@/modules/gatekeeper/services/checkout'
|
||||
import {
|
||||
deleteCheckoutSessionFactory,
|
||||
getWorkspaceCheckoutSessionFactory,
|
||||
getWorkspacePlanFactory,
|
||||
getWorkspaceSubscriptionFactory,
|
||||
saveCheckoutSessionFactory
|
||||
} from '@/modules/gatekeeper/repositories/billing'
|
||||
|
||||
const { FF_GATEKEEPER_MODULE_ENABLED } = getFeatureFlags()
|
||||
|
||||
|
@ -10,6 +31,98 @@ export = FF_GATEKEEPER_MODULE_ENABLED
|
|||
workspacePricingPlans: async () => {
|
||||
return pricingTable
|
||||
}
|
||||
},
|
||||
Workspace: {
|
||||
plan: async (parent) => {
|
||||
return await getWorkspacePlanFactory({ db })({ workspaceId: parent.id })
|
||||
},
|
||||
subscription: async (parent, _, ctx) => {
|
||||
const workspaceId = parent.id
|
||||
await authorizeResolver(
|
||||
ctx.userId,
|
||||
workspaceId,
|
||||
Roles.Workspace.Admin,
|
||||
ctx.resourceAccessRules
|
||||
)
|
||||
return await getWorkspaceSubscriptionFactory({ db })({ workspaceId })
|
||||
},
|
||||
customerPortalUrl: async (parent, _, ctx) => {
|
||||
const workspaceId = parent.id
|
||||
await authorizeResolver(
|
||||
ctx.userId,
|
||||
workspaceId,
|
||||
Roles.Workspace.Admin,
|
||||
ctx.resourceAccessRules
|
||||
)
|
||||
const workspaceSubscription = await getWorkspaceSubscriptionFactory({ db })({
|
||||
workspaceId
|
||||
})
|
||||
if (!workspaceSubscription) return null
|
||||
const workspace = await getWorkspaceFactory({ db })({ workspaceId })
|
||||
if (!workspace)
|
||||
throw new Error('This cannot be, if there is a sub, there is a workspace')
|
||||
return await createCustomerPortalUrlFactory({
|
||||
stripe: getStripeClient(),
|
||||
frontendOrigin: getFrontendOrigin()
|
||||
})({
|
||||
workspaceId: workspaceSubscription.workspaceId,
|
||||
workspaceSlug: workspace.slug,
|
||||
customerId: workspaceSubscription.subscriptionData.customerId
|
||||
})
|
||||
}
|
||||
},
|
||||
WorkspaceMutations: () => ({}),
|
||||
WorkspaceBillingMutations: {
|
||||
cancelCheckoutSession: async (parent, args, ctx) => {
|
||||
const { workspaceId, sessionId } = args.input
|
||||
|
||||
await authorizeResolver(
|
||||
ctx.userId,
|
||||
workspaceId,
|
||||
Roles.Workspace.Admin,
|
||||
ctx.resourceAccessRules
|
||||
)
|
||||
await deleteCheckoutSessionFactory({ db })({ checkoutSessionId: sessionId })
|
||||
return true
|
||||
},
|
||||
createCheckoutSession: async (parent, args, ctx) => {
|
||||
const { workspaceId, workspacePlan, billingInterval } = args.input
|
||||
const workspace = await getWorkspaceFactory({ db })({ workspaceId })
|
||||
|
||||
if (!workspace) throw new WorkspaceNotFoundError()
|
||||
|
||||
await authorizeResolver(
|
||||
ctx.userId,
|
||||
workspaceId,
|
||||
Roles.Workspace.Admin,
|
||||
ctx.resourceAccessRules
|
||||
)
|
||||
|
||||
const createCheckoutSession = createCheckoutSessionFactory({
|
||||
stripe: getStripeClient(),
|
||||
frontendOrigin: getFrontendOrigin(),
|
||||
getWorkspacePlanPrice
|
||||
})
|
||||
|
||||
const countRole = countWorkspaceRoleWithOptionalProjectRoleFactory({ db })
|
||||
|
||||
const session = await startCheckoutSessionFactory({
|
||||
getWorkspaceCheckoutSession: getWorkspaceCheckoutSessionFactory({ db }),
|
||||
getWorkspacePlan: getWorkspacePlanFactory({ db }),
|
||||
countRole,
|
||||
createCheckoutSession,
|
||||
saveCheckoutSession: saveCheckoutSessionFactory({ db }),
|
||||
deleteCheckoutSession: deleteCheckoutSessionFactory({ db })
|
||||
})({
|
||||
workspacePlan,
|
||||
workspaceId,
|
||||
workspaceSlug: workspace.slug,
|
||||
|
||||
billingInterval
|
||||
})
|
||||
|
||||
return session
|
||||
}
|
||||
}
|
||||
} as Resolvers)
|
||||
: {}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
import { MutationsObjectGraphQLReturn } from '@/modules/core/helpers/graphTypes'
|
||||
|
||||
export type WorkspaceBillingMutationsGraphQLReturn = MutationsObjectGraphQLReturn
|
|
@ -3,12 +3,21 @@ import { SpeckleModule } from '@/modules/shared/helpers/typeHelper'
|
|||
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
|
||||
import { validateModuleLicense } from '@/modules/gatekeeper/services/validateLicense'
|
||||
import { getBillingRouter } from '@/modules/gatekeeper/rest/billing'
|
||||
import { registerOrUpdateScopeFactory } from '@/modules/shared/repositories/scopes'
|
||||
import { db } from '@/db/knex'
|
||||
import { gatekeeperScopes } from '@/modules/gatekeeper/scopes'
|
||||
|
||||
const { FF_GATEKEEPER_MODULE_ENABLED, FF_BILLING_INTEGRATION_ENABLED } =
|
||||
getFeatureFlags()
|
||||
|
||||
const initScopes = async () => {
|
||||
const registerFunc = registerOrUpdateScopeFactory({ db })
|
||||
await Promise.all(gatekeeperScopes.map((scope) => registerFunc({ scope })))
|
||||
}
|
||||
|
||||
const gatekeeperModule: SpeckleModule = {
|
||||
async init(app, isInitial) {
|
||||
await initScopes()
|
||||
if (!FF_GATEKEEPER_MODULE_ENABLED) return
|
||||
|
||||
const isLicenseValid = await validateModuleLicense({
|
||||
|
|
|
@ -5,12 +5,14 @@ import {
|
|||
SaveCheckoutSession,
|
||||
UpdateCheckoutSessionStatus,
|
||||
UpsertWorkspacePlan,
|
||||
SaveWorkspaceSubscription,
|
||||
UpsertWorkspaceSubscription,
|
||||
WorkspaceSubscription,
|
||||
WorkspacePlan,
|
||||
UpsertPaidWorkspacePlan,
|
||||
DeleteCheckoutSession,
|
||||
GetWorkspaceCheckoutSession
|
||||
GetWorkspaceCheckoutSession,
|
||||
GetWorkspaceSubscription,
|
||||
GetWorkspaceSubscriptionBySubscriptionId
|
||||
} from '@/modules/gatekeeper/domain/billing'
|
||||
import { Knex } from 'knex'
|
||||
|
||||
|
@ -94,8 +96,34 @@ export const updateCheckoutSessionStatusFactory =
|
|||
.update({ paymentStatus, updatedAt: new Date() })
|
||||
}
|
||||
|
||||
export const saveWorkspaceSubscriptionFactory =
|
||||
({ db }: { db: Knex }): SaveWorkspaceSubscription =>
|
||||
export const upsertWorkspaceSubscriptionFactory =
|
||||
({ db }: { db: Knex }): UpsertWorkspaceSubscription =>
|
||||
async ({ workspaceSubscription }) => {
|
||||
await tables.workspaceSubscriptions(db).insert(workspaceSubscription)
|
||||
await tables
|
||||
.workspaceSubscriptions(db)
|
||||
.insert(workspaceSubscription)
|
||||
.onConflict('workspaceId')
|
||||
.merge()
|
||||
}
|
||||
|
||||
export const getWorkspaceSubscriptionFactory =
|
||||
({ db }: { db: Knex }): GetWorkspaceSubscription =>
|
||||
async ({ workspaceId }) => {
|
||||
const subscription = await tables
|
||||
.workspaceSubscriptions(db)
|
||||
.select()
|
||||
.where({ workspaceId })
|
||||
.first()
|
||||
return subscription || null
|
||||
}
|
||||
|
||||
export const getWorkspaceSubscriptionBySubscriptionIdFactory =
|
||||
({ db }: { db: Knex }): GetWorkspaceSubscriptionBySubscriptionId =>
|
||||
async ({ subscriptionId }) => {
|
||||
const subscription = await tables
|
||||
.workspaceSubscriptions(db)
|
||||
.select()
|
||||
.whereRaw(`"subscriptionData" ->> 'subscriptionId' = ?`, [subscriptionId])
|
||||
.first()
|
||||
return subscription ?? null
|
||||
}
|
||||
|
|
|
@ -1,19 +1,15 @@
|
|||
import { Router } from 'express'
|
||||
import { validateRequest } from 'zod-express'
|
||||
import { z } from 'zod'
|
||||
import { authorizeResolver } from '@/modules/shared'
|
||||
import { ensureError, Roles } from '@speckle/shared'
|
||||
import { authorizeResolver, validateScopes } from '@/modules/shared'
|
||||
import { ensureError, Roles, Scopes } from '@speckle/shared'
|
||||
import { Stripe } from 'stripe'
|
||||
import {
|
||||
getFrontendOrigin,
|
||||
getStringFromEnv,
|
||||
getStripeApiKey,
|
||||
getStripeEndpointSigningKey
|
||||
} from '@/modules/shared/helpers/envHelper'
|
||||
import {
|
||||
WorkspacePlanBillingIntervals,
|
||||
paidWorkspacePlans,
|
||||
WorkspacePricingPlans,
|
||||
workspacePlanBillingIntervals
|
||||
} from '@/modules/gatekeeper/domain/workspacePricing'
|
||||
import {
|
||||
|
@ -28,57 +24,30 @@ import {
|
|||
import { WorkspaceNotFoundError } from '@/modules/workspaces/errors/workspace'
|
||||
import {
|
||||
createCheckoutSessionFactory,
|
||||
getSubscriptionDataFactory
|
||||
createCustomerPortalUrlFactory,
|
||||
getSubscriptionDataFactory,
|
||||
parseSubscriptionData
|
||||
} from '@/modules/gatekeeper/clients/stripe'
|
||||
import {
|
||||
deleteCheckoutSessionFactory,
|
||||
getCheckoutSessionFactory,
|
||||
getWorkspaceCheckoutSessionFactory,
|
||||
getWorkspacePlanFactory,
|
||||
getWorkspaceSubscriptionFactory,
|
||||
saveCheckoutSessionFactory,
|
||||
saveWorkspaceSubscriptionFactory,
|
||||
upsertWorkspaceSubscriptionFactory,
|
||||
updateCheckoutSessionStatusFactory,
|
||||
upsertPaidWorkspacePlanFactory
|
||||
upsertPaidWorkspacePlanFactory,
|
||||
getWorkspaceSubscriptionBySubscriptionIdFactory
|
||||
} from '@/modules/gatekeeper/repositories/billing'
|
||||
import { GetWorkspacePlanPrice } from '@/modules/gatekeeper/domain/billing'
|
||||
import { WorkspaceAlreadyPaidError } from '@/modules/gatekeeper/errors/billing'
|
||||
import { withTransaction } from '@/modules/shared/helpers/dbHelper'
|
||||
import { getStripeClient, getWorkspacePlanPrice } from '@/modules/gatekeeper/stripe'
|
||||
import { handleSubscriptionUpdateFactory } from '@/modules/gatekeeper/services/subscriptions'
|
||||
|
||||
export const getBillingRouter = (): Router => {
|
||||
const workspacePlanPrices: Record<
|
||||
WorkspacePricingPlans,
|
||||
Record<WorkspacePlanBillingIntervals, string> & { productId: string }
|
||||
> = {
|
||||
guest: {
|
||||
productId: getStringFromEnv('WORKSPACE_GUEST_SEAT_STRIPE_PRODUCT_ID'),
|
||||
monthly: getStringFromEnv('WORKSPACE_MONTHLY_GUEST_SEAT_STRIPE_PRICE_ID'),
|
||||
yearly: getStringFromEnv('WORKSPACE_YEARLY_GUEST_SEAT_STRIPE_PRICE_ID')
|
||||
},
|
||||
team: {
|
||||
productId: getStringFromEnv('WORKSPACE_TEAM_SEAT_STRIPE_PRODUCT_ID'),
|
||||
monthly: getStringFromEnv('WORKSPACE_MONTHLY_TEAM_SEAT_STRIPE_PRICE_ID'),
|
||||
yearly: getStringFromEnv('WORKSPACE_YEARLY_TEAM_SEAT_STRIPE_PRICE_ID')
|
||||
},
|
||||
pro: {
|
||||
productId: getStringFromEnv('WORKSPACE_PRO_SEAT_STRIPE_PRODUCT_ID'),
|
||||
monthly: getStringFromEnv('WORKSPACE_MONTHLY_PRO_SEAT_STRIPE_PRICE_ID'),
|
||||
yearly: getStringFromEnv('WORKSPACE_YEARLY_PRO_SEAT_STRIPE_PRICE_ID')
|
||||
},
|
||||
business: {
|
||||
productId: getStringFromEnv('WORKSPACE_BUSINESS_SEAT_STRIPE_PRODUCT_ID'),
|
||||
monthly: getStringFromEnv('WORKSPACE_MONTHLY_BUSINESS_SEAT_STRIPE_PRICE_ID'),
|
||||
yearly: getStringFromEnv('WORKSPACE_YEARLY_BUSINESS_SEAT_STRIPE_PRICE_ID')
|
||||
}
|
||||
}
|
||||
|
||||
const getWorkspacePlanPrice: GetWorkspacePlanPrice = ({
|
||||
workspacePlan,
|
||||
billingInterval
|
||||
}) => workspacePlanPrices[workspacePlan][billingInterval]
|
||||
const router = Router()
|
||||
|
||||
const stripe = new Stripe(getStripeApiKey(), { typescript: true })
|
||||
|
||||
// this prob needs to be turned into a GQL resolver for better frontend integration for errors
|
||||
router.get(
|
||||
'/api/v1/billing/workspaces/:workspaceId/checkout-session/:workspacePlan/:billingInterval',
|
||||
|
@ -95,6 +64,7 @@ export const getBillingRouter = (): Router => {
|
|||
|
||||
if (!workspace) throw new WorkspaceNotFoundError()
|
||||
|
||||
await validateScopes(req.context.scopes, Scopes.Gatekeeper.WorkspaceBilling)
|
||||
await authorizeResolver(
|
||||
req.context.userId,
|
||||
workspaceId,
|
||||
|
@ -103,7 +73,7 @@ export const getBillingRouter = (): Router => {
|
|||
)
|
||||
|
||||
const createCheckoutSession = createCheckoutSessionFactory({
|
||||
stripe,
|
||||
stripe: getStripeClient(),
|
||||
frontendOrigin: getFrontendOrigin(),
|
||||
getWorkspacePlanPrice
|
||||
})
|
||||
|
@ -115,13 +85,49 @@ export const getBillingRouter = (): Router => {
|
|||
getWorkspacePlan: getWorkspacePlanFactory({ db }),
|
||||
countRole,
|
||||
createCheckoutSession,
|
||||
saveCheckoutSession: saveCheckoutSessionFactory({ db })
|
||||
saveCheckoutSession: saveCheckoutSessionFactory({ db }),
|
||||
deleteCheckoutSession: deleteCheckoutSessionFactory({ db })
|
||||
})({ workspacePlan, workspaceId, workspaceSlug: workspace.slug, billingInterval })
|
||||
|
||||
req.res?.redirect(session.url)
|
||||
}
|
||||
)
|
||||
|
||||
router.get(
|
||||
'/api/v1/billing/workspaces/:workspaceId/customer-portal',
|
||||
validateRequest({
|
||||
params: z.object({
|
||||
workspaceId: z.string().min(1)
|
||||
})
|
||||
}),
|
||||
async (req) => {
|
||||
const { workspaceId } = req.params
|
||||
await authorizeResolver(
|
||||
req.context.userId,
|
||||
workspaceId,
|
||||
Roles.Workspace.Admin,
|
||||
req.context.resourceAccessRules
|
||||
)
|
||||
const workspaceSubscription = await getWorkspaceSubscriptionFactory({ db })({
|
||||
workspaceId
|
||||
})
|
||||
if (!workspaceSubscription) return null
|
||||
const workspace = await getWorkspaceFactory({ db })({ workspaceId })
|
||||
if (!workspace)
|
||||
throw new Error('This cannot be, if there is a sub, there is a workspace')
|
||||
const stripe = getStripeClient()
|
||||
const url = await createCustomerPortalUrlFactory({
|
||||
stripe,
|
||||
frontendOrigin: getFrontendOrigin()
|
||||
})({
|
||||
workspaceId: workspaceSubscription.workspaceId,
|
||||
workspaceSlug: workspace.slug,
|
||||
customerId: workspaceSubscription.subscriptionData.customerId
|
||||
})
|
||||
return req.res?.redirect(url)
|
||||
}
|
||||
)
|
||||
|
||||
router.post('/api/v1/billing/webhooks', async (req, res) => {
|
||||
const endpointSecret = getStripeEndpointSigningKey()
|
||||
const sig = req.headers['stripe-signature']
|
||||
|
@ -130,6 +136,7 @@ export const getBillingRouter = (): Router => {
|
|||
return
|
||||
}
|
||||
|
||||
const stripe = getStripeClient()
|
||||
let event: Stripe.Event
|
||||
|
||||
try {
|
||||
|
@ -146,7 +153,10 @@ export const getBillingRouter = (): Router => {
|
|||
|
||||
switch (event.type) {
|
||||
case 'checkout.session.async_payment_failed':
|
||||
// TODO: need to alert the user and delete the session ?
|
||||
// if payment fails, we delete the failed session
|
||||
await deleteCheckoutSessionFactory({ db })({
|
||||
checkoutSessionId: event.data.object.id
|
||||
})
|
||||
break
|
||||
case 'checkout.session.async_payment_succeeded':
|
||||
case 'checkout.session.completed':
|
||||
|
@ -155,52 +165,65 @@ export const getBillingRouter = (): Router => {
|
|||
if (!session.subscription)
|
||||
return res.status(400).send('We only support subscription type checkouts')
|
||||
|
||||
if (session.payment_status === 'paid') {
|
||||
// If the workspace is already on a paid plan, we made a bo bo.
|
||||
// existing subs should be updated via the api, not pushed through the checkout sess again
|
||||
// the start checkout endpoint should guard this!
|
||||
// get checkout session from the DB, if not found CONTACT SUPPORT!!!
|
||||
// if the session is already paid, means, we've already settled this checkout, and this is a webhook recall
|
||||
// set checkout state to paid
|
||||
// go ahead and provision the plan
|
||||
// store customer id and subscription Id associated to the workspace plan
|
||||
switch (session.payment_status) {
|
||||
case 'no_payment_required':
|
||||
// we do not need to support this status
|
||||
break
|
||||
case 'paid':
|
||||
// If the workspace is already on a paid plan, we made a bo bo.
|
||||
// existing subs should be updated via the api, not pushed through the checkout sess again
|
||||
// the start checkout endpoint should guard this!
|
||||
// get checkout session from the DB, if not found CONTACT SUPPORT!!!
|
||||
// if the session is already paid, means, we've already settled this checkout, and this is a webhook recall
|
||||
// set checkout state to paid
|
||||
// go ahead and provision the plan
|
||||
// store customer id and subscription Id associated to the workspace plan
|
||||
|
||||
const subscriptionId =
|
||||
typeof session.subscription === 'string'
|
||||
? session.subscription
|
||||
: session.subscription.id
|
||||
const subscriptionId =
|
||||
typeof session.subscription === 'string'
|
||||
? session.subscription
|
||||
: session.subscription.id
|
||||
|
||||
// this must use a transaction
|
||||
// this must use a transaction
|
||||
|
||||
const trx = await db.transaction()
|
||||
const trx = await db.transaction()
|
||||
|
||||
const completeCheckout = completeCheckoutSessionFactory({
|
||||
getCheckoutSession: getCheckoutSessionFactory({ db: trx }),
|
||||
updateCheckoutSessionStatus: updateCheckoutSessionStatusFactory({
|
||||
db: trx
|
||||
}),
|
||||
upsertPaidWorkspacePlan: upsertPaidWorkspacePlanFactory({ db: trx }),
|
||||
saveWorkspaceSubscription: saveWorkspaceSubscriptionFactory({ db: trx }),
|
||||
getSubscriptionData: getSubscriptionDataFactory({
|
||||
stripe
|
||||
})
|
||||
})
|
||||
|
||||
try {
|
||||
await withTransaction(
|
||||
completeCheckout({
|
||||
sessionId: session.id,
|
||||
subscriptionId
|
||||
const completeCheckout = completeCheckoutSessionFactory({
|
||||
getCheckoutSession: getCheckoutSessionFactory({ db: trx }),
|
||||
updateCheckoutSessionStatus: updateCheckoutSessionStatusFactory({
|
||||
db: trx
|
||||
}),
|
||||
trx
|
||||
)
|
||||
} catch (err) {
|
||||
if (err instanceof WorkspaceAlreadyPaidError) {
|
||||
// ignore the request, this is prob a replay from stripe
|
||||
} else {
|
||||
throw err
|
||||
upsertPaidWorkspacePlan: upsertPaidWorkspacePlanFactory({ db: trx }),
|
||||
upsertWorkspaceSubscription: upsertWorkspaceSubscriptionFactory({
|
||||
db: trx
|
||||
}),
|
||||
getSubscriptionData: getSubscriptionDataFactory({
|
||||
stripe
|
||||
})
|
||||
})
|
||||
|
||||
try {
|
||||
await withTransaction(
|
||||
completeCheckout({
|
||||
sessionId: session.id,
|
||||
subscriptionId
|
||||
}),
|
||||
trx
|
||||
)
|
||||
} catch (err) {
|
||||
if (err instanceof WorkspaceAlreadyPaidError) {
|
||||
// ignore the request, this is prob a replay from stripe
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
case 'unpaid':
|
||||
// if payment fails, we delete the failed session
|
||||
await deleteCheckoutSessionFactory({ db })({
|
||||
checkoutSessionId: event.data.object.id
|
||||
})
|
||||
}
|
||||
break
|
||||
|
||||
|
@ -211,6 +234,18 @@ export const getBillingRouter = (): Router => {
|
|||
})
|
||||
break
|
||||
|
||||
case 'customer.subscription.updated':
|
||||
case 'customer.subscription.deleted':
|
||||
await handleSubscriptionUpdateFactory({
|
||||
getWorkspacePlan: getWorkspacePlanFactory({ db }),
|
||||
upsertPaidWorkspacePlan: upsertPaidWorkspacePlanFactory({ db }),
|
||||
getWorkspaceSubscriptionBySubscriptionId:
|
||||
getWorkspaceSubscriptionBySubscriptionIdFactory({ db }),
|
||||
upsertWorkspaceSubscription: upsertWorkspaceSubscriptionFactory({ db })
|
||||
})({ subscriptionData: parseSubscriptionData(event.data.object) })
|
||||
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
@ -218,9 +253,5 @@ export const getBillingRouter = (): Router => {
|
|||
res.status(200).send('ok')
|
||||
})
|
||||
|
||||
// prob needed when the checkout is cancelled
|
||||
router.delete(
|
||||
'/api/v1/billing/workspaces/:workspaceSlug/checkout-session/:workspacePlan'
|
||||
)
|
||||
return router
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import { TokenScopeData } from '@/modules/shared/domain/rolesAndScopes/types'
|
||||
import { Scopes } from '@speckle/shared'
|
||||
|
||||
export const gatekeeperScopes: TokenScopeData[] = [
|
||||
{
|
||||
name: Scopes.Gatekeeper.WorkspaceBilling,
|
||||
description: 'Scope for managing workspace billing',
|
||||
public: false
|
||||
}
|
||||
]
|
|
@ -5,10 +5,11 @@ import {
|
|||
GetWorkspacePlan,
|
||||
SaveCheckoutSession,
|
||||
UpdateCheckoutSessionStatus,
|
||||
SaveWorkspaceSubscription,
|
||||
UpsertWorkspaceSubscription,
|
||||
UpsertPaidWorkspacePlan,
|
||||
GetSubscriptionData,
|
||||
GetWorkspaceCheckoutSession
|
||||
GetWorkspaceCheckoutSession,
|
||||
DeleteCheckoutSession
|
||||
} from '@/modules/gatekeeper/domain/billing'
|
||||
import {
|
||||
PaidWorkspacePlans,
|
||||
|
@ -25,12 +26,14 @@ import { Roles, throwUncoveredError } from '@speckle/shared'
|
|||
export const startCheckoutSessionFactory =
|
||||
({
|
||||
getWorkspaceCheckoutSession,
|
||||
deleteCheckoutSession,
|
||||
getWorkspacePlan,
|
||||
countRole,
|
||||
createCheckoutSession,
|
||||
saveCheckoutSession
|
||||
}: {
|
||||
getWorkspaceCheckoutSession: GetWorkspaceCheckoutSession
|
||||
deleteCheckoutSession: DeleteCheckoutSession
|
||||
getWorkspacePlan: GetWorkspacePlan
|
||||
countRole: CountWorkspaceRoleWithOptionalProjectRole
|
||||
createCheckoutSession: CreateCheckoutSession
|
||||
|
@ -50,19 +53,51 @@ export const startCheckoutSessionFactory =
|
|||
// get workspace plan, if we're already on a paid plan, do not allow checkout
|
||||
// paid plans should use a subscription modification
|
||||
const existingWorkspacePlan = await getWorkspacePlan({ workspaceId })
|
||||
|
||||
// it will technically not be possible to not have
|
||||
if (existingWorkspacePlan) {
|
||||
// maybe we can just ignore the plan not existing, cause we're putting it on a plan post checkout
|
||||
switch (existingWorkspacePlan.status) {
|
||||
// valid and paymentFailed, but not cancelled status is not something we need a checkout for
|
||||
// valid and paymentFailed, but not canceled status is not something we need a checkout for
|
||||
// we already have their credit card info
|
||||
case 'valid':
|
||||
case 'paymentFailed':
|
||||
case 'cancelationScheduled':
|
||||
throw new WorkspaceAlreadyPaidError()
|
||||
case 'cancelled':
|
||||
// maybe, we can reactivate cancelled plans via the sub in stripe, but this is fine too
|
||||
case 'canceled':
|
||||
const existingCheckoutSession = await getWorkspaceCheckoutSession({
|
||||
workspaceId
|
||||
})
|
||||
if (existingCheckoutSession)
|
||||
await deleteCheckoutSession({
|
||||
checkoutSessionId: existingCheckoutSession?.id
|
||||
})
|
||||
break
|
||||
|
||||
// maybe, we can reactivate canceled plans via the sub in stripe, but this is fine too
|
||||
// it will create a new customer and a new sub though, the reactivation would use the existing customer
|
||||
case 'trial':
|
||||
case 'expired':
|
||||
// if there is already a checkout session for the workspace, stop, someone else is maybe trying to pay for the workspace
|
||||
const workspaceCheckoutSession = await getWorkspaceCheckoutSession({
|
||||
workspaceId
|
||||
})
|
||||
if (workspaceCheckoutSession) {
|
||||
if (workspaceCheckoutSession.paymentStatus === 'paid')
|
||||
// this is should not be possible, but its better to be checking it here, than double charging the customer
|
||||
throw new WorkspaceAlreadyPaidError()
|
||||
if (
|
||||
new Date().getTime() - workspaceCheckoutSession.createdAt.getTime() >
|
||||
10 * 60 * 1000
|
||||
) {
|
||||
await deleteCheckoutSession({
|
||||
checkoutSessionId: workspaceCheckoutSession.id
|
||||
})
|
||||
} else {
|
||||
throw new WorkspaceCheckoutSessionInProgressError()
|
||||
}
|
||||
}
|
||||
|
||||
// lets go ahead and pay
|
||||
break
|
||||
default:
|
||||
|
@ -70,10 +105,6 @@ export const startCheckoutSessionFactory =
|
|||
}
|
||||
}
|
||||
|
||||
// if there is already a checkout session for the workspace, stop, someone else is maybe trying to pay for the workspace
|
||||
const workspaceCheckoutSession = await getWorkspaceCheckoutSession({ workspaceId })
|
||||
if (workspaceCheckoutSession) throw new WorkspaceCheckoutSessionInProgressError()
|
||||
|
||||
const [adminCount, memberCount, guestCount] = await Promise.all([
|
||||
countRole({ workspaceId, workspaceRole: Roles.Workspace.Admin }),
|
||||
countRole({ workspaceId, workspaceRole: Roles.Workspace.Member }),
|
||||
|
@ -98,13 +129,13 @@ export const completeCheckoutSessionFactory =
|
|||
({
|
||||
getCheckoutSession,
|
||||
updateCheckoutSessionStatus,
|
||||
saveWorkspaceSubscription,
|
||||
upsertWorkspaceSubscription,
|
||||
upsertPaidWorkspacePlan,
|
||||
getSubscriptionData
|
||||
}: {
|
||||
getCheckoutSession: GetCheckoutSession
|
||||
updateCheckoutSessionStatus: UpdateCheckoutSessionStatus
|
||||
saveWorkspaceSubscription: SaveWorkspaceSubscription
|
||||
upsertWorkspaceSubscription: UpsertWorkspaceSubscription
|
||||
upsertPaidWorkspacePlan: UpsertPaidWorkspacePlan
|
||||
getSubscriptionData: GetSubscriptionData
|
||||
}) =>
|
||||
|
@ -166,7 +197,7 @@ export const completeCheckoutSessionFactory =
|
|||
subscriptionData
|
||||
}
|
||||
|
||||
await saveWorkspaceSubscription({
|
||||
await upsertWorkspaceSubscription({
|
||||
workspaceSubscription
|
||||
})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
import {
|
||||
GetWorkspacePlan,
|
||||
GetWorkspaceSubscriptionBySubscriptionId,
|
||||
PaidWorkspacePlanStatuses,
|
||||
SubscriptionData,
|
||||
UpsertPaidWorkspacePlan,
|
||||
UpsertWorkspaceSubscription
|
||||
} from '@/modules/gatekeeper/domain/billing'
|
||||
import {
|
||||
WorkspacePlanMismatchError,
|
||||
WorkspacePlanNotFoundError,
|
||||
WorkspaceSubscriptionNotFoundError
|
||||
} from '@/modules/gatekeeper/errors/billing'
|
||||
import { throwUncoveredError } from '@speckle/shared'
|
||||
|
||||
export const handleSubscriptionUpdateFactory =
|
||||
({
|
||||
upsertPaidWorkspacePlan,
|
||||
getWorkspacePlan,
|
||||
getWorkspaceSubscriptionBySubscriptionId,
|
||||
upsertWorkspaceSubscription
|
||||
}: {
|
||||
getWorkspacePlan: GetWorkspacePlan
|
||||
upsertPaidWorkspacePlan: UpsertPaidWorkspacePlan
|
||||
getWorkspaceSubscriptionBySubscriptionId: GetWorkspaceSubscriptionBySubscriptionId
|
||||
upsertWorkspaceSubscription: UpsertWorkspaceSubscription
|
||||
}) =>
|
||||
async ({ subscriptionData }: { subscriptionData: SubscriptionData }) => {
|
||||
// we're only handling marking the sub scheduled for cancelation right now
|
||||
const subscription = await getWorkspaceSubscriptionBySubscriptionId({
|
||||
subscriptionId: subscriptionData.subscriptionId
|
||||
})
|
||||
if (!subscription) throw new WorkspaceSubscriptionNotFoundError()
|
||||
|
||||
const workspacePlan = await getWorkspacePlan({
|
||||
workspaceId: subscription.workspaceId
|
||||
})
|
||||
if (!workspacePlan) throw new WorkspacePlanNotFoundError()
|
||||
|
||||
let status: PaidWorkspacePlanStatuses | undefined = undefined
|
||||
|
||||
if (
|
||||
subscriptionData.status === 'active' &&
|
||||
subscriptionData.cancelAt &&
|
||||
subscriptionData.cancelAt > new Date()
|
||||
) {
|
||||
status = 'cancelationScheduled'
|
||||
} else if (
|
||||
subscriptionData.status === 'active' &&
|
||||
subscriptionData.cancelAt === null
|
||||
) {
|
||||
status = 'valid'
|
||||
} else if (subscriptionData.status === 'past_due') {
|
||||
status = 'paymentFailed'
|
||||
} else if (subscriptionData.status === 'canceled') {
|
||||
status = 'canceled'
|
||||
}
|
||||
|
||||
if (status) {
|
||||
switch (workspacePlan.name) {
|
||||
case 'team':
|
||||
case 'pro':
|
||||
case 'business':
|
||||
break
|
||||
case 'unlimited':
|
||||
case 'academia':
|
||||
throw new WorkspacePlanMismatchError()
|
||||
default:
|
||||
throwUncoveredError(workspacePlan)
|
||||
}
|
||||
|
||||
await upsertPaidWorkspacePlan({
|
||||
workspacePlan: { ...workspacePlan, status }
|
||||
})
|
||||
// if there is a status in the sub, we recognize, we need to update our state
|
||||
await upsertWorkspaceSubscription({
|
||||
workspaceSubscription: { ...subscription, subscriptionData }
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
import { GetWorkspacePlanPrice } from '@/modules/gatekeeper/domain/billing'
|
||||
import {
|
||||
WorkspacePlanBillingIntervals,
|
||||
WorkspacePricingPlans
|
||||
} from '@/modules/gatekeeper/domain/workspacePricing'
|
||||
import { getStringFromEnv, getStripeApiKey } from '@/modules/shared/helpers/envHelper'
|
||||
import { Stripe } from 'stripe'
|
||||
|
||||
let stripeClient: Stripe | undefined = undefined
|
||||
|
||||
export const getStripeClient = () => {
|
||||
if (!stripeClient) stripeClient = new Stripe(getStripeApiKey(), { typescript: true })
|
||||
return stripeClient
|
||||
}
|
||||
|
||||
export const workspacePlanPrices = (): Record<
|
||||
WorkspacePricingPlans,
|
||||
Record<WorkspacePlanBillingIntervals, string> & { productId: string }
|
||||
> => ({
|
||||
guest: {
|
||||
productId: getStringFromEnv('WORKSPACE_GUEST_SEAT_STRIPE_PRODUCT_ID'),
|
||||
monthly: getStringFromEnv('WORKSPACE_MONTHLY_GUEST_SEAT_STRIPE_PRICE_ID'),
|
||||
yearly: getStringFromEnv('WORKSPACE_YEARLY_GUEST_SEAT_STRIPE_PRICE_ID')
|
||||
},
|
||||
team: {
|
||||
productId: getStringFromEnv('WORKSPACE_TEAM_SEAT_STRIPE_PRODUCT_ID'),
|
||||
monthly: getStringFromEnv('WORKSPACE_MONTHLY_TEAM_SEAT_STRIPE_PRICE_ID'),
|
||||
yearly: getStringFromEnv('WORKSPACE_YEARLY_TEAM_SEAT_STRIPE_PRICE_ID')
|
||||
},
|
||||
pro: {
|
||||
productId: getStringFromEnv('WORKSPACE_PRO_SEAT_STRIPE_PRODUCT_ID'),
|
||||
monthly: getStringFromEnv('WORKSPACE_MONTHLY_PRO_SEAT_STRIPE_PRICE_ID'),
|
||||
yearly: getStringFromEnv('WORKSPACE_YEARLY_PRO_SEAT_STRIPE_PRICE_ID')
|
||||
},
|
||||
business: {
|
||||
productId: getStringFromEnv('WORKSPACE_BUSINESS_SEAT_STRIPE_PRODUCT_ID'),
|
||||
monthly: getStringFromEnv('WORKSPACE_MONTHLY_BUSINESS_SEAT_STRIPE_PRICE_ID'),
|
||||
yearly: getStringFromEnv('WORKSPACE_YEARLY_BUSINESS_SEAT_STRIPE_PRICE_ID')
|
||||
}
|
||||
})
|
||||
|
||||
export const getWorkspacePlanPrice: GetWorkspacePlanPrice = ({
|
||||
workspacePlan,
|
||||
billingInterval
|
||||
}) => workspacePlanPrices()[workspacePlan][billingInterval]
|
|
@ -1,13 +1,16 @@
|
|||
import db from '@/db/knex'
|
||||
import { WorkspaceSubscription } from '@/modules/gatekeeper/domain/billing'
|
||||
import {
|
||||
deleteCheckoutSessionFactory,
|
||||
getCheckoutSessionFactory,
|
||||
getWorkspaceCheckoutSessionFactory,
|
||||
getWorkspacePlanFactory,
|
||||
saveCheckoutSessionFactory,
|
||||
saveWorkspaceSubscriptionFactory,
|
||||
upsertWorkspaceSubscriptionFactory,
|
||||
updateCheckoutSessionStatusFactory,
|
||||
upsertPaidWorkspacePlanFactory
|
||||
upsertPaidWorkspacePlanFactory,
|
||||
getWorkspaceSubscriptionFactory,
|
||||
getWorkspaceSubscriptionBySubscriptionIdFactory
|
||||
} from '@/modules/gatekeeper/repositories/billing'
|
||||
import { upsertWorkspaceFactory } from '@/modules/workspaces/repositories/workspaces'
|
||||
import { createAndStoreTestWorkspaceFactory } from '@/test/speckle-helpers/workspaces'
|
||||
|
@ -25,7 +28,10 @@ const deleteCheckoutSession = deleteCheckoutSessionFactory({ db })
|
|||
const getCheckoutSession = getCheckoutSessionFactory({ db })
|
||||
const getWorkspaceCheckoutSession = getWorkspaceCheckoutSessionFactory({ db })
|
||||
const updateCheckoutSessionStatus = updateCheckoutSessionStatusFactory({ db })
|
||||
const saveWorkspaceSubscription = saveWorkspaceSubscriptionFactory({ db })
|
||||
const upsertWorkspaceSubscription = upsertWorkspaceSubscriptionFactory({ db })
|
||||
const getWorkspaceSubscription = getWorkspaceSubscriptionFactory({ db })
|
||||
const getWorkspaceSubscriptionBySubscriptionId =
|
||||
getWorkspaceSubscriptionBySubscriptionIdFactory({ db })
|
||||
|
||||
describe('billing repositories @gatekeeper', () => {
|
||||
describe('workspacePlans', () => {
|
||||
|
@ -194,24 +200,87 @@ describe('billing repositories @gatekeeper', () => {
|
|||
})
|
||||
})
|
||||
describe('workspaceSubscriptions', () => {
|
||||
describe('saveWorkspaceSubscription creates a function, that', () => {
|
||||
it('saves the subscription', async () => {
|
||||
describe('upsertWorkspaceSubscription creates a function, that', () => {
|
||||
it('saves and updates the subscription', async () => {
|
||||
const workspace = await createAndStoreTestWorkspace()
|
||||
const workspaceId = workspace.id
|
||||
await saveWorkspaceSubscription({
|
||||
workspaceSubscription: {
|
||||
billingInterval: 'monthly',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
currentBillingCycleEnd: new Date(),
|
||||
subscriptionData: {
|
||||
customerId: cryptoRandomString({ length: 10 }),
|
||||
products: [],
|
||||
subscriptionId: cryptoRandomString({ length: 10 })
|
||||
},
|
||||
workspaceId
|
||||
}
|
||||
const workspaceSubscription: WorkspaceSubscription = {
|
||||
billingInterval: 'monthly' as const,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
currentBillingCycleEnd: new Date(),
|
||||
subscriptionData: {
|
||||
customerId: cryptoRandomString({ length: 10 }),
|
||||
status: 'active' as const,
|
||||
cancelAt: null,
|
||||
products: [
|
||||
{
|
||||
priceId: cryptoRandomString({ length: 10 }),
|
||||
quantity: 10,
|
||||
productId: cryptoRandomString({ length: 10 }),
|
||||
subscriptionItemId: cryptoRandomString({ length: 10 })
|
||||
}
|
||||
],
|
||||
subscriptionId: cryptoRandomString({ length: 10 })
|
||||
},
|
||||
workspaceId
|
||||
}
|
||||
await upsertWorkspaceSubscription({ workspaceSubscription })
|
||||
let storedSubscription = await getWorkspaceSubscription({ workspaceId })
|
||||
expect(storedSubscription).deep.equal(workspaceSubscription)
|
||||
workspaceSubscription.billingInterval = 'yearly'
|
||||
workspaceSubscription.subscriptionData.products[0].quantity = 3
|
||||
|
||||
await upsertWorkspaceSubscription({ workspaceSubscription })
|
||||
storedSubscription = await getWorkspaceSubscription({ workspaceId })
|
||||
expect(storedSubscription).deep.equal(workspaceSubscription)
|
||||
})
|
||||
})
|
||||
describe('getWorkspaceSubscriptionFactory creates a function, that', () => {
|
||||
it('returns null if the subscription is not found', async () => {
|
||||
const sub = await getWorkspaceSubscription({
|
||||
workspaceId: cryptoRandomString({ length: 10 })
|
||||
})
|
||||
expect(sub).to.be.null
|
||||
})
|
||||
})
|
||||
|
||||
describe('getWorkspaceSubscriptionBySubscriptionIdFactory creates a function, that', () => {
|
||||
it('returns null if the subscription is not found', async () => {
|
||||
const sub = await getWorkspaceSubscriptionBySubscriptionId({
|
||||
subscriptionId: cryptoRandomString({ length: 10 })
|
||||
})
|
||||
expect(sub).to.be.null
|
||||
})
|
||||
it('returns the sub', async () => {
|
||||
const workspace = await createAndStoreTestWorkspace()
|
||||
const workspaceId = workspace.id
|
||||
const workspaceSubscription: WorkspaceSubscription = {
|
||||
billingInterval: 'monthly' as const,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
currentBillingCycleEnd: new Date(),
|
||||
subscriptionData: {
|
||||
customerId: cryptoRandomString({ length: 10 }),
|
||||
status: 'active' as const,
|
||||
cancelAt: null,
|
||||
products: [
|
||||
{
|
||||
priceId: cryptoRandomString({ length: 10 }),
|
||||
quantity: 10,
|
||||
productId: cryptoRandomString({ length: 10 }),
|
||||
subscriptionItemId: cryptoRandomString({ length: 10 })
|
||||
}
|
||||
],
|
||||
subscriptionId: cryptoRandomString({ length: 10 })
|
||||
},
|
||||
workspaceId
|
||||
}
|
||||
await upsertWorkspaceSubscription({ workspaceSubscription })
|
||||
const storedSubscription = await getWorkspaceSubscriptionBySubscriptionId({
|
||||
subscriptionId: workspaceSubscription.subscriptionData.subscriptionId
|
||||
})
|
||||
expect(storedSubscription).deep.equal(workspaceSubscription)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -43,6 +43,9 @@ describe('checkout @gatekeeper', () => {
|
|||
},
|
||||
saveCheckoutSession: () => {
|
||||
expect.fail()
|
||||
},
|
||||
deleteCheckoutSession: () => {
|
||||
expect.fail()
|
||||
}
|
||||
})({
|
||||
workspaceId,
|
||||
|
@ -71,6 +74,9 @@ describe('checkout @gatekeeper', () => {
|
|||
createCheckoutSession: () => {
|
||||
expect.fail()
|
||||
},
|
||||
deleteCheckoutSession: () => {
|
||||
expect.fail()
|
||||
},
|
||||
saveCheckoutSession: () => {
|
||||
expect.fail()
|
||||
}
|
||||
|
@ -83,6 +89,49 @@ describe('checkout @gatekeeper', () => {
|
|||
)
|
||||
expect(err.message).to.be.equal(new WorkspaceAlreadyPaidError().message)
|
||||
})
|
||||
it('does not allow checkout for a workspace, that already has a recent checkout session', async () => {
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
const err = await expectToThrow(() =>
|
||||
startCheckoutSessionFactory({
|
||||
getWorkspacePlan: async () => ({
|
||||
name: 'team',
|
||||
status: 'trial',
|
||||
workspaceId
|
||||
}),
|
||||
getWorkspaceCheckoutSession: async () => ({
|
||||
billingInterval: 'monthly',
|
||||
id: cryptoRandomString({ length: 10 }),
|
||||
paymentStatus: 'unpaid',
|
||||
url: '',
|
||||
workspaceId,
|
||||
workspacePlan: 'business',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}),
|
||||
countRole: () => {
|
||||
expect.fail()
|
||||
},
|
||||
createCheckoutSession: () => {
|
||||
expect.fail()
|
||||
},
|
||||
|
||||
deleteCheckoutSession: () => {
|
||||
expect.fail()
|
||||
},
|
||||
saveCheckoutSession: () => {
|
||||
expect.fail()
|
||||
}
|
||||
})({
|
||||
workspaceId,
|
||||
billingInterval: 'monthly',
|
||||
workspacePlan: 'business',
|
||||
workspaceSlug: cryptoRandomString({ length: 10 })
|
||||
})
|
||||
)
|
||||
expect(err.message).to.be.equal(
|
||||
new WorkspaceCheckoutSessionInProgressError().message
|
||||
)
|
||||
})
|
||||
it('does not allow checkout for a workspace, that already has a checkout session', async () => {
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
const err = await expectToThrow(() =>
|
||||
|
@ -108,6 +157,10 @@ describe('checkout @gatekeeper', () => {
|
|||
createCheckoutSession: () => {
|
||||
expect.fail()
|
||||
},
|
||||
|
||||
deleteCheckoutSession: () => {
|
||||
expect.fail()
|
||||
},
|
||||
saveCheckoutSession: () => {
|
||||
expect.fail()
|
||||
}
|
||||
|
@ -141,6 +194,9 @@ describe('checkout @gatekeeper', () => {
|
|||
getWorkspacePlan: async () => null,
|
||||
getWorkspaceCheckoutSession: async () => null,
|
||||
countRole: async () => 1,
|
||||
deleteCheckoutSession: () => {
|
||||
expect.fail()
|
||||
},
|
||||
createCheckoutSession: async () => checkoutSession,
|
||||
saveCheckoutSession: async ({ checkoutSession }) => {
|
||||
storedCheckoutSession = checkoutSession
|
||||
|
@ -154,7 +210,7 @@ describe('checkout @gatekeeper', () => {
|
|||
expect(checkoutSession).deep.equal(storedCheckoutSession)
|
||||
expect(checkoutSession).deep.equal(createdCheckoutSession)
|
||||
})
|
||||
it('creates and stores a checkout for TRIAL and CANCELLED workspaces', async () => {
|
||||
it('creates and stores a checkout for workspaces without a plan', async () => {
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
const workspacePlan: PaidWorkspacePlans = 'pro'
|
||||
const billingInterval: WorkspacePlanBillingIntervals = 'monthly'
|
||||
|
@ -173,6 +229,9 @@ describe('checkout @gatekeeper', () => {
|
|||
getWorkspacePlan: async () => null,
|
||||
getWorkspaceCheckoutSession: async () => null,
|
||||
countRole: async () => 1,
|
||||
deleteCheckoutSession: () => {
|
||||
expect.fail()
|
||||
},
|
||||
createCheckoutSession: async () => checkoutSession,
|
||||
saveCheckoutSession: async ({ checkoutSession }) => {
|
||||
storedCheckoutSession = checkoutSession
|
||||
|
@ -186,6 +245,222 @@ describe('checkout @gatekeeper', () => {
|
|||
expect(checkoutSession).deep.equal(storedCheckoutSession)
|
||||
expect(checkoutSession).deep.equal(createdCheckoutSession)
|
||||
})
|
||||
|
||||
it('creates and stores a checkout for TRIAL workspaces', async () => {
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
const workspacePlan: PaidWorkspacePlans = 'pro'
|
||||
const billingInterval: WorkspacePlanBillingIntervals = 'monthly'
|
||||
const checkoutSession: CheckoutSession = {
|
||||
id: cryptoRandomString({ length: 10 }),
|
||||
workspaceId,
|
||||
workspacePlan,
|
||||
url: 'https://example.com',
|
||||
billingInterval,
|
||||
paymentStatus: 'unpaid',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
let storedCheckoutSession: CheckoutSession | undefined = undefined
|
||||
const createdCheckoutSession = await startCheckoutSessionFactory({
|
||||
getWorkspacePlan: async () => ({ workspaceId, name: 'team', status: 'trial' }),
|
||||
getWorkspaceCheckoutSession: async () => null,
|
||||
countRole: async () => 1,
|
||||
deleteCheckoutSession: () => {
|
||||
expect.fail()
|
||||
},
|
||||
createCheckoutSession: async () => checkoutSession,
|
||||
saveCheckoutSession: async ({ checkoutSession }) => {
|
||||
storedCheckoutSession = checkoutSession
|
||||
}
|
||||
})({
|
||||
workspaceId,
|
||||
billingInterval,
|
||||
workspacePlan,
|
||||
workspaceSlug: cryptoRandomString({ length: 10 })
|
||||
})
|
||||
expect(checkoutSession).deep.equal(storedCheckoutSession)
|
||||
expect(checkoutSession).deep.equal(createdCheckoutSession)
|
||||
})
|
||||
|
||||
it('creates and stores a checkout for TRIAL workspaces even if it has an old unpaid checkout session', async () => {
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
const workspacePlan: PaidWorkspacePlans = 'pro'
|
||||
const billingInterval: WorkspacePlanBillingIntervals = 'monthly'
|
||||
const checkoutSession: CheckoutSession = {
|
||||
id: cryptoRandomString({ length: 10 }),
|
||||
workspaceId,
|
||||
workspacePlan,
|
||||
url: 'https://example.com',
|
||||
billingInterval,
|
||||
paymentStatus: 'unpaid',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
let existingCheckoutSession: CheckoutSession | undefined = {
|
||||
billingInterval,
|
||||
id: cryptoRandomString({ length: 10 }),
|
||||
createdAt: new Date(1990, 1, 12),
|
||||
updatedAt: new Date(1990, 1, 12),
|
||||
paymentStatus: 'unpaid',
|
||||
url: 'https://example.com',
|
||||
workspaceId,
|
||||
workspacePlan
|
||||
}
|
||||
let storedCheckoutSession: CheckoutSession | undefined = undefined
|
||||
const createdCheckoutSession = await startCheckoutSessionFactory({
|
||||
getWorkspacePlan: async () => ({ workspaceId, name: 'team', status: 'trial' }),
|
||||
getWorkspaceCheckoutSession: async () => existingCheckoutSession!,
|
||||
countRole: async () => 1,
|
||||
deleteCheckoutSession: async () => {
|
||||
existingCheckoutSession = undefined
|
||||
},
|
||||
createCheckoutSession: async () => checkoutSession,
|
||||
saveCheckoutSession: async ({ checkoutSession }) => {
|
||||
storedCheckoutSession = checkoutSession
|
||||
}
|
||||
})({
|
||||
workspaceId,
|
||||
billingInterval,
|
||||
workspacePlan,
|
||||
workspaceSlug: cryptoRandomString({ length: 10 })
|
||||
})
|
||||
expect(existingCheckoutSession).to.be.undefined
|
||||
expect(checkoutSession).deep.equal(storedCheckoutSession)
|
||||
expect(checkoutSession).deep.equal(createdCheckoutSession)
|
||||
})
|
||||
|
||||
it('does not allow checkout for TRIAL workspaces if there is a paid checkout session', async () => {
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
const workspacePlan: PaidWorkspacePlans = 'pro'
|
||||
const billingInterval: WorkspacePlanBillingIntervals = 'monthly'
|
||||
let existingCheckoutSession: CheckoutSession | undefined = {
|
||||
billingInterval,
|
||||
id: cryptoRandomString({ length: 10 }),
|
||||
createdAt: new Date(1990, 1, 12),
|
||||
updatedAt: new Date(1990, 1, 12),
|
||||
paymentStatus: 'paid',
|
||||
url: 'https://example.com',
|
||||
workspaceId,
|
||||
workspacePlan
|
||||
}
|
||||
const err = await expectToThrow(async () => {
|
||||
await startCheckoutSessionFactory({
|
||||
getWorkspacePlan: async () => ({
|
||||
workspaceId,
|
||||
name: 'team',
|
||||
status: 'trial'
|
||||
}),
|
||||
getWorkspaceCheckoutSession: async () => existingCheckoutSession!,
|
||||
countRole: async () => 1,
|
||||
deleteCheckoutSession: async () => {
|
||||
existingCheckoutSession = undefined
|
||||
},
|
||||
createCheckoutSession: async () => {
|
||||
expect.fail()
|
||||
},
|
||||
saveCheckoutSession: async () => {}
|
||||
})({
|
||||
workspaceId,
|
||||
billingInterval,
|
||||
workspacePlan,
|
||||
workspaceSlug: cryptoRandomString({ length: 10 })
|
||||
})
|
||||
})
|
||||
expect(err.message).to.equal(new WorkspaceAlreadyPaidError().message)
|
||||
})
|
||||
|
||||
it('does not allow checkout for TRIAL workspaces if there is a paid checkout session', async () => {
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
const workspacePlan: PaidWorkspacePlans = 'pro'
|
||||
const billingInterval: WorkspacePlanBillingIntervals = 'monthly'
|
||||
let existingCheckoutSession: CheckoutSession | undefined = {
|
||||
billingInterval,
|
||||
id: cryptoRandomString({ length: 10 }),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
paymentStatus: 'unpaid',
|
||||
url: 'https://example.com',
|
||||
workspaceId,
|
||||
workspacePlan
|
||||
}
|
||||
const err = await expectToThrow(async () => {
|
||||
await startCheckoutSessionFactory({
|
||||
getWorkspacePlan: async () => ({
|
||||
workspaceId,
|
||||
name: 'team',
|
||||
status: 'trial'
|
||||
}),
|
||||
getWorkspaceCheckoutSession: async () => existingCheckoutSession!,
|
||||
countRole: async () => 1,
|
||||
deleteCheckoutSession: async () => {
|
||||
existingCheckoutSession = undefined
|
||||
},
|
||||
createCheckoutSession: async () => {
|
||||
expect.fail()
|
||||
},
|
||||
saveCheckoutSession: async () => {}
|
||||
})({
|
||||
workspaceId,
|
||||
billingInterval,
|
||||
workspacePlan,
|
||||
workspaceSlug: cryptoRandomString({ length: 10 })
|
||||
})
|
||||
})
|
||||
expect(err.message).to.equal(
|
||||
new WorkspaceCheckoutSessionInProgressError().message
|
||||
)
|
||||
})
|
||||
|
||||
it('creates and stores a checkout for CANCELED workspaces', async () => {
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
const workspacePlan: PaidWorkspacePlans = 'pro'
|
||||
const billingInterval: WorkspacePlanBillingIntervals = 'monthly'
|
||||
const checkoutSession: CheckoutSession = {
|
||||
id: cryptoRandomString({ length: 10 }),
|
||||
workspaceId,
|
||||
workspacePlan,
|
||||
url: 'https://example.com',
|
||||
billingInterval,
|
||||
paymentStatus: 'unpaid',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
let existingCheckoutSession: CheckoutSession | undefined = {
|
||||
billingInterval: 'monthly',
|
||||
id: cryptoRandomString({ length: 10 }),
|
||||
paymentStatus: 'paid',
|
||||
url: '',
|
||||
workspaceId,
|
||||
workspacePlan: 'business',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
let storedCheckoutSession: CheckoutSession | undefined = undefined
|
||||
const createdCheckoutSession = await startCheckoutSessionFactory({
|
||||
getWorkspacePlan: async () => ({
|
||||
name: 'pro',
|
||||
workspaceId,
|
||||
status: 'canceled'
|
||||
}),
|
||||
getWorkspaceCheckoutSession: async () => existingCheckoutSession!,
|
||||
countRole: async () => 1,
|
||||
deleteCheckoutSession: async () => {
|
||||
existingCheckoutSession = undefined
|
||||
},
|
||||
createCheckoutSession: async () => checkoutSession,
|
||||
saveCheckoutSession: async ({ checkoutSession }) => {
|
||||
storedCheckoutSession = checkoutSession
|
||||
}
|
||||
})({
|
||||
workspaceId,
|
||||
billingInterval,
|
||||
workspacePlan,
|
||||
workspaceSlug: cryptoRandomString({ length: 10 })
|
||||
})
|
||||
expect(existingCheckoutSession).to.be.undefined
|
||||
expect(checkoutSession).deep.equal(storedCheckoutSession)
|
||||
expect(checkoutSession).deep.equal(createdCheckoutSession)
|
||||
})
|
||||
})
|
||||
describe('completeCheckoutSessionFactory creates a function, that', () => {
|
||||
it('throws a CheckoutSessionNotFound if the checkoutSession is null', async () => {
|
||||
|
@ -204,7 +479,7 @@ describe('checkout @gatekeeper', () => {
|
|||
getSubscriptionData: async () => {
|
||||
expect.fail()
|
||||
},
|
||||
saveWorkspaceSubscription: async () => {
|
||||
upsertWorkspaceSubscription: async () => {
|
||||
expect.fail()
|
||||
}
|
||||
})({ sessionId, subscriptionId })
|
||||
|
@ -236,7 +511,7 @@ describe('checkout @gatekeeper', () => {
|
|||
getSubscriptionData: async () => {
|
||||
expect.fail()
|
||||
},
|
||||
saveWorkspaceSubscription: async () => {
|
||||
upsertWorkspaceSubscription: async () => {
|
||||
expect.fail()
|
||||
}
|
||||
})({ sessionId, subscriptionId })
|
||||
|
@ -272,7 +547,9 @@ describe('checkout @gatekeeper', () => {
|
|||
quantity: 10,
|
||||
subscriptionItemId: cryptoRandomString({ length: 10 })
|
||||
}
|
||||
]
|
||||
],
|
||||
status: 'active',
|
||||
cancelAt: null
|
||||
}
|
||||
|
||||
let storedWorkspaceSubscriptionData: WorkspaceSubscription | undefined =
|
||||
|
@ -287,7 +564,7 @@ describe('checkout @gatekeeper', () => {
|
|||
storedWorkspacePlan = workspacePlan
|
||||
},
|
||||
getSubscriptionData: async () => subscriptionData,
|
||||
saveWorkspaceSubscription: async ({ workspaceSubscription }) => {
|
||||
upsertWorkspaceSubscription: async ({ workspaceSubscription }) => {
|
||||
storedWorkspaceSubscriptionData = workspaceSubscription
|
||||
}
|
||||
})({ sessionId, subscriptionId })
|
||||
|
|
|
@ -0,0 +1,262 @@
|
|||
import {
|
||||
SubscriptionData,
|
||||
WorkspacePlan,
|
||||
WorkspaceSubscription
|
||||
} from '@/modules/gatekeeper/domain/billing'
|
||||
import {
|
||||
WorkspacePlanMismatchError,
|
||||
WorkspacePlanNotFoundError,
|
||||
WorkspaceSubscriptionNotFoundError
|
||||
} from '@/modules/gatekeeper/errors/billing'
|
||||
import { handleSubscriptionUpdateFactory } from '@/modules/gatekeeper/services/subscriptions'
|
||||
import { expectToThrow } from '@/test/assertionHelper'
|
||||
import { expect } from 'chai'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
import { merge } from 'lodash'
|
||||
|
||||
const createTestSubscriptionData = (
|
||||
overrides: Partial<SubscriptionData> = {}
|
||||
): SubscriptionData => {
|
||||
const defaultValues: SubscriptionData = {
|
||||
cancelAt: null,
|
||||
customerId: cryptoRandomString({ length: 10 }),
|
||||
products: [
|
||||
{
|
||||
priceId: cryptoRandomString({ length: 10 }),
|
||||
productId: cryptoRandomString({ length: 10 }),
|
||||
quantity: 3,
|
||||
subscriptionItemId: cryptoRandomString({ length: 10 })
|
||||
}
|
||||
],
|
||||
status: 'active',
|
||||
subscriptionId: cryptoRandomString({ length: 10 })
|
||||
}
|
||||
return merge(defaultValues, overrides)
|
||||
}
|
||||
|
||||
describe('subscriptions @gatekeeper', () => {
|
||||
describe('handleSubscriptionUpdateFactory creates a function, that', () => {
|
||||
it('throws if subscription is not found', async () => {
|
||||
const subscriptionData = createTestSubscriptionData()
|
||||
const err = await expectToThrow(async () => {
|
||||
await handleSubscriptionUpdateFactory({
|
||||
getWorkspaceSubscriptionBySubscriptionId: async () => null,
|
||||
getWorkspacePlan: async () => {
|
||||
expect.fail()
|
||||
},
|
||||
upsertWorkspaceSubscription: async () => {
|
||||
expect.fail()
|
||||
},
|
||||
upsertPaidWorkspacePlan: async () => {
|
||||
expect.fail()
|
||||
}
|
||||
})({ subscriptionData })
|
||||
})
|
||||
expect(err.message).to.equal(new WorkspaceSubscriptionNotFoundError().message)
|
||||
})
|
||||
it('throws if workspacePlan is not found', async () => {
|
||||
const subscriptionData = createTestSubscriptionData()
|
||||
const err = await expectToThrow(async () => {
|
||||
await handleSubscriptionUpdateFactory({
|
||||
getWorkspaceSubscriptionBySubscriptionId: async () => ({
|
||||
subscriptionData,
|
||||
billingInterval: 'monthly',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
currentBillingCycleEnd: new Date(),
|
||||
workspaceId: cryptoRandomString({ length: 10 })
|
||||
}),
|
||||
getWorkspacePlan: async () => null,
|
||||
upsertWorkspaceSubscription: async () => {
|
||||
expect.fail()
|
||||
},
|
||||
upsertPaidWorkspacePlan: async () => {
|
||||
expect.fail()
|
||||
}
|
||||
})({ subscriptionData })
|
||||
})
|
||||
expect(err.message).to.equal(new WorkspacePlanNotFoundError().message)
|
||||
})
|
||||
;(['unlimited', 'academia'] as const).forEach((name) =>
|
||||
it(`throws for non paid workspace plan: ${name}`, async () => {
|
||||
const subscriptionData = createTestSubscriptionData()
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
const err = await expectToThrow(async () => {
|
||||
await handleSubscriptionUpdateFactory({
|
||||
getWorkspaceSubscriptionBySubscriptionId: async () => ({
|
||||
subscriptionData,
|
||||
billingInterval: 'monthly',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
currentBillingCycleEnd: new Date(),
|
||||
workspaceId
|
||||
}),
|
||||
getWorkspacePlan: async () => ({ name, workspaceId, status: 'valid' }),
|
||||
upsertWorkspaceSubscription: async () => {
|
||||
expect.fail()
|
||||
},
|
||||
upsertPaidWorkspacePlan: async () => {
|
||||
expect.fail()
|
||||
}
|
||||
})({ subscriptionData })
|
||||
})
|
||||
expect(err.message).to.equal(new WorkspacePlanMismatchError().message)
|
||||
})
|
||||
)
|
||||
it('sets the state to cancelationScheduled', async () => {
|
||||
const subscriptionData = createTestSubscriptionData({
|
||||
status: 'active',
|
||||
cancelAt: new Date(2099, 12, 31)
|
||||
})
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
const workspaceSubscription = {
|
||||
subscriptionData,
|
||||
billingInterval: 'monthly' as const,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
currentBillingCycleEnd: new Date(),
|
||||
workspaceId
|
||||
}
|
||||
|
||||
let updatedSubscription: WorkspaceSubscription | undefined = undefined
|
||||
let updatedPlan: WorkspacePlan | undefined = undefined
|
||||
|
||||
await handleSubscriptionUpdateFactory({
|
||||
getWorkspaceSubscriptionBySubscriptionId: async () => workspaceSubscription,
|
||||
getWorkspacePlan: async () => ({ name: 'team', workspaceId, status: 'trial' }),
|
||||
upsertWorkspaceSubscription: async ({ workspaceSubscription }) => {
|
||||
updatedSubscription = workspaceSubscription
|
||||
},
|
||||
upsertPaidWorkspacePlan: async ({ workspacePlan }) => {
|
||||
updatedPlan = workspacePlan
|
||||
}
|
||||
})({ subscriptionData })
|
||||
expect(updatedPlan!.status).to.be.equal('cancelationScheduled')
|
||||
expect(updatedSubscription).deep.equal(workspaceSubscription)
|
||||
})
|
||||
it('sets the state to valid', async () => {
|
||||
const subscriptionData = createTestSubscriptionData({
|
||||
status: 'active',
|
||||
cancelAt: null
|
||||
})
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
const workspaceSubscription = {
|
||||
subscriptionData,
|
||||
billingInterval: 'monthly' as const,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
currentBillingCycleEnd: new Date(),
|
||||
workspaceId
|
||||
}
|
||||
|
||||
let updatedSubscription: WorkspaceSubscription | undefined = undefined
|
||||
let updatedPlan: WorkspacePlan | undefined = undefined
|
||||
|
||||
await handleSubscriptionUpdateFactory({
|
||||
getWorkspaceSubscriptionBySubscriptionId: async () => workspaceSubscription,
|
||||
getWorkspacePlan: async () => ({ name: 'team', workspaceId, status: 'trial' }),
|
||||
upsertWorkspaceSubscription: async ({ workspaceSubscription }) => {
|
||||
updatedSubscription = workspaceSubscription
|
||||
},
|
||||
upsertPaidWorkspacePlan: async ({ workspacePlan }) => {
|
||||
updatedPlan = workspacePlan
|
||||
}
|
||||
})({ subscriptionData })
|
||||
expect(updatedPlan!.status).to.be.equal('valid')
|
||||
expect(updatedSubscription).deep.equal(workspaceSubscription)
|
||||
})
|
||||
it('sets the state to paymentFailed', async () => {
|
||||
const subscriptionData = createTestSubscriptionData({
|
||||
status: 'past_due'
|
||||
})
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
const workspaceSubscription = {
|
||||
subscriptionData,
|
||||
billingInterval: 'monthly' as const,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
currentBillingCycleEnd: new Date(),
|
||||
workspaceId
|
||||
}
|
||||
|
||||
let updatedSubscription: WorkspaceSubscription | undefined = undefined
|
||||
let updatedPlan: WorkspacePlan | undefined = undefined
|
||||
|
||||
await handleSubscriptionUpdateFactory({
|
||||
getWorkspaceSubscriptionBySubscriptionId: async () => workspaceSubscription,
|
||||
getWorkspacePlan: async () => ({ name: 'team', workspaceId, status: 'trial' }),
|
||||
upsertWorkspaceSubscription: async ({ workspaceSubscription }) => {
|
||||
updatedSubscription = workspaceSubscription
|
||||
},
|
||||
upsertPaidWorkspacePlan: async ({ workspacePlan }) => {
|
||||
updatedPlan = workspacePlan
|
||||
}
|
||||
})({ subscriptionData })
|
||||
expect(updatedPlan!.status).to.be.equal('paymentFailed')
|
||||
expect(updatedSubscription).deep.equal(workspaceSubscription)
|
||||
})
|
||||
it('sets the state to canceled', async () => {
|
||||
const subscriptionData = createTestSubscriptionData({
|
||||
status: 'canceled'
|
||||
})
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
const workspaceSubscription = {
|
||||
subscriptionData,
|
||||
billingInterval: 'monthly' as const,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
currentBillingCycleEnd: new Date(),
|
||||
workspaceId
|
||||
}
|
||||
|
||||
let updatedSubscription: WorkspaceSubscription | undefined = undefined
|
||||
let updatedPlan: WorkspacePlan | undefined = undefined
|
||||
|
||||
await handleSubscriptionUpdateFactory({
|
||||
getWorkspaceSubscriptionBySubscriptionId: async () => workspaceSubscription,
|
||||
getWorkspacePlan: async () => ({ name: 'team', workspaceId, status: 'trial' }),
|
||||
upsertWorkspaceSubscription: async ({ workspaceSubscription }) => {
|
||||
updatedSubscription = workspaceSubscription
|
||||
},
|
||||
upsertPaidWorkspacePlan: async ({ workspacePlan }) => {
|
||||
updatedPlan = workspacePlan
|
||||
}
|
||||
})({ subscriptionData })
|
||||
expect(updatedPlan!.status).to.be.equal('canceled')
|
||||
expect(updatedSubscription).deep.equal(workspaceSubscription)
|
||||
})
|
||||
;(
|
||||
['incomplete', 'incomplete_expired', 'trialing', 'unpaid', 'paused'] as const
|
||||
).forEach((status) => {
|
||||
it(`does not update the plan or the subscription in case of an unhandled status: ${status}`, async () => {
|
||||
const subscriptionData = createTestSubscriptionData({
|
||||
status
|
||||
})
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
const workspaceSubscription = {
|
||||
subscriptionData,
|
||||
billingInterval: 'monthly' as const,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
currentBillingCycleEnd: new Date(),
|
||||
workspaceId
|
||||
}
|
||||
|
||||
await handleSubscriptionUpdateFactory({
|
||||
getWorkspaceSubscriptionBySubscriptionId: async () => workspaceSubscription,
|
||||
getWorkspacePlan: async () => ({
|
||||
name: 'team',
|
||||
workspaceId,
|
||||
status: 'trial'
|
||||
}),
|
||||
upsertWorkspaceSubscription: async () => {
|
||||
expect.fail()
|
||||
},
|
||||
upsertPaidWorkspacePlan: async () => {
|
||||
expect.fail()
|
||||
}
|
||||
})({ subscriptionData })
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -427,6 +427,11 @@ export type BasicGitRepositoryMetadata = {
|
|||
url: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export enum BillingInterval {
|
||||
Monthly = 'monthly',
|
||||
Yearly = 'yearly'
|
||||
}
|
||||
|
||||
export type BlobMetadata = {
|
||||
__typename?: 'BlobMetadata';
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
|
@ -504,6 +509,28 @@ export type BranchUpdateInput = {
|
|||
streamId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type CancelCheckoutSessionInput = {
|
||||
sessionId: Scalars['ID']['input'];
|
||||
workspaceId: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
export type CheckoutSession = {
|
||||
__typename?: 'CheckoutSession';
|
||||
billingInterval: BillingInterval;
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
id: Scalars['ID']['output'];
|
||||
paymentStatus: SessionPaymentStatus;
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
url: Scalars['String']['output'];
|
||||
workspacePlan: PaidWorkspacePlans;
|
||||
};
|
||||
|
||||
export type CheckoutSessionInput = {
|
||||
billingInterval: BillingInterval;
|
||||
workspaceId: Scalars['ID']['input'];
|
||||
workspacePlan: PaidWorkspacePlans;
|
||||
};
|
||||
|
||||
export type Comment = {
|
||||
__typename?: 'Comment';
|
||||
archived: Scalars['Boolean']['output'];
|
||||
|
@ -1713,6 +1740,12 @@ export type ObjectCreateInput = {
|
|||
streamId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export enum PaidWorkspacePlans {
|
||||
Business = 'business',
|
||||
Pro = 'pro',
|
||||
Team = 'team'
|
||||
}
|
||||
|
||||
export type PasswordStrengthCheckFeedback = {
|
||||
__typename?: 'PasswordStrengthCheckFeedback';
|
||||
suggestions: Array<Scalars['String']['output']>;
|
||||
|
@ -2856,6 +2889,11 @@ export type ServerWorkspacesInfo = {
|
|||
workspacesEnabled: Scalars['Boolean']['output'];
|
||||
};
|
||||
|
||||
export enum SessionPaymentStatus {
|
||||
Paid = 'paid',
|
||||
Unpaid = 'unpaid'
|
||||
}
|
||||
|
||||
export type SetPrimaryUserEmailInput = {
|
||||
id: Scalars['ID']['input'];
|
||||
};
|
||||
|
@ -3893,6 +3931,7 @@ export type Workspace = {
|
|||
/** Billing data for Workspaces beta */
|
||||
billing?: Maybe<WorkspaceBilling>;
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
customerPortalUrl?: Maybe<Scalars['String']['output']>;
|
||||
/** Selected fallback when `logo` not set */
|
||||
defaultLogoIndex: Scalars['Int']['output'];
|
||||
/** The default role workspace members will receive for workspace projects. */
|
||||
|
@ -3910,10 +3949,12 @@ export type Workspace = {
|
|||
/** Logo image as base64-encoded string */
|
||||
logo?: Maybe<Scalars['String']['output']>;
|
||||
name: Scalars['String']['output'];
|
||||
plan?: Maybe<WorkspacePlan>;
|
||||
projects: ProjectCollection;
|
||||
/** Active user's role for this workspace. `null` if request is not authenticated, or the workspace is not explicitly shared with you. */
|
||||
role?: Maybe<Scalars['String']['output']>;
|
||||
slug: Scalars['String']['output'];
|
||||
subscription?: Maybe<WorkspaceSubscription>;
|
||||
team: WorkspaceCollaboratorCollection;
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
};
|
||||
|
@ -3943,6 +3984,22 @@ export type WorkspaceBilling = {
|
|||
versionsCount: WorkspaceVersionsCount;
|
||||
};
|
||||
|
||||
export type WorkspaceBillingMutations = {
|
||||
__typename?: 'WorkspaceBillingMutations';
|
||||
cancelCheckoutSession: Scalars['Boolean']['output'];
|
||||
createCheckoutSession: CheckoutSession;
|
||||
};
|
||||
|
||||
|
||||
export type WorkspaceBillingMutationsCancelCheckoutSessionArgs = {
|
||||
input: CancelCheckoutSessionInput;
|
||||
};
|
||||
|
||||
|
||||
export type WorkspaceBillingMutationsCreateCheckoutSessionArgs = {
|
||||
input: CheckoutSessionInput;
|
||||
};
|
||||
|
||||
/** Overridden by `WorkspaceCollaboratorGraphQLReturn` */
|
||||
export type WorkspaceCollaborator = {
|
||||
__typename?: 'WorkspaceCollaborator';
|
||||
|
@ -4084,6 +4141,7 @@ export type WorkspaceInviteUseInput = {
|
|||
export type WorkspaceMutations = {
|
||||
__typename?: 'WorkspaceMutations';
|
||||
addDomain: Workspace;
|
||||
billing: WorkspaceBillingMutations;
|
||||
create: Workspace;
|
||||
delete: Scalars['Boolean']['output'];
|
||||
deleteDomain: Workspace;
|
||||
|
@ -4135,6 +4193,27 @@ export type WorkspaceMutationsUpdateRoleArgs = {
|
|||
input: WorkspaceRoleUpdateInput;
|
||||
};
|
||||
|
||||
export type WorkspacePlan = {
|
||||
__typename?: 'WorkspacePlan';
|
||||
name: WorkspacePlans;
|
||||
status: WorkspacePlanStatuses;
|
||||
};
|
||||
|
||||
export enum WorkspacePlanStatuses {
|
||||
Canceled = 'canceled',
|
||||
PaymentFailed = 'paymentFailed',
|
||||
Trial = 'trial',
|
||||
Valid = 'valid'
|
||||
}
|
||||
|
||||
export enum WorkspacePlans {
|
||||
Academia = 'academia',
|
||||
Business = 'business',
|
||||
Pro = 'pro',
|
||||
Team = 'team',
|
||||
Unlimited = 'unlimited'
|
||||
}
|
||||
|
||||
export type WorkspaceProjectInviteCreateInput = {
|
||||
/** Either this or userId must be filled */
|
||||
email?: InputMaybe<Scalars['String']['input']>;
|
||||
|
@ -4188,6 +4267,14 @@ export type WorkspaceRoleUpdateInput = {
|
|||
workspaceId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type WorkspaceSubscription = {
|
||||
__typename?: 'WorkspaceSubscription';
|
||||
billingInterval: BillingInterval;
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
currentBillingCycleEnd: Scalars['DateTime']['output'];
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
};
|
||||
|
||||
export type WorkspaceTeamFilter = {
|
||||
/** Limit team members to provided role(s) */
|
||||
roles?: InputMaybe<Array<Scalars['String']['input']>>;
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
"main": "./dist/commonjs/index.js",
|
||||
"types": "./dist/commonjs/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "echo \"Building shared...\" && NODE_ENV=production tshy && echo \"Done building shared!\"",
|
||||
"build": "NODE_ENV=production tshy",
|
||||
"dev": "tshy --watch",
|
||||
"prepack": "yarn build",
|
||||
"lint:eslint": "eslint .",
|
||||
|
|
|
@ -123,6 +123,9 @@ export const Scopes = Object.freeze(<const>{
|
|||
Read: 'workspace:read',
|
||||
Update: 'workspace:update',
|
||||
Delete: 'workspace:delete'
|
||||
},
|
||||
Gatekeeper: {
|
||||
WorkspaceBilling: 'workspace:billing'
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -138,6 +141,8 @@ export type AutomateFunctionScopes =
|
|||
(typeof Scopes)['AutomateFunctions'][keyof (typeof Scopes)['AutomateFunctions']]
|
||||
export type WorkspaceScopes =
|
||||
(typeof Scopes)['Workspaces'][keyof (typeof Scopes)['Workspaces']]
|
||||
export type GatekeeperScopes =
|
||||
(typeof Scopes)['Gatekeeper'][keyof (typeof Scopes)['Gatekeeper']]
|
||||
|
||||
export type AvailableScopes =
|
||||
| StreamScopes
|
||||
|
|
Загрузка…
Ссылка в новой задаче