Billing - Fix inactivity (#17703)
- Add suspendedAt column on workspace table - Update suspendedAt field when workspace suspended or re-activated - Compute inactivity on suspendedAt Tested : - Cancel subscription - Cancel subscription + Subscribe again - Cleaning command
This commit is contained in:
+19
@@ -0,0 +1,19 @@
|
||||
import { type MigrationInterface, type QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddSuspendedAtColumnOnWorkspaceTable1770198374736
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddSuspendedAtColumnOnWorkspaceTable1770198374736';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."workspace" ADD "suspendedAt" TIMESTAMP WITH TIME ZONE`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."workspace" DROP COLUMN "suspendedAt"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -3,10 +3,10 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { In, Repository } from 'typeorm';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
|
||||
import type Stripe from 'stripe';
|
||||
|
||||
@@ -145,6 +145,7 @@ export class BillingWebhookSubscriptionService {
|
||||
if (workspace.activationStatus === WorkspaceActivationStatus.ACTIVE) {
|
||||
await this.workspaceRepository.update(workspaceId, {
|
||||
activationStatus: WorkspaceActivationStatus.SUSPENDED,
|
||||
suspendedAt: new Date(),
|
||||
});
|
||||
} else if (
|
||||
workspace.activationStatus ===
|
||||
@@ -157,6 +158,7 @@ export class BillingWebhookSubscriptionService {
|
||||
) {
|
||||
await this.workspaceRepository.update(workspaceId, {
|
||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||
suspendedAt: null,
|
||||
});
|
||||
|
||||
await this.messageQueueService.add<CleanWorkspaceDeletionWarningUserVarsJobData>(
|
||||
|
||||
@@ -31,7 +31,6 @@ import { PostgresCredentialsEntity } from 'src/engine/core-modules/postgres-cred
|
||||
import { PublicDomainEntity } from 'src/engine/core-modules/public-domain/public-domain.entity';
|
||||
import { WorkspaceSSOIdentityProviderEntity } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { WebhookEntity } from 'src/engine/metadata-modules/webhook/entities/webhook.entity';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
|
||||
import {
|
||||
DEFAULT_FAST_MODEL,
|
||||
@@ -51,6 +50,7 @@ import { ViewSortDTO } from 'src/engine/metadata-modules/view-sort/dtos/view-sor
|
||||
import { ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity';
|
||||
import { ViewDTO } from 'src/engine/metadata-modules/view/dtos/view.dto';
|
||||
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
import { WebhookEntity } from 'src/engine/metadata-modules/webhook/entities/webhook.entity';
|
||||
|
||||
registerEnumType(WorkspaceActivationStatus, {
|
||||
name: 'WorkspaceActivationStatus',
|
||||
@@ -159,6 +159,9 @@ export class WorkspaceEntity {
|
||||
@Index('IDX_WORKSPACE_ACTIVATION_STATUS')
|
||||
activationStatus: WorkspaceActivationStatus;
|
||||
|
||||
@Column({ type: 'timestamptz', nullable: true })
|
||||
suspendedAt: Date | null;
|
||||
|
||||
@OneToMany(
|
||||
() => PostgresCredentialsEntity,
|
||||
(postgresCredentials) => postgresCredentials.workspace,
|
||||
|
||||
+12
-28
@@ -79,30 +79,14 @@ export class CleanerWorkspaceService {
|
||||
);
|
||||
}
|
||||
|
||||
async computeDaysSinceSubscriptionUnpaid(
|
||||
async computeDaysSinceSuspended(
|
||||
workspace: WorkspaceEntity,
|
||||
): Promise<number | null> {
|
||||
const lastSubscription =
|
||||
await this.billingSubscriptionRepository.findOneOrFail({
|
||||
where: {
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
order: { updatedAt: 'DESC' },
|
||||
});
|
||||
|
||||
if (
|
||||
lastSubscription.status !== SubscriptionStatus.Unpaid &&
|
||||
lastSubscription.status !== SubscriptionStatus.Canceled
|
||||
) {
|
||||
return null;
|
||||
if (isDefined(workspace.suspendedAt)) {
|
||||
return differenceInDays(new Date(), workspace.suspendedAt);
|
||||
}
|
||||
|
||||
const daysSinceSubscriptionUnpaid = differenceInDays(
|
||||
new Date(),
|
||||
lastSubscription.currentPeriodStart,
|
||||
);
|
||||
|
||||
return daysSinceSubscriptionUnpaid;
|
||||
return null;
|
||||
}
|
||||
|
||||
async checkIfAtLeastOneWorkspaceMemberWarned(
|
||||
@@ -425,20 +409,20 @@ export class CleanerWorkspaceService {
|
||||
continue;
|
||||
}
|
||||
|
||||
const workspaceInactivity =
|
||||
await this.computeDaysSinceSubscriptionUnpaid(workspace);
|
||||
const inactiveDaysSinceSuspended =
|
||||
await this.computeDaysSinceSuspended(workspace);
|
||||
|
||||
if (workspaceInactivity === null) {
|
||||
if (inactiveDaysSinceSuspended === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
(!isDefined(onlyOperation) || onlyOperation === 'soft-delete') &&
|
||||
workspaceInactivity > this.inactiveDaysBeforeSoftDelete
|
||||
inactiveDaysSinceSuspended > this.inactiveDaysBeforeSoftDelete
|
||||
) {
|
||||
await this.informWorkspaceMembersAndSoftDeleteWorkspace(
|
||||
workspace,
|
||||
workspaceInactivity,
|
||||
inactiveDaysSinceSuspended,
|
||||
dryRun,
|
||||
);
|
||||
|
||||
@@ -447,12 +431,12 @@ export class CleanerWorkspaceService {
|
||||
|
||||
if (
|
||||
(!isDefined(onlyOperation) || onlyOperation === 'warn') &&
|
||||
workspaceInactivity > this.inactiveDaysBeforeWarn &&
|
||||
workspaceInactivity <= this.inactiveDaysBeforeSoftDelete
|
||||
inactiveDaysSinceSuspended > this.inactiveDaysBeforeWarn &&
|
||||
inactiveDaysSinceSuspended <= this.inactiveDaysBeforeSoftDelete
|
||||
) {
|
||||
await this.warnWorkspaceMembers(
|
||||
workspace,
|
||||
workspaceInactivity,
|
||||
inactiveDaysSinceSuspended,
|
||||
dryRun,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user