User-Wunsch: Legend-User priorisieren, 24h Approval-SLA, sichtbar wer/wann/Restzeit.
Backend:
- Schema: DomainSubmission.user @relation Profile (FK + composite-index status,createdAt)
- Migration: 20260509_domain_submission_user_relation (additive, FK via DO $$ block,
idempotent IF NOT EXISTS index)
- db/domains.ts getPendingSubmissions enriched:
- include user { id, nickname, plan }
- returns PendingSubmissionRow with planPriority (legend=2, pro=1, free=0)
- deadlineAt = createdAt + 24h
- msUntilDeadline (negative when overdue)
- sort: Legend > Pro > Free, FIFO innerhalb plan-bucket
- Constant ADMIN_APPROVAL_SLA_MS exported
Tests:
- backend/tests/admin/domains.test.ts — 5 cases (priority-sort, FIFO, deadline,
overdue, user-null fallback). 83 backend tests passing total.
Frontend (apps/admin/pages/domains.vue):
- Card-list (statt UTable — sichtbarer urgency-stripe links)
- Filter-chips „Alle | Nur Legend | Überfällig" mit live counts
- Per row: nickname, plan-badge (Legend = sparkles + warning/gold),
request-age (relative), deadline-countdown („noch 18h" / „ÜBERFÄLLIG (6h)")
- Visual urgency-stripe (1px border-left full-height):
- Overdue: red-600 + warning-icon
- <2h: red-500
- Legend: amber-400 (gold)
- <12h: yellow-500
- Normal: gray-700
⚠️ Migration auto-deploy via pipeline (b38bf17 detection).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
123 lines
3.7 KiB
TypeScript
123 lines
3.7 KiB
TypeScript
/**
|
|
* Tests für getPendingSubmissions (Domain-Approval-Queue).
|
|
*
|
|
* Schwerpunkt: Sort-Order-Garantie + Deadline-Computation.
|
|
* - Legend > Pro > Free Plan-Priority
|
|
* - Innerhalb gleicher Priority: älteste createdAt zuerst (FIFO)
|
|
* - deadlineAt = createdAt + 24h, msUntilDeadline negativ wenn überfällig
|
|
*/
|
|
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
|
|
const prismaMock = vi.hoisted(() => ({
|
|
domainSubmission: {
|
|
findMany: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
vi.mock("../../server/utils/prisma", () => ({
|
|
usePrisma: () => prismaMock,
|
|
}));
|
|
|
|
import {
|
|
getPendingSubmissions,
|
|
ADMIN_APPROVAL_SLA_MS,
|
|
} from "../../server/db/domains";
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
function makeRow(overrides: Partial<Record<string, unknown>> = {}) {
|
|
return {
|
|
id: "sub-id",
|
|
domain: "example.com",
|
|
yesVotes: 0,
|
|
noVotes: 0,
|
|
status: "in_review",
|
|
createdAt: new Date("2026-05-09T10:00:00Z"),
|
|
userId: "user-id",
|
|
postId: null,
|
|
customDomain: { id: "cd-id" },
|
|
user: { id: "user-id", nickname: "nick", plan: "free" },
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("getPendingSubmissions — Plan-Priority + Deadline", () => {
|
|
it("sortiert Legend vor Pro vor Free (Plan-Priority)", async () => {
|
|
const sameTime = new Date("2026-05-09T10:00:00Z");
|
|
prismaMock.domainSubmission.findMany.mockResolvedValueOnce([
|
|
makeRow({
|
|
id: "free-1",
|
|
createdAt: sameTime,
|
|
user: { id: "u1", nickname: "a", plan: "free" },
|
|
}),
|
|
makeRow({
|
|
id: "legend-1",
|
|
createdAt: sameTime,
|
|
user: { id: "u2", nickname: "b", plan: "legend" },
|
|
}),
|
|
makeRow({
|
|
id: "pro-1",
|
|
createdAt: sameTime,
|
|
user: { id: "u3", nickname: "c", plan: "pro" },
|
|
}),
|
|
]);
|
|
|
|
const result = await getPendingSubmissions();
|
|
expect(result.map((r) => r.id)).toEqual(["legend-1", "pro-1", "free-1"]);
|
|
});
|
|
|
|
it("innerhalb gleicher Plan-Priority: älteste zuerst (FIFO)", async () => {
|
|
prismaMock.domainSubmission.findMany.mockResolvedValueOnce([
|
|
makeRow({
|
|
id: "legend-newer",
|
|
createdAt: new Date("2026-05-09T12:00:00Z"),
|
|
user: { id: "u1", nickname: "x", plan: "legend" },
|
|
}),
|
|
makeRow({
|
|
id: "legend-older",
|
|
createdAt: new Date("2026-05-09T08:00:00Z"),
|
|
user: { id: "u2", nickname: "y", plan: "legend" },
|
|
}),
|
|
]);
|
|
|
|
const result = await getPendingSubmissions();
|
|
expect(result.map((r) => r.id)).toEqual(["legend-older", "legend-newer"]);
|
|
});
|
|
|
|
it("berechnet deadlineAt = createdAt + 24h pro row", async () => {
|
|
const created = new Date("2026-05-09T10:00:00Z");
|
|
prismaMock.domainSubmission.findMany.mockResolvedValueOnce([
|
|
makeRow({ createdAt: created }),
|
|
]);
|
|
|
|
const result = await getPendingSubmissions();
|
|
const expectedDeadline = new Date(
|
|
created.getTime() + ADMIN_APPROVAL_SLA_MS,
|
|
);
|
|
expect(result[0]!.deadlineAt.toISOString()).toBe(
|
|
expectedDeadline.toISOString(),
|
|
);
|
|
});
|
|
|
|
it("msUntilDeadline ist negativ wenn Submission überfällig (>24h alt)", async () => {
|
|
// 30h alte Submission → 6h überfällig → ms negativ
|
|
const created = new Date(Date.now() - 30 * 60 * 60 * 1000);
|
|
prismaMock.domainSubmission.findMany.mockResolvedValueOnce([
|
|
makeRow({ createdAt: created }),
|
|
]);
|
|
|
|
const result = await getPendingSubmissions();
|
|
expect(result[0]!.msUntilDeadline).toBeLessThan(0);
|
|
});
|
|
|
|
it("planPriority fällt auf 0 zurück wenn user.plan unbekannt / null", async () => {
|
|
prismaMock.domainSubmission.findMany.mockResolvedValueOnce([
|
|
makeRow({ user: null }),
|
|
]);
|
|
const result = await getPendingSubmissions();
|
|
expect(result[0]!.planPriority).toBe(0);
|
|
});
|
|
});
|