From a11c965bec006069cd48489becc714ae111154e5 Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Wed, 5 Feb 2025 11:28:01 +0100 Subject: [PATCH] feat: productivity report unsubscribed users (#9220) --- .../fake-user-subscriptions-read-model.ts | 4 ++++ .../user-subscriptions-read-model-type.ts | 1 + .../user-subscriptions-read-model.test.ts | 18 ++++++++++++++---- .../user-subscriptions-read-model.ts | 15 +++++++++++++++ src/lib/types/experimental.ts | 5 +++++ 5 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/lib/features/user-subscriptions/fake-user-subscriptions-read-model.ts b/src/lib/features/user-subscriptions/fake-user-subscriptions-read-model.ts index 1f04a85747..c14a87e32c 100644 --- a/src/lib/features/user-subscriptions/fake-user-subscriptions-read-model.ts +++ b/src/lib/features/user-subscriptions/fake-user-subscriptions-read-model.ts @@ -7,6 +7,10 @@ export class FakeUserSubscriptionsReadModel return []; } + async getUnsubscribedUsers(subscription: string) { + return []; + } + async getUserSubscriptions() { return ['productivity-report']; } diff --git a/src/lib/features/user-subscriptions/user-subscriptions-read-model-type.ts b/src/lib/features/user-subscriptions/user-subscriptions-read-model-type.ts index d61087905c..c559b8de5b 100644 --- a/src/lib/features/user-subscriptions/user-subscriptions-read-model-type.ts +++ b/src/lib/features/user-subscriptions/user-subscriptions-read-model-type.ts @@ -5,6 +5,7 @@ export type Subscriber = { export interface IUserSubscriptionsReadModel { getSubscribedUsers(subscription: string): Promise; + getUnsubscribedUsers(subscription: string): Promise; getUserSubscriptions(userId: number): Promise; } diff --git a/src/lib/features/user-subscriptions/user-subscriptions-read-model.test.ts b/src/lib/features/user-subscriptions/user-subscriptions-read-model.test.ts index 15c5551cca..78850a981b 100644 --- a/src/lib/features/user-subscriptions/user-subscriptions-read-model.test.ts +++ b/src/lib/features/user-subscriptions/user-subscriptions-read-model.test.ts @@ -31,8 +31,8 @@ afterAll(async () => { await db.destroy(); }); -describe('getSubscribedUsers', () => { - test('returns seen users that did not unsubscribe', async () => { +describe('User subscription read model', () => { + test('returns subscribed and unsubscribed users', async () => { const user1 = await userStore.insert({ email: 'user1@example.com', name: 'User One', @@ -43,11 +43,13 @@ describe('getSubscribedUsers', () => { email: 'user2@example.com', name: 'User Two', }); + // never seen const user3 = await userStore.insert({ email: 'user3@example.com', name: 'User Three', }); + // unsubscribe await userUnsubscribeStore.insert({ userId: user2.id, subscription, @@ -62,6 +64,14 @@ describe('getSubscribedUsers', () => { { email: 'user1@example.com', name: 'User One' }, ]), ); + + const unsubscribers = + await userSubscriptionsReadModel.getUnsubscribedUsers(subscription); + expect(unsubscribers).toEqual( + expect.arrayContaining([ + { email: 'user2@example.com', name: 'User Two' }, + ]), + ); }); test('reflects changes after unsubscribe and resubscribe', async () => { @@ -120,8 +130,8 @@ describe('getSubscribedUsers', () => { }); }); -describe('getUserSubscriptions', () => { - test('returns all subscriptions if user has not unsubscribed', async () => { +describe('User subscription read model', () => { + test('returns all user subscriptions if user has not unsubscribed', async () => { const user = await userStore.insert({ email: 'user4@example.com', name: 'User Four', diff --git a/src/lib/features/user-subscriptions/user-subscriptions-read-model.ts b/src/lib/features/user-subscriptions/user-subscriptions-read-model.ts index 1849ac8f2c..4e16f5f6f8 100644 --- a/src/lib/features/user-subscriptions/user-subscriptions-read-model.ts +++ b/src/lib/features/user-subscriptions/user-subscriptions-read-model.ts @@ -45,6 +45,21 @@ export class UserSubscriptionsReadModel implements IUserSubscriptionsReadModel { return users.map(mapRowToSubscriber); } + async getUnsubscribedUsers(subscription: string) { + const unsubscribedUserIdsQuery = this.db(UNSUBSCRIPTION_TABLE) + .select('user_id') + .where('subscription', subscription); + + const users = await this.db(USERS_TABLE) + .select(USER_COLUMNS) + .whereIn('id', unsubscribedUserIdsQuery) + .andWhere('is_service', false) + .andWhere('deleted_at', null) + .andWhereNot('email', null); + + return users.map(mapRowToSubscriber); + } + async getUserSubscriptions(userId: number) { const unsubscriptionsList = await this.db(UNSUBSCRIPTION_TABLE) .select('subscription') diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts index 9c48824750..ff4f83abf0 100644 --- a/src/lib/types/experimental.ts +++ b/src/lib/types/experimental.ts @@ -51,6 +51,7 @@ export type IFlagKey = | 'releasePlans' | 'releasePlanChangeRequests' | 'productivityReportEmail' + | 'productivityReportUnsubscribers' | 'enterprise-payg' | 'flagOverviewRedesign' | 'showUserDeviceCount' @@ -256,6 +257,10 @@ const flags: IFlags = { process.env.UNLEASH_EXPERIMENTAL_PRODUCTIVITY_REPORT_EMAIL, false, ), + productivityReportUnsubscribers: parseEnvVarBoolean( + process.env.UNLEASH_EXPERIMENTAL_PRODUCTIVITY_REPORT_UNSUBSCRIBERS, + false, + ), 'enterprise-payg': parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_ENTERPRISE_PAYG, false,