fix(engine): finalize united events and auctions compatibly

This commit is contained in:
2026-07-31 14:56:57 +00:00
parent d8a893e6a8
commit bb6d2f524e
15 changed files with 995 additions and 138 deletions
+15 -1
View File
@@ -6,7 +6,7 @@ import type { DatabaseClient, GameApiContext, GeneralRow } from '../../context.j
import { buildAuctionTimerKeys } from '../../auction/keys.js';
import { GamePrisma } from '@sammo-ts/infra';
import { ItemLoader, isItemKey } from '@sammo-ts/logic';
import { asRecord } from '@sammo-ts/common';
import { asNumber, asRecord } from '@sammo-ts/common';
import { buildAuctionAlias } from '@sammo-ts/logic';
import { openAuctionWithDaemon } from '../../auction/open.js';
@@ -91,6 +91,14 @@ const ensureGeneral = async (db: DatabaseClient, userId: string): Promise<Genera
return general;
};
const ensureAuctionSeasonActive = async (db: DatabaseClient): Promise<void> => {
const worldState = await db.worldState.findFirst({ select: { meta: true } });
const meta = asRecord(worldState?.meta);
if (asNumber(meta.isUnited, 0) !== 0 || asNumber(meta.isunited, 0) !== 0) {
throw new TRPCError({ code: 'BAD_REQUEST', message: '천하통일 후에는 경매를 이용할 수 없습니다.' });
}
};
const loadAuction = async (
db: DatabaseClient,
auctionId: number
@@ -323,16 +331,19 @@ export const auctionRouter = router({
openBuyRice: authedProcedure.input(zOpenResourceInput).mutation(async ({ ctx, input }) => {
const auth = requireAuth(ctx);
const general = await ensureGeneral(ctx.db, auth.user.id);
await ensureAuctionSeasonActive(ctx.db);
return openAuctionWithDaemon(ctx, general.id, { auctionType: 'BUY_RICE', ...input });
}),
openSellRice: authedProcedure.input(zOpenResourceInput).mutation(async ({ ctx, input }) => {
const auth = requireAuth(ctx);
const general = await ensureGeneral(ctx.db, auth.user.id);
await ensureAuctionSeasonActive(ctx.db);
return openAuctionWithDaemon(ctx, general.id, { auctionType: 'SELL_RICE', ...input });
}),
openUnique: authedProcedure.input(zOpenUniqueInput).mutation(async ({ ctx, input }) => {
const auth = requireAuth(ctx);
const general = await ensureGeneral(ctx.db, auth.user.id);
await ensureAuctionSeasonActive(ctx.db);
return openAuctionWithDaemon(ctx, general.id, {
auctionType: 'UNIQUE_ITEM',
itemKey: input.itemKey,
@@ -342,6 +353,7 @@ export const auctionRouter = router({
bidBuyRice: authedProcedure.input(zBidInput).mutation(async ({ ctx, input }) => {
const auth = requireAuth(ctx);
const general = await ensureGeneral(ctx.db, auth.user.id);
await ensureAuctionSeasonActive(ctx.db);
const auction = await loadAuction(ctx.db, input.auctionId);
if (!auction || auction.type !== 'BUY_RICE') {
throw new TRPCError({ code: 'NOT_FOUND', message: 'Auction not found.' });
@@ -413,6 +425,7 @@ export const auctionRouter = router({
bidSellRice: authedProcedure.input(zBidInput).mutation(async ({ ctx, input }) => {
const auth = requireAuth(ctx);
const general = await ensureGeneral(ctx.db, auth.user.id);
await ensureAuctionSeasonActive(ctx.db);
const auction = await loadAuction(ctx.db, input.auctionId);
if (!auction || auction.type !== 'SELL_RICE') {
throw new TRPCError({ code: 'NOT_FOUND', message: 'Auction not found.' });
@@ -484,6 +497,7 @@ export const auctionRouter = router({
bidUnique: authedProcedure.input(zUniqueBidInput).mutation(async ({ ctx, input }) => {
const auth = requireAuth(ctx);
const general = await ensureGeneral(ctx.db, auth.user.id);
await ensureAuctionSeasonActive(ctx.db);
const auction = await loadAuction(ctx.db, input.auctionId);
if (!auction || auction.type !== 'UNIQUE_ITEM') {
throw new TRPCError({ code: 'NOT_FOUND', message: 'Auction not found.' });
+25 -1
View File
@@ -76,6 +76,8 @@ const buildContext = (options: {
general?: GeneralRow | null;
auctions?: Array<Record<string, unknown>>;
queryRaw?: (query: GamePrisma.Sql) => Promise<unknown>;
isUnited?: number;
isunited?: number;
}) => {
const auth = options.auth === undefined ? buildAuth() : options.auth;
const general = options.general === undefined ? buildGeneral() : options.general;
@@ -108,7 +110,7 @@ const buildContext = (options: {
allItems: { weapon: { che_무기_12_칠성검: 1 } },
},
},
meta: { hiddenSeed: 'auction-hidden-seed' },
meta: { hiddenSeed: 'auction-hidden-seed', isUnited: options.isUnited ?? 0, isunited: options.isunited ?? 0 },
updatedAt: new Date('2026-07-26T00:00:00Z'),
};
const db = {
@@ -219,6 +221,28 @@ describe('auction router actor and permission boundaries', () => {
});
});
it('rejects auction mutations after unification before sending a daemon command', async () => {
const fixture = buildContext({ isUnited: 0, isunited: 2 });
const caller = appRouter.createCaller(fixture.context);
await expect(
caller.auction.openBuyRice({
amount: 1000,
closeTurnCnt: 3,
startBidAmount: 500,
finishBidAmount: 2000,
})
).rejects.toMatchObject({
code: 'BAD_REQUEST',
message: '천하통일 후에는 경매를 이용할 수 없습니다.',
});
await expect(caller.auction.bidUnique({ auctionId: 31, amount: 110 })).rejects.toMatchObject({
code: 'BAD_REQUEST',
message: '천하통일 후에는 경매를 이용할 수 없습니다.',
});
expect(fixture.requestCommand).not.toHaveBeenCalled();
});
it('redacts real unique-auction identities while preserving caller markers', async () => {
const openedAt = new Date('2026-07-26T01:00:00Z');
const fixture = buildContext({