The coach check-ins are main-bot routines scheduled in the profile time zone, but a later zone change (About you, onboarding, the main bot's update_user_profile tool, or forgetting the whole profile) left them in the old zone until the rhythm was saved again, so they fired at the wrong local time. syncCoachTimeZone, next to the rhythm code, moves the main bot's coach routines that are not in the current profile zone into it, recomputes the next run of armed ones and re-arms their wakeup. Paused check-ins move but stay paused, routines the user made are never touched, users without check-ins get none, and a save that leaves the zone as it is changes nothing. The profile update and forget RPCs and the profile tool call it whenever they write the zone. Quiet hours, the daily nudge cap and coach dates are evaluated in the profile zone when they are used, so they need no migration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
63 lines
1.9 KiB
TypeScript
63 lines
1.9 KiB
TypeScript
import { ORPCError } from "@orpc/server";
|
|
import type { CoachRhythmDeps } from "@rakazo/adapters";
|
|
import { syncCoachTimeZone } from "@rakazo/adapters";
|
|
import type {
|
|
Actor,
|
|
ForgetUserFacts,
|
|
UpdateUserProfile,
|
|
UserProfileState,
|
|
} from "@rakazo/contracts";
|
|
import type { PrismaClient } from "@rakazo/db";
|
|
import {
|
|
forgetUserFacts,
|
|
getUserProfileState,
|
|
InvalidUserFactError,
|
|
UserFactNotFoundError,
|
|
updateUserProfile,
|
|
} from "@rakazo/db";
|
|
|
|
function toRpcError(error: unknown): unknown {
|
|
if (error instanceof UserFactNotFoundError) {
|
|
return new ORPCError("NOT_FOUND", { message: error.message });
|
|
}
|
|
if (error instanceof InvalidUserFactError) {
|
|
return new ORPCError("BAD_REQUEST", { message: error.message });
|
|
}
|
|
return error;
|
|
}
|
|
|
|
export function getProfile(prisma: PrismaClient, actor: Actor): Promise<UserProfileState> {
|
|
return getUserProfileState(prisma, actor.userId);
|
|
}
|
|
|
|
export async function updateProfile(
|
|
deps: CoachRhythmDeps,
|
|
actor: Actor,
|
|
input: UpdateUserProfile,
|
|
): Promise<UserProfileState> {
|
|
try {
|
|
await updateUserProfile(deps.prisma, actor.userId, input);
|
|
} catch (error) {
|
|
throw toRpcError(error);
|
|
}
|
|
if (input.timeZone !== undefined) await syncCoachTimeZone(deps, actor.userId);
|
|
return getUserProfileState(deps.prisma, actor.userId);
|
|
}
|
|
|
|
/** Unknown ids are ignored, so a caller cannot probe another user's facts. */
|
|
export async function forgetProfile(
|
|
deps: CoachRhythmDeps,
|
|
actor: Actor,
|
|
input: ForgetUserFacts,
|
|
): Promise<UserProfileState> {
|
|
const target = input.factIds
|
|
? { factIds: input.factIds }
|
|
: input.category
|
|
? { category: input.category }
|
|
: { all: true as const };
|
|
await forgetUserFacts(deps.prisma, actor.userId, target);
|
|
// Forgetting everything clears the time zone too.
|
|
if ("all" in target) await syncCoachTimeZone(deps, actor.userId);
|
|
return getUserProfileState(deps.prisma, actor.userId);
|
|
}
|