127 lines
3.8 KiB
TypeScript
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;
|
|
},
|
|
});
|