Files
EpicNext-Cms/src/features/catalog/domain/offer-input.ts
T
openhands 3e69b72513 feat(catalog): prevent and repair Arcturus emulator data errors
Block invalid catalog_items writes at the API level (points currency
allowlist, non-negative prices, positive amount, limited stack >= sold
count, unique sibling order numbers) and auto-assign unique order numbers
on bulk create. Add a catalog-maintenance scan + transactional repair that
fixes pre-existing rows: resets unsupported points_type, clamps negative
costs, sets amount to 1, raises limited_stack, renumbers duplicate orders
and deletes offers with missing page/item references. Surface the issue
count and a fix button in the admin maintenance panel.

Also: add enabled/retired flag to clone sources, classify poster and
currency furniture in item-kind, and remove the obsolete update-Nitrov3.sh.
2026-09-10 11:29:28 +02:00

101 lines
3.5 KiB
TypeScript

import { z } from "zod";
export const offerInteger = z.number().int().min(-2147483648).max(2147483647);
export const offerIdSchema = offerInteger.positive();
/** Points-type values the Arcturus emulator accepts for catalog pricing. */
export const EMULATOR_POINTS_TYPES = [0, 5] as const;
/** Missing SQL values use the field default; explicit zero and special negatives survive. */
export function numericValue(value: unknown, fallback: number): number {
if (value == null || (typeof value === "string" && value.trim() === ""))
return fallback;
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : fallback;
}
export function parseFurnitureIds(value: string): number[] {
const parts = value.split(";");
if (!parts.length || parts.some((part) => !/^\d+$/.test(part.trim())))
throw Error("Invalid furniture references");
return parts.map((part) => offerIdSchema.parse(Number(part)));
}
export const itemIds = z
.string()
.max(666)
.refine((value) => {
try {
parseFurnitureIds(value);
return true;
} catch {
return false;
}
}, "Invalid furniture references");
// Keep signed/special values on existing offer fields: bundle/LTD meaning belongs to the emulator.
export const offerPatchSchema = z
.object({
pageId: offerIdSchema.optional(),
itemIds: itemIds.optional(),
catalogName: z.string().max(100).optional(),
costCredits: offerInteger.nonnegative().optional(),
costPoints: offerInteger.nonnegative().optional(),
pointsType: offerInteger.optional(),
amount: offerInteger.positive().optional(),
orderNumber: offerInteger.optional(),
offerId: offerInteger.optional(),
songId: z.number().int().min(0).max(4294967295).optional(),
limitedSells: offerInteger.nonnegative().optional(),
limitedStack: offerInteger.nonnegative().optional(),
extradata: z.string().max(500).optional(),
haveOffer: z.enum(["0", "1"]).optional(),
clubOnly: z.enum(["0", "1"]).optional(),
})
.refine(
(data) =>
data.pointsType === undefined ||
EMULATOR_POINTS_TYPES.includes(data.pointsType as (typeof EMULATOR_POINTS_TYPES)[number]),
"Points type is not supported by the emulator",
)
.refine(
(data) =>
data.limitedStack === undefined ||
data.limitedSells === undefined ||
data.limitedStack >= data.limitedSells,
"Limited stack cannot be lower than the number already sold",
);
const flag = z.union([
z.literal(0),
z.literal(1),
z.literal("0"),
z.literal("1"),
]);
export const furniturePatchSchema = z.object({
publicName: z.string().max(56).optional(),
itemName: z.string().max(70).optional(),
type: z.string().max(3).optional(),
width: offerInteger.optional(),
length: offerInteger.optional(),
stackHeight: z.number().finite().optional(),
allowStack: flag.optional(),
allowSit: flag.optional(),
allowLay: flag.optional(),
allowWalk: flag.optional(),
allowGift: flag.optional(),
allowTrade: flag.optional(),
allowRecycle: flag.optional(),
allowMarketplaceSell: flag.optional(),
allowInventoryStack: flag.optional(),
interactionType: z.string().max(500).optional(),
interactionModesCount: offerInteger.optional(),
vendingIds: z.string().max(255).optional(),
customparams: z.string().max(25600).optional(),
effectIdMale: offerInteger.optional(),
effectIdFemale: offerInteger.optional(),
clothingOnWalk: z.string().max(255).optional(),
});
export function distinctOfferIds(ids: number[]): number[] {
for (const id of ids) offerIdSchema.parse(id);
if (new Set(ids).size !== ids.length)
throw Error("Duplicate catalog item ids");
return ids;
}