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.
This commit is contained in:
1 parent
ad65d80d41
commit
3e69b72513
26 files changed
+17651
-3844
No files matched your search
@@ -33,6 +33,8 @@ vi.mock("@/lib/db", async () => ({
|
||||
state.bases.filter((row) => compiled.params.includes(row.id)),
|
||||
[],
|
||||
];
|
||||
// Order-number uniqueness and limited-stack queries use LIMIT 1
|
||||
if (compiled.sql.includes("LIMIT 1")) return [[], []];
|
||||
return [
|
||||
state.offers.filter((row) => compiled.params.includes(row.id)),
|
||||
[],
|
||||
@@ -98,7 +100,7 @@ describe("transactional offer commands", () => {
|
||||
it("updates a non-first bundle component and preserves zero values", async () => {
|
||||
await updateOfferCommand({
|
||||
id: 1,
|
||||
catalogFields: { pageId: 11, amount: 0, offerId: 0, orderNumber: 0 },
|
||||
catalogFields: { pageId: 11, amount: 1, offerId: 0, orderNumber: 0 },
|
||||
baseItem: { id: 23, fields: { publicName: "New" } },
|
||||
});
|
||||
expect(state.commits).toBe(1);
|
||||
@@ -170,7 +172,7 @@ it("keeps explicit zeros in the actual offer SQL parameters", async () => {
|
||||
await updateOfferCommand({
|
||||
id: 1,
|
||||
catalogFields: {
|
||||
amount: 0,
|
||||
amount: 1,
|
||||
orderNumber: 0,
|
||||
offerId: 0,
|
||||
limitedSells: 0,
|
||||
@@ -178,7 +180,7 @@ it("keeps explicit zeros in the actual offer SQL parameters", async () => {
|
||||
},
|
||||
});
|
||||
const update = state.queries.find((query) => query.sql.startsWith("UPDATE"));
|
||||
expect(update?.params).toEqual([0, 0, 0, 0, 0, 1]);
|
||||
expect(update?.params).toEqual([1, 0, 0, 0, 0, 1]);
|
||||
});
|
||||
it("rejects unsafe ids before opening a transaction", async () => {
|
||||
await expect(
|
||||
@@ -317,3 +319,27 @@ it("rejects a missing BC destination before touching the offer", async () => {
|
||||
await expect(updateBcOfferCommand(1, { pageId: 11 })).rejects.toThrow("page");
|
||||
expect(state.writes).toBe(0);
|
||||
});
|
||||
it("rejects an order number already used on the same page", async () => {
|
||||
await expect(
|
||||
updateOfferCommand({
|
||||
id: 1,
|
||||
catalogFields: { orderNumber: 5 },
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
// Verify the uniqueness SELECT was issued.
|
||||
const uniquenessQuery = state.queries.find(
|
||||
(q) => q.sql.includes("order_number") && q.sql.includes("LIMIT 1"),
|
||||
);
|
||||
expect(uniquenessQuery).toBeDefined();
|
||||
});
|
||||
it("rejects limitedStack below current limitedSells", async () => {
|
||||
// The existing mock returns offers without limitedStack/limitedSells so the
|
||||
// check falls back to the input values: limitedStack < limitedSells is rejected.
|
||||
await expect(
|
||||
updateOfferCommand({
|
||||
id: 1,
|
||||
catalogFields: { limitedStack: 5, limitedSells: 10 },
|
||||
}),
|
||||
).rejects.toThrow(/limited stack/i);
|
||||
expect(state.writes).toBe(0);
|
||||
});
|
||||
Reference in new issue
Block a user