Revert "feat(messaging): message campaign delivery stats + views" (#22452) (#22627)

Revert "feat(messaging): message campaign delivery stats + views
(#22452)"

This reverts commit 2e1117d442.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22627?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Paul Rastoin
2026-07-07 14:17:13 +02:00
committed by GitHub
parent 54aa52d11c
commit bd8bf89653
32 changed files with 721 additions and 2323 deletions
@@ -10,7 +10,6 @@ import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { MessageChannelMetadataModule } from 'src/engine/metadata-modules/message-channel/message-channel-metadata.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
import { provideWorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/provide-workspace-scoped-repository';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/workspace-event-emitter.module';
@@ -19,7 +18,6 @@ import { EmailingSendResolver } from 'src/modules/emailing/resolvers/emailing-se
import { UnsubscribeTopicResolver } from 'src/modules/emailing/resolvers/unsubscribe-topic.resolver';
import { EmailBillingService } from 'src/modules/emailing/services/email-billing.service';
import { EmailingDomainSenderService } from 'src/modules/emailing/services/emailing-domain-sender.service';
import { MessageCampaignStatisticsService } from 'src/modules/emailing/services/message-campaign-statistics.service';
import { MessageCampaignService } from 'src/modules/emailing/services/message-campaign.service';
import { MessageSuppressionService } from 'src/modules/emailing/services/message-suppression.service';
import { UnsubscribeTopicService } from 'src/modules/emailing/services/unsubscribe-topic.service';
@@ -30,7 +28,6 @@ import { UnsubscribeTopicService } from 'src/modules/emailing/services/unsubscri
MessageChannelMetadataModule,
FeatureFlagModule,
PermissionsModule,
UserRoleModule,
BillingModule,
WorkspaceEventEmitterModule,
WorkspaceCacheModule,
@@ -45,7 +42,6 @@ import { UnsubscribeTopicService } from 'src/modules/emailing/services/unsubscri
providers: [
EmailBillingService,
MessageCampaignService,
MessageCampaignStatisticsService,
MessageSuppressionService,
UnsubscribeTopicService,
EmailingDomainSenderService,
@@ -58,7 +54,6 @@ import { UnsubscribeTopicService } from 'src/modules/emailing/services/unsubscri
exports: [
EmailingDomainSenderService,
MessageCampaignService,
MessageCampaignStatisticsService,
MessageSuppressionService,
UnsubscribeTopicService,
],
@@ -1,21 +0,0 @@
import { REFRESH_CAMPAIGN_STATS_JOB } from 'src/engine/core-modules/emailing-domain/constants/campaign.constant';
import { type RefreshCampaignStatsJobData } from 'src/engine/core-modules/emailing-domain/types/refresh-campaign-stats-job-data.type';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageCampaignStatisticsService } from 'src/modules/emailing/services/message-campaign-statistics.service';
@Processor(MessageQueue.emailQueue)
export class RefreshCampaignStatsJob {
constructor(
private readonly messageCampaignStatisticsService: MessageCampaignStatisticsService,
) {}
@Process(REFRESH_CAMPAIGN_STATS_JOB)
async handle(data: RefreshCampaignStatsJobData): Promise<void> {
await this.messageCampaignStatisticsService.refreshCampaignCounts({
workspaceId: data.workspaceId,
campaignId: data.campaignId,
});
}
}
@@ -97,13 +97,11 @@ export class EmailingSendResolver {
async previewMessageCampaignAudience(
@Args('input') input: PreviewMessageCampaignAudienceInput,
@AuthWorkspace() currentWorkspace: WorkspaceEntity,
@AuthUserWorkspaceId() userWorkspaceId: string,
): Promise<CampaignAudiencePreviewDTO> {
this.emailGroupAccessService.validateEmailGroupAccessOrThrow();
return this.messageCampaignService.previewAudience({
workspaceId: currentWorkspace.id,
userWorkspaceId,
listId: input.listId,
unsubscribeTopicId: input.unsubscribeTopicId,
});
@@ -1,75 +0,0 @@
import { Injectable } from '@nestjs/common';
import { CAMPAIGN_MESSAGE_DELIVERY_STATUS } from 'src/engine/core-modules/emailing-domain/constants/campaign.constant';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
import { MessageCampaignWorkspaceEntity } from 'src/modules/emailing/standard-objects/message-campaign.workspace-entity';
import { MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity';
type DeliveryStatusCountRow = {
deliveryStatus: string | null;
count: string;
};
@Injectable()
export class MessageCampaignStatisticsService {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
) {}
async refreshCampaignCounts({
workspaceId,
campaignId,
}: {
workspaceId: string;
campaignId: string;
}): Promise<void> {
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
const messageRepository =
await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
MessageWorkspaceEntity,
{ shouldBypassPermissionChecks: true },
);
const deliveryStatusCountRows = await messageRepository
.createQueryBuilder('message')
.select('message.deliveryStatus', 'deliveryStatus')
.addSelect('COUNT(*)', 'count')
.where('message.messageCampaignId = :campaignId', { campaignId })
.groupBy('message.deliveryStatus')
.getRawMany<DeliveryStatusCountRow>();
const countByDeliveryStatus = new Map(
deliveryStatusCountRows.map((row) => [
row.deliveryStatus,
Number(row.count),
]),
);
const sentCount =
countByDeliveryStatus.get(CAMPAIGN_MESSAGE_DELIVERY_STATUS.SENT) ?? 0;
const failedCount =
countByDeliveryStatus.get(CAMPAIGN_MESSAGE_DELIVERY_STATUS.FAILED) ?? 0;
const bouncedCount =
countByDeliveryStatus.get(CAMPAIGN_MESSAGE_DELIVERY_STATUS.BOUNCED) ??
0;
const complainedCount =
countByDeliveryStatus.get(
CAMPAIGN_MESSAGE_DELIVERY_STATUS.COMPLAINED,
) ?? 0;
const campaignRepository =
await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
MessageCampaignWorkspaceEntity,
{ shouldBypassPermissionChecks: true },
);
await campaignRepository.update(
{ id: campaignId },
{ sentCount, failedCount, bouncedCount, complainedCount },
);
}, buildSystemAuthContext(workspaceId));
}
}
@@ -7,12 +7,9 @@ import { v4, v5 } from 'uuid';
import {
CAMPAIGN_MESSAGE_DELIVERY_STATUS,
CAMPAIGN_MESSAGE_ID_NAMESPACE,
CAMPAIGN_STATS_REFRESH_DEBOUNCE_MS,
CAMPAIGN_STATS_REFRESH_DELAY_MS,
CAMPAIGN_STATUS,
MATERIALIZE_CAMPAIGN_JOB,
MAX_CAMPAIGN_RECIPIENTS,
REFRESH_CAMPAIGN_STATS_JOB,
SEND_CAMPAIGN_EMAIL_JOB,
} from 'src/engine/core-modules/emailing-domain/constants/campaign.constant';
import {
@@ -26,17 +23,12 @@ import { type CampaignRecipient } from 'src/engine/core-modules/emailing-domain/
import { type CampaignSkippedBreakdown } from 'src/engine/core-modules/emailing-domain/types/campaign-skipped-breakdown.type';
import { type MaterializeCampaignJobData } from 'src/engine/core-modules/emailing-domain/types/materialize-campaign-job-data.type';
import { type RawCampaignRecipient } from 'src/engine/core-modules/emailing-domain/types/raw-campaign-recipient.type';
import { type RefreshCampaignStatsJobData } from 'src/engine/core-modules/emailing-domain/types/refresh-campaign-stats-job-data.type';
import { type SendCampaignEmailJobData } from 'src/engine/core-modules/emailing-domain/types/send-campaign-email-job-data.type';
import { normalizeCampaignRecipients } from 'src/engine/core-modules/emailing-domain/utils/normalize-campaign-recipients.util';
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { MessageChannelMetadataService } from 'src/engine/metadata-modules/message-channel/message-channel-metadata.service';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
@@ -44,7 +36,6 @@ import { InjectWorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace
import { WorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/workspace-scoped-repository';
import { EmailBillingService } from 'src/modules/emailing/services/email-billing.service';
import { EmailingDomainSenderService } from 'src/modules/emailing/services/emailing-domain-sender.service';
import { MessageCampaignStatisticsService } from 'src/modules/emailing/services/message-campaign-statistics.service';
import { MessageSuppressionService } from 'src/modules/emailing/services/message-suppression.service';
import { MessageCampaignWorkspaceEntity } from 'src/modules/emailing/standard-objects/message-campaign.workspace-entity';
import { MessageListMemberWorkspaceEntity } from 'src/modules/emailing/standard-objects/message-list-member.workspace-entity';
@@ -108,21 +99,14 @@ export class MessageCampaignService {
private readonly messageQueueService: MessageQueueService,
private readonly messageChannelMetadataService: MessageChannelMetadataService,
private readonly messageSuppressionService: MessageSuppressionService,
private readonly userRoleService: UserRoleService,
private readonly messageCampaignStatisticsService: MessageCampaignStatisticsService,
private readonly emailBillingService: EmailBillingService,
@InjectCacheStorage(CacheStorageNamespace.ModuleEmailing)
private readonly cacheStorageService: CacheStorageService,
) {}
private getUserRepository<T extends ObjectLiteral>(
workspaceId: string,
entity: Type<T>,
roleId: string,
) {
return this.globalWorkspaceOrmManager.getRepository(workspaceId, entity, {
unionOf: [roleId],
});
return this.globalWorkspaceOrmManager.getRepository(workspaceId, entity);
}
private getSystemRepository<T extends ObjectLiteral>(
@@ -156,18 +140,12 @@ export class MessageCampaignService {
);
}
const roleId = await this.userRoleService.getRoleIdForUserWorkspace({
workspaceId,
userWorkspaceId,
});
const { campaignId, recipients, skipped } =
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
async () => {
const rawRecipients = await this.resolveRecipientsFromList(
workspaceId,
listId,
roleId,
);
const normalized = normalizeCampaignRecipients(
@@ -177,7 +155,6 @@ export class MessageCampaignService {
const newCampaignId = await this.createCampaign({
workspaceId,
roleId,
subject,
html,
fromAddress,
@@ -493,17 +470,11 @@ export class MessageCampaignService {
}
await messageRepository.update(message.id, { deliveryStatus });
await this.scheduleCampaignStatsRefresh({
workspaceId,
campaignId: message.messageCampaignId,
});
}, buildSystemAuthContext(workspaceId));
}
private async createCampaign({
workspaceId,
roleId,
subject,
html,
fromAddress,
@@ -511,7 +482,6 @@ export class MessageCampaignService {
listId,
}: {
workspaceId: string;
roleId: string;
subject: string;
html: string;
fromAddress: string;
@@ -521,7 +491,6 @@ export class MessageCampaignService {
const campaignRepository = await this.getUserRepository(
workspaceId,
MessageCampaignWorkspaceEntity,
roleId,
);
const { identifiers } = await campaignRepository.insert({
@@ -685,58 +654,22 @@ export class MessageCampaignService {
sentAt: new Date(),
},
);
await this.scheduleCampaignStatsRefresh({
workspaceId,
campaignId,
});
}
private async scheduleCampaignStatsRefresh({
workspaceId,
campaignId,
}: {
workspaceId: string;
campaignId: string;
}): Promise<void> {
const acquired = await this.cacheStorageService.acquireLock(
`campaign-stats-refresh:${workspaceId}:${campaignId}`,
CAMPAIGN_STATS_REFRESH_DEBOUNCE_MS,
);
if (!acquired) {
return;
}
await this.messageQueueService.add<RefreshCampaignStatsJobData>(
REFRESH_CAMPAIGN_STATS_JOB,
{ workspaceId, campaignId },
{ delay: CAMPAIGN_STATS_REFRESH_DELAY_MS },
);
}
async previewAudience({
workspaceId,
userWorkspaceId,
listId,
unsubscribeTopicId,
}: {
workspaceId: string;
userWorkspaceId: string;
listId: string;
unsubscribeTopicId?: string;
}): Promise<CampaignAudiencePreview> {
const roleId = await this.userRoleService.getRoleIdForUserWorkspace({
workspaceId,
userWorkspaceId,
});
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
async () => {
const rawRecipients = await this.resolveRecipientsFromList(
workspaceId,
listId,
roleId,
);
const totalMembers = rawRecipients.length;
@@ -791,12 +724,10 @@ export class MessageCampaignService {
private async resolveRecipientsFromList(
workspaceId: string,
listId: string,
roleId: string,
): Promise<RawCampaignRecipient[]> {
const listMemberRepository = await this.getUserRepository(
workspaceId,
MessageListMemberWorkspaceEntity,
roleId,
);
const members = await listMemberRepository.find({
@@ -806,14 +737,12 @@ export class MessageCampaignService {
return this.loadRecipientsByPersonIds(
workspaceId,
members.map((member) => member.personId),
roleId,
);
}
private async loadRecipientsByPersonIds(
workspaceId: string,
personIds: string[],
roleId: string,
): Promise<RawCampaignRecipient[]> {
if (personIds.length === 0) {
return [];
@@ -822,7 +751,6 @@ export class MessageCampaignService {
const personRepository = await this.getUserRepository(
workspaceId,
PersonWorkspaceEntity,
roleId,
);
const people = await personRepository.find({
@@ -12,10 +12,6 @@ export class MessageCampaignWorkspaceEntity extends BaseWorkspaceEntity {
fromAddress: EmailsMetadata | null;
status: string;
sentAt: Date | null;
sentCount: number;
failedCount: number;
bouncedCount: number;
complainedCount: number;
unsubscribeTopicId: string | null;
list: EntityRelation<MessageListWorkspaceEntity> | null;
listId: string | null;