mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-23 03:51:36 +03:00
add info to customer table and stripe customer data (#9004)
Solves (https://github.com/twentyhq/private-issues/issues/194) **TLDR** Updates the billingCustomer table data using stripe webhooks event, also updates the customer's metadata in stripe, in order to contain the workspaceId associated to this customer. **In order to test** Billing: - Set IS_BILLING_ENABLED to true - Add your BILLING_STRIPE_SECRET and BILLING_STRIPE_API_KEY - Add your BILLING_STRIPE_BASE_PLAN_PRODUCT_ID (use the one in testMode > Base Plan) - Authenticate with your account in the stripe CLI Run the command: stripe listen --forward-to http://localhost:3000/billing/webhooks Run the twenty workker Authenticate yourself on the app choose a plan and run the app normally. In stripe and in posgress the customer table data should be added. **Next steps** Learn more about integrations tests and implement some for this PR. --------- Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
parent
c776179ecc
commit
bce5be85a3
@ -16,7 +16,8 @@ import {
|
|||||||
} from 'src/engine/core-modules/billing/billing.exception';
|
} from 'src/engine/core-modules/billing/billing.exception';
|
||||||
import { WebhookEvent } from 'src/engine/core-modules/billing/enums/billing-webhook-events.enum';
|
import { WebhookEvent } from 'src/engine/core-modules/billing/enums/billing-webhook-events.enum';
|
||||||
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
||||||
import { BillingWebhookService } from 'src/engine/core-modules/billing/services/billing-webhook.service';
|
import { BillingWebhookEntitlementService } from 'src/engine/core-modules/billing/services/billing-webhook-entitlement.service';
|
||||||
|
import { BillingWebhookSubscriptionService } from 'src/engine/core-modules/billing/services/billing-webhook-subscription.service';
|
||||||
import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service';
|
import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service';
|
||||||
@Controller('billing')
|
@Controller('billing')
|
||||||
export class BillingController {
|
export class BillingController {
|
||||||
@ -24,7 +25,8 @@ export class BillingController {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly stripeService: StripeService,
|
private readonly stripeService: StripeService,
|
||||||
private readonly billingWehbookService: BillingWebhookService,
|
private readonly billingWebhookSubscriptionService: BillingWebhookSubscriptionService,
|
||||||
|
private readonly billingWebhookEntitlementService: BillingWebhookEntitlementService,
|
||||||
private readonly billingSubscriptionService: BillingSubscriptionService,
|
private readonly billingSubscriptionService: BillingSubscriptionService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -61,7 +63,7 @@ export class BillingController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.billingWehbookService.processStripeEvent(
|
await this.billingWebhookSubscriptionService.processStripeEvent(
|
||||||
workspaceId,
|
workspaceId,
|
||||||
event.data,
|
event.data,
|
||||||
);
|
);
|
||||||
@ -70,7 +72,7 @@ export class BillingController {
|
|||||||
event.type === WebhookEvent.CUSTOMER_ACTIVE_ENTITLEMENT_SUMMARY_UPDATED
|
event.type === WebhookEvent.CUSTOMER_ACTIVE_ENTITLEMENT_SUMMARY_UPDATED
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
await this.billingWehbookService.processCustomerActiveEntitlement(
|
await this.billingWebhookEntitlementService.processStripeEvent(
|
||||||
event.data,
|
event.data,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -82,6 +84,7 @@ export class BillingController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(200).end();
|
res.status(200).end();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,14 +13,15 @@ import { BillingSubscription } from 'src/engine/core-modules/billing/entities/bi
|
|||||||
import { BillingWorkspaceMemberListener } from 'src/engine/core-modules/billing/listeners/billing-workspace-member.listener';
|
import { BillingWorkspaceMemberListener } from 'src/engine/core-modules/billing/listeners/billing-workspace-member.listener';
|
||||||
import { BillingPortalWorkspaceService } from 'src/engine/core-modules/billing/services/billing-portal.workspace-service';
|
import { BillingPortalWorkspaceService } from 'src/engine/core-modules/billing/services/billing-portal.workspace-service';
|
||||||
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
||||||
import { BillingWebhookService } from 'src/engine/core-modules/billing/services/billing-webhook.service';
|
import { BillingWebhookEntitlementService } from 'src/engine/core-modules/billing/services/billing-webhook-entitlement.service';
|
||||||
|
import { BillingWebhookSubscriptionService } from 'src/engine/core-modules/billing/services/billing-webhook-subscription.service';
|
||||||
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
|
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
|
||||||
import { StripeModule } from 'src/engine/core-modules/billing/stripe/stripe.module';
|
import { StripeModule } from 'src/engine/core-modules/billing/stripe/stripe.module';
|
||||||
|
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
|
||||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -46,7 +47,8 @@ import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/doma
|
|||||||
controllers: [BillingController],
|
controllers: [BillingController],
|
||||||
providers: [
|
providers: [
|
||||||
BillingSubscriptionService,
|
BillingSubscriptionService,
|
||||||
BillingWebhookService,
|
BillingWebhookSubscriptionService,
|
||||||
|
BillingWebhookEntitlementService,
|
||||||
BillingPortalWorkspaceService,
|
BillingPortalWorkspaceService,
|
||||||
BillingResolver,
|
BillingResolver,
|
||||||
BillingWorkspaceMemberListener,
|
BillingWorkspaceMemberListener,
|
||||||
@ -55,7 +57,6 @@ import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/doma
|
|||||||
exports: [
|
exports: [
|
||||||
BillingSubscriptionService,
|
BillingSubscriptionService,
|
||||||
BillingPortalWorkspaceService,
|
BillingPortalWorkspaceService,
|
||||||
BillingWebhookService,
|
|
||||||
BillingService,
|
BillingService,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
@ -4,4 +4,7 @@ export enum WebhookEvent {
|
|||||||
CUSTOMER_SUBSCRIPTION_DELETED = 'customer.subscription.deleted',
|
CUSTOMER_SUBSCRIPTION_DELETED = 'customer.subscription.deleted',
|
||||||
SETUP_INTENT_SUCCEEDED = 'setup_intent.succeeded',
|
SETUP_INTENT_SUCCEEDED = 'setup_intent.succeeded',
|
||||||
CUSTOMER_ACTIVE_ENTITLEMENT_SUMMARY_UPDATED = 'entitlements.active_entitlement_summary.updated',
|
CUSTOMER_ACTIVE_ENTITLEMENT_SUMMARY_UPDATED = 'entitlements.active_entitlement_summary.updated',
|
||||||
|
CUSTOMER_CREATED = 'customer.created',
|
||||||
|
CUSTOMER_DELETED = 'customer.deleted',
|
||||||
|
CUSTOMER_UPDATED = 'customer.updated',
|
||||||
}
|
}
|
||||||
|
@ -7,14 +7,14 @@ import { Processor } from 'src/engine/core-modules/message-queue/decorators/proc
|
|||||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||||
export type UpdateSubscriptionJobData = { workspaceId: string };
|
export type UpdateSubscriptionQuantityJobData = { workspaceId: string };
|
||||||
|
|
||||||
@Processor({
|
@Processor({
|
||||||
queueName: MessageQueue.billingQueue,
|
queueName: MessageQueue.billingQueue,
|
||||||
scope: Scope.REQUEST,
|
scope: Scope.REQUEST,
|
||||||
})
|
})
|
||||||
export class UpdateSubscriptionJob {
|
export class UpdateSubscriptionQuantityJob {
|
||||||
protected readonly logger = new Logger(UpdateSubscriptionJob.name);
|
protected readonly logger = new Logger(UpdateSubscriptionQuantityJob.name);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly billingSubscriptionService: BillingSubscriptionService,
|
private readonly billingSubscriptionService: BillingSubscriptionService,
|
||||||
@ -22,8 +22,8 @@ export class UpdateSubscriptionJob {
|
|||||||
private readonly twentyORMManager: TwentyORMManager,
|
private readonly twentyORMManager: TwentyORMManager,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Process(UpdateSubscriptionJob.name)
|
@Process(UpdateSubscriptionQuantityJob.name)
|
||||||
async handle(data: UpdateSubscriptionJobData): Promise<void> {
|
async handle(data: UpdateSubscriptionQuantityJobData): Promise<void> {
|
||||||
const workspaceMemberRepository =
|
const workspaceMemberRepository =
|
||||||
await this.twentyORMManager.getRepository<WorkspaceMemberWorkspaceEntity>(
|
await this.twentyORMManager.getRepository<WorkspaceMemberWorkspaceEntity>(
|
||||||
'workspaceMember',
|
'workspaceMember',
|
@ -1,9 +1,11 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator';
|
||||||
|
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||||
import {
|
import {
|
||||||
UpdateSubscriptionJob,
|
UpdateSubscriptionQuantityJob,
|
||||||
UpdateSubscriptionJobData,
|
UpdateSubscriptionQuantityJobData,
|
||||||
} from 'src/engine/core-modules/billing/jobs/update-subscription.job';
|
} from 'src/engine/core-modules/billing/jobs/update-subscription-quantity.job';
|
||||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||||
import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
|
import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
|
||||||
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
||||||
@ -11,8 +13,6 @@ import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queu
|
|||||||
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
||||||
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type';
|
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type';
|
||||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||||
import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator';
|
|
||||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BillingWorkspaceMemberListener {
|
export class BillingWorkspaceMemberListener {
|
||||||
@ -33,8 +33,8 @@ export class BillingWorkspaceMemberListener {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.messageQueueService.add<UpdateSubscriptionJobData>(
|
await this.messageQueueService.add<UpdateSubscriptionQuantityJobData>(
|
||||||
UpdateSubscriptionJob.name,
|
UpdateSubscriptionQuantityJob.name,
|
||||||
{ workspaceId: payload.workspaceId },
|
{ workspaceId: payload.workspaceId },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -6,12 +6,11 @@ import { Repository } from 'typeorm';
|
|||||||
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
||||||
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
||||||
import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service';
|
import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service';
|
||||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
|
||||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { assert } from 'src/utils/assert';
|
import { assert } from 'src/utils/assert';
|
||||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BillingPortalWorkspaceService {
|
export class BillingPortalWorkspaceService {
|
||||||
@ -19,7 +18,6 @@ export class BillingPortalWorkspaceService {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly stripeService: StripeService,
|
private readonly stripeService: StripeService,
|
||||||
private readonly domainManagerService: DomainManagerService,
|
private readonly domainManagerService: DomainManagerService,
|
||||||
private readonly environmentService: EnvironmentService,
|
|
||||||
@InjectRepository(BillingSubscription, 'core')
|
@InjectRepository(BillingSubscription, 'core')
|
||||||
private readonly billingSubscriptionRepository: Repository<BillingSubscription>,
|
private readonly billingSubscriptionRepository: Repository<BillingSubscription>,
|
||||||
@InjectRepository(UserWorkspace, 'core')
|
@InjectRepository(UserWorkspace, 'core')
|
||||||
|
@ -0,0 +1,52 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import Stripe from 'stripe';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import {
|
||||||
|
BillingException,
|
||||||
|
BillingExceptionCode,
|
||||||
|
} from 'src/engine/core-modules/billing/billing.exception';
|
||||||
|
import { BillingEntitlement } from 'src/engine/core-modules/billing/entities/billing-entitlement.entity';
|
||||||
|
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
||||||
|
import { transformStripeEntitlementUpdatedEventToEntitlementRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-entitlement-updated-event-to-entitlement-repository-data.util';
|
||||||
|
@Injectable()
|
||||||
|
export class BillingWebhookEntitlementService {
|
||||||
|
protected readonly logger = new Logger(BillingWebhookEntitlementService.name);
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(BillingSubscription, 'core')
|
||||||
|
private readonly billingSubscriptionRepository: Repository<BillingSubscription>,
|
||||||
|
@InjectRepository(BillingEntitlement, 'core')
|
||||||
|
private readonly billingEntitlementRepository: Repository<BillingEntitlement>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async processStripeEvent(
|
||||||
|
data: Stripe.EntitlementsActiveEntitlementSummaryUpdatedEvent.Data,
|
||||||
|
) {
|
||||||
|
const billingSubscription =
|
||||||
|
await this.billingSubscriptionRepository.findOne({
|
||||||
|
where: { stripeCustomerId: data.object.customer },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!billingSubscription) {
|
||||||
|
throw new BillingException(
|
||||||
|
'Billing customer not found',
|
||||||
|
BillingExceptionCode.BILLING_CUSTOMER_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspaceId = billingSubscription.workspaceId;
|
||||||
|
|
||||||
|
await this.billingEntitlementRepository.upsert(
|
||||||
|
transformStripeEntitlementUpdatedEventToEntitlementRepositoryData(
|
||||||
|
workspaceId,
|
||||||
|
data,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
conflictPaths: ['workspaceId', 'key'],
|
||||||
|
skipUpdateIfNoValuesChanged: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,114 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import Stripe from 'stripe';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
|
||||||
|
import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity';
|
||||||
|
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
||||||
|
import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
|
||||||
|
import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service';
|
||||||
|
import { transformStripeSubscriptionEventToCustomerRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-customer-repository-data.util';
|
||||||
|
import { transformStripeSubscriptionEventToSubscriptionItemRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-item-repository-data.util';
|
||||||
|
import { transformStripeSubscriptionEventToSubscriptionRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-repository-data.util';
|
||||||
|
import {
|
||||||
|
Workspace,
|
||||||
|
WorkspaceActivationStatus,
|
||||||
|
} from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
|
@Injectable()
|
||||||
|
export class BillingWebhookSubscriptionService {
|
||||||
|
protected readonly logger = new Logger(
|
||||||
|
BillingWebhookSubscriptionService.name,
|
||||||
|
);
|
||||||
|
constructor(
|
||||||
|
private readonly stripeService: StripeService,
|
||||||
|
@InjectRepository(BillingSubscription, 'core')
|
||||||
|
private readonly billingSubscriptionRepository: Repository<BillingSubscription>,
|
||||||
|
@InjectRepository(BillingSubscriptionItem, 'core')
|
||||||
|
private readonly billingSubscriptionItemRepository: Repository<BillingSubscriptionItem>,
|
||||||
|
@InjectRepository(Workspace, 'core')
|
||||||
|
private readonly workspaceRepository: Repository<Workspace>,
|
||||||
|
@InjectRepository(BillingCustomer, 'core')
|
||||||
|
private readonly billingCustomerRepository: Repository<BillingCustomer>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async processStripeEvent(
|
||||||
|
workspaceId: string,
|
||||||
|
data:
|
||||||
|
| Stripe.CustomerSubscriptionUpdatedEvent.Data
|
||||||
|
| Stripe.CustomerSubscriptionCreatedEvent.Data
|
||||||
|
| Stripe.CustomerSubscriptionDeletedEvent.Data,
|
||||||
|
) {
|
||||||
|
const workspace = await this.workspaceRepository.findOne({
|
||||||
|
where: { id: workspaceId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!workspace) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.billingCustomerRepository.upsert(
|
||||||
|
transformStripeSubscriptionEventToCustomerRepositoryData(
|
||||||
|
workspaceId,
|
||||||
|
data,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
conflictPaths: ['workspaceId', 'stripeCustomerId'],
|
||||||
|
skipUpdateIfNoValuesChanged: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.billingSubscriptionRepository.upsert(
|
||||||
|
transformStripeSubscriptionEventToSubscriptionRepositoryData(
|
||||||
|
workspaceId,
|
||||||
|
data,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
conflictPaths: ['stripeSubscriptionId'],
|
||||||
|
skipUpdateIfNoValuesChanged: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const billingSubscription =
|
||||||
|
await this.billingSubscriptionRepository.findOneOrFail({
|
||||||
|
where: { stripeSubscriptionId: data.object.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.billingSubscriptionItemRepository.upsert(
|
||||||
|
transformStripeSubscriptionEventToSubscriptionItemRepositoryData(
|
||||||
|
billingSubscription.id,
|
||||||
|
data,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
conflictPaths: ['billingSubscriptionId', 'stripeProductId'],
|
||||||
|
skipUpdateIfNoValuesChanged: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
data.object.status === SubscriptionStatus.Canceled ||
|
||||||
|
data.object.status === SubscriptionStatus.Unpaid
|
||||||
|
) {
|
||||||
|
await this.workspaceRepository.update(workspaceId, {
|
||||||
|
activationStatus: WorkspaceActivationStatus.INACTIVE,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(data.object.status === SubscriptionStatus.Active ||
|
||||||
|
data.object.status === SubscriptionStatus.Trialing ||
|
||||||
|
data.object.status === SubscriptionStatus.PastDue) &&
|
||||||
|
workspace.activationStatus == WorkspaceActivationStatus.INACTIVE
|
||||||
|
) {
|
||||||
|
await this.workspaceRepository.update(workspaceId, {
|
||||||
|
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.stripeService.updateCustomerMetadataWorkspaceId(
|
||||||
|
String(data.object.customer),
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,171 +0,0 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import Stripe from 'stripe';
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
|
|
||||||
import {
|
|
||||||
BillingException,
|
|
||||||
BillingExceptionCode,
|
|
||||||
} from 'src/engine/core-modules/billing/billing.exception';
|
|
||||||
import { BillingEntitlement } from 'src/engine/core-modules/billing/entities/billing-entitlement.entity';
|
|
||||||
import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity';
|
|
||||||
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
|
||||||
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
|
|
||||||
import { BillingSubscriptionCollectionMethod } from 'src/engine/core-modules/billing/enums/billing-subscription-collection-method.enum';
|
|
||||||
import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
|
|
||||||
import {
|
|
||||||
Workspace,
|
|
||||||
WorkspaceActivationStatus,
|
|
||||||
} from 'src/engine/core-modules/workspace/workspace.entity';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class BillingWebhookService {
|
|
||||||
protected readonly logger = new Logger(BillingWebhookService.name);
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(BillingSubscription, 'core')
|
|
||||||
private readonly billingSubscriptionRepository: Repository<BillingSubscription>,
|
|
||||||
@InjectRepository(BillingEntitlement, 'core')
|
|
||||||
private readonly billingEntitlementRepository: Repository<BillingEntitlement>,
|
|
||||||
@InjectRepository(BillingSubscriptionItem, 'core')
|
|
||||||
private readonly billingSubscriptionItemRepository: Repository<BillingSubscriptionItem>,
|
|
||||||
@InjectRepository(Workspace, 'core')
|
|
||||||
private readonly workspaceRepository: Repository<Workspace>,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async processStripeEvent(
|
|
||||||
workspaceId: string,
|
|
||||||
data:
|
|
||||||
| Stripe.CustomerSubscriptionUpdatedEvent.Data
|
|
||||||
| Stripe.CustomerSubscriptionCreatedEvent.Data
|
|
||||||
| Stripe.CustomerSubscriptionDeletedEvent.Data,
|
|
||||||
) {
|
|
||||||
const workspace = await this.workspaceRepository.findOne({
|
|
||||||
where: { id: workspaceId },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!workspace) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.billingSubscriptionRepository.upsert(
|
|
||||||
{
|
|
||||||
workspaceId,
|
|
||||||
stripeCustomerId: data.object.customer as string,
|
|
||||||
stripeSubscriptionId: data.object.id,
|
|
||||||
status: data.object.status as SubscriptionStatus,
|
|
||||||
interval: data.object.items.data[0].plan.interval,
|
|
||||||
cancelAtPeriodEnd: data.object.cancel_at_period_end,
|
|
||||||
currency: data.object.currency.toUpperCase(),
|
|
||||||
currentPeriodEnd: new Date(data.object.current_period_end * 1000),
|
|
||||||
currentPeriodStart: new Date(data.object.current_period_start * 1000),
|
|
||||||
metadata: data.object.metadata,
|
|
||||||
collectionMethod:
|
|
||||||
data.object.collection_method.toUpperCase() as BillingSubscriptionCollectionMethod,
|
|
||||||
automaticTax: data.object.automatic_tax ?? undefined,
|
|
||||||
cancellationDetails: data.object.cancellation_details ?? undefined,
|
|
||||||
endedAt: data.object.ended_at
|
|
||||||
? new Date(data.object.ended_at * 1000)
|
|
||||||
: undefined,
|
|
||||||
trialStart: data.object.trial_start
|
|
||||||
? new Date(data.object.trial_start * 1000)
|
|
||||||
: undefined,
|
|
||||||
trialEnd: data.object.trial_end
|
|
||||||
? new Date(data.object.trial_end * 1000)
|
|
||||||
: undefined,
|
|
||||||
cancelAt: data.object.cancel_at
|
|
||||||
? new Date(data.object.cancel_at * 1000)
|
|
||||||
: undefined,
|
|
||||||
canceledAt: data.object.canceled_at
|
|
||||||
? new Date(data.object.canceled_at * 1000)
|
|
||||||
: undefined,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
conflictPaths: ['stripeSubscriptionId'],
|
|
||||||
skipUpdateIfNoValuesChanged: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const billingSubscription =
|
|
||||||
await this.billingSubscriptionRepository.findOneOrFail({
|
|
||||||
where: { stripeSubscriptionId: data.object.id },
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.billingSubscriptionItemRepository.upsert(
|
|
||||||
data.object.items.data.map((item) => {
|
|
||||||
return {
|
|
||||||
billingSubscriptionId: billingSubscription.id,
|
|
||||||
stripeSubscriptionId: data.object.id,
|
|
||||||
stripeProductId: item.price.product as string,
|
|
||||||
stripePriceId: item.price.id,
|
|
||||||
stripeSubscriptionItemId: item.id,
|
|
||||||
quantity: item.quantity,
|
|
||||||
metadata: item.metadata,
|
|
||||||
billingThresholds: item.billing_thresholds ?? undefined,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
conflictPaths: ['billingSubscriptionId', 'stripeProductId'],
|
|
||||||
skipUpdateIfNoValuesChanged: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
data.object.status === SubscriptionStatus.Canceled ||
|
|
||||||
data.object.status === SubscriptionStatus.Unpaid
|
|
||||||
) {
|
|
||||||
await this.workspaceRepository.update(workspaceId, {
|
|
||||||
activationStatus: WorkspaceActivationStatus.INACTIVE,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
(data.object.status === SubscriptionStatus.Active ||
|
|
||||||
data.object.status === SubscriptionStatus.Trialing ||
|
|
||||||
data.object.status === SubscriptionStatus.PastDue) &&
|
|
||||||
workspace.activationStatus == WorkspaceActivationStatus.INACTIVE
|
|
||||||
) {
|
|
||||||
await this.workspaceRepository.update(workspaceId, {
|
|
||||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async processCustomerActiveEntitlement(
|
|
||||||
data: Stripe.EntitlementsActiveEntitlementSummaryUpdatedEvent.Data,
|
|
||||||
) {
|
|
||||||
const billingSubscription =
|
|
||||||
await this.billingSubscriptionRepository.findOne({
|
|
||||||
where: { stripeCustomerId: data.object.customer },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!billingSubscription) {
|
|
||||||
throw new BillingException(
|
|
||||||
'Billing customer not found',
|
|
||||||
BillingExceptionCode.BILLING_CUSTOMER_NOT_FOUND,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const workspaceId = billingSubscription.workspaceId;
|
|
||||||
const stripeCustomerId = data.object.customer;
|
|
||||||
|
|
||||||
const activeEntitlementsKeys = data.object.entitlements.data.map(
|
|
||||||
(entitlement) => entitlement.lookup_key,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.billingEntitlementRepository.upsert(
|
|
||||||
Object.values(BillingEntitlementKey).map((key) => {
|
|
||||||
return {
|
|
||||||
workspaceId,
|
|
||||||
key,
|
|
||||||
value: activeEntitlementsKeys.includes(key),
|
|
||||||
stripeCustomerId,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
conflictPaths: ['workspaceId', 'key'],
|
|
||||||
skipUpdateIfNoValuesChanged: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,9 +5,9 @@ import Stripe from 'stripe';
|
|||||||
import { ProductPriceEntity } from 'src/engine/core-modules/billing/dto/product-price.entity';
|
import { ProductPriceEntity } from 'src/engine/core-modules/billing/dto/product-price.entity';
|
||||||
import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity';
|
import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity';
|
||||||
import { AvailableProduct } from 'src/engine/core-modules/billing/enums/billing-available-product.enum';
|
import { AvailableProduct } from 'src/engine/core-modules/billing/enums/billing-available-product.enum';
|
||||||
|
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
|
||||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class StripeService {
|
export class StripeService {
|
||||||
@ -114,7 +114,10 @@ export class StripeService {
|
|||||||
success_url: successUrl,
|
success_url: successUrl,
|
||||||
cancel_url: cancelUrl,
|
cancel_url: cancelUrl,
|
||||||
});
|
});
|
||||||
}
|
} // I prefered to not create a customer with metadat before the checkout, because it would break the tax calculation
|
||||||
|
// Indeed when the checkout session is created, the customer is created and the tax calculation is done
|
||||||
|
// If we create a customer before the checkout session, the tax calculation is not done and the checkout session will fail
|
||||||
|
// I think that it's not risk worth to create a customer before the checkout session, it would only complicate the code for no signigicant gain
|
||||||
|
|
||||||
async collectLastInvoice(stripeSubscriptionId: string) {
|
async collectLastInvoice(stripeSubscriptionId: string) {
|
||||||
const subscription = await this.stripe.subscriptions.retrieve(
|
const subscription = await this.stripe.subscriptions.retrieve(
|
||||||
@ -148,6 +151,19 @@ export class StripeService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateCustomerMetadataWorkspaceId(
|
||||||
|
stripeCustomerId: string,
|
||||||
|
workspaceId: string,
|
||||||
|
) {
|
||||||
|
await this.stripe.customers.update(stripeCustomerId, {
|
||||||
|
metadata: { workspaceId: workspaceId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCustomer(stripeCustomerId: string) {
|
||||||
|
return await this.stripe.customers.retrieve(stripeCustomerId);
|
||||||
|
}
|
||||||
|
|
||||||
formatProductPrices(prices: Stripe.Price[]): ProductPriceEntity[] {
|
formatProductPrices(prices: Stripe.Price[]): ProductPriceEntity[] {
|
||||||
const productPrices: ProductPriceEntity[] = Object.values(
|
const productPrices: ProductPriceEntity[] = Object.values(
|
||||||
prices
|
prices
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
import Stripe from 'stripe';
|
||||||
|
|
||||||
|
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
|
||||||
|
|
||||||
|
export const transformStripeEntitlementUpdatedEventToEntitlementRepositoryData =
|
||||||
|
(
|
||||||
|
workspaceId: string,
|
||||||
|
data: Stripe.EntitlementsActiveEntitlementSummaryUpdatedEvent.Data,
|
||||||
|
) => {
|
||||||
|
const stripeCustomerId = data.object.customer;
|
||||||
|
const activeEntitlementsKeys = data.object.entitlements.data.map(
|
||||||
|
(entitlement) => entitlement.lookup_key,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Object.values(BillingEntitlementKey).map((key) => {
|
||||||
|
return {
|
||||||
|
workspaceId,
|
||||||
|
key,
|
||||||
|
value: activeEntitlementsKeys.includes(key),
|
||||||
|
stripeCustomerId,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,14 @@
|
|||||||
|
import Stripe from 'stripe';
|
||||||
|
|
||||||
|
export const transformStripeSubscriptionEventToCustomerRepositoryData = (
|
||||||
|
workspaceId: string,
|
||||||
|
data:
|
||||||
|
| Stripe.CustomerSubscriptionUpdatedEvent.Data
|
||||||
|
| Stripe.CustomerSubscriptionCreatedEvent.Data
|
||||||
|
| Stripe.CustomerSubscriptionDeletedEvent.Data,
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
workspaceId,
|
||||||
|
stripeCustomerId: String(data.object.customer),
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,23 @@
|
|||||||
|
import Stripe from 'stripe';
|
||||||
|
|
||||||
|
export const transformStripeSubscriptionEventToSubscriptionItemRepositoryData =
|
||||||
|
(
|
||||||
|
billingSubscriptionId: string,
|
||||||
|
data:
|
||||||
|
| Stripe.CustomerSubscriptionUpdatedEvent.Data
|
||||||
|
| Stripe.CustomerSubscriptionCreatedEvent.Data
|
||||||
|
| Stripe.CustomerSubscriptionDeletedEvent.Data,
|
||||||
|
) => {
|
||||||
|
return data.object.items.data.map((item) => {
|
||||||
|
return {
|
||||||
|
billingSubscriptionId,
|
||||||
|
stripeSubscriptionId: data.object.id,
|
||||||
|
stripeProductId: String(item.price.product),
|
||||||
|
stripePriceId: item.price.id,
|
||||||
|
stripeSubscriptionItemId: item.id,
|
||||||
|
quantity: item.quantity,
|
||||||
|
metadata: item.metadata,
|
||||||
|
billingThresholds: item.billing_thresholds ?? undefined,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,67 @@
|
|||||||
|
import Stripe from 'stripe';
|
||||||
|
|
||||||
|
import { BillingSubscriptionCollectionMethod } from 'src/engine/core-modules/billing/enums/billing-subscription-collection-method.enum';
|
||||||
|
import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
|
||||||
|
|
||||||
|
export const transformStripeSubscriptionEventToSubscriptionRepositoryData = (
|
||||||
|
workspaceId: string,
|
||||||
|
data:
|
||||||
|
| Stripe.CustomerSubscriptionUpdatedEvent.Data
|
||||||
|
| Stripe.CustomerSubscriptionCreatedEvent.Data
|
||||||
|
| Stripe.CustomerSubscriptionDeletedEvent.Data,
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
workspaceId,
|
||||||
|
stripeCustomerId: String(data.object.customer),
|
||||||
|
stripeSubscriptionId: data.object.id,
|
||||||
|
status: getSubscriptionStatus(data.object.status),
|
||||||
|
interval: data.object.items.data[0].plan.interval,
|
||||||
|
cancelAtPeriodEnd: data.object.cancel_at_period_end,
|
||||||
|
currency: data.object.currency.toUpperCase(),
|
||||||
|
currentPeriodEnd: new Date(data.object.current_period_end * 1000),
|
||||||
|
currentPeriodStart: new Date(data.object.current_period_start * 1000),
|
||||||
|
metadata: data.object.metadata,
|
||||||
|
collectionMethod:
|
||||||
|
BillingSubscriptionCollectionMethod[
|
||||||
|
data.object.collection_method.toUpperCase()
|
||||||
|
],
|
||||||
|
automaticTax: data.object.automatic_tax ?? undefined,
|
||||||
|
cancellationDetails: data.object.cancellation_details ?? undefined,
|
||||||
|
endedAt: data.object.ended_at
|
||||||
|
? new Date(data.object.ended_at * 1000)
|
||||||
|
: undefined,
|
||||||
|
trialStart: data.object.trial_start
|
||||||
|
? new Date(data.object.trial_start * 1000)
|
||||||
|
: undefined,
|
||||||
|
trialEnd: data.object.trial_end
|
||||||
|
? new Date(data.object.trial_end * 1000)
|
||||||
|
: undefined,
|
||||||
|
cancelAt: data.object.cancel_at
|
||||||
|
? new Date(data.object.cancel_at * 1000)
|
||||||
|
: undefined,
|
||||||
|
canceledAt: data.object.canceled_at
|
||||||
|
? new Date(data.object.canceled_at * 1000)
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSubscriptionStatus = (status: Stripe.Subscription.Status) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'active':
|
||||||
|
return SubscriptionStatus.Active;
|
||||||
|
case 'canceled':
|
||||||
|
return SubscriptionStatus.Canceled;
|
||||||
|
case 'incomplete':
|
||||||
|
return SubscriptionStatus.Incomplete;
|
||||||
|
case 'incomplete_expired':
|
||||||
|
return SubscriptionStatus.IncompleteExpired;
|
||||||
|
case 'past_due':
|
||||||
|
return SubscriptionStatus.PastDue;
|
||||||
|
case 'paused':
|
||||||
|
return SubscriptionStatus.Paused;
|
||||||
|
case 'trialing':
|
||||||
|
return SubscriptionStatus.Trialing;
|
||||||
|
case 'unpaid':
|
||||||
|
return SubscriptionStatus.Unpaid;
|
||||||
|
}
|
||||||
|
};
|
@ -8,15 +8,15 @@ import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
|||||||
import { WorkspaceQueryRunnerJobModule } from 'src/engine/api/graphql/workspace-query-runner/jobs/workspace-query-runner-job.module';
|
import { WorkspaceQueryRunnerJobModule } from 'src/engine/api/graphql/workspace-query-runner/jobs/workspace-query-runner-job.module';
|
||||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||||
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
|
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
|
||||||
import { UpdateSubscriptionJob } from 'src/engine/core-modules/billing/jobs/update-subscription.job';
|
import { UpdateSubscriptionQuantityJob } from 'src/engine/core-modules/billing/jobs/update-subscription-quantity.job';
|
||||||
import { StripeModule } from 'src/engine/core-modules/billing/stripe/stripe.module';
|
import { StripeModule } from 'src/engine/core-modules/billing/stripe/stripe.module';
|
||||||
|
import { EmailSenderJob } from 'src/engine/core-modules/email/email-sender.job';
|
||||||
|
import { EmailModule } from 'src/engine/core-modules/email/email.module';
|
||||||
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
|
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
|
||||||
import { UserModule } from 'src/engine/core-modules/user/user.module';
|
import { UserModule } from 'src/engine/core-modules/user/user.module';
|
||||||
import { HandleWorkspaceMemberDeletedJob } from 'src/engine/core-modules/workspace/handle-workspace-member-deleted.job';
|
import { HandleWorkspaceMemberDeletedJob } from 'src/engine/core-modules/workspace/handle-workspace-member-deleted.job';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
|
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
|
||||||
import { EmailSenderJob } from 'src/engine/core-modules/email/email-sender.job';
|
|
||||||
import { EmailModule } from 'src/engine/core-modules/email/email.module';
|
|
||||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||||
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
||||||
import { CleanInactiveWorkspaceJob } from 'src/engine/workspace-manager/workspace-cleaner/crons/clean-inactive-workspace.job';
|
import { CleanInactiveWorkspaceJob } from 'src/engine/workspace-manager/workspace-cleaner/crons/clean-inactive-workspace.job';
|
||||||
@ -26,8 +26,8 @@ import { AutoCompaniesAndContactsCreationJobModule } from 'src/modules/contact-c
|
|||||||
import { MessagingModule } from 'src/modules/messaging/messaging.module';
|
import { MessagingModule } from 'src/modules/messaging/messaging.module';
|
||||||
import { TimelineJobModule } from 'src/modules/timeline/jobs/timeline-job.module';
|
import { TimelineJobModule } from 'src/modules/timeline/jobs/timeline-job.module';
|
||||||
import { TimelineActivityModule } from 'src/modules/timeline/timeline-activity.module';
|
import { TimelineActivityModule } from 'src/modules/timeline/timeline-activity.module';
|
||||||
import { WorkflowModule } from 'src/modules/workflow/workflow.module';
|
|
||||||
import { WebhookJobModule } from 'src/modules/webhook/jobs/webhook-job.module';
|
import { WebhookJobModule } from 'src/modules/webhook/jobs/webhook-job.module';
|
||||||
|
import { WorkflowModule } from 'src/modules/workflow/workflow.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -57,7 +57,7 @@ import { WebhookJobModule } from 'src/modules/webhook/jobs/webhook-job.module';
|
|||||||
CleanInactiveWorkspaceJob,
|
CleanInactiveWorkspaceJob,
|
||||||
EmailSenderJob,
|
EmailSenderJob,
|
||||||
DataSeedDemoWorkspaceJob,
|
DataSeedDemoWorkspaceJob,
|
||||||
UpdateSubscriptionJob,
|
UpdateSubscriptionQuantityJob,
|
||||||
HandleWorkspaceMemberDeletedJob,
|
HandleWorkspaceMemberDeletedJob,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user