fix: 투표 UTC wall과 migration 시간대 경계를 고정한다

This commit is contained in:
2026-08-24 09:18:42 +00:00
parent 821b8a0723
commit 7a1254cb3a
15 changed files with 667 additions and 25 deletions
@@ -2479,12 +2479,14 @@ const insertVoteSelection = async (
if (!ctx.commandDb) {
return 'missing';
}
const createdAt = new Date();
const rows = await ctx.commandDb.$queryRaw<Array<{ id: number }>>(GamePrisma.sql`
INSERT INTO vote (vote_id, general_id, nation_id, selection)
INSERT INTO vote (vote_id, general_id, nation_id, selection, created_at)
SELECT poll.id,
${general.id},
${general.nationId},
CAST(${JSON.stringify(selection)} AS jsonb)
CAST(${JSON.stringify(selection)} AS jsonb),
${createdAt}
FROM vote_poll poll
WHERE poll.id = ${command.voteId}
ON CONFLICT (vote_id, general_id) DO NOTHING
@@ -286,4 +286,37 @@ integration('game cancellation transaction', () => {
generalMode: 'DELETE',
});
});
it('keeps a native inheritance log inside the cancellation boundary under a KST session', async () => {
const text = '신규/복귀 생성으로 포인트 1500 지급';
await db.inheritanceLog.deleteMany({ where: { userId, text } });
const [session] = await db.$queryRaw<Array<{ timeZone: string }>>`
SELECT current_setting('TIMEZONE') AS "timeZone"
`;
expect(session?.timeZone).toBe('Asia/Seoul');
const createdAfter = Date.now();
const log = await db.inheritanceLog.create({
data: { userId, serverId, year: 190, month: 7, text },
});
const createdBefore = Date.now();
expect(log.createdAt.getTime()).toBeGreaterThanOrEqual(createdAfter);
expect(log.createdAt.getTime()).toBeLessThanOrEqual(createdBefore);
const result = await cancelGame({
cancellationId: 'game-cancellation-kst-default-fixture',
databaseUrl: databaseUrl!,
cancelledBy: 'admin',
reason: 'KST 기본값 경계 검증',
historyMode: 'RETAIN_ABANDONED',
generalMode: 'RETAIN',
earnedPointRetentionPercent: 40,
cancelledAt: new Date(createdBefore + 1_000),
});
expect(result.settlements[userId]).toMatchObject({
earnedPoint: 1_790.005,
retainedEarnedPoint: 716,
finalPoint: 10_716,
});
});
});
+14 -1
View File
@@ -222,11 +222,12 @@ describe('voteReward command', () => {
let voteInserted = false;
let voteQueryCount = 0;
let voteInsertQuery: { strings: readonly string[]; values: readonly unknown[] } | undefined;
const commandDb = {
auction: {
findMany: async () => [],
},
$queryRaw: async (query: { strings: readonly string[] }) => {
$queryRaw: async (query: { strings: readonly string[]; values: readonly unknown[] }) => {
voteQueryCount += 1;
if (query.strings.join(' ').includes('SELECT options')) {
return [
@@ -239,6 +240,7 @@ describe('voteReward command', () => {
},
];
}
voteInsertQuery = query;
if (voteInserted) return [];
voteInserted = true;
return [{ id: 11 }];
@@ -255,12 +257,23 @@ describe('voteReward command', () => {
acceptedGameTick: 0,
};
const writerWindowStart = Date.now();
const result = await handler.handle(command, { db: commandDb as any });
const writerWindowEnd = Date.now();
expect(result && result.type).toBe('voteReward');
if (!result || result.type !== 'voteReward' || !result.ok) {
throw new Error('voteReward result missing');
}
expect(result.awardedUnique).toBe(true);
expect(voteInsertQuery?.strings.join(' ')).toContain('created_at');
expect(
voteInsertQuery?.values.filter(
(value) =>
value instanceof Date &&
value.getTime() >= writerWindowStart &&
value.getTime() <= writerWindowEnd
)
).toHaveLength(1);
const updated = world.getGeneralById(1);
// ENGINE is the single reward linearization point, so it uses the