Skip to content

Commit 2e0b840

Browse files
prazgaitisclaude
andcommitted
Filter PR day calc by activity type kind instead of source
PR day calculations now only include core, special, and penalty activities, excluding bonus-kind activities (mindfulness, skiing, mini-game bonuses, category leader, etc.) per admin ruling. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c7f9fec commit 2e0b840

5 files changed

Lines changed: 63 additions & 29 deletions

File tree

apps/web/tests/api/mini-games-lifecycle.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ describe('Mini-Games Lifecycle & Configuration', () => {
5353
return await ctx.db.insert("activityTypes", {
5454
challengeId,
5555
name: 'Running',
56+
kind: 'core',
5657
scoringConfig: { unit: 'minutes', pointsPerUnit: 1, basePoints: 0 },
5758
contributesToStreak: true,
5859
isNegative: false,

apps/web/tests/api/mini-games-preview.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ describe('Mini-Games Preview (Dry Run)', () => {
5151
return await ctx.db.insert("activityTypes", {
5252
challengeId,
5353
name: 'Running',
54+
kind: 'core',
5455
scoringConfig: { unit: 'minutes', pointsPerUnit: 1, basePoints: 0 },
5556
contributesToStreak: true,
5657
isNegative: false,

apps/web/tests/api/mini-games.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ describe('Mini-Games Scoring', () => {
5252
return await ctx.db.insert("activityTypes", {
5353
challengeId,
5454
name: 'Running',
55+
kind: 'core',
5556
scoringConfig: { unit: 'minutes', pointsPerUnit: 1, basePoints: 0 },
5657
contributesToStreak: true,
5758
isNegative: false,

packages/backend/lib/activityFilters.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ export const USER_ACTIVITY_SOURCES = new Set(["manual", "strava", "apple_health"
99
export function isUserLoggedActivity(activity: { source: string }): boolean {
1010
return USER_ACTIVITY_SOURCES.has(activity.source);
1111
}
12+
13+
/** Activity type kinds that count toward PR day calculations (real fitness effort). */
14+
export const PR_ELIGIBLE_KINDS = new Set(["core", "special", "penalty"]);

packages/backend/lib/miniGameCalculations.ts

Lines changed: 57 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77
import type { Id } from "../_generated/dataModel";
88
import type { QueryCtx } from "../_generated/server";
9-
import { notDeleted, isUserLoggedActivity } from "./activityFilters";
9+
import { notDeleted, PR_ELIGIBLE_KINDS } from "./activityFilters";
1010
import { formatDateOnlyFromUtcMs } from "./dateOnly";
1111

1212
// ─── Types ───────────────────────────────────────────────────────────────────
@@ -419,29 +419,51 @@ export async function previewPrWeekEnd(
419419

420420
// ─── Shared Helpers ──────────────────────────────────────────────────────────
421421

422+
/**
423+
* Build a set of activity type IDs whose kind is eligible for PR day calculations.
424+
* Only core, special, and penalty activities count — bonus activities (mindfulness,
425+
* mini-game bonuses, category leader, skiing, etc.) are excluded.
426+
*/
427+
async function getPrEligibleTypeIds(
428+
ctx: ReadCtx,
429+
challengeId: Id<"challenges">,
430+
): Promise<Set<Id<"activityTypes">>> {
431+
const types = await ctx.db
432+
.query("activityTypes")
433+
.withIndex("challengeId", (q) => q.eq("challengeId", challengeId))
434+
.collect();
435+
436+
return new Set(
437+
types.filter((t) => PR_ELIGIBLE_KINDS.has(t.kind ?? "")).map((t) => t._id),
438+
);
439+
}
440+
422441
/**
423442
* Get a user's max single-day points total from all days before `beforeDate`.
424-
* Excludes system-generated bonus activities (mini_game, category_leader, etc.).
443+
* Only includes core, special, and penalty activities (excludes bonus kind).
425444
*/
426445
export async function calculateMaxDailyPoints(
427446
ctx: ReadCtx,
428447
userId: Id<"users">,
429448
challengeId: Id<"challenges">,
430449
beforeDate: number,
431450
): Promise<number> {
432-
const activities = await ctx.db
433-
.query("activities")
434-
.withIndex("by_user_challenge_date", (q) =>
435-
q.eq("userId", userId).eq("challengeId", challengeId),
436-
)
437-
.filter(notDeleted)
438-
.collect();
451+
const [activities, eligibleTypeIds] = await Promise.all([
452+
ctx.db
453+
.query("activities")
454+
.withIndex("by_user_challenge_date", (q) =>
455+
q.eq("userId", userId).eq("challengeId", challengeId),
456+
)
457+
.filter(notDeleted)
458+
.collect(),
459+
getPrEligibleTypeIds(ctx, challengeId),
460+
]);
439461

440462
const dailyPoints: Record<string, number> = {};
441463

442464
for (const activity of activities) {
443465
if (activity.loggedDate >= beforeDate) continue;
444-
if (!isUserLoggedActivity(activity)) continue;
466+
if (!eligibleTypeIds.has(activity.activityTypeId)) continue;
445467

446468
const dateStr = formatDateOnlyFromUtcMs(activity.loggedDate);
447469
dailyPoints[dateStr] = (dailyPoints[dateStr] || 0) + activity.pointsEarned;
@@ -452,8 +474,8 @@ export async function calculateMaxDailyPoints(
452474
}
453475

454476
/**
455-
* Get total non-bonus points earned by a user during a time period.
456-
* Excludes system-generated bonus activities to prevent circular scoring.
477+
* Get total points earned by a user during a time period.
478+
* Only includes core, special, and penalty activities (excludes bonus kind).
457479
*/
458480
export async function getPointsInPeriod(
459481
ctx: ReadCtx,
@@ -462,27 +484,30 @@ export async function getPointsInPeriod(
462484
startDate: number,
463485
endDate: number,
464486
): Promise<number> {
465-
const activities = await ctx.db
466-
.query("activities")
467-
.withIndex("by_user_challenge_date", (q) =>
468-
q.eq("userId", userId).eq("challengeId", challengeId),
469-
)
470-
.filter(notDeleted)
471-
.collect();
487+
const [activities, eligibleTypeIds] = await Promise.all([
488+
ctx.db
489+
.query("activities")
490+
.withIndex("by_user_challenge_date", (q) =>
491+
q.eq("userId", userId).eq("challengeId", challengeId),
492+
)
493+
.filter(notDeleted)
494+
.collect(),
495+
getPrEligibleTypeIds(ctx, challengeId),
496+
]);
472497

473498
return activities
474499
.filter(
475500
(a) =>
476501
a.loggedDate >= startDate &&
477502
a.loggedDate <= endDate &&
478-
isUserLoggedActivity(a),
503+
eligibleTypeIds.has(a.activityTypeId),
479504
)
480505
.reduce((sum, a) => sum + a.pointsEarned, 0);
481506
}
482507

483508
/**
484509
* Get the maximum single-day points total during a time period.
485-
* Excludes system-generated bonus activities (mini_game, category_leader, etc.).
510+
* Only includes core, special, and penalty activities (excludes bonus kind).
486511
*/
487512
export async function getMaxDailyPointsInPeriod(
488513
ctx: ReadCtx,
@@ -491,21 +516,24 @@ export async function getMaxDailyPointsInPeriod(
491516
startDate: number,
492517
endDate: number,
493518
): Promise<number> {
494-
const activities = await ctx.db
495-
.query("activities")
496-
.withIndex("by_user_challenge_date", (q) =>
497-
q.eq("userId", userId).eq("challengeId", challengeId),
498-
)
499-
.filter(notDeleted)
500-
.collect();
519+
const [activities, eligibleTypeIds] = await Promise.all([
520+
ctx.db
521+
.query("activities")
522+
.withIndex("by_user_challenge_date", (q) =>
523+
q.eq("userId", userId).eq("challengeId", challengeId),
524+
)
525+
.filter(notDeleted)
526+
.collect(),
527+
getPrEligibleTypeIds(ctx, challengeId),
528+
]);
501529

502530
const dailyPoints: Record<string, number> = {};
503531

504532
for (const activity of activities) {
505533
if (
506534
activity.loggedDate < startDate ||
507535
activity.loggedDate > endDate ||
508-
!isUserLoggedActivity(activity)
536+
!eligibleTypeIds.has(activity.activityTypeId)
509537
)
510538
continue;
511539

0 commit comments

Comments
 (0)