chahinebrini f743556dc5 feat(domain-approval): Legend-priority + 24h-SLA-deadline + user-info cards
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>
2026-05-09 15:55:18 +02:00

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);
});
});