Skip to content

Commit

Permalink
Merge branch 'jxl9/achievements-tracking' into nikita/admin-and-data-…
Browse files Browse the repository at this point in the history
…fixes
  • Loading branch information
neketka committed Apr 29, 2024
2 parents d781b21 + 22864a3 commit c6b4a9d
Show file tree
Hide file tree
Showing 8 changed files with 332 additions and 4 deletions.
4 changes: 2 additions & 2 deletions server/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -248,9 +248,9 @@ model AchievementTracker {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt()
subject String @default("AchievementTracker")
user User @relation(fields: [userId], references: [id])
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
progress Int
achievement Achievement @relation(fields: [achievementId], references: [id])
achievement Achievement @relation(fields: [achievementId], references: [id], onDelete: Cascade)
dateComplete DateTime?
achievementId String
userId String
Expand Down
5 changes: 5 additions & 0 deletions server/src/achievement/achievement.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ export interface AchievementTrackerDto {
dateComplete?: string;
}

/** DTO for updateAchievementTrackerData */
export interface UpdateAchievementTrackerDataDto {
tracker: AchievementTrackerDto;
}

export interface UpdateAchievementDataDto {
achievement: AchievementDto;
deleted: boolean;
Expand Down
102 changes: 101 additions & 1 deletion server/src/achievement/achievement.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { AchievementService } from './achievement.service';
import { AppModule } from '../app.module';
import { PrismaService } from '../prisma/prisma.service';
import { UserService } from '../user/user.service';
import { ChallengeService } from '../challenge/challenge.service';

import {
AuthType,
User,
Expand Down Expand Up @@ -39,6 +41,7 @@ describe('AchievementModule E2E', () => {
let abilityFactory: CaslAbilityFactory;
let fullAbility: AppAbility;
let orgUsage: OrganizationSpecialUsage;
let challengeService : ChallengeService;

/** beforeAll runs before anything else. It adds new users and prerequisites.
* afterAll runs after all the tests. It removes lingering values in the database.
Expand Down Expand Up @@ -69,6 +72,7 @@ describe('AchievementModule E2E', () => {
console.log = function () {};

achievementService = module.get<AchievementService>(AchievementService);
challengeService = module.get<ChallengeService>(ChallengeService);
prisma = module.get<PrismaService>(PrismaService);
userService = module.get<UserService>(UserService);
eventService = module.get<EventService>(EventService);
Expand All @@ -95,6 +99,7 @@ describe('AchievementModule E2E', () => {
include: { memberOf: true },
});

// tracker = await achievementService.getAchievementsByIdsForAbility(fullAbility, )
console.log = log;
});

Expand All @@ -104,7 +109,7 @@ describe('AchievementModule E2E', () => {
});

describe('Create and read functions', () => {
it('should add an achievement: upsertAchievementFromDto', async () => {
it('should add an achievement: upsertAchievementFromDto; should create a tracker with progress 0', async () => {
const orgUsage = OrganizationSpecialUsage;
const orgId = (
await organizationService.getDefaultOrganization(orgUsage.DEVICE_LOGIN)
Expand All @@ -130,7 +135,16 @@ describe('AchievementModule E2E', () => {
const findAch = await prisma.achievement.findFirstOrThrow({
where: { id: ach!.id },
});

if (ach) {
tracker = await achievementService.createAchievementTracker(user, ach.id);
console.log(tracker);
}

expect(findAch.description).toEqual('ach dto');
expect(tracker.progress).toEqual(0);
expect(tracker.dateComplete).toEqual(null);

});

it('should read achievements: getAchievementFromId, getAchievementsByIdsForAbility', async () => {
Expand Down Expand Up @@ -186,6 +200,90 @@ describe('AchievementModule E2E', () => {
});
});

describe('Testing achievement tracker', () => {
it('should update tracker progress when a challenge is completed', async () => {
// Assuming a challenge completion would update an existing tracker
const initialProgress = tracker.progress;
await challengeService.completeChallenge(user, 'challengeId');

const updatedTracker = await prisma.achievementTracker.findUnique({
where: { id: tracker.id },
});
if (updatedTracker) {
expect(updatedTracker.progress).toBeGreaterThan(initialProgress);
}
});
});

describe('Achievement tracker functions', () => {
it('should create a tracker when an achievement is added and applicable', async () => {
const achId = (await prisma.achievement.findFirstOrThrow()).id;
const orgUsage = OrganizationSpecialUsage;
const orgId = (
await organizationService.getDefaultOrganization(orgUsage.DEVICE_LOGIN)
).id;
const achDto: AchievementDto = {
id: achId,
eventId: 'event123',
name: 'test',
description: 'ach dto',
requiredPoints: 1,
imageUrl: 'tracker test',
locationType: ChallengeLocationDto.ENG_QUAD,
achievementType: AchievementTypeDto.TOTAL_CHALLENGES_OR_JOURNEYS,
initialOrganizationId: orgId,
};

await achievementService.upsertAchievementFromDto(fullAbility, achDto);
const ach = await prisma.achievement.findFirstOrThrow({
where: { id: achId },
});

expect(ach).toBeDefined();

// Simulate challenge completion
await achievementService.checkAchievementProgress(user, 'event123', false);

// Check if tracker was created
const tracker = await prisma.achievementTracker.findFirst({
where: { achievementId: ach.id, userId: user.id },
});
expect(tracker).toBeDefined();
expect(tracker?.progress).toBe(1);
});

// it('should create an achievement tracker', async () => {
// const achId = (await prisma.achievement.findFirstOrThrow()).id;
// const achTrackerDto: AchievementTrackerDto = {
// userId: user.id,
// achievementId: achId,
// progress: 0,
// };

// const achTracker = await achievementService.upsertAchievementTrackerFromDto(
// fullAbility,
// achTrackerDto,
// );

// const findAchTracker = await prisma.achievementTracker.findFirstOrThrow({
// where: { id: achTracker.id },
// });
// expect(findAchTracker.points).toEqual(0);
// });

it('should mark tracker as complete when achievement criteria are met', async () => {
// Complete a challenge that gives the final point needed
await challengeService.completeChallenge(user, 'event123');

const completedTracker = await prisma.achievementTracker.findUnique({
where: { id: tracker.id },
});
expect(completedTracker).not.toBeNull();
expect(completedTracker!.dateComplete).not.toBeNull();
});
});


describe('Delete functions', () => {
it('should remove achievement: removeAchievement', async () => {
const ach = await prisma.achievement.findFirstOrThrow({
Expand All @@ -206,6 +304,8 @@ describe('AchievementModule E2E', () => {
id: user.id,
},
});
await prisma.achievementTracker.deleteMany({});
await prisma.achievement.deleteMany({});
await app.close();
});
});
192 changes: 191 additions & 1 deletion server/src/achievement/achievement.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import { accessibleBy } from '@casl/prisma';
import { AppAbility, CaslAbilityFactory } from '../casl/casl-ability.factory';
import { Action } from '../casl/action.enum';
import { subject } from '@casl/ability';
import { defaultAchievementData } from '../organization/organization.service';
import {
defaultAchievementData,
OrganizationService,
} from '../organization/organization.service';
import {
AchievementTypeDto,
AchievementDto,
Expand All @@ -30,6 +33,7 @@ export class AchievementService {
private readonly prisma: PrismaService,
private clientService: ClientService,
private abilityFactory: CaslAbilityFactory,
// private orgService: OrganizationService,
) {}

/** get an achievement by its ID */
Expand Down Expand Up @@ -208,4 +212,190 @@ export class AchievementService {
achievementType: ach.achievementType as AchievementTypeDto,
};
}

/** AchievementTracker functions */

/** Creates an achievement tracker */
async createAchievementTracker(user: User, achievementId: string) {
const existing = await this.prisma.achievementTracker.findFirst({
where: { userId: user.id, achievementId },
});

if (existing) {
return existing;
}

const progress = await this.prisma.achievementTracker.create({
data: {
userId: user.id,
progress: 0,
achievementId,
},
});

return progress;
}

async getAchievementTrackerByAchievementId(
user: User,
achievementId: string,
) {
return await this.prisma.achievementTracker.findFirst({
where: { userId: user.id, achievementId },
});
}

async dtoForAchievementTracker(
tracker: AchievementTracker,
): Promise<AchievementTrackerDto> {
const achievement = await this.getAchievementFromId(tracker.achievementId);
return {
userId: tracker.userId,
progress: tracker.progress,
achievementId: tracker.achievementId,
dateComplete: tracker.dateComplete?.toISOString(),
};
}

/** Emits & updates an achievement tracker */
async emitUpdateAchievementTracker(
tracker: AchievementTracker,
target?: User,
) {
const dto = await this.dtoForAchievementTracker(tracker);

await this.clientService.sendProtected(
'updateAchievementTrackerData',
target?.id ?? tracker.userId,
dto,
{
id: dto.achievementId,
subject: 'AchievementTracker',
prismaStore: this.prisma.achievementTracker,
},
);
}

/** checks for all achievements associated with a user for a given completed challenge. */
async checkAchievementProgress(
user: User,
challengeId: string,
isJourneyCompleted: boolean,
) {
// find challenge corresponding to challengeId
const curChallenge = await this.prisma.challenge.findUniqueOrThrow({
where: { id: challengeId },
include: {linkedEvent: true}
});

const ability = await this.abilityFactory.createForUser(user);


// find all achievements associated with the challenge that are accessible
// by user and have incomplete trackers; joins tracker to resulting query
const achs = await this.prisma.achievement.findMany({
where: {
OR: [
// { linkedEventId: challengeId }, // achievements linked to the specific event of the challenge
{ linkedEventId: curChallenge.linkedEventId }, // achievements linked to the specific event of the challenge
{ linkedEventId: null }, // achievements not linked to any specific event
{ achievementType : AchievementType.},
{ locationType : LocationType.ANY },
],
AND: [
accessibleBy(ability, Action.Read).Achievement,
// {locationType: curChallenge.location},
],
trackers: {
every: { //
OR: [
{ userId: { not: user.id } }, // Trackers not belonging to the user
{ dateComplete: { not: null } } // Trackers that are completed
]
},
},
},
include: {
trackers: {
where: {
userId: user.id,
dateComplete: null, // trackers for achievements that are not complete
},
},
},
});

// iterate through each achievement and update progress
for (const achId in achs) {
// find tracker associated with ach
let tracker = await this.prisma.achievementTracker.findFirst({
where: {
userId: user.id,
achievementId: achId,
},
});

// if tracker doesn't exist, create a new tracker associated with ach
if (tracker == null) {
tracker = await this.createAchievementTracker(user, achId);
}

// update tracker with new progress; complete tracker if necessary
const ach = await this.getAchievementFromId(achId);

const journeyOrChalAchShouldProgress =
ach.achievementType ===
AchievementTypeDto.TOTAL_CHALLENGES_OR_JOURNEYS ||
(isJourneyCompleted &&
ach.achievementType === AchievementTypeDto.TOTAL_JOURNEYS) ||
(!isJourneyCompleted &&
ach.achievementType === AchievementTypeDto.TOTAL_CHALLENGES);


if (ach.achievementType === AchievementTypeDto.TOTAL_POINTS) {
const updatedTracker = await this.prisma.achievementTracker.update({
where: { id: tracker.id },
data: {
progress: {
increment: curChallenge.points // increment tracker progress by points of current challenge
}
},
});
if (tracker.progress >= ach.requiredPoints) {
// ach is newly completed; update tracker with completion date
await this.prisma.achievementTracker.update({
where: { id: tracker.id },
data: {
dateComplete: new Date() // add new date
}
});
}
} else if (journeyOrChalAchShouldProgress) {
const updatedTracker = await this.prisma.achievementTracker.update({
where: { id: tracker.id },
data: {
progress: {
increment: 1 // increment tracker progress by 1
}
},
});
if (updatedTracker.progress >= ach.requiredPoints) {
await this.prisma.achievementTracker.update({
where: { id: tracker.id },
data: {
dateComplete: new Date() // add new date
}
});
}
}

await this.prisma.achievementTracker.update({
where: { id: tracker.id },
data: {
progress: tracker.progress,
dateComplete: tracker.dateComplete,
},
});
}
}
}
Loading

0 comments on commit c6b4a9d

Please sign in to comment.