Skip to content

Commit

Permalink
Merge branch 'main' into MI-753
Browse files Browse the repository at this point in the history
  • Loading branch information
ilasw authored Jan 29, 2025
2 parents 1ff17ae + 7ec573c commit b345007
Show file tree
Hide file tree
Showing 16 changed files with 288 additions and 13 deletions.
6 changes: 5 additions & 1 deletion .infra/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ export const workers: Worker[] = [
topic: 'user-updated',
subscription: 'api.user-updated-plus-subscribed-custom-feed',
},
{
topic: 'user-updated',
subscription: 'api.user-gifted-plus-notification',
},
{
topic: 'user-deleted',
subscription: 'api.user-deleted-cio',
Expand Down Expand Up @@ -328,7 +332,7 @@ export const workers: Worker[] = [
{
topic: 'kvasir.v1.post-translated',
subscription: 'api.post-translated',
},
}
];

export const personalizedDigestWorkers: Worker[] = [
Expand Down
28 changes: 27 additions & 1 deletion __tests__/notifications/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
NotificationCommentContext,
NotificationCommenterContext,
NotificationDoneByContext,
NotificationGiftPlusContext,
NotificationPostContext,
NotificationPostModerationContext,
NotificationSourceContext,
Expand All @@ -15,9 +16,9 @@ import {
NotificationSubmissionContext,
NotificationUpvotersContext,
NotificationUserContext,
type NotificationUserTopReaderContext,
Reference,
storeNotificationBundleV2,
type NotificationUserTopReaderContext,
} from '../../src/notifications';
import { postsFixture } from '../fixture/post';
import {
Expand Down Expand Up @@ -1513,3 +1514,28 @@ describe('storeNotificationBundle', () => {
expect(actual.attachments!.length).toEqual(0);
});
});

describe('plus notifications', () => {
it('should notify user when they are gifted a subscription', async () => {
const type = NotificationType.UserGiftedPlus;
const gifter = usersFixture[1] as Reference<User>;
const recipient = usersFixture[0] as Reference<User>;
const squad = sourcesFixture[5] as Reference<SquadSource>;
const ctx: NotificationGiftPlusContext = {
userIds: [recipient.id],
gifter,
recipient,
squad,
};
const actual = generateNotificationV2(type, ctx);

expect(actual.notification.type).toEqual(type);
expect(actual.userIds).toEqual([recipient.id]);
expect(actual.notification.public).toEqual(true);
expect(actual.notification.referenceId).toBe('squad');
expect(actual.notification.description).toBeFalsy();
expect(actual.notification.targetUrl).toContain(`/squads/${squad.handle}`);
expect(actual.avatars?.[0]?.image).toEqual(gifter.image);
expect(actual.attachments!.length).toEqual(0);
});
});
26 changes: 26 additions & 0 deletions __tests__/workers/cdc/primary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ import {
} from '../../../src/entity/SourcePostModeration';
import { NotificationType } from '../../../src/notifications/common';
import type { UserReport } from '../../../src/entity/UserReport';
import { SubscriptionCycles } from '../../../src/paddle';
import { addYears } from 'date-fns';

jest.mock('../../../src/common', () => ({
...(jest.requireActual('../../../src/common') as Record<string, unknown>),
Expand Down Expand Up @@ -894,6 +896,30 @@ describe('user', () => {
[after],
);
});

it('should notify on gift plus subscription', async () => {
const after: ChangeObject<ObjectType> = {
...base,
subscriptionFlags: JSON.stringify({
cycle: SubscriptionCycles.Yearly,
gifterId: '2',
giftExpirationDate: addYears(new Date(), 1),
}),
};
await expectSuccessfulBackground(
worker,
mockChangeMessage<ObjectType>({
after,
before: base,
table: 'user',
op: 'u',
}),
);
expectTypedEvent('user-updated', {
user: base,
newProfile: after,
} as unknown as PubSubSchema['user-updated']);
});
});

describe('user_state', () => {
Expand Down
94 changes: 94 additions & 0 deletions __tests__/workers/notifications/userGiftedPlusNotification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import createOrGetConnection from '../../../src/db';
import { DataSource } from 'typeorm';
import { ChangeObject } from '../../../src/types';
import { usersFixture } from '../../fixture';
import { SubscriptionCycles } from '../../../src/paddle';
import { addYears } from 'date-fns';
import { invokeNotificationWorker, saveFixtures } from '../../helpers';
import { PLUS_MEMBER_SQUAD_ID } from '../../../src/workers/userUpdatedPlusSubscriptionSquad';
import { SourceType, User, Source } from '../../../src/entity';
import { NotificationGiftPlusContext } from '../../../src/notifications';
import { NotificationType } from '../../../src/notifications/common';

let con: DataSource;

beforeAll(async () => {
con = await createOrGetConnection();
});

type ObjectType = Partial<User>;
const base: ChangeObject<ObjectType> = {
...usersFixture[0],
subscriptionFlags: JSON.stringify({}),
createdAt: new Date().getTime(),
updatedAt: new Date().getTime(),
flags: JSON.stringify({}),
};
const plusUser = {
...base,
subscriptionFlags: JSON.stringify({ cycle: SubscriptionCycles.Yearly }),
};
const giftedPlusUser = {
...base,
subscriptionFlags: JSON.stringify({
cycle: SubscriptionCycles.Yearly,
gifterId: 2,
giftExpirationDate: addYears(new Date(), 1),
}),
};

beforeEach(async () => {
jest.resetAllMocks();
await saveFixtures(con, User, usersFixture);
await saveFixtures(con, Source, [
{
id: PLUS_MEMBER_SQUAD_ID,
name: 'Plus Squad',
image: 'http://image.com/a',
handle: 'plus-squad-notify',
type: SourceType.Squad,
},
]);
});

const WORKER_RELATIVE_PATH =
'../../../src/workers/notifications/userGiftedPlusNotification';

describe('plus subscription gift', () => {
it('should early return for currently non plus user', async () => {
const worker = await import(WORKER_RELATIVE_PATH);
const actual = await invokeNotificationWorker(worker.default, {
user: base,
newProfile: base,
});
expect(actual).toBeUndefined();
});

it('should not return anything plus user since before', async () => {
const worker = await import(WORKER_RELATIVE_PATH);
const actual = await invokeNotificationWorker(worker.default, {
user: plusUser,
newProfile: plusUser,
});
expect(actual).toBeUndefined();
});

it('should return notification for gifted plus user', async () => {
const worker = await import(WORKER_RELATIVE_PATH);
const actual = (await invokeNotificationWorker(worker.default, {
user: base,
newProfile: giftedPlusUser,
})) as Array<{
type: string;
ctx: NotificationGiftPlusContext;
}>;
expect(actual).toBeTruthy();
expect(actual.length).toEqual(1);
expect(actual[0].type).toEqual(NotificationType.UserGiftedPlus);

const notification = actual[0].ctx;
expect(notification.recipient.id).toEqual(base.id);
expect(notification.gifter.id).toEqual('2');
expect(notification.squad.id).toEqual(PLUS_MEMBER_SQUAD_ID);
});
});
1 change: 0 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
"@fastify/helmet": "^13.0.1",
"@fastify/http-proxy": "^11.0.0",
"@fastify/rate-limit": "^10.2.1",
"@fastify/websocket": "^11.0.1",
"@google-cloud/bigquery": "^7.9.1",
"@google-cloud/opentelemetry-resource-util": "^2.4.0",
"@google-cloud/pubsub": "^4.9.0",
Expand Down
2 changes: 2 additions & 0 deletions src/common/mailing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export enum CioUnsubscribeTopic {
export enum CioTransactionalMessageTemplateId {
VerifyCompany = '51',
UserGivenTopReader = '52',
UserSentPlusGift = '65',
UserReceivedPlusGift = '66',
}

export const cioApi = new APIClient(process.env.CIO_APP_KEY);
Expand Down
8 changes: 0 additions & 8 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import MercuriusGQLUpload from 'mercurius-upload';
import MercuriusCache from 'mercurius-cache';
import proxy, { type FastifyHttpProxyOptions } from '@fastify/http-proxy';
import { NoSchemaIntrospectionCustomRule } from 'graphql';
// import fastifyWebsocket from '@fastify/websocket';

import './config';

Expand Down Expand Up @@ -151,13 +150,6 @@ export default async function app(
res.send(stringifyHealthCheck({ status: 'ok' }));
});

// app.register(fastifyWebsocket, {
// options: {
// maxPayload: 1048576,
// verifyClient: (info, next) => next(true),
// },
// });

app.register(MercuriusGQLUpload, {
maxFileSize: 1024 * 1024 * 20,
maxFiles: 1,
Expand Down
1 change: 1 addition & 0 deletions src/notifications/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export enum NotificationType {
StreakResetRestore = 'streak_reset_restore',
UserPostAdded = 'user_post_added',
UserTopReaderBadge = 'user_given_top_reader',
UserGiftedPlus = 'user_gifted_plus',
}

export enum NotificationPreferenceType {
Expand Down
12 changes: 12 additions & 0 deletions src/notifications/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
NotificationCommentContext,
NotificationCommenterContext,
NotificationDoneByContext,
NotificationGiftPlusContext,
NotificationPostContext,
NotificationPostModerationContext,
NotificationSourceContext,
Expand Down Expand Up @@ -145,6 +146,8 @@ export const notificationTitleMap: Record<
},
source_post_submitted: (ctx: NotificationPostModerationContext) =>
`${ctx.user.name} just posted in ${ctx.source.name}. This post is waiting for your review before it gets published on the squad.`,
user_gifted_plus: (ctx: NotificationGiftPlusContext) =>
`Surprise! 🎁 ${ctx.gifter.username} thought of you and gifted you a one-year daily.dev Plus membership! How’s that for a thoughtful surprise?`,
};

export const generateNotificationMap: Record<
Expand Down Expand Up @@ -430,4 +433,13 @@ export const generateNotificationMap: Record<
.icon(NotificationIcon.Bell)
.avatarUser(ctx.user)
.objectPost(ctx.post, ctx.source, ctx.sharedPost!),
user_gifted_plus: (builder, ctx: NotificationGiftPlusContext) =>
builder
.uniqueKey(
`${ctx.gifter.id}-${ctx.recipient.id}-${new Date().toISOString()}`,
)
.icon(NotificationIcon.Bell)
.avatarUser(ctx.gifter)
.referenceSource(ctx.squad)
.targetSource(ctx.squad),
};
7 changes: 7 additions & 0 deletions src/notifications/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
UserStreak,
type Keyword,
type UserTopReader,
SquadSource,
} from '../entity';
import { ChangeObject } from '../types';
import { DeepPartial } from 'typeorm';
Expand Down Expand Up @@ -68,6 +69,12 @@ export type NotificationStreakContext = NotificationBaseContext & {
};
};

export type NotificationGiftPlusContext = NotificationBaseContext & {
gifter: Reference<User>;
recipient: Reference<User>;
squad: Reference<SquadSource>;
};

export type NotificationCommenterContext = NotificationCommentContext & {
commenter: Reference<User>;
};
Expand Down
8 changes: 8 additions & 0 deletions src/paddle.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { UserSubscriptionFlags } from './entity';

export enum SubscriptionCycles {
Monthly = 'monthly',
Yearly = 'yearly',
Expand All @@ -8,3 +10,9 @@ export const subscriptionGiftDuration = 31557600000;

export const isPlusMember = (cycle: SubscriptionCycles | undefined): boolean =>
!!cycle?.length || false;

export const isGiftedPlus = (
subscriptionFlags: UserSubscriptionFlags,
): boolean =>
(!!subscriptionFlags?.gifterId || false) &&
isPlusMember(subscriptionFlags.cycle);
24 changes: 24 additions & 0 deletions src/workers/newNotificationV2Mail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export const notificationToTemplateId: Record<NotificationType, string> = {
squad_featured: '56',
user_post_added: '58',
user_given_top_reader: CioTransactionalMessageTemplateId.UserGivenTopReader,
user_gifted_plus: CioTransactionalMessageTemplateId.UserReceivedPlusGift,
};

type TemplateData = Record<string, unknown>;
Expand Down Expand Up @@ -856,6 +857,29 @@ const notificationToTemplateData: Record<NotificationType, TemplateDataFunc> = {
keyword: keyword?.flags?.title || keyword.value,
};
},
user_gifted_plus: async (con, user) => {
const { subscriptionFlags } = await con.getRepository(User).findOneOrFail({
where: {
id: user.id,
},
select: ['subscriptionFlags'],
});

if (!subscriptionFlags?.gifterId) {
throw new Error('Gifter user not found');
}

const gifter = await con.getRepository(User).findOneOrFail({
where: {
id: subscriptionFlags.gifterId,
},
select: ['name', 'image'],
});
return {
gifter_name: gifter.name,
gifter_image: gifter.image,
};
},
};

const formatTemplateDate = <T extends TemplateData>(data: T): T => {
Expand Down
2 changes: 2 additions & 0 deletions src/workers/notifications/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { collectionUpdated } from './collectionUpdated';
import devCardUnlocked from './devCardUnlocked';
import postBookmarkReminder from './postBookmarkReminder';
import userStreakResetNotification from './userStreakResetNotification';
import userGiftedPlusNotification from './userGiftedPlusNotification';
import squadFeaturedUpdated from './squadFeaturedUpdated';
import sourcePostModerationSubmittedNotification from './sourcePostModerationSubmittedNotification';
import sourcePostModerationApprovedNotification from './sourcePostModerationApprovedNotification';
Expand Down Expand Up @@ -94,6 +95,7 @@ const notificationWorkers: NotificationWorker[] = [
sourcePostModerationApprovedNotification,
sourcePostModerationRejectedNotification,
userTopReaderAdded,
userGiftedPlusNotification,
];

export const workers = [...notificationWorkers.map(notificationWorkerToWorker)];
Loading

0 comments on commit b345007

Please sign in to comment.