Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions __tests__/workers/cdc/primary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8343,6 +8343,150 @@ describe('post analytics achievement progress', () => {
});
});

describe('share click achievement progress', () => {
let sharePostsClickedAchievementId: string;
let shareClickMilestoneAchievementId: string;
let authorId: string;

const postIds = ['share-click-1', 'share-click-2', 'share-click-3'];

const emitClickChange = (
postId: string,
beforeClicks: number,
afterClicks: number,
) =>
expectSuccessfulBackground(
worker,
mockChangeMessage<PostAnalytics>({
before: {
id: postId,
impressions: 0,
impressionsAds: 0,
clicks: beforeClicks,
},
after: {
id: postId,
impressions: 0,
impressionsAds: 0,
clicks: afterClicks,
},
op: 'u',
table: 'post_analytics',
}),
);

beforeEach(async () => {
sharePostsClickedAchievementId = randomUUID();
shareClickMilestoneAchievementId = randomUUID();
authorId = randomUUID();

await saveFixtures(con, User, [
{
id: authorId,
bio: null,
name: 'Share Click Author',
image: 'https://daily.dev/share-click-author.jpg',
email: `share-click-author-${authorId}@daily.dev`,
createdAt: new Date(),
username: `share${authorId.slice(0, 8)}`,
infoConfirmed: true,
},
]);
await saveFixtures(con, Source, sourcesFixture);

await con.getRepository(Achievement).save([
{
id: sharePostsClickedAchievementId,
name: 'Share Posts Clicked Achievement',
description: 'Get clicks on 10 distinct share posts',
image: '',
type: AchievementType.Milestone,
eventType: AchievementEventType.SharePostsClicked,
criteria: { targetCount: 10 },
points: 10,
},
{
id: shareClickMilestoneAchievementId,
name: 'Share Click Milestone Achievement',
description: 'Reach 100 clicks on a share post',
image: '',
type: AchievementType.Milestone,
eventType: AchievementEventType.ShareClickMilestone,
criteria: { targetCount: 100 },
points: 10,
},
]);

await con.getRepository(Post).save(
postIds.map((id, index) => ({
id,
shortId: id,
title: `Share post ${index + 1}`,
authorId,
sourceId: 'a',
type: PostType.Share,
sharedPostId: null,
})),
);

await con.getRepository(PostAnalytics).save(
postIds.map((id) => ({
id,
impressions: 0,
impressionsAds: 0,
clicks: 0,
})),
);
});

it('should set SharePostsClicked progress to the count of share posts with clicks (not accumulate)', async () => {
await con
.getRepository(PostAnalytics)
.update({ id: postIds[0] }, { clicks: 1 });
await emitClickChange(postIds[0], 0, 1);

await con
.getRepository(PostAnalytics)
.update({ id: postIds[1] }, { clicks: 1 });
await emitClickChange(postIds[1], 0, 1);

await con
.getRepository(PostAnalytics)
.update({ id: postIds[2] }, { clicks: 1 });
await emitClickChange(postIds[2], 0, 1);

const userAchievement = await con.getRepository(UserAchievement).findOneBy({
achievementId: sharePostsClickedAchievementId,
userId: authorId,
});

expect(userAchievement).not.toBeNull();
expect(userAchievement!.progress).toEqual(3);
expect(userAchievement!.unlockedAt).toBeNull();
});

it('should set ShareClickMilestone progress to the user-level max clicks across share posts', async () => {
await con
.getRepository(PostAnalytics)
.update({ id: postIds[0] }, { clicks: 50 });
await emitClickChange(postIds[0], 0, 50);

await con
.getRepository(PostAnalytics)
.update({ id: postIds[1] }, { clicks: 10 });
await emitClickChange(postIds[1], 0, 10);

const userAchievement = await con.getRepository(UserAchievement).findOneBy({
achievementId: shareClickMilestoneAchievementId,
userId: authorId,
});

expect(userAchievement).not.toBeNull();
expect(userAchievement!.progress).toEqual(50);
expect(userAchievement!.unlockedAt).toBeNull();
});
});

describe('user experience change', () => {
type ObjectType = UserExperience;
let experienceId: string;
Expand Down
2 changes: 2 additions & 0 deletions src/common/achievement/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,8 @@ export async function checkAchievementProgress(
AchievementEventType.ReadingStreak,
AchievementEventType.CoresSpent,
AchievementEventType.PostImpressions,
AchievementEventType.ShareClickMilestone,
AchievementEventType.SharePostsClicked,
];

if (absoluteValueEventTypes.includes(eventType)) {
Expand Down
25 changes: 16 additions & 9 deletions src/workers/cdc/primary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2743,22 +2743,29 @@ const onPostAnalyticsChange = async (
);
}

const stats = await con
.getRepository(SharePost)
.createQueryBuilder('sp')
.innerJoin(PostAnalytics, 'pa', 'pa.id = sp.id')
.where('sp.authorId = :userId', { userId: sharePost.authorId })
.select('COALESCE(MAX(pa.clicks), 0)::int', 'maxClicks')
.addSelect(
'COUNT(*) FILTER (WHERE pa.clicks > 0)::int',
'postsWithClicks',
)
.getRawOne<{ maxClicks: number; postsWithClicks: number }>();

const maxClicks = stats?.maxClicks ?? 0;
const postsWithClicks = stats?.postsWithClicks ?? 0;

await checkAchievementProgress(
con,
logger,
sharePost.authorId,
AchievementEventType.ShareClickMilestone,
clicks,
maxClicks,
);

const postsWithClicks = await con
.getRepository(SharePost)
.createQueryBuilder('sp')
.innerJoin(PostAnalytics, 'pa', 'pa.id = sp.id')
.where('sp.authorId = :userId', { userId: sharePost.authorId })
.andWhere('pa.clicks > 0')
.getCount();

await checkAchievementProgress(
con,
logger,
Expand Down
Loading