import { PERMS } from "@/lib/permission-slugs"; import { satisfiesCapability } from "../../../foundation/capability-context"; import { anyCapability, fail, type HousekeepingCapabilityContext, type HousekeepingErrorCode, type HousekeepingPartialCompletion, type HousekeepingResult, ok, } from "../../../foundation/contracts"; export const ECONOMY_MUTATION_OPERATIONS = [ "catalog-page.change", "catalog-page.reorder", "catalog-page.delete-tree", "bc-page.change", "bc-page.reorder", "bc-page.delete-tree", "bc-item.change", "catalog-item.change", "catalog-item.bulk-create", "catalog-item.move", "catalog-item.reorder", "catalog-item.translate", "maintenance.fix-offers", "maintenance.fix-everything", "maintenance.fix-sprite-ids", "maintenance.reconcile-ids", "maintenance.align-ids", "maintenance.remove-duplicates", "items-base.update", "shop-article.change", "marketplace.cancel", "voucher.change", "rare-category.change", "rare-value.change", "badge.give", "badge.upload", "soundtrack.change", ] as const; export type EconomyMutationOperation = (typeof ECONOMY_MUTATION_OPERATIONS)[number]; export interface EconomyMutationSnapshot { readonly before: Readonly> | null; readonly after: Readonly> | null; readonly output?: Readonly>; readonly completion?: HousekeepingPartialCompletion; } export interface EconomyMutationInvocation { readonly correlationId: string; readonly expectedActorId: number; readonly legacy?: boolean; } export interface EconomyMutationContext { readonly capability: HousekeepingCapabilityContext; readonly correlationId: string; readonly legacy: boolean; } export interface EconomyMutationAdapter { execute( operation: EconomyMutationOperation, input: unknown, context: EconomyMutationContext, ): Promise; } export interface EconomyMutationService { execute( invocation: EconomyMutationInvocation, operation: EconomyMutationOperation, input: unknown, ): Promise>; } export class EconomyMutationFailure extends Error { constructor( readonly code: HousekeepingErrorCode, readonly messageKey: string, readonly fieldErrors?: Readonly>, ) { super(messageKey); this.name = "EconomyMutationFailure"; } } const OPERATION_PERMISSION = Object.freeze( Object.fromEntries( ECONOMY_MUTATION_OPERATIONS.map((operation) => [ operation, operation.startsWith("shop-") || operation.startsWith("marketplace.") || operation.startsWith("voucher.") || operation.startsWith("rare-") ? PERMS.SHOP_EDIT : PERMS.CATALOG_EDIT, ]), ) as Record, ); export function economyMutationCapability(operation: EconomyMutationOperation) { return anyCapability(OPERATION_PERMISSION[operation]); } export function createEconomyMutationService( adapter: EconomyMutationAdapter, resolveCapabilityContext: () => Promise, ): EconomyMutationService { return { async execute(invocation, operation, input) { let capability: HousekeepingCapabilityContext; try { capability = await resolveCapabilityContext(); } catch { return fail( "UNAUTHENTICATED", "errors.housekeeping.unauthenticated", invocation.correlationId, ); } if ( capability.actor.id !== invocation.expectedActorId || !satisfiesCapability(capability, economyMutationCapability(operation)) ) { return fail( "FORBIDDEN", "errors.housekeeping.forbidden", invocation.correlationId, ); } try { const snapshot = await adapter.execute(operation, input, { capability, correlationId: invocation.correlationId, legacy: invocation.legacy === true, }); return ok(snapshot, invocation.correlationId, snapshot.completion); } catch (error) { if (error instanceof EconomyMutationFailure) { return fail( error.code, error.messageKey, invocation.correlationId, error.fieldErrors, ); } return fail( "DEPENDENCY_UNAVAILABLE", "errors.housekeeping.dependencyUnavailable", invocation.correlationId, ); } }, }; } export function createEconomyMutationInvocation( actor: { readonly id: number }, correlationId: string, ): EconomyMutationInvocation { return { correlationId, expectedActorId: actor.id, legacy: true }; } const productionAdapter: EconomyMutationAdapter = { async execute(operation, input, context) { const { economyProductionMutationAdapter } = await import( "./mutations-production" ); return economyProductionMutationAdapter.execute(operation, input, context); }, }; export const economyMutationService = createEconomyMutationService( productionAdapter, async () => { const { getHousekeepingCapabilityContext } = await import( "../../../foundation/server-capability-context" ); return getHousekeepingCapabilityContext(); }, ); export async function executeLegacyEconomyMutation( actor: { readonly id: number }, operation: EconomyMutationOperation, input: unknown, ): Promise { const { createCorrelationId } = await import( "../../../foundation/correlation" ); const result = await economyMutationService.execute( { correlationId: createCorrelationId(), expectedActorId: actor.id, legacy: true, }, operation, input, ); if (!result.ok) { throw new EconomyMutationFailure( result.error.code, result.error.messageKey, result.error.fieldErrors, ); } if (result.completion?.external === "failed") { throw new EconomyMutationFailure( "DEPENDENCY_UNAVAILABLE", "errors.housekeeping.dependencyUnavailable", ); } return result.data; }