Files
EpicNext-Cms/src/features/housekeeping/domains/hotel/inbox.ts
T
Simo 2e95a106e0
CI / check (pull_request) Successful in 38s
CI / release (pull_request) Skipped
CI / deploy (pull_request) Skipped
feat(housekeeping): integrate studio operations
2026-08-30 15:29:58 +02:00

127 lines
3.8 KiB
TypeScript

import { PERMS } from "@/lib/permission-slugs";
import { authorizeHousekeeping } from "../../foundation/authorization";
import { satisfiesCapability } from "../../foundation/capability-context";
import {
anyCapability,
fail,
type HousekeepingCapabilityContext,
type HousekeepingInboxSource,
ok,
} from "../../foundation/contracts";
import {
type StudioOperationRecord,
studioOperationRepository,
} from "./queries/studio";
export const HOTEL_INBOX_SOURCE_IDS = ["hotel.studio.operations"] as const;
type HotelOperationLoader = (
context: HousekeepingCapabilityContext,
signal: AbortSignal,
) => Promise<readonly StudioOperationRecord[]>;
export interface HotelInboxAdapters {
readonly operations: HotelOperationLoader;
}
export function createHotelInboxSources(
adapters: HotelInboxAdapters,
): readonly HousekeepingInboxSource[] {
const capability = anyCapability(PERMS.ASSETS_IMPORT, PERMS.CATALOG_EDIT);
return [
{
id: "hotel.studio.operations",
owner: "hotel",
capability,
async getItems(context, signal) {
const authorization = authorizeHousekeeping(context, capability);
if (!authorization.ok) return authorization;
try {
if (signal.aborted) throw new Error("aborted Hotel inbox");
const operations = await adapters.operations(context, signal);
return ok(
{
availability: "available" as const,
items: operations
.filter((operation) => operation.phase !== "completed")
.filter((operation) =>
satisfiesCapability(
context,
operation.kind === "maintenance"
? anyCapability(PERMS.CATALOG_EDIT)
: anyCapability(PERMS.ASSETS_IMPORT),
),
)
.slice(0, 25)
.map((operation) => {
const timestamp = Date.parse(operation.updatedAt);
const ageMs = Number.isFinite(timestamp)
? Math.max(0, Date.now() - timestamp)
: 0;
return {
sourceId: "hotel.studio.operations",
itemId: operation.operationId,
deduplicationKey: `hotel.studio.operations:${operation.operationId}`,
domain: "hotel" as const,
capability:
operation.kind === "maintenance"
? anyCapability(PERMS.CATALOG_EDIT)
: anyCapability(PERMS.ASSETS_IMPORT),
severity:
operation.phase === "failed"
? ("critical" as const)
: operation.phase === "partial"
? ("warning" as const)
: ("info" as const),
priority:
operation.phase === "failed"
? ("high" as const)
: ("normal" as const),
ageMs,
state: operation.phase,
occurredAt: operation.updatedAt,
titleKey: "pages.housekeeping.items.hotelStudioOperation",
context: {
kind: operation.kind,
completed: operation.completed,
total: operation.total,
},
href: `/ase/hotel/studio/${
operation.kind === "badge"
? "badges"
: operation.kind === "effect"
? "effects"
: operation.kind === "pet"
? "pets"
: operation.kind
}` as `/ase/${string}`,
freshness:
ageMs > 86_400_000
? ("stale" as const)
: ("fresh" as const),
actions: [],
};
}),
},
authorization.correlationId,
);
} catch {
return fail(
"DEPENDENCY_UNAVAILABLE",
"errors.housekeeping.dependencyUnavailable",
authorization.correlationId,
);
}
},
},
];
}
export const HOTEL_INBOX_SOURCES = createHotelInboxSources({
async operations(_context, signal) {
if (signal.aborted) throw new Error("aborted Hotel operations");
return (await studioOperationRepository.list({ pageSize: 25, offset: 0 }))
.items;
},
});