fix(curator): gate profile and day progression
This commit is contained in:
@@ -0,0 +1,20 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { resolveProfileTaskPolicy } from "../src/v1/curator/streak-utils.js";
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
resolveProfileTaskPolicy({ resumeAvailable: true, linkedinAvailable: false }),
|
||||||
|
{ complete: false, serviceId: "social-branding-service" },
|
||||||
|
"a resume upload must not complete or reroute the LinkedIn profile task",
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
resolveProfileTaskPolicy({ resumeAvailable: false, linkedinAvailable: true }),
|
||||||
|
{ complete: true, serviceId: "social-branding-service" },
|
||||||
|
"an existing LinkedIn connection completes the task on the next sprint read",
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
resolveProfileTaskPolicy({ resumeAvailable: true, linkedinAvailable: true }),
|
||||||
|
{ complete: true, serviceId: "social-branding-service" },
|
||||||
|
"LinkedIn remains the completion signal when both profile assets exist",
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("curator profile-task policy regression: ok");
|
||||||
@@ -5,8 +5,21 @@ import {
|
|||||||
resolveAgedTrialReuse,
|
resolveAgedTrialReuse,
|
||||||
} from "../src/v1/curator/streak-utils.js";
|
} from "../src/v1/curator/streak-utils.js";
|
||||||
|
|
||||||
assert.equal(curatorLogicalDate(new Date("2026-07-14T18:29:59.999Z")), "2026-07-14");
|
assert.equal(
|
||||||
assert.equal(curatorLogicalDate(new Date("2026-07-14T18:30:00.000Z")), "2026-07-15");
|
curatorLogicalDate(new Date("2026-07-15T03:29:59.999Z")),
|
||||||
|
"2026-07-14",
|
||||||
|
"the previous Curator day remains active until 09:00 Asia/Kolkata",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
curatorLogicalDate(new Date("2026-07-15T03:30:00.000Z")),
|
||||||
|
"2026-07-15",
|
||||||
|
"the next Curator day starts at 09:00 Asia/Kolkata",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
curatorLogicalDate(new Date("2026-07-15T02:30:00.000Z"), 8),
|
||||||
|
"2026-07-15",
|
||||||
|
"deployments may explicitly configure a different business-morning hour",
|
||||||
|
);
|
||||||
const reuse = resolveAgedTrialReuse({
|
const reuse = resolveAgedTrialReuse({
|
||||||
existing: { startDate: "2026-07-14", version: "icp-v10-static", durationDays: 2, access: "trial" },
|
existing: { startDate: "2026-07-14", version: "icp-v10-static", durationDays: 2, access: "trial" },
|
||||||
desired: { durationDays: 2, access: "trial" },
|
desired: { durationDays: 2, access: "trial" },
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import {
|
|||||||
} from "./task-registry.js";
|
} from "./task-registry.js";
|
||||||
import { kpiDefinitionsForIcp } from "./kpi-registry.js";
|
import { kpiDefinitionsForIcp } from "./kpi-registry.js";
|
||||||
import { outcomeDefinitionsForIcp } from "./outcome-report-registry.js";
|
import { outcomeDefinitionsForIcp } from "./outcome-report-registry.js";
|
||||||
import { activeCuratorDayIndex, addDaysIso, composeTrialDay2, computeStreakFromDays, curatorLogicalDate, resolveAgedTrialReuse, todayIso } from "./streak-utils.js";
|
import { activeCuratorDayIndex, addDaysIso, composeTrialDay2, computeStreakFromDays, curatorLogicalDate, resolveAgedTrialReuse, resolveProfileTaskPolicy, todayIso } from "./streak-utils.js";
|
||||||
|
|
||||||
// A streak day represents meaningful correlated activity, not necessarily a
|
// A streak day represents meaningful correlated activity, not necessarily a
|
||||||
// terminal artifact. Keep this intentionally broader than full task completion.
|
// terminal artifact. Keep this intentionally broader than full task completion.
|
||||||
@@ -1224,14 +1224,16 @@ function buildTask(
|
|||||||
isPaidTask: seedTask.isPaidTask !== false,
|
isPaidTask: seedTask.isPaidTask !== false,
|
||||||
isRecoveryPractice: Boolean(seedTask.isRecoveryPractice),
|
isRecoveryPractice: Boolean(seedTask.isRecoveryPractice),
|
||||||
};
|
};
|
||||||
const profileComplete = isProfileTask && (userContext.profile.resumeAvailable || userContext.profile.linkedinAvailable);
|
const profilePolicy = isProfileTask
|
||||||
const routeTask = isProfileTask && userContext.profile.linkedinAvailable && !userContext.profile.resumeAvailable
|
? resolveProfileTaskPolicy(userContext.profile)
|
||||||
? { ...task, serviceId: "resume-service" as const }
|
: undefined;
|
||||||
|
const routeTask = profilePolicy
|
||||||
|
? { ...task, serviceId: profilePolicy.serviceId }
|
||||||
: task;
|
: task;
|
||||||
const taskWithRoute = {
|
const taskWithRoute = {
|
||||||
...task,
|
...task,
|
||||||
route: buildCuratorTaskDeepLink(routeTask, targetRole),
|
route: buildCuratorTaskDeepLink(routeTask, targetRole),
|
||||||
status: profileComplete ? "completed" as const : classifyTaskStatus({ task, focusDate, completionRows, recentRows }),
|
status: profilePolicy?.complete ? "completed" as const : classifyTaskStatus({ task, focusDate, completionRows, recentRows }),
|
||||||
} satisfies CuratorTask;
|
} satisfies CuratorTask;
|
||||||
return taskWithRoute;
|
return taskWithRoute;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,37 @@ export function indiaTodayIso(date = new Date()): string {
|
|||||||
}).format(date);
|
}).format(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timestamps stay UTC; only the product-day boundary is interpreted for India.
|
const CURATOR_BUSINESS_DAY_START_HOUR = 9;
|
||||||
export function curatorLogicalDate(date = new Date()): string {
|
|
||||||
return indiaTodayIso(date);
|
// Timestamps stay UTC; the Curator product day rolls over at the next
|
||||||
|
// business morning in India rather than at calendar midnight.
|
||||||
|
export function curatorLogicalDate(date = new Date(), businessDayStartHour = CURATOR_BUSINESS_DAY_START_HOUR): string {
|
||||||
|
const indiaParts = new Intl.DateTimeFormat("en-CA", {
|
||||||
|
timeZone: "Asia/Kolkata",
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
hour: "2-digit",
|
||||||
|
hourCycle: "h23",
|
||||||
|
}).formatToParts(date);
|
||||||
|
const part = (type: Intl.DateTimeFormatPartTypes) => indiaParts.find((item) => item.type === type)?.value ?? "";
|
||||||
|
const indiaDate = `${part("year")}-${part("month")}-${part("day")}`;
|
||||||
|
return Number(part("hour")) < businessDayStartHour
|
||||||
|
? addDaysIso(indiaDate, -1)
|
||||||
|
: indiaDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProfileTaskPolicyInput = {
|
||||||
|
resumeAvailable: boolean;
|
||||||
|
linkedinAvailable: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function resolveProfileTaskPolicy(input: ProfileTaskPolicyInput) {
|
||||||
|
void input.resumeAvailable;
|
||||||
|
return {
|
||||||
|
complete: input.linkedinAvailable,
|
||||||
|
serviceId: "social-branding-service" as const,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addDaysIso(startDate: string, days: number): string {
|
export function addDaysIso(startDate: string, days: number): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user