Files
Jyotisha/frontend/tests/account-api.test.ts
T

708 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import { existsSync, readFileSync } from "node:fs";
import test from "node:test";
import {
accountProfilePatchSchema,
resolveAccountBirthTimeApplicationPatch,
resolveAppliedAccountBirthTime,
} from "../src/lib/account-profile-patch.ts";
const source = readFileSync(new URL("../src/app/api/account/route.ts", import.meta.url), "utf8");
const patchSource = readFileSync(new URL("../src/lib/account-profile-patch.ts", import.meta.url), "utf8");
const reportedStatusMigration = readFileSync(
new URL("../supabase/migrations/20260726010000_backfill_reported_birth_time_status.sql", import.meta.url),
"utf8",
);
const acceptedExactFamilyMigration = readFileSync(
new URL("../supabase/migrations/20260816010000_accept_exact_family_birth_times.sql", import.meta.url),
"utf8",
);
const selfHostedMigrator = readFileSync(
new URL("../scripts/db-migrate.mjs", import.meta.url),
"utf8",
);
test("account API reads and returns the server-configured rectification price", () => {
assert.match(source, /parseRectificationPriceCredits\(\s*process\.env\.RECTIFICATION_PRICE_CREDITS,?\s*\)/);
assert.match(source, /rectificationPriceCredits/);
assert.doesNotMatch(source, /RECTIFICATION_PRICE_CREDITS[^\n]*\?\?\s*["']1["']/);
});
test("account API separates engine confirmation from an accepted usable chart time", () => {
assert.match(source, /hasConfirmedBirthTime:\s*profile\.birth_time_status === "confirmed"/);
assert.match(source, /hasUsableBirthTime:\s*\(profile\.birth_time_status === "accepted" \|\| profile\.birth_time_status === "confirmed"\)/);
});
test("account API routes staging admins to the configured exact admin host", () => {
assert.match(source, /readSelfHostedIdentityConfig\(process\.env\)\.adminOrigin/);
assert.match(source, /const adminUrl = isAdmin \? adminEntryUrl\(\) : null/);
assert.match(source, /adminUrl,/);
assert.doesNotMatch(source, /NEXT_PUBLIC_ADMIN|AUTH_ADMIN_ORIGIN/);
});
test("account GET falls back when global birthplace columns are not migrated yet", () => {
const getSource = source.slice(source.indexOf("export async function GET"), source.indexOf("export async function PATCH"));
assert.match(getSource, /profileError && isMissingProfileColumn\(profileError\)/);
assert.match(getSource, /select\("credits,active_birth_time[^"]*timezone_offset(?:,[^"]*)?"\)/);
assert.match(getSource, /birth_place_label: undefined/);
assert.match(getSource, /timezone_id: undefined/);
assert.match(getSource, /timezone_source,ayanamsa,name,birth_time,rectification_case_id/);
});
test("account API no longer reads or returns retired rectification cases", () => {
assert.doesNotMatch(source, /birth_time_rectification_v4_cases|birth_time_rectification_cases/);
assert.doesNotMatch(source, /rectificationCase/);
});
test("profile patch schema validates calendar, clock, source requirements, and location bounds", () => {
const valid = {
name: "岳辰",
birth_date: "2000-02-29",
reported_birth_time: "05:30",
birth_time_source: "family_exact",
birth_time_period: null,
birth_time_clue: null,
uncertainty_before_minutes: 0,
uncertainty_after_minutes: 0,
country_code: "CN",
province_code: "130000",
city_code: "130400",
district_code: "130406",
latitude: 36.420487,
longitude: 114.209936,
timezone_offset: 8,
};
assert.equal(accountProfilePatchSchema.safeParse(valid).success, true);
assert.equal(accountProfilePatchSchema.safeParse({ ...valid, uncertainty_before_minutes: 10, uncertainty_after_minutes: 10 }).success, true);
assert.equal(accountProfilePatchSchema.safeParse({ ...valid, birth_date: "2001-02-29" }).success, false);
assert.equal(accountProfilePatchSchema.safeParse({ ...valid, reported_birth_time: "24:00" }).success, false);
assert.equal(accountProfilePatchSchema.safeParse({ ...valid, uncertainty_before_minutes: 7, uncertainty_after_minutes: 7 }).success, false);
assert.equal(accountProfilePatchSchema.safeParse({
...valid,
birth_time_source: "approximate",
uncertainty_before_minutes: 120,
uncertainty_after_minutes: 120,
}).success, true);
assert.equal(accountProfilePatchSchema.safeParse({
...valid,
birth_time_source: "approximate",
uncertainty_before_minutes: 90,
uncertainty_after_minutes: 90,
}).success, false);
assert.equal(accountProfilePatchSchema.safeParse({ ...valid, latitude: 91 }).success, false);
assert.equal(accountProfilePatchSchema.safeParse({ reported_birth_time: "05:30" }).success, false);
assert.equal(accountProfilePatchSchema.safeParse({ ...valid, longitude: null }).success, false);
assert.equal(accountProfilePatchSchema.safeParse({
...valid,
timezone_offset: null,
birth_place_label: "New York, New York, United States",
birth_place_type: "place",
birth_place_provider: "mapbox",
birth_place_provider_id: "place.123",
timezone_id: "America/New_York",
timezone_source: "iana_historical",
}).success, true);
assert.equal(accountProfilePatchSchema.safeParse({ name: "只改称呼" }).success, true);
assert.equal(accountProfilePatchSchema.safeParse({ ayanamsa: "lahiri" }).success, true);
assert.equal(accountProfilePatchSchema.safeParse({ ayanamsa: "kp" }).success, true);
assert.equal(accountProfilePatchSchema.safeParse({ ayanamsa: "true_pushya" }).success, true);
assert.equal(accountProfilePatchSchema.safeParse({ ayanamsa: "raman" }).success, true);
assert.equal(accountProfilePatchSchema.safeParse({ ayanamsa: "fagan" }).success, false);
assert.equal(accountProfilePatchSchema.safeParse({
...valid,
reported_birth_time: null,
birth_time_source: "period_only",
birth_time_period: null,
uncertainty_before_minutes: null,
uncertainty_after_minutes: null,
}).success, false);
assert.equal(accountProfilePatchSchema.safeParse({
...valid,
reported_birth_time: null,
birth_time_source: "period_only",
birth_time_period: "evening",
uncertainty_before_minutes: null,
uncertainty_after_minutes: null,
}).success, true);
assert.equal(accountProfilePatchSchema.safeParse({
...valid,
reported_birth_time: null,
birth_time_source: "period_only",
birth_time_period: null,
declared_window_start: "14:30",
declared_window_end: "15:00",
uncertainty_before_minutes: null,
uncertainty_after_minutes: null,
}).success, true);
assert.equal(accountProfilePatchSchema.safeParse({
...valid,
reported_birth_time: null,
birth_time_source: "period_only",
birth_time_period: null,
declared_window_start: "14:30",
declared_window_end: "14:30",
uncertainty_before_minutes: null,
uncertainty_after_minutes: null,
}).success, false);
assert.equal(accountProfilePatchSchema.safeParse({
...valid,
reported_birth_time: null,
birth_time_source: "unknown",
birth_time_period: "morning",
uncertainty_before_minutes: null,
uncertainty_after_minutes: null,
}).success, false);
});
test("ordinary declaration edits clear stale candidate application, and a new declaration drops a confirmed minute too", () => {
// 原值(测试名):…but never overwrite confirmed active time
// 新值:…and a new declaration drops a confirmed minute too
// 原因:产品 2026-09-29 推翻 BUG-264 约定:改了出生日期 / 时间 / 地点就按新资料排盘,即使原来已确认(TASK-mobile-chart-and-confirmed-edit M2)
const candidate = {
birth_date: "1997-08-08",
reported_birth_time: "05:30",
birth_time_source: "approximate",
birth_time_period: null,
birth_time_clue: null,
uncertainty_before_minutes: 30,
uncertainty_after_minutes: 30,
active_birth_time: "05:18",
birth_time: "05:18",
birth_time_status: "candidate",
rectification_case_id: "11111111-1111-4111-8111-111111111111",
latitude: 36.420487,
longitude: 114.209936,
timezone_offset: 8,
} as const;
const edited = {
birth_date: candidate.birth_date,
reported_birth_time: "06:10",
birth_time_source: candidate.birth_time_source,
birth_time_period: null,
birth_time_clue: null,
uncertainty_before_minutes: 30,
uncertainty_after_minutes: 30,
} as const;
assert.deepEqual(resolveAccountBirthTimeApplicationPatch(candidate, edited), {
active_birth_time: null,
birth_time_status: "reported",
rectification_case_id: null,
});
assert.deepEqual(resolveAccountBirthTimeApplicationPatch(candidate, {
district_code: "130407",
}), {
active_birth_time: null,
birth_time_status: "reported",
rectification_case_id: null,
});
for (const coordinatePatch of [
{ latitude: 36.420488 },
{ longitude: 114.209937 },
{ timezone_offset: 9 },
]) {
assert.deepEqual(resolveAccountBirthTimeApplicationPatch(candidate, coordinatePatch), {
active_birth_time: null,
birth_time_status: "reported",
rectification_case_id: null,
});
}
assert.deepEqual(resolveAccountBirthTimeApplicationPatch({
...candidate,
birth_time_status: "accepted",
}, edited), {
active_birth_time: null,
birth_time_status: "reported",
rectification_case_id: null,
});
// 原值:confirmed + 新声明 → {}(已确认分钟不动)
// 新值:confirmed + 新声明 → 回到 reported,并清掉遗留 birth_time(图表会回落读它)
// 原因:产品 2026-09-29 M2;只改岁差仍然不动(下一条断言原样保留)
assert.deepEqual(resolveAccountBirthTimeApplicationPatch({
...candidate,
birth_time_status: "confirmed",
}, edited), {
active_birth_time: null,
birth_time_status: "reported",
rectification_case_id: null,
birth_time: null,
});
assert.deepEqual(resolveAccountBirthTimeApplicationPatch({
...candidate,
birth_time_status: "confirmed",
}, { ayanamsa: "lahiri" }), {});
assert.deepEqual(resolveAccountBirthTimeApplicationPatch({
...candidate,
active_birth_time: null,
birth_time_status: "reported",
}, { timezone_offset: 7 }), {
active_birth_time: null,
birth_time_status: "reported",
rectification_case_id: null,
});
assert.deepEqual(resolveAccountBirthTimeApplicationPatch({
...candidate,
active_birth_time: null,
birth_time: null,
birth_time_status: null,
rectification_case_id: null,
}, edited), {
active_birth_time: null,
birth_time_status: "reported",
rectification_case_id: null,
});
assert.deepEqual(resolveAccountBirthTimeApplicationPatch({
...candidate,
active_birth_time: null,
birth_time: null,
birth_time_status: null,
rectification_case_id: null,
reported_birth_time: edited.reported_birth_time,
}, edited), {
active_birth_time: null,
birth_time_status: "reported",
rectification_case_id: null,
});
assert.deepEqual(resolveAccountBirthTimeApplicationPatch(null, edited), {
active_birth_time: null,
birth_time_status: "reported",
rectification_case_id: null,
});
assert.deepEqual(resolveAccountBirthTimeApplicationPatch({
...candidate,
birth_time_status: "accepted",
}, edited), {
active_birth_time: null,
birth_time_status: "reported",
rectification_case_id: null,
});
// 原值 / 新值 / 原因:同上一处 confirmed 断言(产品 2026-09-29 M2)
assert.deepEqual(resolveAccountBirthTimeApplicationPatch({
...candidate,
birth_time_status: "confirmed",
}, edited), {
active_birth_time: null,
birth_time_status: "reported",
rectification_case_id: null,
birth_time: null,
});
assert.deepEqual(resolveAccountBirthTimeApplicationPatch({
...candidate,
active_birth_time: null,
birth_time_status: null,
reported_birth_time: edited.reported_birth_time,
}, edited), {});
});
test("zero-uncertainty family exact time becomes an accepted usable chart time", () => {
const exactDeclaration = {
birth_date: "1997-08-08",
reported_birth_time: "05:00",
birth_time_source: "family_exact",
birth_time_period: null,
birth_time_clue: null,
uncertainty_before_minutes: 0,
uncertainty_after_minutes: 0,
} as const;
const reportedExactProfile = {
...exactDeclaration,
reported_birth_time: "05:00:00",
active_birth_time: null,
birth_time: null,
birth_time_status: "reported",
rectification_case_id: null,
} as const;
assert.deepEqual(resolveAccountBirthTimeApplicationPatch(null, exactDeclaration), {
active_birth_time: "05:00",
birth_time_status: "accepted",
rectification_case_id: null,
});
assert.deepEqual(resolveAccountBirthTimeApplicationPatch(reportedExactProfile, exactDeclaration), {
active_birth_time: "05:00",
birth_time_status: "accepted",
rectification_case_id: null,
});
assert.deepEqual(resolveAccountBirthTimeApplicationPatch({
...reportedExactProfile,
active_birth_time: "05:00:00",
birth_time_status: "accepted",
}, {
...exactDeclaration,
reported_birth_time: "05:40",
}), {
active_birth_time: "05:40",
birth_time_status: "accepted",
rectification_case_id: null,
});
// 原值:confirmed / legacy 分钟 + 新的精确家人时间 → {}
// 新值:→ accepted + 新分钟;legacy 行另清遗留 birth_time
// 原因:产品 2026-09-29 M2,已确认分钟也跟随新声明
assert.deepEqual(resolveAccountBirthTimeApplicationPatch({
...reportedExactProfile,
active_birth_time: "05:00:00",
birth_time_status: "confirmed",
}, {
...exactDeclaration,
reported_birth_time: "05:40",
}), {
active_birth_time: "05:40",
birth_time_status: "accepted",
rectification_case_id: null,
});
assert.deepEqual(resolveAccountBirthTimeApplicationPatch({
...reportedExactProfile,
active_birth_time: "05:00:00",
birth_time: "05:00:00",
birth_time_status: null,
}, {
...exactDeclaration,
reported_birth_time: "05:40",
}), {
active_birth_time: "05:40",
birth_time_status: "accepted",
rectification_case_id: null,
birth_time: null,
});
});
test("account PATCH answers with the birth-time truth it derived server-side", () => {
const exactDeclaration = {
birth_date: "1997-08-08",
reported_birth_time: "05:00",
birth_time_source: "family_exact",
birth_time_period: null,
birth_time_clue: null,
uncertainty_before_minutes: 0,
uncertainty_after_minutes: 0,
} as const;
const reportedExactProfile = {
...exactDeclaration,
reported_birth_time: "05:00:00",
active_birth_time: null,
birth_time: null,
birth_time_status: "reported",
rectification_case_id: null,
} as const;
// A client that keeps the declaration it submitted would consult as
// unverified_birth_time and be rejected by the consultation truth check.
assert.deepEqual(
resolveAppliedAccountBirthTime(
null,
resolveAccountBirthTimeApplicationPatch(null, exactDeclaration),
),
{ status: "accepted", activeTime: "05:00" },
);
assert.deepEqual(
resolveAppliedAccountBirthTime(
reportedExactProfile,
resolveAccountBirthTimeApplicationPatch(reportedExactProfile, exactDeclaration),
),
{ status: "accepted", activeTime: "05:00" },
);
const periodDeclaration = {
...exactDeclaration,
reported_birth_time: null,
birth_time_source: "period_only",
birth_time_period: "early_morning",
uncertainty_before_minutes: null,
uncertainty_after_minutes: null,
} as const;
assert.deepEqual(
resolveAppliedAccountBirthTime(
reportedExactProfile,
resolveAccountBirthTimeApplicationPatch(reportedExactProfile, periodDeclaration),
),
{ status: "reported", activeTime: null },
);
// An untouched application keeps the stored truth, including a legacy confirmed minute.
// 原值:confirmed / legacy 分钟 + 改 district_code → 仍 confirmed 05:18
// 新值:改地点属于新声明 → 按声明走(本夹具声明是零误差家人时间 05:00,于是 accepted 05:00);只改称呼 / 岁差仍保留 confirmed 05:18
// 原因:产品 2026-09-29 M2(改日期、时间或地点就按新资料排盘)
const confirmedProfile = {
...reportedExactProfile,
active_birth_time: "05:18:00",
birth_time_status: "confirmed",
} as const;
assert.deepEqual(
resolveAppliedAccountBirthTime(
confirmedProfile,
resolveAccountBirthTimeApplicationPatch(confirmedProfile, { district_code: "130407" }),
),
{ status: "accepted", activeTime: "05:00" },
);
assert.deepEqual(
resolveAppliedAccountBirthTime(
confirmedProfile,
resolveAccountBirthTimeApplicationPatch(confirmedProfile, { name: "示例", ayanamsa: "lahiri" }),
),
{ status: "confirmed", activeTime: "05:18" },
);
const legacyProfile = {
...reportedExactProfile,
birth_time: "05:18:00",
birth_time_status: null,
} as const;
assert.deepEqual(
resolveAppliedAccountBirthTime(
legacyProfile,
resolveAccountBirthTimeApplicationPatch(legacyProfile, { district_code: "130407" }),
),
{ status: "accepted", activeTime: "05:00" },
);
assert.deepEqual(
resolveAppliedAccountBirthTime(
legacyProfile,
resolveAccountBirthTimeApplicationPatch(legacyProfile, { name: "示例" }),
),
{ status: "confirmed", activeTime: "05:18" },
);
assert.deepEqual(
resolveAppliedAccountBirthTime(null, resolveAccountBirthTimeApplicationPatch(null, { name: "岳辰" })),
{ status: null, activeTime: null },
);
assert.match(source, /\.\.\.resolveAppliedAccountBirthTime\(currentProfile, applicationPatch\)/);
assert.match(source, /activeDate: data\.active_birth_date \?\? null/);
assert.match(source, /activeTimezoneOffset: data\.active_birth_timezone_offset \?\? null/);
});
test("only a strict family exact zero-uncertainty declaration is auto-accepted", () => {
const base = {
birth_date: "1997-08-08",
reported_birth_time: "05:00",
birth_time_source: "family_exact",
birth_time_period: null,
birth_time_clue: null,
uncertainty_before_minutes: 0,
uncertainty_after_minutes: 0,
} as const;
const declarations = [
{ ...base, uncertainty_before_minutes: 5, uncertainty_after_minutes: 5 },
{ ...base, uncertainty_before_minutes: 10, uncertainty_after_minutes: 10 },
{ ...base, uncertainty_before_minutes: 15, uncertainty_after_minutes: 15 },
{ ...base, birth_time_source: "approximate", uncertainty_before_minutes: 30, uncertainty_after_minutes: 30 },
{
...base,
reported_birth_time: null,
birth_time_source: "period_only",
birth_time_period: "morning",
uncertainty_before_minutes: null,
uncertainty_after_minutes: null,
},
{
...base,
reported_birth_time: null,
birth_time_source: "unknown",
uncertainty_before_minutes: null,
uncertainty_after_minutes: null,
},
] as const;
for (const declaration of declarations) {
assert.deepEqual(resolveAccountBirthTimeApplicationPatch(null, declaration), {
active_birth_time: null,
birth_time_status: "reported",
rectification_case_id: null,
});
}
});
test("reported birth-time status repair is forward-only and applied by the self-hosted migrator", () => {
assert.match(reportedStatusMigration, /birth_time_status is null/);
assert.match(reportedStatusMigration, /birth_time_status = 'reported'/);
assert.match(reportedStatusMigration, /active_birth_time is null/);
assert.match(reportedStatusMigration, /rectification_case_id is null/);
// Staging and production migrations run through db-migrate.mjs, which applies
// frontend/supabase/migrations as the compatibility directory; the repair file
// must stay there rather than in a retired workflow's explicit file list.
assert.ok(
existsSync(new URL("../supabase/migrations/20260726010000_backfill_reported_birth_time_status.sql", import.meta.url)),
);
assert.match(selfHostedMigrator, /"\.\.\/supabase\/migrations"/);
assert.match(selfHostedMigrator, /supabaseCompatibilityDirectory,/);
});
test("profile ayanamsa is added by the self-hosted migrator with Raman as the default", () => {
const ayanamsaMigration = readFileSync(
new URL("../supabase/migrations/20260903010000_profile_ayanamsa.sql", import.meta.url),
"utf8",
);
assert.match(ayanamsaMigration, /add column if not exists ayanamsa text not null default 'raman'/);
assert.match(ayanamsaMigration, /'raman', 'lahiri', 'kp', 'true_pushya'/);
assert.equal(
existsSync(new URL("../db/migrations/20260903010000_profile_ayanamsa.sql", import.meta.url)),
false,
);
});
test("existing exact family declarations are forward-repaired without claiming confirmation", () => {
assert.match(acceptedExactFamilyMigration, /update public\.profiles/);
assert.match(acceptedExactFamilyMigration, /active_birth_time = reported_birth_time/);
assert.match(acceptedExactFamilyMigration, /birth_time_status = 'accepted'/);
assert.match(acceptedExactFamilyMigration, /birth_time_source = 'family_exact'/);
assert.match(acceptedExactFamilyMigration, /uncertainty_before_minutes = 0/);
assert.match(acceptedExactFamilyMigration, /uncertainty_after_minutes = 0/);
assert.match(acceptedExactFamilyMigration, /birth_time_status = 'reported'/);
assert.match(acceptedExactFamilyMigration, /active_birth_time is null/);
assert.match(acceptedExactFamilyMigration, /rectification_case_id is null/);
assert.doesNotMatch(acceptedExactFamilyMigration, /birth_time_status = 'confirmed'/);
assert.equal(
existsSync(new URL("../db/migrations/20260816010000_accept_exact_family_birth_times.sql", import.meta.url)),
false,
);
});
test("account PATCH uses the shared validator and never writes client birth_time over account truth", () => {
assert.match(source, /accountProfilePatchSchema\.safeParse/);
assert.match(source, /resolveAccountBirthTimeApplicationPatch/);
assert.match(patchSource, /birthTimeSource[\s\S]*birth_time_status: "reported"/);
assert.doesNotMatch(source, /birth_time:\s*nullableString\(payload\.birth_time\)/);
assert.match(source, /invalidatesUnconfirmedApplication/);
assert.match(patchSource, /"birth_time_status"/);
assert.match(patchSource, /"active_birth_time"/);
assert.match(source, /latitude,longitude,timezone_offset/);
assert.match(source, /birth_place_label,birth_place_type,birth_place_provider,birth_place_provider_id,timezone_id,timezone_source/);
assert.match(source, /payload\.ayanamsa !== undefined \? \{ ayanamsa: payload\.ayanamsa \}/);
assert.match(patchSource, /ayanamsa: z\.enum\(AYANAMSA_VALUES\)\.optional\(\)/);
const declarationBlock = patchSource.slice(
patchSource.indexOf("const declarationFields"),
patchSource.indexOf("const concurrencyFields"),
);
assert.doesNotMatch(declarationBlock, /ayanamsa/);
assert.match(source, /applyAccountProfileConcurrencyGuards/);
assert.match(source, /最新确认结果已保留/);
});
test("candidate invalidation compares coordinates and timezone in the conditional write", async () => {
const { applyAccountProfileConcurrencyGuards } = await import("../src/lib/account-profile-patch.ts");
const calls: Array<["eq" | "is", string, unknown]> = [];
const query = {
eq(column: string, value: unknown) {
calls.push(["eq", column, value]);
return this;
},
is(column: string, value: null) {
calls.push(["is", column, value]);
return this;
},
};
const current = {
birth_date: "1997-08-08",
reported_birth_time: "05:30",
birth_time_source: "approximate",
birth_time_period: null,
birth_time_clue: null,
uncertainty_before_minutes: 30,
uncertainty_after_minutes: 30,
active_birth_time: "05:18",
birth_time: "05:18",
birth_time_status: "candidate",
rectification_case_id: "11111111-1111-4111-8111-111111111111",
country_code: "CN",
province_code: "130000",
city_code: "130400",
district_code: "130406",
latitude: 36.420487,
longitude: 114.209936,
timezone_offset: 8,
};
applyAccountProfileConcurrencyGuards(query, current);
assert.deepEqual(calls.filter((call) => ["latitude", "longitude", "timezone_offset"].includes(call[1])), [
["eq", "latitude", 36.420487],
["eq", "longitude", 114.209936],
["eq", "timezone_offset", 8],
]);
assert.deepEqual(calls.find((call) => call[1] === "active_birth_time"), ["eq", "active_birth_time", "05:18"]);
assert.deepEqual(calls.find((call) => call[1] === "birth_time_status"), ["eq", "birth_time_status", "candidate"]);
});
// TASK-mobile-chart-and-confirmed-edit-20260929 M2 (product 2026-09-29): a new
// declaration drops a confirmed or adopted minute; nothing else does.
test("confirmed minute follows a new declaration and survives everything else", () => {
const confirmed = {
birth_date: "1994-05-18",
reported_birth_time: "07:40:00",
birth_time_source: "approximate",
birth_time_period: null,
birth_time_clue: null,
uncertainty_before_minutes: 30,
uncertainty_after_minutes: 30,
active_birth_time: "07:52:00",
active_birth_date: "1994-05-18",
active_birth_timezone_offset: 8,
birth_time: null,
birth_time_status: "confirmed",
rectification_case_id: "11111111-1111-4111-8111-111111111111",
country_code: "CN",
province_code: "330000",
city_code: "330100",
district_code: "330106",
latitude: 30.27,
longitude: 120.15,
timezone_offset: 8,
} as const;
// The form resubmits every birth field; stored `HH:MM:SS` vs submitted `HH:MM` is not a change.
const resubmitted = {
birth_date: "1994-05-18",
reported_birth_time: "07:40",
birth_time_source: "approximate",
birth_time_period: null,
birth_time_clue: null,
uncertainty_before_minutes: 30,
uncertainty_after_minutes: 30,
country_code: "CN",
province_code: "330000",
city_code: "330100",
district_code: "330106",
latitude: 30.27,
longitude: 120.15,
timezone_offset: 8,
} as const;
assert.deepEqual(resolveAccountBirthTimeApplicationPatch(confirmed, { ...resubmitted, name: "示例" }), {});
assert.deepEqual(resolveAccountBirthTimeApplicationPatch(confirmed, { ayanamsa: "lahiri" }), {});
assert.deepEqual(resolveAccountBirthTimeApplicationPatch(confirmed, { gender: "female" }), {});
const cleared = { active_birth_date: null, active_birth_timezone_offset: null, active_birth_provenance: null };
// New exact family time → accepted at the new minute.
assert.deepEqual(resolveAccountBirthTimeApplicationPatch(confirmed, {
...resubmitted,
reported_birth_time: "08:05",
birth_time_source: "family_exact",
uncertainty_before_minutes: 0,
uncertainty_after_minutes: 0,
}), { active_birth_time: "08:05", birth_time_status: "accepted", rectification_case_id: null, ...cleared });
// Period only → reported, no minute.
assert.deepEqual(resolveAccountBirthTimeApplicationPatch(confirmed, {
...resubmitted,
reported_birth_time: null,
birth_time_source: "period_only",
birth_time_period: "morning",
uncertainty_before_minutes: null,
uncertainty_after_minutes: null,
}), { active_birth_time: null, birth_time_status: "reported", rectification_case_id: null, ...cleared });
// A new date drops the adopted cross-midnight date, offset and provenance.
assert.deepEqual(resolveAccountBirthTimeApplicationPatch(confirmed, { ...resubmitted, birth_date: "1994-05-19" }),
{ active_birth_time: null, birth_time_status: "reported", rectification_case_id: null, ...cleared });
// A new place (coordinates) is a new declaration too.
assert.deepEqual(resolveAccountBirthTimeApplicationPatch(confirmed, { ...resubmitted, latitude: 31.23, longitude: 121.47 }),
{ active_birth_time: null, birth_time_status: "reported", rectification_case_id: null, ...cleared });
// Without adoption columns, nothing extra is written.
assert.deepEqual(resolveAccountBirthTimeApplicationPatch({
...confirmed, active_birth_date: null, active_birth_timezone_offset: null,
}, { ...resubmitted, birth_date: "1994-05-19" }),
{ active_birth_time: null, birth_time_status: "reported", rectification_case_id: null });
// Legacy minute (status null, birth_time set): same rule, and the legacy column the chart falls back to is cleared.
const legacy = { ...confirmed, active_birth_time: null, active_birth_date: null, active_birth_timezone_offset: null, birth_time: "07:52:00", birth_time_status: null };
assert.deepEqual(resolveAccountBirthTimeApplicationPatch(legacy, { ...resubmitted, name: "示例" }), {});
assert.deepEqual(resolveAccountBirthTimeApplicationPatch(legacy, { ...resubmitted, birth_date: "1994-05-19" }),
{ active_birth_time: null, birth_time_status: "reported", rectification_case_id: null, birth_time: null });
});