@open-mercato/core 0.9.1-develop.7518.1.343d9198f1 → 0.9.1-develop.7519.1.38f51f8dbb

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- [build:core] found 5734 entry points
1
+ [build:core] found 5735 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 252 entry points
4
4
  [build:core:generated] built successfully
@@ -496,6 +496,7 @@ const createTimeEntryCommand = {
496
496
  const em = ctx.container.resolve("em").fork();
497
497
  const entry = await em.findOne(StaffTimeEntry, scopedStaffSnapshotWhere(after.id, staffSnapshotScopeFromSnapshot(after)));
498
498
  if (entry) {
499
+ assertTimeEntryUnlocked(entry, (await resolveTranslations()).translate);
499
500
  entry.deletedAt = /* @__PURE__ */ new Date();
500
501
  await em.flush();
501
502
  await emitCrudUndoSideEffects({
@@ -653,6 +654,7 @@ const startTimerCommand = {
653
654
  const em = ctx.container.resolve("em").fork();
654
655
  const entry = await em.findOne(StaffTimeEntry, scopedStaffSnapshotWhere(after.id, staffSnapshotScopeFromSnapshot(after)));
655
656
  if (entry) {
657
+ assertTimeEntryUnlocked(entry, (await resolveTranslations()).translate);
656
658
  entry.deletedAt = /* @__PURE__ */ new Date();
657
659
  await em.flush();
658
660
  await emitCrudUndoSideEffects({
@@ -830,6 +832,7 @@ const updateTimeEntryCommand = {
830
832
  const em = ctx.container.resolve("em").fork();
831
833
  const entry = await em.findOne(StaffTimeEntry, scopedStaffSnapshotWhere(before.id, staffSnapshotScopeFromSnapshot(before)));
832
834
  if (!entry) return;
835
+ assertTimeEntryUnlocked(entry, (await resolveTranslations()).translate);
833
836
  entry.date = before.date;
834
837
  entry.durationMinutes = before.durationMinutes;
835
838
  entry.roundedMinutes = before.roundedMinutes ?? null;
@@ -964,6 +967,7 @@ const deleteTimeEntryCommand = {
964
967
  });
965
968
  em.persist(entry);
966
969
  } else {
970
+ assertTimeEntryUnlocked(entry, (await resolveTranslations()).translate);
967
971
  entry.staffMemberId = before.staffMemberId;
968
972
  entry.date = before.date;
969
973
  entry.durationMinutes = before.durationMinutes;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/staff/commands/timesheets-entries.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\nimport type { CommandBus, CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { buildChanges, emitCrudSideEffects, emitCrudUndoSideEffects } from '@open-mercato/shared/lib/commands/helpers'\nimport { makeCreateRedo } from '@open-mercato/shared/lib/commands/redo'\nimport type { CrudIndexerConfig } from '@open-mercato/shared/lib/crud/types'\nimport type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'\nimport {\n StaffTimeEntry,\n StaffTimeEntrySegment,\n StaffTimeProject,\n StaffTimeTask,\n type StaffTimeEntrySource,\n} from '../data/entities'\nimport { emitStaffEvent } from '../events'\nimport { deriveInterval, formatClock, parseClock } from '../lib/time-tracking/interval'\nimport { roundMinutes } from '../lib/time-tracking/rounding'\nimport { resolveBillability } from '../lib/time-tracking/billability'\nimport type { ScopedResolverContext } from '../lib/time-tracking/registries/scope'\nimport {\n DEFAULT_TIME_TRACKING_SETTINGS,\n readTimeTrackingSettings,\n type TimeTrackingSettings,\n} from '../lib/time-tracking/settings'\nimport { resolveProjectAccess, type ProjectAccess } from '../lib/time-tracking/access'\n// Imported for the command ids AND for the registration side effect: the tag\n// commands must be in the registry before this file asks the bus to run one.\nimport { staffTimeTagCommandIds, TAG_TARGET_LOCKED_CODE } from './timesheets-tags'\n\nconst timeEntryCrudIndexer: CrudIndexerConfig<StaffTimeEntry> = {\n entityType: 'staff:staff_time_entry',\n}\nimport {\n staffTimeEntryCreateSchema,\n staffTimeEntryStartTimerSchema,\n staffTimeEntryUpdateSchema,\n type StaffTimeEntryCreateInput,\n type StaffTimeEntryStartTimerInput,\n type StaffTimeEntryUpdateInput,\n} from '../data/validators'\nimport { staffTimeEntryCrudEvents } from '../lib/crud'\nimport {\n applyScopeToWhere,\n commandActorScope,\n commandInputScope,\n ensureOrganizationScope,\n ensureTenantScope,\n extractUndoPayload,\n scopeForDecryption,\n scopedStaffSnapshotWhere,\n staffSnapshotDecryptionScope,\n staffSnapshotScopeFromContext,\n staffSnapshotScopeFromSnapshot,\n type StaffSnapshotScope,\n} from './shared'\nimport { getStaffMemberByUserId } from '../lib/staffMemberResolver'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('staff')\n\ntype ContainerLike = { resolve: (token: string) => unknown }\n\ntype Translate = (key: string, fallback?: string) => string\n\ntype RbacServiceLike = {\n userHasAllFeatures: (\n userId: string,\n required: string[],\n scope: { tenantId: string | null; organizationId: string | null },\n ) => Promise<boolean>\n getGrantedFeatures?: (\n userId: string,\n scope: { tenantId: string | null; organizationId: string | null },\n ) => Promise<string[]>\n}\n\n/**\n * Returns true when the caller holds `staff.timesheets.manage_all`, honoring\n * wildcard ACL grants (`staff.*`, `*`) and the super-admin flag via the cached\n * rbacService. Returns false when no auth context (e.g. system/CLI ctx) so\n * write paths that lack a caller identity are NOT silently elevated.\n */\nasync function callerHasManageAll(ctx: {\n auth?: { sub?: string | null; tenantId?: string | null; orgId?: string | null } | null\n container: { resolve: (token: string) => unknown }\n}): Promise<boolean> {\n const userId = ctx.auth?.sub\n if (!userId) return false\n try {\n const rbac = ctx.container.resolve('rbacService') as RbacServiceLike | undefined\n if (!rbac?.userHasAllFeatures) return false\n return await rbac.userHasAllFeatures(\n userId,\n ['staff.timesheets.manage_all'],\n { tenantId: ctx.auth?.tenantId ?? null, organizationId: ctx.auth?.orgId ?? null },\n )\n } catch {\n return false\n }\n}\n\nasync function resolveCallerStaffMemberId(\n em: EntityManager,\n ctx: { auth?: { sub?: string | null; tenantId?: string | null; orgId?: string | null } | null },\n): Promise<string | null> {\n const userId = ctx.auth?.sub\n if (!userId) return null\n const member = await getStaffMemberByUserId(\n em,\n userId,\n ctx.auth?.tenantId ?? null,\n ctx.auth?.orgId ?? null,\n )\n return member?.id ?? null\n}\n\n/**\n * Verifies the referenced time project exists and is in-scope (same tenant + org,\n * not soft-deleted) and hands the row back \u2014 the write path reads its\n * `billableByDefault` and `currencyCode` to seed the entry. Throws 422 if the ID\n * is provided but unresolvable. Returns null when projectId is null/undefined\n * (timeProjectId is optional on entries).\n */\nasync function requireTimeProjectInScope(\n em: EntityManager,\n projectId: string | null | undefined,\n tenantId: string,\n organizationId: string,\n): Promise<StaffTimeProject | null> {\n if (!projectId) return null\n const project = await em.findOne(StaffTimeProject, {\n id: projectId,\n tenantId,\n organizationId,\n deletedAt: null,\n })\n if (!project) {\n const { translate } = await resolveTranslations()\n throw new CrudHttpError(422, {\n error: translate('staff.timesheets.errors.projectNotFound', 'Time project not found or not accessible.'),\n fieldErrors: {\n timeProjectId: translate('staff.timesheets.errors.projectNotFound', 'Time project not found or not accessible.'),\n },\n })\n }\n return project\n}\n\n/**\n * Same contract as the project check, for `task_id`. A dangling task reference is\n * refused rather than stored: the task rollups on screens 6 and 7 aggregate entries\n * by `task_id`, so a stale or foreign UUID would silently move hours onto another\n * project's card.\n */\nasync function requireTaskInScope(\n em: EntityManager,\n taskId: string | null | undefined,\n tenantId: string,\n organizationId: string,\n): Promise<StaffTimeTask | null> {\n if (!taskId) return null\n const task = await em.findOne(StaffTimeTask, {\n id: taskId,\n tenantId,\n organizationId,\n deletedAt: null,\n })\n if (!task) {\n const { translate } = await resolveTranslations()\n const message = translate('staff.timesheets.errors.taskNotFound', 'Task not found or not accessible.')\n throw new CrudHttpError(422, { error: message, fieldErrors: { taskId: message } })\n }\n return task\n}\n\n/**\n * A task belongs to exactly one project, so an entry naming both must name the\n * same one. Letting them disagree would file the hours under a project whose\n * board never shows the task \u2014 the same rollup corruption the task-scope check\n * above prevents, one level up.\n */\nasync function assertTaskBelongsToProject(\n task: StaffTimeTask | null,\n projectId: string | null,\n): Promise<void> {\n if (timeEntryTaskMatchesProject(task, projectId)) return\n const { translate } = await resolveTranslations()\n const message = translate('staff.timesheets.errors.taskNotFound', 'Task not found or not accessible.')\n throw new CrudHttpError(422, { error: message, fieldErrors: { taskId: message } })\n}\n\n/**\n * The rule the assertion above enforces, as a predicate: a task belongs to\n * exactly one project, so an entry naming both must name the same one. Exported\n * because the grid bulk save validates a whole batch at once and answers with a\n * per-row error list rather than a thrown 422 \u2014 it reuses the rule and only\n * differs in how it reports a violation.\n */\nexport function timeEntryTaskMatchesProject(\n task: { timeProjectId?: string | null } | null | undefined,\n projectId: string | null | undefined,\n): boolean {\n if (!task || !projectId) return true\n return task.timeProjectId === projectId\n}\n\n/**\n * Screen 8 picks a task and lets the project follow from it (\"Projekt i klient\n * wynikaj\u0105 z zadania\"), so a write that names only a task still lands on the\n * project the task belongs to \u2014 and with it the billable and currency defaults.\n * One derivation for every write path, including the grid bulk save.\n */\nexport function resolveTimeEntryProjectId(\n requestedProjectId: string | null | undefined,\n task: { timeProjectId?: string | null } | null | undefined,\n): string | null {\n return requestedProjectId ?? task?.timeProjectId ?? null\n}\n\n/**\n * The billable default chain, in one place: an explicit value wins, then the\n * project's own default \u2014 a fixed-price engagement is non-billable however the\n * tenant normally works \u2014 and the tenant setting only decides what is left.\n *\n * EP-34 moved that chain into the billability registry's built-in resolver; this\n * stays the module's entry point and forwards the scope so a contributed\n * resolver can be consulted ahead of it.\n */\nexport function resolveTimeEntryBillable(params: {\n requested?: boolean | null\n project?: { id?: string | null; billableByDefault?: boolean | null } | null\n settings: TimeTrackingSettings\n scope?: ScopedResolverContext | null\n}): boolean {\n return resolveBillability({\n ...(params.scope ?? {}),\n requested: params.requested,\n project: params.project ?? null,\n settings: params.settings,\n })\n}\n\n/**\n * Rounding and the billable default are tenant settings. A settings read that\n * fails must not fail the write, so it degrades to the documented defaults \u2014\n * the same fail-safe the projects and tasks routes apply to the grace window.\n */\nasync function readEntrySettingsFrom(\n container: ContainerLike,\n tenantId: string,\n): Promise<TimeTrackingSettings> {\n try {\n const configService = container.resolve('moduleConfigService') as ModuleConfigService | undefined\n if (!configService?.getRecord) return DEFAULT_TIME_TRACKING_SETTINGS\n return await readTimeTrackingSettings(configService, { tenantId })\n } catch {\n return DEFAULT_TIME_TRACKING_SETTINGS\n }\n}\n\nasync function readEntrySettings(ctx: CommandRuntimeContext, tenantId: string): Promise<TimeTrackingSettings> {\n return readEntrySettingsFrom(ctx.container, tenantId)\n}\n\n/**\n * The same tenant-settings read, for write paths that run outside a command \u2014\n * the grid bulk save reads it ONCE per request (it is identical for every row)\n * and then seeds rounding and the billable default from that one snapshot.\n */\nexport async function resolveTimeEntrySettings(\n container: ContainerLike,\n tenantId: string,\n): Promise<TimeTrackingSettings> {\n return readEntrySettingsFrom(container, tenantId)\n}\n\n/**\n * The single rounding entry point for write paths that cannot run inside this\n * command file \u2014 the grid bulk save and timer-stop both compute a duration in\n * their own transaction, and D-7 makes `rounded_minutes` the ONLY input to cost,\n * so a duration written without its rounded twin bills nothing. They call this\n * rather than reaching for `roundMinutes` themselves, which keeps the tenant\n * settings read, its fail-safe, and the rule itself in one place.\n */\nexport async function resolveRoundedMinutes(\n container: ContainerLike,\n tenantId: string,\n durationMinutes: number,\n organizationId?: string | null,\n): Promise<number> {\n return roundedMinutesFor(durationMinutes, await readEntrySettingsFrom(container, tenantId), {\n tenantId,\n organizationId: organizationId ?? undefined,\n })\n}\n\nasync function resolveGrantedFeatures(\n ctx: CommandRuntimeContext,\n tenantId: string,\n organizationId: string,\n): Promise<string[]> {\n const userId = ctx.auth?.sub\n if (!userId) return []\n try {\n const rbac = ctx.container.resolve('rbacService') as RbacServiceLike | undefined\n if (!rbac?.getGrantedFeatures) return []\n return (await rbac.getGrantedFeatures(userId, { tenantId, organizationId })) ?? []\n } catch {\n // Fail closed: an unreadable grant set is no grant set, so the caller falls\n // back to their project memberships instead of being treated as a manager.\n return []\n }\n}\n\nasync function resolveEntryProjectAccess(\n em: EntityManager,\n ctx: CommandRuntimeContext,\n tenantId: string,\n organizationId: string,\n settings: TimeTrackingSettings,\n): Promise<ProjectAccess> {\n return resolveProjectAccess({\n em: em.fork(),\n userId: ctx.auth?.sub ?? null,\n tenantId,\n organizationId,\n userFeatures: await resolveGrantedFeatures(ctx, tenantId, organizationId),\n assignmentGraceDays: settings.access.assignmentGraceDays,\n })\n}\n\n/**\n * Every entry write intersects the caller's project access. An entry that names a\n * project is writable by a project manager or an assigned member; an entry with no\n * project belongs to nobody else, so only the member who logged it may touch it.\n * `staff.timesheets.manage_all` stays a superset of both, which is what it has\n * always meant on this route.\n */\nasync function assertEntryProjectAccess(params: {\n access: ProjectAccess\n hasManageAll: boolean\n projectId: string | null\n entryStaffMemberId: string\n}): Promise<void> {\n if (params.hasManageAll || params.access.canManageAll) return\n const { translate } = await resolveTranslations()\n if (params.projectId) {\n if (params.access.projectIds.includes(params.projectId)) return\n throw new CrudHttpError(403, {\n error: translate('staff.timesheets.errors.notAssigned', 'You are not assigned to this project.'),\n })\n }\n if (params.access.staffMemberId && params.access.staffMemberId === params.entryStaffMemberId) return\n throw new CrudHttpError(403, {\n error: translate('staff.timesheets.errors.notOwner', 'You can only manage your own time entries.'),\n })\n}\n\n/**\n * `notes` is the column; `description` is the name every consulting-suite screen\n * uses for it. Both keys are accepted on write and `description` wins when a\n * request carries both \u2014 the precedence UPGRADE_NOTES.md already published, and\n * the one that matches intent: a client sending the new key means it.\n * Returns `undefined` when neither key was supplied, so a PUT that omits both\n * leaves the stored note alone.\n *\n * Exported so the grid bulk save applies the SAME precedence \u2014 it accepts both\n * keys too, and a second interpretation of which one wins is exactly the kind of\n * divergence that makes one screen's edit disappear on another.\n */\nexport function resolveTimeEntryNotesInput(parsed: {\n notes?: string | null\n description?: string | null\n}): string | null | undefined {\n if (parsed.description !== undefined) return parsed.description ?? null\n if (parsed.notes !== undefined) return parsed.notes ?? null\n return undefined\n}\n\n/**\n * `rate_override_amount` is `numeric(14,4)`, which MikroORM surfaces as a string;\n * the validator accepts a JSON number. One conversion point keeps the two apart,\n * for the grid bulk save as much as for this file.\n */\nexport function toStoredTimeEntryRateOverride(amount: number | null | undefined): string | null {\n return amount === null || amount === undefined ? null : String(amount)\n}\n\n/**\n * D-7: `duration_minutes` keeps the RAW value the user entered \u2014 it is what the\n * timesheet and the project hour totals display \u2014 while `rounded_minutes` is\n * derived from the tenant's rounding rule at write time and is the only input to\n * cost. Storing the rounded value rather than deriving it on read is what stops a\n * later settings change from silently restating money that has already been\n * invoiced (risk R1).\n */\nexport function roundedMinutesFor(\n durationMinutes: number,\n settings: TimeTrackingSettings,\n scope?: ScopedResolverContext | null,\n): number {\n return roundMinutes(durationMinutes, settings.rounding, scope)\n}\n\nexport type TimeEntryInterval = {\n startedAt: Date | null\n endedAt: Date | null\n durationMinutes: number\n}\n\n/**\n * `started_at` / `ended_at` are absolute timestamps; `deriveInterval` reasons in\n * wall-clock minutes. Both directions read and write the SAME basis (UTC), so a\n * timestamp that needs no correction survives the round trip untouched.\n */\nfunction clockOfTimestamp(value: Date): string {\n return formatClock(value.getUTCHours() * 60 + value.getUTCMinutes())\n}\n\nfunction timestampOnDayOf(reference: Date, clock: string, dayOffset: number): Date {\n const minutesOfDay = parseClock(clock) ?? 0\n return new Date(\n Date.UTC(\n reference.getUTCFullYear(),\n reference.getUTCMonth(),\n reference.getUTCDate() + dayOffset,\n Math.floor(minutesOfDay / 60),\n minutesOfDay % 60,\n 0,\n 0,\n ),\n )\n}\n\n/**\n * D-8. An end whose clock is earlier than its start's is a midnight crossing, not\n * a validation error, so the end belongs on the NEXT calendar day while `date`\n * keeps naming the day the work started. A caller that already sent the end on\n * the following day (the entry dialog does) is left exactly as it sent it.\n */\nfunction alignEndToStart(startedAt: Date, endedAt: Date, endClock: string, crossesMidnight: boolean): Date {\n if (endedAt.getTime() >= startedAt.getTime()) return endedAt\n return timestampOnDayOf(startedAt, endClock, crossesMidnight ? 1 : 0)\n}\n\n/**\n * US-D3, on the update path: `duration_minutes`, `started_at` and `ended_at` can\n * each be written on their own, and the server must never store a set that\n * contradicts itself \u2014 a two-hour duration on a 09:00\u201310:00 span is a\n * defensible-invoice problem, not a cosmetic one (D-7 bills the rounded twin of\n * whatever duration lands here).\n *\n * The rule, in one line: **the clocks are the observation and the duration is\n * their summary, so the clocks win \u2014 unless the duration is the field the request\n * actually pinned, in which case the untouched clock moves to match it.**\n *\n * - both clocks in the request \u2192 the duration is recomputed from them, and a\n * duration sent alongside is advisory (this is what the entry dialog sends);\n * - one clock plus a duration \u2192 the OTHER clock is derived from that pair;\n * - one clock alone \u2192 the duration is recomputed against the stored counterpart;\n * - the duration alone \u2192 `started_at` anchors and `ended_at` shifts to match;\n * - none of the three \u2192 every one of them is left exactly as stored.\n *\n * Derivation only ever RESTATES a clock that already exists: with one side null\n * there is nothing for a duration to contradict, and manufacturing the missing\n * side would silently stop a running timer.\n *\n * Exported because the grid bulk save writes durations in its own transaction\n * (per-row, pessimistically locked) and cannot go through this command \u2014 it calls\n * the same reconciliation rather than repeating the arithmetic, which is what\n * keeps one rule for one invariant.\n */\nexport function reconcileTimeEntryInterval(params: {\n stored: TimeEntryInterval\n startedAt: Date | null | undefined\n endedAt: Date | null | undefined\n durationMinutes: number | undefined\n}): TimeEntryInterval {\n const startProvided = params.startedAt !== undefined\n const endProvided = params.endedAt !== undefined\n const durationProvided = params.durationMinutes !== undefined\n if (!startProvided && !endProvided && !durationProvided) return params.stored\n\n const startedAt = startProvided ? params.startedAt ?? null : params.stored.startedAt\n const endedAt = endProvided ? params.endedAt ?? null : params.stored.endedAt\n const durationMinutes = durationProvided\n ? (params.durationMinutes as number)\n : params.stored.durationMinutes\n if (!startedAt || !endedAt) return { startedAt, endedAt, durationMinutes }\n\n const startClock = clockOfTimestamp(startedAt)\n const endClock = clockOfTimestamp(endedAt)\n const durationAnchored = durationProvided && !(startProvided && endProvided)\n\n if (durationAnchored && !endProvided) {\n const derived = deriveInterval({ start: startClock, durationMinutes })\n return {\n startedAt,\n endedAt: timestampOnDayOf(startedAt, derived.end ?? endClock, derived.crossesMidnight ? 1 : 0),\n durationMinutes,\n }\n }\n\n if (durationAnchored && !startProvided) {\n const derived = deriveInterval({ end: endClock, durationMinutes })\n return {\n startedAt: timestampOnDayOf(endedAt, derived.start ?? startClock, derived.crossesMidnight ? -1 : 0),\n endedAt,\n durationMinutes,\n }\n }\n\n const derived = deriveInterval({ start: startClock, end: endClock, durationMinutes })\n return {\n startedAt,\n endedAt: alignEndToStart(startedAt, endedAt, endClock, derived.crossesMidnight),\n durationMinutes: derived.durationMinutes ?? durationMinutes,\n }\n}\n\nconst MS_PER_DAY = 86_400_000\n\nfunction toDateOrNull(value: Date | string | null | undefined): Date | null {\n if (value === null || value === undefined) return null\n const date = value instanceof Date ? value : new Date(value)\n return Number.isNaN(date.getTime()) ? null : date\n}\n\nfunction utcDayIndex(value: Date): number {\n return Math.round(\n Date.UTC(value.getUTCFullYear(), value.getUTCMonth(), value.getUTCDate()) / MS_PER_DAY,\n )\n}\n\n/**\n * T4.10(b): moving an entry to another day moves the work with it.\n *\n * `date` names the day the work STARTED (D-8), so an entry re-dated to Tuesday\n * whose clocks stayed on Monday is the same self-contradicting triple T4.9\n * eliminated one field over \u2014 a 09:00\u201310:00 span filed against a day it did not\n * happen on, and a defensible-invoice problem the moment anyone reads the times\n * back. \"Move this entry to Tuesday\" means the whole observation moves.\n *\n * The clocks are SHIFTED by whole days rather than recomposed from the new date,\n * which preserves two things a recomposition would destroy: the stored offset\n * between `date` and the timestamps (for a tenant east of UTC a 01:00 local start\n * legitimately sits on the previous UTC day), and a midnight crossing (an end\n * already on the following day stays on the following day).\n *\n * Two things it never does, both inherited from the reconciliation rule above:\n *\n * - a clock that does not exist is not manufactured, and\n * - a RUNNING timer \u2014 a start with no end \u2014 is left exactly where it is, because\n * re-anchoring its start would restate the elapsed time of work still in\n * progress, and nothing about re-dating the row says the timer began later.\n */\nexport function rebaseTimeEntryIntervalToDate(params: {\n stored: TimeEntryInterval\n fromDate: Date | string | null | undefined\n toDate: Date | string | null | undefined\n}): TimeEntryInterval {\n const fromDate = toDateOrNull(params.fromDate)\n const toDate = toDateOrNull(params.toDate)\n if (!fromDate || !toDate) return params.stored\n\n const { startedAt, endedAt } = params.stored\n if (!startedAt && !endedAt) return params.stored\n if (startedAt && !endedAt) return params.stored\n\n const shiftMs = (utcDayIndex(toDate) - utcDayIndex(fromDate)) * MS_PER_DAY\n if (shiftMs === 0) return params.stored\n return {\n startedAt: startedAt ? new Date(startedAt.getTime() + shiftMs) : null,\n endedAt: endedAt ? new Date(endedAt.getTime() + shiftMs) : null,\n durationMinutes: params.stored.durationMinutes,\n }\n}\n\n/**\n * Reuses the code the tag commands already refuse a frozen entry with, rather\n * than inventing a second one \u2014 a client that learned to handle `time_entry_locked`\n * from `\u2026/tags/entry-assignments` handles it identically everywhere else.\n */\nexport const TIME_ENTRY_LOCKED_CODE = TAG_TARGET_LOCKED_CODE\n\nexport type LockedTimeEntryRef = { id: string; lockedReportId?: string | null }\n\n/**\n * The 409 body names the report that froze the entries so screen 10 note 4 can\n * link straight to it, and lists the offending ids so a grid save can mark those\n * cells read-only instead of showing an opaque failure. `lockedReportId` is kept\n * beside the arrays for shape-compatibility with the tag commands' single-target\n * refusal.\n */\nexport function buildTimeEntryLockedError(\n lockedEntries: LockedTimeEntryRef[],\n translate: Translate,\n): CrudHttpError {\n const lockedEntryIds = lockedEntries.map((entry) => entry.id)\n const lockedReportIds = Array.from(\n new Set(\n lockedEntries\n .map((entry) => entry.lockedReportId)\n .filter((reportId): reportId is string => typeof reportId === 'string' && reportId.length > 0),\n ),\n )\n return new CrudHttpError(409, {\n code: TIME_ENTRY_LOCKED_CODE,\n error:\n lockedEntryIds.length > 1\n ? translate(\n 'staff.time_tracking.entries.errors.entriesLocked',\n 'Some of these time entries are locked in a closed report and cannot be changed. Unlock the report first.',\n )\n : translate(\n 'staff.time_tracking.entries.errors.entryLocked',\n 'This time entry is locked in a closed report and cannot be changed. Unlock the report first.',\n ),\n lockedReportId: lockedReportIds[0] ?? null,\n lockedEntryIds,\n lockedReportIds,\n })\n}\n\n/**\n * The lock gate (risk R3). It sits in the command layer rather than in each write\n * route because the routes are the part that keeps growing: duplicate, copy-day\n * and the grid bulk save all arrived after the lock did, and any one of them\n * could have forgotten a per-route check. Every command that resolves an entry\n * for mutation runs this immediately after the row is loaded, so a field added to\n * the update command later is frozen without anybody remembering to freeze it.\n *\n * No permission lifts it \u2014 not `staff.timesheets.manage_all`. An entry frozen in\n * a closed report has already been billed at those minutes (D-7), so the only way\n * back is the Phase-6 unlock command, which clears `locked_report_id` itself \u2014 or\n * hands it to another closed report still quoting the entry \u2014 and therefore never\n * reaches this gate.\n */\nexport function assertTimeEntryUnlocked(\n entry: { id: string; lockedReportId?: string | null },\n translate: Translate,\n): void {\n if (!entry.lockedReportId) return\n throw buildTimeEntryLockedError([{ id: entry.id, lockedReportId: entry.lockedReportId }], translate)\n}\n\n/**\n * Tags are written through the tag commands, never through the junction table, so\n * an entry write inherits their guarantees unchanged: unknown ids are refused,\n * re-sending a tag is idempotent, an entry frozen in a closed report is refused\n * with 409 `time_entry_locked`, and each change lands in the audit log with its\n * own undo.\n *\n * `tagIds` is a SET, not an append. The assign command answers with the target's\n * full tag list, and that answer is what tells us which tags must then be\n * unassigned \u2014 so the desired state is reached without this file ever reading or\n * writing `staff_time_entry_tags` itself.\n */\nasync function syncEntryTags(\n ctx: CommandRuntimeContext,\n params: { timeEntryId: string; tenantId: string; organizationId: string; tagIds: string[] },\n): Promise<void> {\n const bus = ctx.container.resolve('commandBus') as CommandBus\n const desired = Array.from(new Set(params.tagIds))\n const scoped = {\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n timeEntryId: params.timeEntryId,\n }\n\n const assigned = await bus.execute<unknown, { tagIds?: string[] }>(staffTimeTagCommandIds.assignEntry, {\n input: { ...scoped, tagIds: desired },\n ctx,\n })\n\n const current = assigned?.result?.tagIds ?? desired\n const stale = current.filter((tagId) => !desired.includes(tagId))\n if (stale.length === 0) return\n\n await bus.execute(staffTimeTagCommandIds.unassignEntry, {\n input: { ...scoped, tagIds: stale },\n ctx,\n })\n}\n\ntype TimeEntrySnapshot = {\n id: string\n tenantId: string\n organizationId: string\n staffMemberId: string\n date: string\n durationMinutes: number\n roundedMinutes: number | null\n startedAt: string | null\n endedAt: string | null\n notes: string | null\n timeProjectId: string | null\n taskId: string | null\n customerId: string | null\n dealId: string | null\n orderId: string | null\n isBillable: boolean\n rateOverrideAmount: string | null\n rateCurrencyCode: string | null\n source: string\n deletedAt: string | null\n}\n\ntype TimeEntryUndoPayload = {\n before?: TimeEntrySnapshot | null\n after?: TimeEntrySnapshot | null\n}\n\nasync function loadTimeEntrySnapshot(em: EntityManager, id: string, scope?: StaffSnapshotScope | null): Promise<TimeEntrySnapshot | null> {\n const entry = await findOneWithDecryption(\n em,\n StaffTimeEntry,\n scopedStaffSnapshotWhere(id, scope),\n undefined,\n staffSnapshotDecryptionScope(scope),\n )\n if (!entry) return null\n return {\n id: entry.id,\n tenantId: entry.tenantId,\n organizationId: entry.organizationId,\n staffMemberId: entry.staffMemberId,\n date: entry.date instanceof Date ? entry.date.toISOString().split('T')[0] : String(entry.date),\n durationMinutes: entry.durationMinutes,\n roundedMinutes: entry.roundedMinutes ?? null,\n startedAt: entry.startedAt ? entry.startedAt.toISOString() : null,\n endedAt: entry.endedAt ? entry.endedAt.toISOString() : null,\n notes: entry.notes ?? null,\n timeProjectId: entry.timeProjectId ?? null,\n taskId: entry.taskId ?? null,\n customerId: entry.customerId ?? null,\n dealId: entry.dealId ?? null,\n orderId: entry.orderId ?? null,\n isBillable: entry.isBillable,\n rateOverrideAmount: entry.rateOverrideAmount ?? null,\n rateCurrencyCode: entry.rateCurrencyCode ?? null,\n source: entry.source,\n deletedAt: entry.deletedAt ? entry.deletedAt.toISOString() : null,\n }\n}\n\nfunction timeEntrySeedFromSnapshot(snapshot: TimeEntrySnapshot): Record<string, unknown> {\n return {\n id: snapshot.id,\n tenantId: snapshot.tenantId,\n organizationId: snapshot.organizationId,\n staffMemberId: snapshot.staffMemberId,\n date: snapshot.date,\n durationMinutes: snapshot.durationMinutes,\n roundedMinutes: snapshot.roundedMinutes ?? null,\n startedAt: snapshot.startedAt ? new Date(snapshot.startedAt) : null,\n endedAt: snapshot.endedAt ? new Date(snapshot.endedAt) : null,\n notes: snapshot.notes ?? null,\n timeProjectId: snapshot.timeProjectId ?? null,\n taskId: snapshot.taskId ?? null,\n customerId: snapshot.customerId ?? null,\n dealId: snapshot.dealId ?? null,\n orderId: snapshot.orderId ?? null,\n isBillable: snapshot.isBillable ?? true,\n rateOverrideAmount: snapshot.rateOverrideAmount ?? null,\n rateCurrencyCode: snapshot.rateCurrencyCode ?? null,\n source: (snapshot.source ?? 'manual') as StaffTimeEntrySource,\n createdAt: new Date(),\n updatedAt: new Date(),\n deletedAt: null,\n }\n}\n\nconst createTimeEntryCommand: CommandHandler<StaffTimeEntryCreateInput, { timeEntryId: string }> = {\n id: 'staff.timesheets.time_entries.create',\n async execute(rawInput, ctx) {\n const parsed = staffTimeEntryCreateSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n commandInputScope(ctx, parsed.tenantId, parsed.organizationId)\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n\n // Ownership enforcement: callers without `staff.timesheets.manage_all`\n // can only create entries attributed to themselves. Silent override\n // (mirrors `bulk/route.ts` behavior) so the request body's staffMemberId\n // can't forge an entry under a colleague's identity.\n let effectiveStaffMemberId = parsed.staffMemberId\n if (!(await callerHasManageAll(ctx))) {\n const callerStaffMemberId = await resolveCallerStaffMemberId(em, ctx)\n if (!callerStaffMemberId) {\n const { translate } = await resolveTranslations()\n throw new CrudHttpError(403, {\n error: translate('staff.timesheets.errors.noStaffMember', 'No staff member linked to your account.'),\n })\n }\n effectiveStaffMemberId = callerStaffMemberId\n }\n\n // Validate referenced task and timeProjectId are in-scope before persisting.\n // Without these checks a foreign or stale UUID would produce a dangling reference.\n const task = await requireTaskInScope(em, parsed.taskId ?? null, parsed.tenantId, parsed.organizationId)\n const requestedProjectId = resolveTimeEntryProjectId(parsed.timeProjectId, task)\n const project = await requireTimeProjectInScope(em, requestedProjectId, parsed.tenantId, parsed.organizationId)\n await assertTaskBelongsToProject(task, project?.id ?? null)\n\n const settings = await readEntrySettings(ctx, parsed.tenantId)\n await assertEntryProjectAccess({\n access: await resolveEntryProjectAccess(em, ctx, parsed.tenantId, parsed.organizationId, settings),\n hasManageAll: await callerHasManageAll(ctx),\n projectId: project?.id ?? null,\n entryStaffMemberId: effectiveStaffMemberId,\n })\n\n const now = new Date()\n const entry = em.create(StaffTimeEntry, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n staffMemberId: effectiveStaffMemberId,\n date: parsed.date,\n durationMinutes: parsed.durationMinutes,\n roundedMinutes: roundedMinutesFor(parsed.durationMinutes, settings, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n }),\n startedAt: parsed.startedAt ?? null,\n endedAt: parsed.endedAt ?? null,\n notes: resolveTimeEntryNotesInput(parsed) ?? null,\n timeProjectId: project?.id ?? null,\n taskId: task?.id ?? null,\n customerId: parsed.customerId ?? null,\n dealId: parsed.dealId ?? null,\n orderId: parsed.orderId ?? null,\n isBillable: resolveTimeEntryBillable({\n requested: parsed.isBillable,\n project,\n settings,\n scope: { tenantId: parsed.tenantId, organizationId: parsed.organizationId },\n }),\n rateOverrideAmount: toStoredTimeEntryRateOverride(parsed.rateOverrideAmount),\n // Snapshotted rather than joined at read time so a later project currency\n // change cannot re-denominate money that has already been reported (D-3).\n rateCurrencyCode: project?.currencyCode ?? null,\n source: parsed.source ?? 'manual',\n createdAt: now,\n updatedAt: now,\n deletedAt: null,\n })\n em.persist(entry)\n await em.flush()\n\n if (parsed.tagIds !== undefined) {\n await syncEntryTags(ctx, {\n timeEntryId: entry.id,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n tagIds: parsed.tagIds,\n })\n }\n\n await emitCrudSideEffects({\n dataEngine: ctx.container.resolve('dataEngine'),\n action: 'created',\n entity: entry,\n identifiers: { id: entry.id, organizationId: entry.organizationId, tenantId: entry.tenantId },\n events: staffTimeEntryCrudEvents,\n indexer: timeEntryCrudIndexer,\n })\n\n return { timeEntryId: entry.id }\n },\n captureAfter: async (_input, result, ctx) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const snapshot = await loadTimeEntrySnapshot(em, result.timeEntryId, staffSnapshotScopeFromContext(ctx))\n if (!snapshot) return null\n return { snapshot }\n },\n buildLog: async ({ result, ctx }) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const snapshot = await loadTimeEntrySnapshot(em, result.timeEntryId, staffSnapshotScopeFromContext(ctx))\n if (!snapshot) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('staff.audit.timesheets.time_entries.create', 'Create time entry'),\n resourceKind: 'staff.timesheets.time_entry',\n resourceId: snapshot.id,\n tenantId: snapshot.tenantId,\n organizationId: snapshot.organizationId,\n snapshotAfter: snapshot,\n payload: {\n undo: {\n after: snapshot,\n } satisfies TimeEntryUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<TimeEntryUndoPayload>(logEntry)\n const after = payload?.after\n if (!after) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const entry = await em.findOne(StaffTimeEntry, scopedStaffSnapshotWhere(after.id, staffSnapshotScopeFromSnapshot(after)))\n if (entry) {\n entry.deletedAt = new Date()\n await em.flush()\n\n await emitCrudUndoSideEffects({\n dataEngine: ctx.container.resolve('dataEngine'),\n action: 'deleted',\n entity: entry,\n identifiers: { id: entry.id, organizationId: entry.organizationId, tenantId: entry.tenantId },\n events: staffTimeEntryCrudEvents,\n indexer: timeEntryCrudIndexer,\n })\n }\n },\n redo: makeCreateRedo<StaffTimeEntry, TimeEntrySnapshot, StaffTimeEntryCreateInput, { timeEntryId: string }>({\n entityClass: StaffTimeEntry,\n getSnapshotId: (snapshot) => snapshot.id,\n seedFromSnapshot: timeEntrySeedFromSnapshot,\n buildResult: (entity) => ({ timeEntryId: entity.id }),\n events: staffTimeEntryCrudEvents,\n indexer: timeEntryCrudIndexer,\n }),\n}\n\nconst startTimerCommand: CommandHandler<StaffTimeEntryStartTimerInput, { timeEntryId: string }> = {\n id: 'staff.timesheets.time_entries.start_timer',\n async execute(rawInput, ctx) {\n const parsed = staffTimeEntryStartTimerSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n commandInputScope(ctx, parsed.tenantId, parsed.organizationId)\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n\n // Ownership enforcement mirrors createTimeEntryCommand: callers without\n // `staff.timesheets.manage_all` can only start their own timer, so the\n // request body's staffMemberId can't start a timer under a colleague's id.\n let effectiveStaffMemberId = parsed.staffMemberId\n if (!(await callerHasManageAll(ctx))) {\n const callerStaffMemberId = await resolveCallerStaffMemberId(em, ctx)\n if (!callerStaffMemberId) {\n const { translate } = await resolveTranslations()\n throw new CrudHttpError(403, {\n error: translate('staff.timesheets.errors.noStaffMember', 'No staff member linked to your account.'),\n })\n }\n effectiveStaffMemberId = callerStaffMemberId\n }\n\n // Starting a timer on a task is ONE write. The task is validated here, in the\n // same command, so the entry is created already carrying it \u2014 the alternative\n // (start, then link) can fail after the timer is running and strand it against\n // the project instead of the task.\n const task = await requireTaskInScope(em, parsed.taskId ?? null, parsed.tenantId, parsed.organizationId)\n // A card knows its task, not always its project, and a task belongs to exactly\n // one project \u2014 so the project follows from the task, exactly as it does when\n // an entry is created from screen 8.\n const requestedProjectId = resolveTimeEntryProjectId(parsed.timeProjectId, task)\n const project = await requireTimeProjectInScope(em, requestedProjectId, parsed.tenantId, parsed.organizationId)\n await assertTaskBelongsToProject(task, project?.id ?? null)\n\n // A timer entry starts at zero minutes, and D-7 makes `rounded_minutes` the\n // only input to cost \u2014 leaving it null here would price the entry at nothing\n // until something else happened to rewrite it. Seeded from the same rule as\n // every other create so timer-stop only ever restates it.\n const settings = await readEntrySettings(ctx, parsed.tenantId)\n\n const scopeCtx = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }\n\n // Create the timer entry AND start it inside a single transaction so a\n // partial failure can never leave an orphaned, never-started timer entry\n // (issue #3311 \u2014 the legacy two-request create-then-start flow). The\n // single-active-timer invariant (#2855) is re-checked here so a second\n // surface cannot create a parallel running timer for the same staff member.\n const { entry, startedAt } = await em.transactional(async (trx) => {\n const otherRunningEntry = await findOneWithDecryption(\n trx,\n StaffTimeEntry,\n {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n staffMemberId: effectiveStaffMemberId,\n startedAt: { $ne: null },\n endedAt: null,\n deletedAt: null,\n },\n {},\n scopeCtx,\n )\n if (otherRunningEntry) {\n const { translate } = await resolveTranslations()\n throw new CrudHttpError(409, {\n error: translate(\n 'staff.timesheets.errors.timerAlreadyRunning',\n 'Another timer is already running. Stop it before starting a new one.',\n ),\n })\n }\n\n const startedAt = new Date()\n const entry = trx.create(StaffTimeEntry, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n staffMemberId: effectiveStaffMemberId,\n date: parsed.date,\n durationMinutes: 0,\n roundedMinutes: roundedMinutesFor(0, settings, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n }),\n startedAt,\n endedAt: null,\n notes: parsed.notes ?? null,\n timeProjectId: project?.id ?? null,\n taskId: task?.id ?? null,\n customerId: null,\n dealId: null,\n orderId: null,\n source: 'timer',\n createdAt: startedAt,\n updatedAt: startedAt,\n deletedAt: null,\n })\n // Flush so the DB-generated id is populated before the work segment\n // references it; both writes commit together when the transaction closes.\n await trx.flush()\n\n const segmentData = {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n timeEntryId: entry.id,\n startedAt,\n segmentType: 'work' as const,\n }\n trx.create(StaffTimeEntrySegment, segmentData as never)\n await trx.flush()\n\n return { entry, startedAt }\n })\n\n await emitCrudSideEffects({\n dataEngine: ctx.container.resolve('dataEngine'),\n action: 'created',\n entity: entry,\n identifiers: { id: entry.id, organizationId: entry.organizationId, tenantId: entry.tenantId },\n events: staffTimeEntryCrudEvents,\n indexer: timeEntryCrudIndexer,\n })\n\n void emitStaffEvent('staff.timesheets.time_entry.timer_started', {\n id: entry.id,\n staffMemberId: effectiveStaffMemberId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n startedAt: startedAt.toISOString(),\n }, { persistent: true }).catch((err) => {\n logger.error('staff.timesheets emit timer_started failed', { err })\n })\n\n return { timeEntryId: entry.id }\n },\n captureAfter: async (_input, result, ctx) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const snapshot = await loadTimeEntrySnapshot(em, result.timeEntryId, staffSnapshotScopeFromContext(ctx))\n if (!snapshot) return null\n return { snapshot }\n },\n buildLog: async ({ result, ctx }) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const snapshot = await loadTimeEntrySnapshot(em, result.timeEntryId, staffSnapshotScopeFromContext(ctx))\n if (!snapshot) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('staff.audit.timesheets.time_entries.startTimer', 'Start timer'),\n resourceKind: 'staff.timesheets.time_entry',\n resourceId: snapshot.id,\n tenantId: snapshot.tenantId,\n organizationId: snapshot.organizationId,\n snapshotAfter: snapshot,\n payload: {\n undo: {\n after: snapshot,\n } satisfies TimeEntryUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<TimeEntryUndoPayload>(logEntry)\n const after = payload?.after\n if (!after) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const entry = await em.findOne(StaffTimeEntry, scopedStaffSnapshotWhere(after.id, staffSnapshotScopeFromSnapshot(after)))\n if (entry) {\n entry.deletedAt = new Date()\n await em.flush()\n\n await emitCrudUndoSideEffects({\n dataEngine: ctx.container.resolve('dataEngine'),\n action: 'deleted',\n entity: entry,\n identifiers: { id: entry.id, organizationId: entry.organizationId, tenantId: entry.tenantId },\n events: staffTimeEntryCrudEvents,\n indexer: timeEntryCrudIndexer,\n })\n }\n },\n}\n\nconst updateTimeEntryCommand: CommandHandler<StaffTimeEntryUpdateInput, { timeEntryId: string }> = {\n id: 'staff.timesheets.time_entries.update',\n async prepare(rawInput, ctx) {\n const parsed = staffTimeEntryUpdateSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager)\n const snapshot = await loadTimeEntrySnapshot(em, parsed.id, staffSnapshotScopeFromContext(ctx))\n if (!snapshot) return {}\n return { before: snapshot }\n },\n async execute(rawInput, ctx) {\n const parsed = staffTimeEntryUpdateSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const scope = commandActorScope(ctx)\n const entry = await findOneWithDecryption(\n em,\n StaffTimeEntry,\n applyScopeToWhere<StaffTimeEntry>({ id: parsed.id, deletedAt: null }, scope),\n undefined,\n scopeForDecryption(scope),\n )\n if (!entry) throw new CrudHttpError(404, { error: 'Time entry not found.' })\n ensureTenantScope(ctx, entry.tenantId)\n ensureOrganizationScope(ctx, entry.organizationId)\n\n // Before ownership, before access, before a single field is read off the\n // request: a frozen entry is frozen for everyone, so nothing below can be\n // reached with a lock in place.\n assertTimeEntryUnlocked(entry, (await resolveTranslations()).translate)\n\n // Ownership enforcement: callers without `staff.timesheets.manage_all`\n // can only update entries they own.\n const hasManageAll = await callerHasManageAll(ctx)\n if (!hasManageAll) {\n const callerStaffMemberId = await resolveCallerStaffMemberId(em, ctx)\n if (!callerStaffMemberId || entry.staffMemberId !== callerStaffMemberId) {\n const { translate } = await resolveTranslations()\n throw new CrudHttpError(403, {\n error: translate('staff.timesheets.errors.notOwner', 'You can only manage your own time entries.'),\n })\n }\n }\n\n const task =\n parsed.taskId !== undefined\n ? await requireTaskInScope(em, parsed.taskId, entry.tenantId, entry.organizationId)\n : null\n\n // A task carries its project (screen 8), so re-pointing an entry at a task\n // moves the entry with it unless the request names a project explicitly.\n const nextProjectId =\n parsed.timeProjectId !== undefined\n ? parsed.timeProjectId ?? null\n : task?.timeProjectId ?? entry.timeProjectId ?? null\n const projectChanged = nextProjectId !== (entry.timeProjectId ?? null)\n const project = projectChanged\n ? await requireTimeProjectInScope(em, nextProjectId, entry.tenantId, entry.organizationId)\n : null\n await assertTaskBelongsToProject(task, nextProjectId)\n\n const settings = await readEntrySettings(ctx, entry.tenantId)\n const access = await resolveEntryProjectAccess(em, ctx, entry.tenantId, entry.organizationId, settings)\n // Both ends of a move are checked: a caller may not lift an entry out of a\n // project they cannot see, nor drop one into a project they cannot see.\n await assertEntryProjectAccess({\n access,\n hasManageAll,\n projectId: entry.timeProjectId ?? null,\n entryStaffMemberId: entry.staffMemberId,\n })\n if (projectChanged) {\n await assertEntryProjectAccess({\n access,\n hasManageAll,\n projectId: nextProjectId,\n entryStaffMemberId: entry.staffMemberId,\n })\n }\n\n const notes = resolveTimeEntryNotesInput(parsed)\n\n // A `date` move re-anchors the STORED clocks first, so what reconciliation\n // reasons about is the entry as it will exist on its new day. Order matters:\n // clocks the request sends explicitly still win over the rebased ones (the\n // entry dialog rebuilds all three from the new date and would otherwise be\n // shifted twice), while clocks it omits travel with the entry instead of\n // being stranded on the old day.\n const stored = rebaseTimeEntryIntervalToDate({\n stored: {\n startedAt: entry.startedAt ?? null,\n endedAt: entry.endedAt ?? null,\n durationMinutes: entry.durationMinutes,\n },\n fromDate: entry.date,\n toDate: parsed.date,\n })\n\n // The three time fields are settled together, before anything is assigned, so\n // no combination of them can be stored contradicting itself (US-D3, D-8).\n const interval = reconcileTimeEntryInterval({\n stored,\n startedAt: parsed.startedAt,\n endedAt: parsed.endedAt,\n durationMinutes: parsed.durationMinutes,\n })\n\n if (parsed.date !== undefined) entry.date = parsed.date\n if (interval.startedAt !== (entry.startedAt ?? null)) entry.startedAt = interval.startedAt\n if (interval.endedAt !== (entry.endedAt ?? null)) entry.endedAt = interval.endedAt\n if (parsed.durationMinutes !== undefined || interval.durationMinutes !== entry.durationMinutes) {\n entry.durationMinutes = interval.durationMinutes\n // Raw minutes moved \u2014 because the duration was written, or because the\n // clocks were edited and restated it \u2014 so the frozen rounded value is\n // recomputed from the tenant's current rule. D-7 makes it the only input to\n // cost, so a clock edit that changes the length of the work and leaves the\n // rounded twin behind would mis-bill it.\n entry.roundedMinutes = roundedMinutesFor(interval.durationMinutes, settings, {\n tenantId: entry.tenantId,\n organizationId: entry.organizationId,\n })\n }\n if (projectChanged) {\n entry.timeProjectId = nextProjectId\n entry.rateCurrencyCode = project?.currencyCode ?? null\n }\n if (parsed.taskId !== undefined) entry.taskId = task?.id ?? null\n if (parsed.customerId !== undefined) entry.customerId = parsed.customerId ?? null\n if (parsed.dealId !== undefined) entry.dealId = parsed.dealId ?? null\n if (parsed.orderId !== undefined) entry.orderId = parsed.orderId ?? null\n if (parsed.isBillable !== undefined) entry.isBillable = parsed.isBillable\n if (parsed.rateOverrideAmount !== undefined) {\n entry.rateOverrideAmount = toStoredTimeEntryRateOverride(parsed.rateOverrideAmount)\n }\n if (notes !== undefined) entry.notes = notes\n entry.updatedAt = new Date()\n await em.flush()\n\n if (parsed.tagIds !== undefined) {\n await syncEntryTags(ctx, {\n timeEntryId: entry.id,\n tenantId: entry.tenantId,\n organizationId: entry.organizationId,\n tagIds: parsed.tagIds,\n })\n }\n\n await emitCrudSideEffects({\n dataEngine: ctx.container.resolve('dataEngine'),\n action: 'updated',\n entity: entry,\n identifiers: { id: entry.id, organizationId: entry.organizationId, tenantId: entry.tenantId },\n events: staffTimeEntryCrudEvents,\n indexer: timeEntryCrudIndexer,\n })\n\n return { timeEntryId: entry.id }\n },\n buildLog: async ({ snapshots, ctx }) => {\n const before = snapshots.before as TimeEntrySnapshot | undefined\n if (!before) return null\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const after = await loadTimeEntrySnapshot(em, before.id, staffSnapshotScopeFromSnapshot(before))\n if (!after) return null\n const changes = buildChanges(before as unknown as Record<string, unknown>, after as unknown as Record<string, unknown>, [\n 'date',\n 'durationMinutes',\n 'roundedMinutes',\n // A time correction is exactly the edit somebody will later have to justify\n // on an invoice, so the clocks are named in the trail beside the minutes.\n 'startedAt',\n 'endedAt',\n 'timeProjectId',\n 'taskId',\n 'customerId',\n 'dealId',\n 'orderId',\n 'isBillable',\n 'rateOverrideAmount',\n 'rateCurrencyCode',\n 'notes',\n 'deletedAt',\n ])\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('staff.audit.timesheets.time_entries.update', 'Update time entry'),\n resourceKind: 'staff.timesheets.time_entry',\n resourceId: before.id,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n snapshotBefore: before,\n snapshotAfter: after,\n changes,\n payload: {\n undo: {\n before,\n after,\n } satisfies TimeEntryUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<TimeEntryUndoPayload>(logEntry)\n const before = payload?.before\n if (!before) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const entry = await em.findOne(StaffTimeEntry, scopedStaffSnapshotWhere(before.id, staffSnapshotScopeFromSnapshot(before)))\n if (!entry) return\n entry.date = before.date as unknown as Date\n entry.durationMinutes = before.durationMinutes\n entry.roundedMinutes = before.roundedMinutes ?? null\n // Restored beside the minutes they belong to: now that an update can move the\n // clocks, an undo that left them where the edit put them would hand back an\n // entry whose duration and span disagree.\n entry.startedAt = before.startedAt ? new Date(before.startedAt) : null\n entry.endedAt = before.endedAt ? new Date(before.endedAt) : null\n entry.timeProjectId = before.timeProjectId ?? null\n entry.taskId = before.taskId ?? null\n entry.customerId = before.customerId ?? null\n entry.dealId = before.dealId ?? null\n entry.orderId = before.orderId ?? null\n entry.isBillable = before.isBillable ?? true\n entry.rateOverrideAmount = before.rateOverrideAmount ?? null\n entry.rateCurrencyCode = before.rateCurrencyCode ?? null\n entry.notes = before.notes ?? null\n entry.deletedAt = before.deletedAt ? new Date(before.deletedAt) : null\n entry.updatedAt = new Date()\n await em.flush()\n\n await emitCrudUndoSideEffects({\n dataEngine: ctx.container.resolve('dataEngine'),\n action: 'updated',\n entity: entry,\n identifiers: { id: entry.id, organizationId: entry.organizationId, tenantId: entry.tenantId },\n events: staffTimeEntryCrudEvents,\n indexer: timeEntryCrudIndexer,\n })\n },\n}\n\nconst deleteTimeEntryCommand: CommandHandler<{ id?: string }, { timeEntryId: string }> = {\n id: 'staff.timesheets.time_entries.delete',\n async prepare(input, ctx) {\n const id = input?.id\n if (!id) throw new CrudHttpError(400, { error: 'Time entry id is required.' })\n const em = (ctx.container.resolve('em') as EntityManager)\n const snapshot = await loadTimeEntrySnapshot(em, id, staffSnapshotScopeFromContext(ctx))\n if (!snapshot) return {}\n return { before: snapshot }\n },\n async execute(input, ctx) {\n const id = input?.id\n if (!id) throw new CrudHttpError(400, { error: 'Time entry id is required.' })\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const scope = commandActorScope(ctx)\n const entry = await findOneWithDecryption(\n em,\n StaffTimeEntry,\n applyScopeToWhere<StaffTimeEntry>({ id, deletedAt: null }, scope),\n undefined,\n scopeForDecryption(scope),\n )\n if (!entry) throw new CrudHttpError(404, { error: 'Time entry not found.' })\n ensureTenantScope(ctx, entry.tenantId)\n ensureOrganizationScope(ctx, entry.organizationId)\n\n // A closed report renders these minutes; deleting the row would leave the\n // report totalling hours that no longer exist.\n assertTimeEntryUnlocked(entry, (await resolveTranslations()).translate)\n\n // Ownership enforcement: callers without `staff.timesheets.manage_all`\n // can only delete entries they own.\n const hasManageAll = await callerHasManageAll(ctx)\n if (!hasManageAll) {\n const callerStaffMemberId = await resolveCallerStaffMemberId(em, ctx)\n if (!callerStaffMemberId || entry.staffMemberId !== callerStaffMemberId) {\n const { translate } = await resolveTranslations()\n throw new CrudHttpError(403, {\n error: translate('staff.timesheets.errors.notOwner', 'You can only manage your own time entries.'),\n })\n }\n }\n\n const settings = await readEntrySettings(ctx, entry.tenantId)\n await assertEntryProjectAccess({\n access: await resolveEntryProjectAccess(em, ctx, entry.tenantId, entry.organizationId, settings),\n hasManageAll,\n projectId: entry.timeProjectId ?? null,\n entryStaffMemberId: entry.staffMemberId,\n })\n\n entry.deletedAt = new Date()\n entry.updatedAt = new Date()\n await em.flush()\n\n await emitCrudSideEffects({\n dataEngine: ctx.container.resolve('dataEngine'),\n action: 'deleted',\n entity: entry,\n identifiers: { id: entry.id, organizationId: entry.organizationId, tenantId: entry.tenantId },\n events: staffTimeEntryCrudEvents,\n indexer: timeEntryCrudIndexer,\n })\n\n return { timeEntryId: entry.id }\n },\n buildLog: async ({ snapshots }) => {\n const before = snapshots.before as TimeEntrySnapshot | undefined\n if (!before) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('staff.audit.timesheets.time_entries.delete', 'Delete time entry'),\n resourceKind: 'staff.timesheets.time_entry',\n resourceId: before.id,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n snapshotBefore: before,\n payload: {\n undo: {\n before,\n } satisfies TimeEntryUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<TimeEntryUndoPayload>(logEntry)\n const before = payload?.before\n if (!before) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n let entry = await em.findOne(StaffTimeEntry, scopedStaffSnapshotWhere(before.id, staffSnapshotScopeFromSnapshot(before)))\n if (!entry) {\n entry = em.create(StaffTimeEntry, {\n id: before.id,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n staffMemberId: before.staffMemberId,\n date: before.date as unknown as Date,\n durationMinutes: before.durationMinutes,\n roundedMinutes: before.roundedMinutes ?? null,\n startedAt: before.startedAt ? new Date(before.startedAt) : null,\n endedAt: before.endedAt ? new Date(before.endedAt) : null,\n notes: before.notes ?? null,\n timeProjectId: before.timeProjectId ?? null,\n taskId: before.taskId ?? null,\n customerId: before.customerId ?? null,\n dealId: before.dealId ?? null,\n orderId: before.orderId ?? null,\n isBillable: before.isBillable ?? true,\n rateOverrideAmount: before.rateOverrideAmount ?? null,\n rateCurrencyCode: before.rateCurrencyCode ?? null,\n source: (before.source ?? 'manual') as StaffTimeEntrySource,\n deletedAt: null,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(entry)\n } else {\n entry.staffMemberId = before.staffMemberId\n entry.date = before.date as unknown as Date\n entry.durationMinutes = before.durationMinutes\n entry.roundedMinutes = before.roundedMinutes ?? null\n entry.startedAt = before.startedAt ? new Date(before.startedAt) : null\n entry.endedAt = before.endedAt ? new Date(before.endedAt) : null\n entry.notes = before.notes ?? null\n entry.timeProjectId = before.timeProjectId ?? null\n entry.taskId = before.taskId ?? null\n entry.customerId = before.customerId ?? null\n entry.dealId = before.dealId ?? null\n entry.orderId = before.orderId ?? null\n entry.isBillable = before.isBillable ?? true\n entry.rateOverrideAmount = before.rateOverrideAmount ?? null\n entry.rateCurrencyCode = before.rateCurrencyCode ?? null\n entry.source = (before.source ?? 'manual') as StaffTimeEntrySource\n entry.deletedAt = null\n entry.updatedAt = new Date()\n }\n await em.flush()\n\n await emitCrudUndoSideEffects({\n dataEngine: ctx.container.resolve('dataEngine'),\n action: 'created',\n entity: entry,\n identifiers: { id: entry.id, organizationId: entry.organizationId, tenantId: entry.tenantId },\n events: staffTimeEntryCrudEvents,\n indexer: timeEntryCrudIndexer,\n })\n },\n}\n\nexport const staffTimeEntryDuplicateSchema = z.object({\n id: z.string().uuid(),\n // The copy usually lands on another day (US-D6 \"log the same thing again\"), so\n // the caller may move it; omitted, it keeps the source's day.\n date: z.coerce.date().optional(),\n // US-D6 ends \"\u2026I adjust the duration and the date\", so both adjustments travel\n // with the copy request rather than costing a second round trip that would\n // leave the unadjusted duration visible in between. Omitted, the copy keeps the\n // source's minutes. The rounded twin is always recomputed by the create command\n // from the tenant rule, so an overridden duration can never carry the source's\n // stale `rounded_minutes` into a new entry (D-7).\n durationMinutes: z.number().int().min(0).max(1440).optional(),\n // Tags are NOT read off the source here: this file writes tags only through the\n // tag commands and never touches `staff_time_entry_tags`, so the caller \u2014 which\n // already has the source's `tags[]` from the list response \u2014 passes them on.\n tagIds: z.array(z.string().uuid()).max(50).optional(),\n})\n\nexport type StaffTimeEntryDuplicateInput = z.infer<typeof staffTimeEntryDuplicateSchema>\n\n/**\n * US-D6. The fourth write path TC-TT-018 exercises. It resolves the SOURCE entry\n * and runs it past the lock gate before copying: screen 10 note 4 hides every row\n * action on a locked row, so an API that happily duplicated one would be a side\n * door back into a frozen report's numbers.\n *\n * The copy itself is delegated to the create command rather than re-implemented,\n * so it inherits ownership enforcement, project access, the billable/currency\n * defaults, rounding (D-7), tag syncing and the audit entry unchanged.\n */\nconst duplicateTimeEntryCommand: CommandHandler<StaffTimeEntryDuplicateInput, { timeEntryId: string }> = {\n id: 'staff.timesheets.time_entries.duplicate',\n async execute(rawInput, ctx) {\n const parsed = staffTimeEntryDuplicateSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const scope = commandActorScope(ctx)\n const source = await findOneWithDecryption(\n em,\n StaffTimeEntry,\n applyScopeToWhere<StaffTimeEntry>({ id: parsed.id, deletedAt: null }, scope),\n undefined,\n scopeForDecryption(scope),\n )\n if (!source) throw new CrudHttpError(404, { error: 'Time entry not found.' })\n ensureTenantScope(ctx, source.tenantId)\n ensureOrganizationScope(ctx, source.organizationId)\n\n assertTimeEntryUnlocked(source, (await resolveTranslations()).translate)\n\n const bus = ctx.container.resolve('commandBus') as CommandBus\n const created = await bus.execute<unknown, { timeEntryId: string }>(createTimeEntryCommand.id, {\n input: {\n tenantId: source.tenantId,\n organizationId: source.organizationId,\n staffMemberId: source.staffMemberId,\n date: parsed.date ?? source.date,\n durationMinutes: parsed.durationMinutes ?? source.durationMinutes,\n timeProjectId: source.timeProjectId ?? null,\n taskId: source.taskId ?? null,\n customerId: source.customerId ?? null,\n dealId: source.dealId ?? null,\n orderId: source.orderId ?? null,\n isBillable: source.isBillable,\n rateOverrideAmount:\n source.rateOverrideAmount === null || source.rateOverrideAmount === undefined\n ? null\n : Number(source.rateOverrideAmount),\n description: source.notes ?? null,\n // A copy is typed, not timed: carrying `started_at`/`ended_at` over would\n // manufacture an overlap with the entry it was copied from (US-D7).\n source: 'manual' as StaffTimeEntrySource,\n ...(parsed.tagIds !== undefined ? { tagIds: parsed.tagIds } : {}),\n },\n ctx,\n })\n\n const timeEntryId = created?.result?.timeEntryId\n if (!timeEntryId) throw new CrudHttpError(400, { error: 'Failed to duplicate time entry.' })\n return { timeEntryId }\n },\n}\n\nexport const staffTimeEntryReapplyRoundingSchema = z.object({\n tenantId: z.string().uuid(),\n organizationId: z.string().uuid(),\n entryIds: z.array(z.string().uuid()).min(1).max(500),\n})\n\nexport type StaffTimeEntryReapplyRoundingInput = z.infer<typeof staffTimeEntryReapplyRoundingSchema>\n\nexport type StaffTimeEntryReapplyRoundingResult = {\n /** Entries whose `rounded_minutes` actually moved. */\n updatedCount: number\n /** Entries already at the current rule \u2014 read, compared, left alone. */\n unchangedCount: number\n /** Requested ids that were locked, deleted or out of scope, and therefore never loaded. */\n skippedCount: number\n}\n\n/**\n * Retroactive rounding (T7.3, screen 16 note 3) \u2014 restates `rounded_minutes` on a\n * batch of existing entries under the tenant's current rule.\n *\n * Three properties, in the order they matter:\n *\n * 1. **A locked entry is unreachable, not merely skipped.** `lockedReportId: null`\n * is part of the WHERE clause, so an entry frozen into a closed report is never\n * loaded, never compared and never assigned. The guarantee is structural: there\n * is no branch in this handler that could write one.\n * 2. **It is an explicit, batched administrative action**, driven by the\n * `reapply-rounding` ProgressJob worker, never a side effect of saving the\n * settings form. Rewriting billing history silently is exactly the failure the\n * spec's risk R1 is about.\n * 3. **It writes one derived column and nothing else.** No clock is re-anchored,\n * no duration is touched \u2014 `duration_minutes` remains the raw observation.\n *\n * Reachable only by a privileged actor: the queue worker (`systemActor`) or a\n * super-admin. An interactive caller cannot rewrite a tenant's history through the\n * command bus by guessing the id; the route that enqueues the job is the gate, and\n * it is gated on `staff.timesheets.settings.manage`.\n */\nconst reapplyRoundingCommand: CommandHandler<\n StaffTimeEntryReapplyRoundingInput,\n StaffTimeEntryReapplyRoundingResult\n> = {\n id: 'staff.timesheets.time_entries.reapply_rounding',\n async execute(input, ctx) {\n const parsed = staffTimeEntryReapplyRoundingSchema.parse(input ?? {})\n if (ctx.systemActor !== true && ctx.auth?.isSuperAdmin !== true) {\n const { translate } = await resolveTranslations()\n throw new CrudHttpError(403, { error: translate('staff.errors.forbidden', 'Forbidden') })\n }\n\n const scope = commandInputScope(ctx, parsed.tenantId, parsed.organizationId)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const settings = await readEntrySettings(ctx, parsed.tenantId)\n\n const entries = await em.find(\n StaffTimeEntry,\n applyScopeToWhere<StaffTimeEntry>(\n {\n id: { $in: parsed.entryIds },\n deletedAt: null,\n // Non-negotiable: a frozen entry has already been billed at the value it\n // carries, so it is excluded at the query, not at an `if`.\n lockedReportId: null,\n },\n scope,\n ),\n )\n\n const changed: StaffTimeEntry[] = []\n let unchangedCount = 0\n for (const entry of entries) {\n const next = roundedMinutesFor(entry.durationMinutes, settings, {\n tenantId: entry.tenantId,\n organizationId: entry.organizationId,\n })\n if (entry.roundedMinutes === next) {\n unchangedCount += 1\n continue\n }\n entry.roundedMinutes = next\n entry.updatedAt = new Date()\n changed.push(entry)\n }\n\n if (changed.length > 0) await em.flush()\n\n const dataEngine = ctx.container.resolve('dataEngine') as Parameters<\n typeof emitCrudSideEffects\n >[0]['dataEngine']\n for (const entry of changed) {\n await emitCrudSideEffects({\n dataEngine,\n action: 'updated',\n entity: entry,\n identifiers: { id: entry.id, organizationId: entry.organizationId, tenantId: entry.tenantId },\n events: staffTimeEntryCrudEvents,\n indexer: timeEntryCrudIndexer,\n })\n }\n\n return {\n updatedCount: changed.length,\n unchangedCount,\n skippedCount: Math.max(0, parsed.entryIds.length - entries.length),\n }\n },\n}\n\nregisterCommand(createTimeEntryCommand)\nregisterCommand(startTimerCommand)\nregisterCommand(updateTimeEntryCommand)\nregisterCommand(deleteTimeEntryCommand)\nregisterCommand(duplicateTimeEntryCommand)\nregisterCommand(reapplyRoundingCommand)\n\nexport const staffTimeEntryCommandIds = {\n create: createTimeEntryCommand.id,\n startTimer: startTimerCommand.id,\n update: updateTimeEntryCommand.id,\n delete: deleteTimeEntryCommand.id,\n duplicate: duplicateTimeEntryCommand.id,\n reapplyRounding: reapplyRoundingCommand.id,\n} as const\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { z } from 'zod'\nimport type { CommandBus, CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { buildChanges, emitCrudSideEffects, emitCrudUndoSideEffects } from '@open-mercato/shared/lib/commands/helpers'\nimport { makeCreateRedo } from '@open-mercato/shared/lib/commands/redo'\nimport type { CrudIndexerConfig } from '@open-mercato/shared/lib/crud/types'\nimport type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'\nimport {\n StaffTimeEntry,\n StaffTimeEntrySegment,\n StaffTimeProject,\n StaffTimeTask,\n type StaffTimeEntrySource,\n} from '../data/entities'\nimport { emitStaffEvent } from '../events'\nimport { deriveInterval, formatClock, parseClock } from '../lib/time-tracking/interval'\nimport { roundMinutes } from '../lib/time-tracking/rounding'\nimport { resolveBillability } from '../lib/time-tracking/billability'\nimport type { ScopedResolverContext } from '../lib/time-tracking/registries/scope'\nimport {\n DEFAULT_TIME_TRACKING_SETTINGS,\n readTimeTrackingSettings,\n type TimeTrackingSettings,\n} from '../lib/time-tracking/settings'\nimport { resolveProjectAccess, type ProjectAccess } from '../lib/time-tracking/access'\n// Imported for the command ids AND for the registration side effect: the tag\n// commands must be in the registry before this file asks the bus to run one.\nimport { staffTimeTagCommandIds, TAG_TARGET_LOCKED_CODE } from './timesheets-tags'\n\nconst timeEntryCrudIndexer: CrudIndexerConfig<StaffTimeEntry> = {\n entityType: 'staff:staff_time_entry',\n}\nimport {\n staffTimeEntryCreateSchema,\n staffTimeEntryStartTimerSchema,\n staffTimeEntryUpdateSchema,\n type StaffTimeEntryCreateInput,\n type StaffTimeEntryStartTimerInput,\n type StaffTimeEntryUpdateInput,\n} from '../data/validators'\nimport { staffTimeEntryCrudEvents } from '../lib/crud'\nimport {\n applyScopeToWhere,\n commandActorScope,\n commandInputScope,\n ensureOrganizationScope,\n ensureTenantScope,\n extractUndoPayload,\n scopeForDecryption,\n scopedStaffSnapshotWhere,\n staffSnapshotDecryptionScope,\n staffSnapshotScopeFromContext,\n staffSnapshotScopeFromSnapshot,\n type StaffSnapshotScope,\n} from './shared'\nimport { getStaffMemberByUserId } from '../lib/staffMemberResolver'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('staff')\n\ntype ContainerLike = { resolve: (token: string) => unknown }\n\ntype Translate = (key: string, fallback?: string) => string\n\ntype RbacServiceLike = {\n userHasAllFeatures: (\n userId: string,\n required: string[],\n scope: { tenantId: string | null; organizationId: string | null },\n ) => Promise<boolean>\n getGrantedFeatures?: (\n userId: string,\n scope: { tenantId: string | null; organizationId: string | null },\n ) => Promise<string[]>\n}\n\n/**\n * Returns true when the caller holds `staff.timesheets.manage_all`, honoring\n * wildcard ACL grants (`staff.*`, `*`) and the super-admin flag via the cached\n * rbacService. Returns false when no auth context (e.g. system/CLI ctx) so\n * write paths that lack a caller identity are NOT silently elevated.\n */\nasync function callerHasManageAll(ctx: {\n auth?: { sub?: string | null; tenantId?: string | null; orgId?: string | null } | null\n container: { resolve: (token: string) => unknown }\n}): Promise<boolean> {\n const userId = ctx.auth?.sub\n if (!userId) return false\n try {\n const rbac = ctx.container.resolve('rbacService') as RbacServiceLike | undefined\n if (!rbac?.userHasAllFeatures) return false\n return await rbac.userHasAllFeatures(\n userId,\n ['staff.timesheets.manage_all'],\n { tenantId: ctx.auth?.tenantId ?? null, organizationId: ctx.auth?.orgId ?? null },\n )\n } catch {\n return false\n }\n}\n\nasync function resolveCallerStaffMemberId(\n em: EntityManager,\n ctx: { auth?: { sub?: string | null; tenantId?: string | null; orgId?: string | null } | null },\n): Promise<string | null> {\n const userId = ctx.auth?.sub\n if (!userId) return null\n const member = await getStaffMemberByUserId(\n em,\n userId,\n ctx.auth?.tenantId ?? null,\n ctx.auth?.orgId ?? null,\n )\n return member?.id ?? null\n}\n\n/**\n * Verifies the referenced time project exists and is in-scope (same tenant + org,\n * not soft-deleted) and hands the row back \u2014 the write path reads its\n * `billableByDefault` and `currencyCode` to seed the entry. Throws 422 if the ID\n * is provided but unresolvable. Returns null when projectId is null/undefined\n * (timeProjectId is optional on entries).\n */\nasync function requireTimeProjectInScope(\n em: EntityManager,\n projectId: string | null | undefined,\n tenantId: string,\n organizationId: string,\n): Promise<StaffTimeProject | null> {\n if (!projectId) return null\n const project = await em.findOne(StaffTimeProject, {\n id: projectId,\n tenantId,\n organizationId,\n deletedAt: null,\n })\n if (!project) {\n const { translate } = await resolveTranslations()\n throw new CrudHttpError(422, {\n error: translate('staff.timesheets.errors.projectNotFound', 'Time project not found or not accessible.'),\n fieldErrors: {\n timeProjectId: translate('staff.timesheets.errors.projectNotFound', 'Time project not found or not accessible.'),\n },\n })\n }\n return project\n}\n\n/**\n * Same contract as the project check, for `task_id`. A dangling task reference is\n * refused rather than stored: the task rollups on screens 6 and 7 aggregate entries\n * by `task_id`, so a stale or foreign UUID would silently move hours onto another\n * project's card.\n */\nasync function requireTaskInScope(\n em: EntityManager,\n taskId: string | null | undefined,\n tenantId: string,\n organizationId: string,\n): Promise<StaffTimeTask | null> {\n if (!taskId) return null\n const task = await em.findOne(StaffTimeTask, {\n id: taskId,\n tenantId,\n organizationId,\n deletedAt: null,\n })\n if (!task) {\n const { translate } = await resolveTranslations()\n const message = translate('staff.timesheets.errors.taskNotFound', 'Task not found or not accessible.')\n throw new CrudHttpError(422, { error: message, fieldErrors: { taskId: message } })\n }\n return task\n}\n\n/**\n * A task belongs to exactly one project, so an entry naming both must name the\n * same one. Letting them disagree would file the hours under a project whose\n * board never shows the task \u2014 the same rollup corruption the task-scope check\n * above prevents, one level up.\n */\nasync function assertTaskBelongsToProject(\n task: StaffTimeTask | null,\n projectId: string | null,\n): Promise<void> {\n if (timeEntryTaskMatchesProject(task, projectId)) return\n const { translate } = await resolveTranslations()\n const message = translate('staff.timesheets.errors.taskNotFound', 'Task not found or not accessible.')\n throw new CrudHttpError(422, { error: message, fieldErrors: { taskId: message } })\n}\n\n/**\n * The rule the assertion above enforces, as a predicate: a task belongs to\n * exactly one project, so an entry naming both must name the same one. Exported\n * because the grid bulk save validates a whole batch at once and answers with a\n * per-row error list rather than a thrown 422 \u2014 it reuses the rule and only\n * differs in how it reports a violation.\n */\nexport function timeEntryTaskMatchesProject(\n task: { timeProjectId?: string | null } | null | undefined,\n projectId: string | null | undefined,\n): boolean {\n if (!task || !projectId) return true\n return task.timeProjectId === projectId\n}\n\n/**\n * Screen 8 picks a task and lets the project follow from it (\"Projekt i klient\n * wynikaj\u0105 z zadania\"), so a write that names only a task still lands on the\n * project the task belongs to \u2014 and with it the billable and currency defaults.\n * One derivation for every write path, including the grid bulk save.\n */\nexport function resolveTimeEntryProjectId(\n requestedProjectId: string | null | undefined,\n task: { timeProjectId?: string | null } | null | undefined,\n): string | null {\n return requestedProjectId ?? task?.timeProjectId ?? null\n}\n\n/**\n * The billable default chain, in one place: an explicit value wins, then the\n * project's own default \u2014 a fixed-price engagement is non-billable however the\n * tenant normally works \u2014 and the tenant setting only decides what is left.\n *\n * EP-34 moved that chain into the billability registry's built-in resolver; this\n * stays the module's entry point and forwards the scope so a contributed\n * resolver can be consulted ahead of it.\n */\nexport function resolveTimeEntryBillable(params: {\n requested?: boolean | null\n project?: { id?: string | null; billableByDefault?: boolean | null } | null\n settings: TimeTrackingSettings\n scope?: ScopedResolverContext | null\n}): boolean {\n return resolveBillability({\n ...(params.scope ?? {}),\n requested: params.requested,\n project: params.project ?? null,\n settings: params.settings,\n })\n}\n\n/**\n * Rounding and the billable default are tenant settings. A settings read that\n * fails must not fail the write, so it degrades to the documented defaults \u2014\n * the same fail-safe the projects and tasks routes apply to the grace window.\n */\nasync function readEntrySettingsFrom(\n container: ContainerLike,\n tenantId: string,\n): Promise<TimeTrackingSettings> {\n try {\n const configService = container.resolve('moduleConfigService') as ModuleConfigService | undefined\n if (!configService?.getRecord) return DEFAULT_TIME_TRACKING_SETTINGS\n return await readTimeTrackingSettings(configService, { tenantId })\n } catch {\n return DEFAULT_TIME_TRACKING_SETTINGS\n }\n}\n\nasync function readEntrySettings(ctx: CommandRuntimeContext, tenantId: string): Promise<TimeTrackingSettings> {\n return readEntrySettingsFrom(ctx.container, tenantId)\n}\n\n/**\n * The same tenant-settings read, for write paths that run outside a command \u2014\n * the grid bulk save reads it ONCE per request (it is identical for every row)\n * and then seeds rounding and the billable default from that one snapshot.\n */\nexport async function resolveTimeEntrySettings(\n container: ContainerLike,\n tenantId: string,\n): Promise<TimeTrackingSettings> {\n return readEntrySettingsFrom(container, tenantId)\n}\n\n/**\n * The single rounding entry point for write paths that cannot run inside this\n * command file \u2014 the grid bulk save and timer-stop both compute a duration in\n * their own transaction, and D-7 makes `rounded_minutes` the ONLY input to cost,\n * so a duration written without its rounded twin bills nothing. They call this\n * rather than reaching for `roundMinutes` themselves, which keeps the tenant\n * settings read, its fail-safe, and the rule itself in one place.\n */\nexport async function resolveRoundedMinutes(\n container: ContainerLike,\n tenantId: string,\n durationMinutes: number,\n organizationId?: string | null,\n): Promise<number> {\n return roundedMinutesFor(durationMinutes, await readEntrySettingsFrom(container, tenantId), {\n tenantId,\n organizationId: organizationId ?? undefined,\n })\n}\n\nasync function resolveGrantedFeatures(\n ctx: CommandRuntimeContext,\n tenantId: string,\n organizationId: string,\n): Promise<string[]> {\n const userId = ctx.auth?.sub\n if (!userId) return []\n try {\n const rbac = ctx.container.resolve('rbacService') as RbacServiceLike | undefined\n if (!rbac?.getGrantedFeatures) return []\n return (await rbac.getGrantedFeatures(userId, { tenantId, organizationId })) ?? []\n } catch {\n // Fail closed: an unreadable grant set is no grant set, so the caller falls\n // back to their project memberships instead of being treated as a manager.\n return []\n }\n}\n\nasync function resolveEntryProjectAccess(\n em: EntityManager,\n ctx: CommandRuntimeContext,\n tenantId: string,\n organizationId: string,\n settings: TimeTrackingSettings,\n): Promise<ProjectAccess> {\n return resolveProjectAccess({\n em: em.fork(),\n userId: ctx.auth?.sub ?? null,\n tenantId,\n organizationId,\n userFeatures: await resolveGrantedFeatures(ctx, tenantId, organizationId),\n assignmentGraceDays: settings.access.assignmentGraceDays,\n })\n}\n\n/**\n * Every entry write intersects the caller's project access. An entry that names a\n * project is writable by a project manager or an assigned member; an entry with no\n * project belongs to nobody else, so only the member who logged it may touch it.\n * `staff.timesheets.manage_all` stays a superset of both, which is what it has\n * always meant on this route.\n */\nasync function assertEntryProjectAccess(params: {\n access: ProjectAccess\n hasManageAll: boolean\n projectId: string | null\n entryStaffMemberId: string\n}): Promise<void> {\n if (params.hasManageAll || params.access.canManageAll) return\n const { translate } = await resolveTranslations()\n if (params.projectId) {\n if (params.access.projectIds.includes(params.projectId)) return\n throw new CrudHttpError(403, {\n error: translate('staff.timesheets.errors.notAssigned', 'You are not assigned to this project.'),\n })\n }\n if (params.access.staffMemberId && params.access.staffMemberId === params.entryStaffMemberId) return\n throw new CrudHttpError(403, {\n error: translate('staff.timesheets.errors.notOwner', 'You can only manage your own time entries.'),\n })\n}\n\n/**\n * `notes` is the column; `description` is the name every consulting-suite screen\n * uses for it. Both keys are accepted on write and `description` wins when a\n * request carries both \u2014 the precedence UPGRADE_NOTES.md already published, and\n * the one that matches intent: a client sending the new key means it.\n * Returns `undefined` when neither key was supplied, so a PUT that omits both\n * leaves the stored note alone.\n *\n * Exported so the grid bulk save applies the SAME precedence \u2014 it accepts both\n * keys too, and a second interpretation of which one wins is exactly the kind of\n * divergence that makes one screen's edit disappear on another.\n */\nexport function resolveTimeEntryNotesInput(parsed: {\n notes?: string | null\n description?: string | null\n}): string | null | undefined {\n if (parsed.description !== undefined) return parsed.description ?? null\n if (parsed.notes !== undefined) return parsed.notes ?? null\n return undefined\n}\n\n/**\n * `rate_override_amount` is `numeric(14,4)`, which MikroORM surfaces as a string;\n * the validator accepts a JSON number. One conversion point keeps the two apart,\n * for the grid bulk save as much as for this file.\n */\nexport function toStoredTimeEntryRateOverride(amount: number | null | undefined): string | null {\n return amount === null || amount === undefined ? null : String(amount)\n}\n\n/**\n * D-7: `duration_minutes` keeps the RAW value the user entered \u2014 it is what the\n * timesheet and the project hour totals display \u2014 while `rounded_minutes` is\n * derived from the tenant's rounding rule at write time and is the only input to\n * cost. Storing the rounded value rather than deriving it on read is what stops a\n * later settings change from silently restating money that has already been\n * invoiced (risk R1).\n */\nexport function roundedMinutesFor(\n durationMinutes: number,\n settings: TimeTrackingSettings,\n scope?: ScopedResolverContext | null,\n): number {\n return roundMinutes(durationMinutes, settings.rounding, scope)\n}\n\nexport type TimeEntryInterval = {\n startedAt: Date | null\n endedAt: Date | null\n durationMinutes: number\n}\n\n/**\n * `started_at` / `ended_at` are absolute timestamps; `deriveInterval` reasons in\n * wall-clock minutes. Both directions read and write the SAME basis (UTC), so a\n * timestamp that needs no correction survives the round trip untouched.\n */\nfunction clockOfTimestamp(value: Date): string {\n return formatClock(value.getUTCHours() * 60 + value.getUTCMinutes())\n}\n\nfunction timestampOnDayOf(reference: Date, clock: string, dayOffset: number): Date {\n const minutesOfDay = parseClock(clock) ?? 0\n return new Date(\n Date.UTC(\n reference.getUTCFullYear(),\n reference.getUTCMonth(),\n reference.getUTCDate() + dayOffset,\n Math.floor(minutesOfDay / 60),\n minutesOfDay % 60,\n 0,\n 0,\n ),\n )\n}\n\n/**\n * D-8. An end whose clock is earlier than its start's is a midnight crossing, not\n * a validation error, so the end belongs on the NEXT calendar day while `date`\n * keeps naming the day the work started. A caller that already sent the end on\n * the following day (the entry dialog does) is left exactly as it sent it.\n */\nfunction alignEndToStart(startedAt: Date, endedAt: Date, endClock: string, crossesMidnight: boolean): Date {\n if (endedAt.getTime() >= startedAt.getTime()) return endedAt\n return timestampOnDayOf(startedAt, endClock, crossesMidnight ? 1 : 0)\n}\n\n/**\n * US-D3, on the update path: `duration_minutes`, `started_at` and `ended_at` can\n * each be written on their own, and the server must never store a set that\n * contradicts itself \u2014 a two-hour duration on a 09:00\u201310:00 span is a\n * defensible-invoice problem, not a cosmetic one (D-7 bills the rounded twin of\n * whatever duration lands here).\n *\n * The rule, in one line: **the clocks are the observation and the duration is\n * their summary, so the clocks win \u2014 unless the duration is the field the request\n * actually pinned, in which case the untouched clock moves to match it.**\n *\n * - both clocks in the request \u2192 the duration is recomputed from them, and a\n * duration sent alongside is advisory (this is what the entry dialog sends);\n * - one clock plus a duration \u2192 the OTHER clock is derived from that pair;\n * - one clock alone \u2192 the duration is recomputed against the stored counterpart;\n * - the duration alone \u2192 `started_at` anchors and `ended_at` shifts to match;\n * - none of the three \u2192 every one of them is left exactly as stored.\n *\n * Derivation only ever RESTATES a clock that already exists: with one side null\n * there is nothing for a duration to contradict, and manufacturing the missing\n * side would silently stop a running timer.\n *\n * Exported because the grid bulk save writes durations in its own transaction\n * (per-row, pessimistically locked) and cannot go through this command \u2014 it calls\n * the same reconciliation rather than repeating the arithmetic, which is what\n * keeps one rule for one invariant.\n */\nexport function reconcileTimeEntryInterval(params: {\n stored: TimeEntryInterval\n startedAt: Date | null | undefined\n endedAt: Date | null | undefined\n durationMinutes: number | undefined\n}): TimeEntryInterval {\n const startProvided = params.startedAt !== undefined\n const endProvided = params.endedAt !== undefined\n const durationProvided = params.durationMinutes !== undefined\n if (!startProvided && !endProvided && !durationProvided) return params.stored\n\n const startedAt = startProvided ? params.startedAt ?? null : params.stored.startedAt\n const endedAt = endProvided ? params.endedAt ?? null : params.stored.endedAt\n const durationMinutes = durationProvided\n ? (params.durationMinutes as number)\n : params.stored.durationMinutes\n if (!startedAt || !endedAt) return { startedAt, endedAt, durationMinutes }\n\n const startClock = clockOfTimestamp(startedAt)\n const endClock = clockOfTimestamp(endedAt)\n const durationAnchored = durationProvided && !(startProvided && endProvided)\n\n if (durationAnchored && !endProvided) {\n const derived = deriveInterval({ start: startClock, durationMinutes })\n return {\n startedAt,\n endedAt: timestampOnDayOf(startedAt, derived.end ?? endClock, derived.crossesMidnight ? 1 : 0),\n durationMinutes,\n }\n }\n\n if (durationAnchored && !startProvided) {\n const derived = deriveInterval({ end: endClock, durationMinutes })\n return {\n startedAt: timestampOnDayOf(endedAt, derived.start ?? startClock, derived.crossesMidnight ? -1 : 0),\n endedAt,\n durationMinutes,\n }\n }\n\n const derived = deriveInterval({ start: startClock, end: endClock, durationMinutes })\n return {\n startedAt,\n endedAt: alignEndToStart(startedAt, endedAt, endClock, derived.crossesMidnight),\n durationMinutes: derived.durationMinutes ?? durationMinutes,\n }\n}\n\nconst MS_PER_DAY = 86_400_000\n\nfunction toDateOrNull(value: Date | string | null | undefined): Date | null {\n if (value === null || value === undefined) return null\n const date = value instanceof Date ? value : new Date(value)\n return Number.isNaN(date.getTime()) ? null : date\n}\n\nfunction utcDayIndex(value: Date): number {\n return Math.round(\n Date.UTC(value.getUTCFullYear(), value.getUTCMonth(), value.getUTCDate()) / MS_PER_DAY,\n )\n}\n\n/**\n * T4.10(b): moving an entry to another day moves the work with it.\n *\n * `date` names the day the work STARTED (D-8), so an entry re-dated to Tuesday\n * whose clocks stayed on Monday is the same self-contradicting triple T4.9\n * eliminated one field over \u2014 a 09:00\u201310:00 span filed against a day it did not\n * happen on, and a defensible-invoice problem the moment anyone reads the times\n * back. \"Move this entry to Tuesday\" means the whole observation moves.\n *\n * The clocks are SHIFTED by whole days rather than recomposed from the new date,\n * which preserves two things a recomposition would destroy: the stored offset\n * between `date` and the timestamps (for a tenant east of UTC a 01:00 local start\n * legitimately sits on the previous UTC day), and a midnight crossing (an end\n * already on the following day stays on the following day).\n *\n * Two things it never does, both inherited from the reconciliation rule above:\n *\n * - a clock that does not exist is not manufactured, and\n * - a RUNNING timer \u2014 a start with no end \u2014 is left exactly where it is, because\n * re-anchoring its start would restate the elapsed time of work still in\n * progress, and nothing about re-dating the row says the timer began later.\n */\nexport function rebaseTimeEntryIntervalToDate(params: {\n stored: TimeEntryInterval\n fromDate: Date | string | null | undefined\n toDate: Date | string | null | undefined\n}): TimeEntryInterval {\n const fromDate = toDateOrNull(params.fromDate)\n const toDate = toDateOrNull(params.toDate)\n if (!fromDate || !toDate) return params.stored\n\n const { startedAt, endedAt } = params.stored\n if (!startedAt && !endedAt) return params.stored\n if (startedAt && !endedAt) return params.stored\n\n const shiftMs = (utcDayIndex(toDate) - utcDayIndex(fromDate)) * MS_PER_DAY\n if (shiftMs === 0) return params.stored\n return {\n startedAt: startedAt ? new Date(startedAt.getTime() + shiftMs) : null,\n endedAt: endedAt ? new Date(endedAt.getTime() + shiftMs) : null,\n durationMinutes: params.stored.durationMinutes,\n }\n}\n\n/**\n * Reuses the code the tag commands already refuse a frozen entry with, rather\n * than inventing a second one \u2014 a client that learned to handle `time_entry_locked`\n * from `\u2026/tags/entry-assignments` handles it identically everywhere else.\n */\nexport const TIME_ENTRY_LOCKED_CODE = TAG_TARGET_LOCKED_CODE\n\nexport type LockedTimeEntryRef = { id: string; lockedReportId?: string | null }\n\n/**\n * The 409 body names the report that froze the entries so screen 10 note 4 can\n * link straight to it, and lists the offending ids so a grid save can mark those\n * cells read-only instead of showing an opaque failure. `lockedReportId` is kept\n * beside the arrays for shape-compatibility with the tag commands' single-target\n * refusal.\n */\nexport function buildTimeEntryLockedError(\n lockedEntries: LockedTimeEntryRef[],\n translate: Translate,\n): CrudHttpError {\n const lockedEntryIds = lockedEntries.map((entry) => entry.id)\n const lockedReportIds = Array.from(\n new Set(\n lockedEntries\n .map((entry) => entry.lockedReportId)\n .filter((reportId): reportId is string => typeof reportId === 'string' && reportId.length > 0),\n ),\n )\n return new CrudHttpError(409, {\n code: TIME_ENTRY_LOCKED_CODE,\n error:\n lockedEntryIds.length > 1\n ? translate(\n 'staff.time_tracking.entries.errors.entriesLocked',\n 'Some of these time entries are locked in a closed report and cannot be changed. Unlock the report first.',\n )\n : translate(\n 'staff.time_tracking.entries.errors.entryLocked',\n 'This time entry is locked in a closed report and cannot be changed. Unlock the report first.',\n ),\n lockedReportId: lockedReportIds[0] ?? null,\n lockedEntryIds,\n lockedReportIds,\n })\n}\n\n/**\n * The lock gate (risk R3). It sits in the command layer rather than in each write\n * route because the routes are the part that keeps growing: duplicate, copy-day\n * and the grid bulk save all arrived after the lock did, and any one of them\n * could have forgotten a per-route check. Every command that resolves an entry\n * for mutation runs this immediately after the row is loaded, so a field added to\n * the update command later is frozen without anybody remembering to freeze it.\n *\n * No permission lifts it \u2014 not `staff.timesheets.manage_all`. An entry frozen in\n * a closed report has already been billed at those minutes (D-7), so the only way\n * back is the Phase-6 unlock command, which clears `locked_report_id` itself \u2014 or\n * hands it to another closed report still quoting the entry \u2014 and therefore never\n * reaches this gate.\n */\nexport function assertTimeEntryUnlocked(\n entry: { id: string; lockedReportId?: string | null },\n translate: Translate,\n): void {\n if (!entry.lockedReportId) return\n throw buildTimeEntryLockedError([{ id: entry.id, lockedReportId: entry.lockedReportId }], translate)\n}\n\n/**\n * Tags are written through the tag commands, never through the junction table, so\n * an entry write inherits their guarantees unchanged: unknown ids are refused,\n * re-sending a tag is idempotent, an entry frozen in a closed report is refused\n * with 409 `time_entry_locked`, and each change lands in the audit log with its\n * own undo.\n *\n * `tagIds` is a SET, not an append. The assign command answers with the target's\n * full tag list, and that answer is what tells us which tags must then be\n * unassigned \u2014 so the desired state is reached without this file ever reading or\n * writing `staff_time_entry_tags` itself.\n */\nasync function syncEntryTags(\n ctx: CommandRuntimeContext,\n params: { timeEntryId: string; tenantId: string; organizationId: string; tagIds: string[] },\n): Promise<void> {\n const bus = ctx.container.resolve('commandBus') as CommandBus\n const desired = Array.from(new Set(params.tagIds))\n const scoped = {\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n timeEntryId: params.timeEntryId,\n }\n\n const assigned = await bus.execute<unknown, { tagIds?: string[] }>(staffTimeTagCommandIds.assignEntry, {\n input: { ...scoped, tagIds: desired },\n ctx,\n })\n\n const current = assigned?.result?.tagIds ?? desired\n const stale = current.filter((tagId) => !desired.includes(tagId))\n if (stale.length === 0) return\n\n await bus.execute(staffTimeTagCommandIds.unassignEntry, {\n input: { ...scoped, tagIds: stale },\n ctx,\n })\n}\n\ntype TimeEntrySnapshot = {\n id: string\n tenantId: string\n organizationId: string\n staffMemberId: string\n date: string\n durationMinutes: number\n roundedMinutes: number | null\n startedAt: string | null\n endedAt: string | null\n notes: string | null\n timeProjectId: string | null\n taskId: string | null\n customerId: string | null\n dealId: string | null\n orderId: string | null\n isBillable: boolean\n rateOverrideAmount: string | null\n rateCurrencyCode: string | null\n source: string\n deletedAt: string | null\n}\n\ntype TimeEntryUndoPayload = {\n before?: TimeEntrySnapshot | null\n after?: TimeEntrySnapshot | null\n}\n\nasync function loadTimeEntrySnapshot(em: EntityManager, id: string, scope?: StaffSnapshotScope | null): Promise<TimeEntrySnapshot | null> {\n const entry = await findOneWithDecryption(\n em,\n StaffTimeEntry,\n scopedStaffSnapshotWhere(id, scope),\n undefined,\n staffSnapshotDecryptionScope(scope),\n )\n if (!entry) return null\n return {\n id: entry.id,\n tenantId: entry.tenantId,\n organizationId: entry.organizationId,\n staffMemberId: entry.staffMemberId,\n date: entry.date instanceof Date ? entry.date.toISOString().split('T')[0] : String(entry.date),\n durationMinutes: entry.durationMinutes,\n roundedMinutes: entry.roundedMinutes ?? null,\n startedAt: entry.startedAt ? entry.startedAt.toISOString() : null,\n endedAt: entry.endedAt ? entry.endedAt.toISOString() : null,\n notes: entry.notes ?? null,\n timeProjectId: entry.timeProjectId ?? null,\n taskId: entry.taskId ?? null,\n customerId: entry.customerId ?? null,\n dealId: entry.dealId ?? null,\n orderId: entry.orderId ?? null,\n isBillable: entry.isBillable,\n rateOverrideAmount: entry.rateOverrideAmount ?? null,\n rateCurrencyCode: entry.rateCurrencyCode ?? null,\n source: entry.source,\n deletedAt: entry.deletedAt ? entry.deletedAt.toISOString() : null,\n }\n}\n\nfunction timeEntrySeedFromSnapshot(snapshot: TimeEntrySnapshot): Record<string, unknown> {\n return {\n id: snapshot.id,\n tenantId: snapshot.tenantId,\n organizationId: snapshot.organizationId,\n staffMemberId: snapshot.staffMemberId,\n date: snapshot.date,\n durationMinutes: snapshot.durationMinutes,\n roundedMinutes: snapshot.roundedMinutes ?? null,\n startedAt: snapshot.startedAt ? new Date(snapshot.startedAt) : null,\n endedAt: snapshot.endedAt ? new Date(snapshot.endedAt) : null,\n notes: snapshot.notes ?? null,\n timeProjectId: snapshot.timeProjectId ?? null,\n taskId: snapshot.taskId ?? null,\n customerId: snapshot.customerId ?? null,\n dealId: snapshot.dealId ?? null,\n orderId: snapshot.orderId ?? null,\n isBillable: snapshot.isBillable ?? true,\n rateOverrideAmount: snapshot.rateOverrideAmount ?? null,\n rateCurrencyCode: snapshot.rateCurrencyCode ?? null,\n source: (snapshot.source ?? 'manual') as StaffTimeEntrySource,\n createdAt: new Date(),\n updatedAt: new Date(),\n deletedAt: null,\n }\n}\n\nconst createTimeEntryCommand: CommandHandler<StaffTimeEntryCreateInput, { timeEntryId: string }> = {\n id: 'staff.timesheets.time_entries.create',\n async execute(rawInput, ctx) {\n const parsed = staffTimeEntryCreateSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n commandInputScope(ctx, parsed.tenantId, parsed.organizationId)\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n\n // Ownership enforcement: callers without `staff.timesheets.manage_all`\n // can only create entries attributed to themselves. Silent override\n // (mirrors `bulk/route.ts` behavior) so the request body's staffMemberId\n // can't forge an entry under a colleague's identity.\n let effectiveStaffMemberId = parsed.staffMemberId\n if (!(await callerHasManageAll(ctx))) {\n const callerStaffMemberId = await resolveCallerStaffMemberId(em, ctx)\n if (!callerStaffMemberId) {\n const { translate } = await resolveTranslations()\n throw new CrudHttpError(403, {\n error: translate('staff.timesheets.errors.noStaffMember', 'No staff member linked to your account.'),\n })\n }\n effectiveStaffMemberId = callerStaffMemberId\n }\n\n // Validate referenced task and timeProjectId are in-scope before persisting.\n // Without these checks a foreign or stale UUID would produce a dangling reference.\n const task = await requireTaskInScope(em, parsed.taskId ?? null, parsed.tenantId, parsed.organizationId)\n const requestedProjectId = resolveTimeEntryProjectId(parsed.timeProjectId, task)\n const project = await requireTimeProjectInScope(em, requestedProjectId, parsed.tenantId, parsed.organizationId)\n await assertTaskBelongsToProject(task, project?.id ?? null)\n\n const settings = await readEntrySettings(ctx, parsed.tenantId)\n await assertEntryProjectAccess({\n access: await resolveEntryProjectAccess(em, ctx, parsed.tenantId, parsed.organizationId, settings),\n hasManageAll: await callerHasManageAll(ctx),\n projectId: project?.id ?? null,\n entryStaffMemberId: effectiveStaffMemberId,\n })\n\n const now = new Date()\n const entry = em.create(StaffTimeEntry, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n staffMemberId: effectiveStaffMemberId,\n date: parsed.date,\n durationMinutes: parsed.durationMinutes,\n roundedMinutes: roundedMinutesFor(parsed.durationMinutes, settings, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n }),\n startedAt: parsed.startedAt ?? null,\n endedAt: parsed.endedAt ?? null,\n notes: resolveTimeEntryNotesInput(parsed) ?? null,\n timeProjectId: project?.id ?? null,\n taskId: task?.id ?? null,\n customerId: parsed.customerId ?? null,\n dealId: parsed.dealId ?? null,\n orderId: parsed.orderId ?? null,\n isBillable: resolveTimeEntryBillable({\n requested: parsed.isBillable,\n project,\n settings,\n scope: { tenantId: parsed.tenantId, organizationId: parsed.organizationId },\n }),\n rateOverrideAmount: toStoredTimeEntryRateOverride(parsed.rateOverrideAmount),\n // Snapshotted rather than joined at read time so a later project currency\n // change cannot re-denominate money that has already been reported (D-3).\n rateCurrencyCode: project?.currencyCode ?? null,\n source: parsed.source ?? 'manual',\n createdAt: now,\n updatedAt: now,\n deletedAt: null,\n })\n em.persist(entry)\n await em.flush()\n\n if (parsed.tagIds !== undefined) {\n await syncEntryTags(ctx, {\n timeEntryId: entry.id,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n tagIds: parsed.tagIds,\n })\n }\n\n await emitCrudSideEffects({\n dataEngine: ctx.container.resolve('dataEngine'),\n action: 'created',\n entity: entry,\n identifiers: { id: entry.id, organizationId: entry.organizationId, tenantId: entry.tenantId },\n events: staffTimeEntryCrudEvents,\n indexer: timeEntryCrudIndexer,\n })\n\n return { timeEntryId: entry.id }\n },\n captureAfter: async (_input, result, ctx) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const snapshot = await loadTimeEntrySnapshot(em, result.timeEntryId, staffSnapshotScopeFromContext(ctx))\n if (!snapshot) return null\n return { snapshot }\n },\n buildLog: async ({ result, ctx }) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const snapshot = await loadTimeEntrySnapshot(em, result.timeEntryId, staffSnapshotScopeFromContext(ctx))\n if (!snapshot) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('staff.audit.timesheets.time_entries.create', 'Create time entry'),\n resourceKind: 'staff.timesheets.time_entry',\n resourceId: snapshot.id,\n tenantId: snapshot.tenantId,\n organizationId: snapshot.organizationId,\n snapshotAfter: snapshot,\n payload: {\n undo: {\n after: snapshot,\n } satisfies TimeEntryUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<TimeEntryUndoPayload>(logEntry)\n const after = payload?.after\n if (!after) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const entry = await em.findOne(StaffTimeEntry, scopedStaffSnapshotWhere(after.id, staffSnapshotScopeFromSnapshot(after)))\n if (entry) {\n assertTimeEntryUnlocked(entry, (await resolveTranslations()).translate)\n entry.deletedAt = new Date()\n await em.flush()\n\n await emitCrudUndoSideEffects({\n dataEngine: ctx.container.resolve('dataEngine'),\n action: 'deleted',\n entity: entry,\n identifiers: { id: entry.id, organizationId: entry.organizationId, tenantId: entry.tenantId },\n events: staffTimeEntryCrudEvents,\n indexer: timeEntryCrudIndexer,\n })\n }\n },\n redo: makeCreateRedo<StaffTimeEntry, TimeEntrySnapshot, StaffTimeEntryCreateInput, { timeEntryId: string }>({\n entityClass: StaffTimeEntry,\n getSnapshotId: (snapshot) => snapshot.id,\n seedFromSnapshot: timeEntrySeedFromSnapshot,\n buildResult: (entity) => ({ timeEntryId: entity.id }),\n events: staffTimeEntryCrudEvents,\n indexer: timeEntryCrudIndexer,\n }),\n}\n\nconst startTimerCommand: CommandHandler<StaffTimeEntryStartTimerInput, { timeEntryId: string }> = {\n id: 'staff.timesheets.time_entries.start_timer',\n async execute(rawInput, ctx) {\n const parsed = staffTimeEntryStartTimerSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n commandInputScope(ctx, parsed.tenantId, parsed.organizationId)\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n\n // Ownership enforcement mirrors createTimeEntryCommand: callers without\n // `staff.timesheets.manage_all` can only start their own timer, so the\n // request body's staffMemberId can't start a timer under a colleague's id.\n let effectiveStaffMemberId = parsed.staffMemberId\n if (!(await callerHasManageAll(ctx))) {\n const callerStaffMemberId = await resolveCallerStaffMemberId(em, ctx)\n if (!callerStaffMemberId) {\n const { translate } = await resolveTranslations()\n throw new CrudHttpError(403, {\n error: translate('staff.timesheets.errors.noStaffMember', 'No staff member linked to your account.'),\n })\n }\n effectiveStaffMemberId = callerStaffMemberId\n }\n\n // Starting a timer on a task is ONE write. The task is validated here, in the\n // same command, so the entry is created already carrying it \u2014 the alternative\n // (start, then link) can fail after the timer is running and strand it against\n // the project instead of the task.\n const task = await requireTaskInScope(em, parsed.taskId ?? null, parsed.tenantId, parsed.organizationId)\n // A card knows its task, not always its project, and a task belongs to exactly\n // one project \u2014 so the project follows from the task, exactly as it does when\n // an entry is created from screen 8.\n const requestedProjectId = resolveTimeEntryProjectId(parsed.timeProjectId, task)\n const project = await requireTimeProjectInScope(em, requestedProjectId, parsed.tenantId, parsed.organizationId)\n await assertTaskBelongsToProject(task, project?.id ?? null)\n\n // A timer entry starts at zero minutes, and D-7 makes `rounded_minutes` the\n // only input to cost \u2014 leaving it null here would price the entry at nothing\n // until something else happened to rewrite it. Seeded from the same rule as\n // every other create so timer-stop only ever restates it.\n const settings = await readEntrySettings(ctx, parsed.tenantId)\n\n const scopeCtx = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }\n\n // Create the timer entry AND start it inside a single transaction so a\n // partial failure can never leave an orphaned, never-started timer entry\n // (issue #3311 \u2014 the legacy two-request create-then-start flow). The\n // single-active-timer invariant (#2855) is re-checked here so a second\n // surface cannot create a parallel running timer for the same staff member.\n const { entry, startedAt } = await em.transactional(async (trx) => {\n const otherRunningEntry = await findOneWithDecryption(\n trx,\n StaffTimeEntry,\n {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n staffMemberId: effectiveStaffMemberId,\n startedAt: { $ne: null },\n endedAt: null,\n deletedAt: null,\n },\n {},\n scopeCtx,\n )\n if (otherRunningEntry) {\n const { translate } = await resolveTranslations()\n throw new CrudHttpError(409, {\n error: translate(\n 'staff.timesheets.errors.timerAlreadyRunning',\n 'Another timer is already running. Stop it before starting a new one.',\n ),\n })\n }\n\n const startedAt = new Date()\n const entry = trx.create(StaffTimeEntry, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n staffMemberId: effectiveStaffMemberId,\n date: parsed.date,\n durationMinutes: 0,\n roundedMinutes: roundedMinutesFor(0, settings, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n }),\n startedAt,\n endedAt: null,\n notes: parsed.notes ?? null,\n timeProjectId: project?.id ?? null,\n taskId: task?.id ?? null,\n customerId: null,\n dealId: null,\n orderId: null,\n source: 'timer',\n createdAt: startedAt,\n updatedAt: startedAt,\n deletedAt: null,\n })\n // Flush so the DB-generated id is populated before the work segment\n // references it; both writes commit together when the transaction closes.\n await trx.flush()\n\n const segmentData = {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n timeEntryId: entry.id,\n startedAt,\n segmentType: 'work' as const,\n }\n trx.create(StaffTimeEntrySegment, segmentData as never)\n await trx.flush()\n\n return { entry, startedAt }\n })\n\n await emitCrudSideEffects({\n dataEngine: ctx.container.resolve('dataEngine'),\n action: 'created',\n entity: entry,\n identifiers: { id: entry.id, organizationId: entry.organizationId, tenantId: entry.tenantId },\n events: staffTimeEntryCrudEvents,\n indexer: timeEntryCrudIndexer,\n })\n\n void emitStaffEvent('staff.timesheets.time_entry.timer_started', {\n id: entry.id,\n staffMemberId: effectiveStaffMemberId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n startedAt: startedAt.toISOString(),\n }, { persistent: true }).catch((err) => {\n logger.error('staff.timesheets emit timer_started failed', { err })\n })\n\n return { timeEntryId: entry.id }\n },\n captureAfter: async (_input, result, ctx) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const snapshot = await loadTimeEntrySnapshot(em, result.timeEntryId, staffSnapshotScopeFromContext(ctx))\n if (!snapshot) return null\n return { snapshot }\n },\n buildLog: async ({ result, ctx }) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const snapshot = await loadTimeEntrySnapshot(em, result.timeEntryId, staffSnapshotScopeFromContext(ctx))\n if (!snapshot) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('staff.audit.timesheets.time_entries.startTimer', 'Start timer'),\n resourceKind: 'staff.timesheets.time_entry',\n resourceId: snapshot.id,\n tenantId: snapshot.tenantId,\n organizationId: snapshot.organizationId,\n snapshotAfter: snapshot,\n payload: {\n undo: {\n after: snapshot,\n } satisfies TimeEntryUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<TimeEntryUndoPayload>(logEntry)\n const after = payload?.after\n if (!after) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const entry = await em.findOne(StaffTimeEntry, scopedStaffSnapshotWhere(after.id, staffSnapshotScopeFromSnapshot(after)))\n if (entry) {\n assertTimeEntryUnlocked(entry, (await resolveTranslations()).translate)\n entry.deletedAt = new Date()\n await em.flush()\n\n await emitCrudUndoSideEffects({\n dataEngine: ctx.container.resolve('dataEngine'),\n action: 'deleted',\n entity: entry,\n identifiers: { id: entry.id, organizationId: entry.organizationId, tenantId: entry.tenantId },\n events: staffTimeEntryCrudEvents,\n indexer: timeEntryCrudIndexer,\n })\n }\n },\n}\n\nconst updateTimeEntryCommand: CommandHandler<StaffTimeEntryUpdateInput, { timeEntryId: string }> = {\n id: 'staff.timesheets.time_entries.update',\n async prepare(rawInput, ctx) {\n const parsed = staffTimeEntryUpdateSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager)\n const snapshot = await loadTimeEntrySnapshot(em, parsed.id, staffSnapshotScopeFromContext(ctx))\n if (!snapshot) return {}\n return { before: snapshot }\n },\n async execute(rawInput, ctx) {\n const parsed = staffTimeEntryUpdateSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const scope = commandActorScope(ctx)\n const entry = await findOneWithDecryption(\n em,\n StaffTimeEntry,\n applyScopeToWhere<StaffTimeEntry>({ id: parsed.id, deletedAt: null }, scope),\n undefined,\n scopeForDecryption(scope),\n )\n if (!entry) throw new CrudHttpError(404, { error: 'Time entry not found.' })\n ensureTenantScope(ctx, entry.tenantId)\n ensureOrganizationScope(ctx, entry.organizationId)\n\n // Before ownership, before access, before a single field is read off the\n // request: a frozen entry is frozen for everyone, so nothing below can be\n // reached with a lock in place.\n assertTimeEntryUnlocked(entry, (await resolveTranslations()).translate)\n\n // Ownership enforcement: callers without `staff.timesheets.manage_all`\n // can only update entries they own.\n const hasManageAll = await callerHasManageAll(ctx)\n if (!hasManageAll) {\n const callerStaffMemberId = await resolveCallerStaffMemberId(em, ctx)\n if (!callerStaffMemberId || entry.staffMemberId !== callerStaffMemberId) {\n const { translate } = await resolveTranslations()\n throw new CrudHttpError(403, {\n error: translate('staff.timesheets.errors.notOwner', 'You can only manage your own time entries.'),\n })\n }\n }\n\n const task =\n parsed.taskId !== undefined\n ? await requireTaskInScope(em, parsed.taskId, entry.tenantId, entry.organizationId)\n : null\n\n // A task carries its project (screen 8), so re-pointing an entry at a task\n // moves the entry with it unless the request names a project explicitly.\n const nextProjectId =\n parsed.timeProjectId !== undefined\n ? parsed.timeProjectId ?? null\n : task?.timeProjectId ?? entry.timeProjectId ?? null\n const projectChanged = nextProjectId !== (entry.timeProjectId ?? null)\n const project = projectChanged\n ? await requireTimeProjectInScope(em, nextProjectId, entry.tenantId, entry.organizationId)\n : null\n await assertTaskBelongsToProject(task, nextProjectId)\n\n const settings = await readEntrySettings(ctx, entry.tenantId)\n const access = await resolveEntryProjectAccess(em, ctx, entry.tenantId, entry.organizationId, settings)\n // Both ends of a move are checked: a caller may not lift an entry out of a\n // project they cannot see, nor drop one into a project they cannot see.\n await assertEntryProjectAccess({\n access,\n hasManageAll,\n projectId: entry.timeProjectId ?? null,\n entryStaffMemberId: entry.staffMemberId,\n })\n if (projectChanged) {\n await assertEntryProjectAccess({\n access,\n hasManageAll,\n projectId: nextProjectId,\n entryStaffMemberId: entry.staffMemberId,\n })\n }\n\n const notes = resolveTimeEntryNotesInput(parsed)\n\n // A `date` move re-anchors the STORED clocks first, so what reconciliation\n // reasons about is the entry as it will exist on its new day. Order matters:\n // clocks the request sends explicitly still win over the rebased ones (the\n // entry dialog rebuilds all three from the new date and would otherwise be\n // shifted twice), while clocks it omits travel with the entry instead of\n // being stranded on the old day.\n const stored = rebaseTimeEntryIntervalToDate({\n stored: {\n startedAt: entry.startedAt ?? null,\n endedAt: entry.endedAt ?? null,\n durationMinutes: entry.durationMinutes,\n },\n fromDate: entry.date,\n toDate: parsed.date,\n })\n\n // The three time fields are settled together, before anything is assigned, so\n // no combination of them can be stored contradicting itself (US-D3, D-8).\n const interval = reconcileTimeEntryInterval({\n stored,\n startedAt: parsed.startedAt,\n endedAt: parsed.endedAt,\n durationMinutes: parsed.durationMinutes,\n })\n\n if (parsed.date !== undefined) entry.date = parsed.date\n if (interval.startedAt !== (entry.startedAt ?? null)) entry.startedAt = interval.startedAt\n if (interval.endedAt !== (entry.endedAt ?? null)) entry.endedAt = interval.endedAt\n if (parsed.durationMinutes !== undefined || interval.durationMinutes !== entry.durationMinutes) {\n entry.durationMinutes = interval.durationMinutes\n // Raw minutes moved \u2014 because the duration was written, or because the\n // clocks were edited and restated it \u2014 so the frozen rounded value is\n // recomputed from the tenant's current rule. D-7 makes it the only input to\n // cost, so a clock edit that changes the length of the work and leaves the\n // rounded twin behind would mis-bill it.\n entry.roundedMinutes = roundedMinutesFor(interval.durationMinutes, settings, {\n tenantId: entry.tenantId,\n organizationId: entry.organizationId,\n })\n }\n if (projectChanged) {\n entry.timeProjectId = nextProjectId\n entry.rateCurrencyCode = project?.currencyCode ?? null\n }\n if (parsed.taskId !== undefined) entry.taskId = task?.id ?? null\n if (parsed.customerId !== undefined) entry.customerId = parsed.customerId ?? null\n if (parsed.dealId !== undefined) entry.dealId = parsed.dealId ?? null\n if (parsed.orderId !== undefined) entry.orderId = parsed.orderId ?? null\n if (parsed.isBillable !== undefined) entry.isBillable = parsed.isBillable\n if (parsed.rateOverrideAmount !== undefined) {\n entry.rateOverrideAmount = toStoredTimeEntryRateOverride(parsed.rateOverrideAmount)\n }\n if (notes !== undefined) entry.notes = notes\n entry.updatedAt = new Date()\n await em.flush()\n\n if (parsed.tagIds !== undefined) {\n await syncEntryTags(ctx, {\n timeEntryId: entry.id,\n tenantId: entry.tenantId,\n organizationId: entry.organizationId,\n tagIds: parsed.tagIds,\n })\n }\n\n await emitCrudSideEffects({\n dataEngine: ctx.container.resolve('dataEngine'),\n action: 'updated',\n entity: entry,\n identifiers: { id: entry.id, organizationId: entry.organizationId, tenantId: entry.tenantId },\n events: staffTimeEntryCrudEvents,\n indexer: timeEntryCrudIndexer,\n })\n\n return { timeEntryId: entry.id }\n },\n buildLog: async ({ snapshots, ctx }) => {\n const before = snapshots.before as TimeEntrySnapshot | undefined\n if (!before) return null\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const after = await loadTimeEntrySnapshot(em, before.id, staffSnapshotScopeFromSnapshot(before))\n if (!after) return null\n const changes = buildChanges(before as unknown as Record<string, unknown>, after as unknown as Record<string, unknown>, [\n 'date',\n 'durationMinutes',\n 'roundedMinutes',\n // A time correction is exactly the edit somebody will later have to justify\n // on an invoice, so the clocks are named in the trail beside the minutes.\n 'startedAt',\n 'endedAt',\n 'timeProjectId',\n 'taskId',\n 'customerId',\n 'dealId',\n 'orderId',\n 'isBillable',\n 'rateOverrideAmount',\n 'rateCurrencyCode',\n 'notes',\n 'deletedAt',\n ])\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('staff.audit.timesheets.time_entries.update', 'Update time entry'),\n resourceKind: 'staff.timesheets.time_entry',\n resourceId: before.id,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n snapshotBefore: before,\n snapshotAfter: after,\n changes,\n payload: {\n undo: {\n before,\n after,\n } satisfies TimeEntryUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<TimeEntryUndoPayload>(logEntry)\n const before = payload?.before\n if (!before) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const entry = await em.findOne(StaffTimeEntry, scopedStaffSnapshotWhere(before.id, staffSnapshotScopeFromSnapshot(before)))\n if (!entry) return\n assertTimeEntryUnlocked(entry, (await resolveTranslations()).translate)\n entry.date = before.date as unknown as Date\n entry.durationMinutes = before.durationMinutes\n entry.roundedMinutes = before.roundedMinutes ?? null\n // Restored beside the minutes they belong to: now that an update can move the\n // clocks, an undo that left them where the edit put them would hand back an\n // entry whose duration and span disagree.\n entry.startedAt = before.startedAt ? new Date(before.startedAt) : null\n entry.endedAt = before.endedAt ? new Date(before.endedAt) : null\n entry.timeProjectId = before.timeProjectId ?? null\n entry.taskId = before.taskId ?? null\n entry.customerId = before.customerId ?? null\n entry.dealId = before.dealId ?? null\n entry.orderId = before.orderId ?? null\n entry.isBillable = before.isBillable ?? true\n entry.rateOverrideAmount = before.rateOverrideAmount ?? null\n entry.rateCurrencyCode = before.rateCurrencyCode ?? null\n entry.notes = before.notes ?? null\n entry.deletedAt = before.deletedAt ? new Date(before.deletedAt) : null\n entry.updatedAt = new Date()\n await em.flush()\n\n await emitCrudUndoSideEffects({\n dataEngine: ctx.container.resolve('dataEngine'),\n action: 'updated',\n entity: entry,\n identifiers: { id: entry.id, organizationId: entry.organizationId, tenantId: entry.tenantId },\n events: staffTimeEntryCrudEvents,\n indexer: timeEntryCrudIndexer,\n })\n },\n}\n\nconst deleteTimeEntryCommand: CommandHandler<{ id?: string }, { timeEntryId: string }> = {\n id: 'staff.timesheets.time_entries.delete',\n async prepare(input, ctx) {\n const id = input?.id\n if (!id) throw new CrudHttpError(400, { error: 'Time entry id is required.' })\n const em = (ctx.container.resolve('em') as EntityManager)\n const snapshot = await loadTimeEntrySnapshot(em, id, staffSnapshotScopeFromContext(ctx))\n if (!snapshot) return {}\n return { before: snapshot }\n },\n async execute(input, ctx) {\n const id = input?.id\n if (!id) throw new CrudHttpError(400, { error: 'Time entry id is required.' })\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const scope = commandActorScope(ctx)\n const entry = await findOneWithDecryption(\n em,\n StaffTimeEntry,\n applyScopeToWhere<StaffTimeEntry>({ id, deletedAt: null }, scope),\n undefined,\n scopeForDecryption(scope),\n )\n if (!entry) throw new CrudHttpError(404, { error: 'Time entry not found.' })\n ensureTenantScope(ctx, entry.tenantId)\n ensureOrganizationScope(ctx, entry.organizationId)\n\n // A closed report renders these minutes; deleting the row would leave the\n // report totalling hours that no longer exist.\n assertTimeEntryUnlocked(entry, (await resolveTranslations()).translate)\n\n // Ownership enforcement: callers without `staff.timesheets.manage_all`\n // can only delete entries they own.\n const hasManageAll = await callerHasManageAll(ctx)\n if (!hasManageAll) {\n const callerStaffMemberId = await resolveCallerStaffMemberId(em, ctx)\n if (!callerStaffMemberId || entry.staffMemberId !== callerStaffMemberId) {\n const { translate } = await resolveTranslations()\n throw new CrudHttpError(403, {\n error: translate('staff.timesheets.errors.notOwner', 'You can only manage your own time entries.'),\n })\n }\n }\n\n const settings = await readEntrySettings(ctx, entry.tenantId)\n await assertEntryProjectAccess({\n access: await resolveEntryProjectAccess(em, ctx, entry.tenantId, entry.organizationId, settings),\n hasManageAll,\n projectId: entry.timeProjectId ?? null,\n entryStaffMemberId: entry.staffMemberId,\n })\n\n entry.deletedAt = new Date()\n entry.updatedAt = new Date()\n await em.flush()\n\n await emitCrudSideEffects({\n dataEngine: ctx.container.resolve('dataEngine'),\n action: 'deleted',\n entity: entry,\n identifiers: { id: entry.id, organizationId: entry.organizationId, tenantId: entry.tenantId },\n events: staffTimeEntryCrudEvents,\n indexer: timeEntryCrudIndexer,\n })\n\n return { timeEntryId: entry.id }\n },\n buildLog: async ({ snapshots }) => {\n const before = snapshots.before as TimeEntrySnapshot | undefined\n if (!before) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('staff.audit.timesheets.time_entries.delete', 'Delete time entry'),\n resourceKind: 'staff.timesheets.time_entry',\n resourceId: before.id,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n snapshotBefore: before,\n payload: {\n undo: {\n before,\n } satisfies TimeEntryUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<TimeEntryUndoPayload>(logEntry)\n const before = payload?.before\n if (!before) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n let entry = await em.findOne(StaffTimeEntry, scopedStaffSnapshotWhere(before.id, staffSnapshotScopeFromSnapshot(before)))\n if (!entry) {\n entry = em.create(StaffTimeEntry, {\n id: before.id,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n staffMemberId: before.staffMemberId,\n date: before.date as unknown as Date,\n durationMinutes: before.durationMinutes,\n roundedMinutes: before.roundedMinutes ?? null,\n startedAt: before.startedAt ? new Date(before.startedAt) : null,\n endedAt: before.endedAt ? new Date(before.endedAt) : null,\n notes: before.notes ?? null,\n timeProjectId: before.timeProjectId ?? null,\n taskId: before.taskId ?? null,\n customerId: before.customerId ?? null,\n dealId: before.dealId ?? null,\n orderId: before.orderId ?? null,\n isBillable: before.isBillable ?? true,\n rateOverrideAmount: before.rateOverrideAmount ?? null,\n rateCurrencyCode: before.rateCurrencyCode ?? null,\n source: (before.source ?? 'manual') as StaffTimeEntrySource,\n deletedAt: null,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(entry)\n } else {\n assertTimeEntryUnlocked(entry, (await resolveTranslations()).translate)\n entry.staffMemberId = before.staffMemberId\n entry.date = before.date as unknown as Date\n entry.durationMinutes = before.durationMinutes\n entry.roundedMinutes = before.roundedMinutes ?? null\n entry.startedAt = before.startedAt ? new Date(before.startedAt) : null\n entry.endedAt = before.endedAt ? new Date(before.endedAt) : null\n entry.notes = before.notes ?? null\n entry.timeProjectId = before.timeProjectId ?? null\n entry.taskId = before.taskId ?? null\n entry.customerId = before.customerId ?? null\n entry.dealId = before.dealId ?? null\n entry.orderId = before.orderId ?? null\n entry.isBillable = before.isBillable ?? true\n entry.rateOverrideAmount = before.rateOverrideAmount ?? null\n entry.rateCurrencyCode = before.rateCurrencyCode ?? null\n entry.source = (before.source ?? 'manual') as StaffTimeEntrySource\n entry.deletedAt = null\n entry.updatedAt = new Date()\n }\n await em.flush()\n\n await emitCrudUndoSideEffects({\n dataEngine: ctx.container.resolve('dataEngine'),\n action: 'created',\n entity: entry,\n identifiers: { id: entry.id, organizationId: entry.organizationId, tenantId: entry.tenantId },\n events: staffTimeEntryCrudEvents,\n indexer: timeEntryCrudIndexer,\n })\n },\n}\n\nexport const staffTimeEntryDuplicateSchema = z.object({\n id: z.string().uuid(),\n // The copy usually lands on another day (US-D6 \"log the same thing again\"), so\n // the caller may move it; omitted, it keeps the source's day.\n date: z.coerce.date().optional(),\n // US-D6 ends \"\u2026I adjust the duration and the date\", so both adjustments travel\n // with the copy request rather than costing a second round trip that would\n // leave the unadjusted duration visible in between. Omitted, the copy keeps the\n // source's minutes. The rounded twin is always recomputed by the create command\n // from the tenant rule, so an overridden duration can never carry the source's\n // stale `rounded_minutes` into a new entry (D-7).\n durationMinutes: z.number().int().min(0).max(1440).optional(),\n // Tags are NOT read off the source here: this file writes tags only through the\n // tag commands and never touches `staff_time_entry_tags`, so the caller \u2014 which\n // already has the source's `tags[]` from the list response \u2014 passes them on.\n tagIds: z.array(z.string().uuid()).max(50).optional(),\n})\n\nexport type StaffTimeEntryDuplicateInput = z.infer<typeof staffTimeEntryDuplicateSchema>\n\n/**\n * US-D6. The fourth write path TC-TT-018 exercises. It resolves the SOURCE entry\n * and runs it past the lock gate before copying: screen 10 note 4 hides every row\n * action on a locked row, so an API that happily duplicated one would be a side\n * door back into a frozen report's numbers.\n *\n * The copy itself is delegated to the create command rather than re-implemented,\n * so it inherits ownership enforcement, project access, the billable/currency\n * defaults, rounding (D-7), tag syncing and the audit entry unchanged.\n */\nconst duplicateTimeEntryCommand: CommandHandler<StaffTimeEntryDuplicateInput, { timeEntryId: string }> = {\n id: 'staff.timesheets.time_entries.duplicate',\n async execute(rawInput, ctx) {\n const parsed = staffTimeEntryDuplicateSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const scope = commandActorScope(ctx)\n const source = await findOneWithDecryption(\n em,\n StaffTimeEntry,\n applyScopeToWhere<StaffTimeEntry>({ id: parsed.id, deletedAt: null }, scope),\n undefined,\n scopeForDecryption(scope),\n )\n if (!source) throw new CrudHttpError(404, { error: 'Time entry not found.' })\n ensureTenantScope(ctx, source.tenantId)\n ensureOrganizationScope(ctx, source.organizationId)\n\n assertTimeEntryUnlocked(source, (await resolveTranslations()).translate)\n\n const bus = ctx.container.resolve('commandBus') as CommandBus\n const created = await bus.execute<unknown, { timeEntryId: string }>(createTimeEntryCommand.id, {\n input: {\n tenantId: source.tenantId,\n organizationId: source.organizationId,\n staffMemberId: source.staffMemberId,\n date: parsed.date ?? source.date,\n durationMinutes: parsed.durationMinutes ?? source.durationMinutes,\n timeProjectId: source.timeProjectId ?? null,\n taskId: source.taskId ?? null,\n customerId: source.customerId ?? null,\n dealId: source.dealId ?? null,\n orderId: source.orderId ?? null,\n isBillable: source.isBillable,\n rateOverrideAmount:\n source.rateOverrideAmount === null || source.rateOverrideAmount === undefined\n ? null\n : Number(source.rateOverrideAmount),\n description: source.notes ?? null,\n // A copy is typed, not timed: carrying `started_at`/`ended_at` over would\n // manufacture an overlap with the entry it was copied from (US-D7).\n source: 'manual' as StaffTimeEntrySource,\n ...(parsed.tagIds !== undefined ? { tagIds: parsed.tagIds } : {}),\n },\n ctx,\n })\n\n const timeEntryId = created?.result?.timeEntryId\n if (!timeEntryId) throw new CrudHttpError(400, { error: 'Failed to duplicate time entry.' })\n return { timeEntryId }\n },\n}\n\nexport const staffTimeEntryReapplyRoundingSchema = z.object({\n tenantId: z.string().uuid(),\n organizationId: z.string().uuid(),\n entryIds: z.array(z.string().uuid()).min(1).max(500),\n})\n\nexport type StaffTimeEntryReapplyRoundingInput = z.infer<typeof staffTimeEntryReapplyRoundingSchema>\n\nexport type StaffTimeEntryReapplyRoundingResult = {\n /** Entries whose `rounded_minutes` actually moved. */\n updatedCount: number\n /** Entries already at the current rule \u2014 read, compared, left alone. */\n unchangedCount: number\n /** Requested ids that were locked, deleted or out of scope, and therefore never loaded. */\n skippedCount: number\n}\n\n/**\n * Retroactive rounding (T7.3, screen 16 note 3) \u2014 restates `rounded_minutes` on a\n * batch of existing entries under the tenant's current rule.\n *\n * Three properties, in the order they matter:\n *\n * 1. **A locked entry is unreachable, not merely skipped.** `lockedReportId: null`\n * is part of the WHERE clause, so an entry frozen into a closed report is never\n * loaded, never compared and never assigned. The guarantee is structural: there\n * is no branch in this handler that could write one.\n * 2. **It is an explicit, batched administrative action**, driven by the\n * `reapply-rounding` ProgressJob worker, never a side effect of saving the\n * settings form. Rewriting billing history silently is exactly the failure the\n * spec's risk R1 is about.\n * 3. **It writes one derived column and nothing else.** No clock is re-anchored,\n * no duration is touched \u2014 `duration_minutes` remains the raw observation.\n *\n * Reachable only by a privileged actor: the queue worker (`systemActor`) or a\n * super-admin. An interactive caller cannot rewrite a tenant's history through the\n * command bus by guessing the id; the route that enqueues the job is the gate, and\n * it is gated on `staff.timesheets.settings.manage`.\n */\nconst reapplyRoundingCommand: CommandHandler<\n StaffTimeEntryReapplyRoundingInput,\n StaffTimeEntryReapplyRoundingResult\n> = {\n id: 'staff.timesheets.time_entries.reapply_rounding',\n async execute(input, ctx) {\n const parsed = staffTimeEntryReapplyRoundingSchema.parse(input ?? {})\n if (ctx.systemActor !== true && ctx.auth?.isSuperAdmin !== true) {\n const { translate } = await resolveTranslations()\n throw new CrudHttpError(403, { error: translate('staff.errors.forbidden', 'Forbidden') })\n }\n\n const scope = commandInputScope(ctx, parsed.tenantId, parsed.organizationId)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const settings = await readEntrySettings(ctx, parsed.tenantId)\n\n const entries = await em.find(\n StaffTimeEntry,\n applyScopeToWhere<StaffTimeEntry>(\n {\n id: { $in: parsed.entryIds },\n deletedAt: null,\n // Non-negotiable: a frozen entry has already been billed at the value it\n // carries, so it is excluded at the query, not at an `if`.\n lockedReportId: null,\n },\n scope,\n ),\n )\n\n const changed: StaffTimeEntry[] = []\n let unchangedCount = 0\n for (const entry of entries) {\n const next = roundedMinutesFor(entry.durationMinutes, settings, {\n tenantId: entry.tenantId,\n organizationId: entry.organizationId,\n })\n if (entry.roundedMinutes === next) {\n unchangedCount += 1\n continue\n }\n entry.roundedMinutes = next\n entry.updatedAt = new Date()\n changed.push(entry)\n }\n\n if (changed.length > 0) await em.flush()\n\n const dataEngine = ctx.container.resolve('dataEngine') as Parameters<\n typeof emitCrudSideEffects\n >[0]['dataEngine']\n for (const entry of changed) {\n await emitCrudSideEffects({\n dataEngine,\n action: 'updated',\n entity: entry,\n identifiers: { id: entry.id, organizationId: entry.organizationId, tenantId: entry.tenantId },\n events: staffTimeEntryCrudEvents,\n indexer: timeEntryCrudIndexer,\n })\n }\n\n return {\n updatedCount: changed.length,\n unchangedCount,\n skippedCount: Math.max(0, parsed.entryIds.length - entries.length),\n }\n },\n}\n\nregisterCommand(createTimeEntryCommand)\nregisterCommand(startTimerCommand)\nregisterCommand(updateTimeEntryCommand)\nregisterCommand(deleteTimeEntryCommand)\nregisterCommand(duplicateTimeEntryCommand)\nregisterCommand(reapplyRoundingCommand)\n\nexport const staffTimeEntryCommandIds = {\n create: createTimeEntryCommand.id,\n startTimer: startTimerCommand.id,\n update: updateTimeEntryCommand.id,\n delete: deleteTimeEntryCommand.id,\n duplicate: duplicateTimeEntryCommand.id,\n reapplyRounding: reapplyRoundingCommand.id,\n} as const\n"],
5
+ "mappings": "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",
6
6
  "names": ["derived", "startedAt", "entry"]
7
7
  }
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import { registerCommand } from "@open-mercato/shared/lib/commands";
3
3
  import { CrudHttpError, isUniqueViolation } from "@open-mercato/shared/lib/crud/errors";
4
4
  import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
5
+ import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
5
6
  import { buildChanges, emitCrudSideEffects, emitCrudUndoSideEffects } from "@open-mercato/shared/lib/commands/helpers";
6
7
  import { withAtomicFlush } from "@open-mercato/shared/lib/commands/flush";
7
8
  import {
@@ -639,17 +640,33 @@ async function requireEntryTarget(em, timeEntryId, scope, translate) {
639
640
  );
640
641
  if (!entry) throw targetNotFoundError(translate, "time_entry");
641
642
  if (entry.lockedReportId) {
642
- throw new CrudHttpError(409, {
643
- code: TAG_TARGET_LOCKED_CODE,
644
- error: translate(
645
- "staff.time_tracking.tags.errors.entryLocked",
646
- "This time entry is locked in a closed report and its tags cannot be changed."
647
- ),
648
- lockedReportId: entry.lockedReportId
649
- });
643
+ throw entryTagsLockedError(translate, entry.lockedReportId);
650
644
  }
651
645
  return { id: entry.id, tenantId: entry.tenantId, organizationId: entry.organizationId };
652
646
  }
647
+ function entryTagsLockedError(translate, lockedReportId) {
648
+ return new CrudHttpError(409, {
649
+ code: TAG_TARGET_LOCKED_CODE,
650
+ error: translate(
651
+ "staff.time_tracking.tags.errors.entryLocked",
652
+ "This time entry is locked in a closed report and its tags cannot be changed."
653
+ ),
654
+ lockedReportId
655
+ });
656
+ }
657
+ async function assertEntryTagUndoAllowed(payload, ctx) {
658
+ const em = ctx.container.resolve("em").fork();
659
+ const entry = await findOneWithDecryption(
660
+ em,
661
+ StaffTimeEntry,
662
+ { id: payload.targetId, tenantId: payload.tenantId, organizationId: payload.organizationId },
663
+ void 0,
664
+ { tenantId: payload.tenantId, organizationId: payload.organizationId }
665
+ );
666
+ if (!entry?.lockedReportId) return;
667
+ const { translate } = await resolveTranslations();
668
+ throw entryTagsLockedError(translate, entry.lockedReportId);
669
+ }
653
670
  function refreshTargetSideEffects(ops) {
654
671
  return async (ctx, target) => {
655
672
  const dataEngine = ctx.container.resolve("dataEngine");
@@ -951,6 +968,7 @@ const assignEntryTagsCommand = {
951
968
  undo: async ({ logEntry, ctx }) => {
952
969
  const payload = extractUndoPayload(logEntry);
953
970
  if (!payload) return;
971
+ await assertEntryTagUndoAllowed(payload, ctx);
954
972
  await restoreAssignmentState(entryJunctionOps, payload, ctx);
955
973
  }
956
974
  };
@@ -994,6 +1012,7 @@ const unassignEntryTagsCommand = {
994
1012
  undo: async ({ logEntry, ctx }) => {
995
1013
  const payload = extractUndoPayload(logEntry);
996
1014
  if (!payload) return;
1015
+ await assertEntryTagUndoAllowed(payload, ctx);
997
1016
  await restoreAssignmentState(entryJunctionOps, payload, ctx);
998
1017
  }
999
1018
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/staff/commands/timesheets-tags.ts"],
4
- "sourcesContent": ["/**\n * Tag commands for the consulting suite (T3.4).\n *\n * A tag is org-wide vocabulary \u2014 screen 6 renders it as a card badge, screen 7 as\n * a chip in the task drawer, screens 8 and 10 on the time entry. The tag itself is\n * therefore project-agnostic; only its *assignments* touch project-scoped rows,\n * which is why the access filtering lives on the assignment routes rather than\n * here.\n *\n * Three decisions the callers depend on:\n *\n * 1. `slug` is unique per organization+tenant (partial unique index, live rows\n * only). A collision comes back as a 409 carrying `fieldErrors.slug` so the\n * form marks the field instead of showing a dead 500.\n * 2. **Deleting a tag cascades to its assignments and reports the count.** The\n * alternatives are worse: silently orphaning 200 entry badges hides the blast\n * radius, and refusing until the caller hand-unpicks 200 rows gives them no\n * way forward. The tag is soft-deleted, the junction rows are removed, the\n * response carries `removedTaskAssignments` / `removedEntryAssignments`, and\n * undo puts both the tag and every removed assignment back.\n * 3. **Assignment is idempotent.** Both junctions carry a unique index, so\n * re-sending a tag the target already has is a no-op success, never a unique\n * violation. The already-present ids come back in `alreadyAssignedTagIds` so\n * the UI can tell \"added\" from \"was already there\". Unassigning a tag that is\n * not assigned is the same kind of no-op.\n */\n\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandHandler } from '@open-mercato/shared/lib/commands'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport { CrudHttpError, isUniqueViolation } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { buildChanges, emitCrudSideEffects, emitCrudUndoSideEffects } from '@open-mercato/shared/lib/commands/helpers'\nimport { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport type { CrudIndexerConfig } from '@open-mercato/shared/lib/crud/types'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport {\n StaffTimeEntry,\n StaffTimeEntryTag,\n StaffTimeTag,\n StaffTimeTask,\n StaffTimeTaskTag,\n} from '../data/entities'\nimport {\n staffTimeTagCreateSchema,\n staffTimeEntryTagAssignmentSchema,\n staffTimeTaskTagAssignmentSchema,\n type StaffTimeTagCreateInput,\n type StaffTimeEntryTagAssignmentInput,\n type StaffTimeTaskTagAssignmentInput,\n} from '../data/validators'\nimport {\n applyScopeToWhere,\n commandActorScope,\n commandInputScope,\n ensureOrganizationScope,\n ensureTenantScope,\n extractUndoPayload,\n scopedStaffSnapshotWhere,\n staffSnapshotScopeFromContext,\n staffSnapshotScopeFromSnapshot,\n type StaffCommandScope,\n type StaffSnapshotScope,\n} from './shared'\n\nexport const staffTimeTagCommandIds = {\n create: 'staff.timesheets.tags.create',\n update: 'staff.timesheets.tags.update',\n delete: 'staff.timesheets.tags.delete',\n assignTask: 'staff.timesheets.tags.assign_task',\n unassignTask: 'staff.timesheets.tags.unassign_task',\n assignEntry: 'staff.timesheets.tags.assign_entry',\n unassignEntry: 'staff.timesheets.tags.unassign_entry',\n} as const\n\nexport const TAG_SLUG_DUPLICATE_CODE = 'time_tag_slug_duplicate'\nexport const TAG_UNKNOWN_IDS_CODE = 'time_tag_unknown_ids'\nexport const TAG_TARGET_LOCKED_CODE = 'time_entry_locked'\n\n/**\n * An assignment snapshot is written into the audit payload so undo can put the\n * junction rows back. A tag used across thousands of rows would bloat the log, so\n * the snapshot stops here and says so; undo then restores what it captured.\n */\nexport const TAG_ASSIGNMENT_SNAPSHOT_LIMIT = 500\n\nconst tagCrudIndexer: CrudIndexerConfig<StaffTimeTag> = {\n entityType: 'staff:staff_time_tag',\n}\n\nconst taskCrudIndexer: CrudIndexerConfig<StaffTimeTask> = {\n entityType: 'staff:staff_time_task',\n}\n\nconst timeEntryCrudIndexer: CrudIndexerConfig<StaffTimeEntry> = {\n entityType: 'staff:staff_time_entry',\n}\n\n// No CRUD event ids are declared for tags in `events.ts`: a tag write is\n// vocabulary maintenance the screens re-read, so these commands queue index and\n// cache side effects only.\n\ntype Translate = (key: string, fallback: string) => string\n\n/**\n * `data/validators.ts` ships the create shape only. The update schema is derived\n * from it rather than restated so the slug/label/color rules cannot drift apart.\n */\nexport const staffTimeTagUpdateSchema = z.object({\n id: z.string().uuid(),\n slug: staffTimeTagCreateSchema.shape.slug.optional(),\n label: staffTimeTagCreateSchema.shape.label.optional(),\n color: staffTimeTagCreateSchema.shape.color,\n})\n\nexport type StaffTimeTagUpdateInput = z.infer<typeof staffTimeTagUpdateSchema>\n\ntype TagSnapshot = {\n id: string\n tenantId: string\n organizationId: string\n slug: string\n label: string\n color: string | null\n deletedAt: string | null\n}\n\ntype TagAssignmentSnapshot = {\n id: string\n tenantId: string\n organizationId: string\n tagId: string\n targetId: string\n}\n\ntype TagUndoPayload = {\n before?: TagSnapshot | null\n after?: TagSnapshot | null\n taskAssignments?: TagAssignmentSnapshot[] | null\n entryAssignments?: TagAssignmentSnapshot[] | null\n assignmentsTruncated?: boolean\n}\n\ntype AssignmentUndoPayload = {\n target: 'task' | 'time_entry'\n targetId: string\n tenantId: string\n organizationId: string\n tagIdsBefore: string[]\n tagIdsAfter: string[]\n added: string[]\n removed: TagAssignmentSnapshot[]\n}\n\nfunction toTagSnapshot(tag: StaffTimeTag): TagSnapshot {\n return {\n id: tag.id,\n tenantId: tag.tenantId,\n organizationId: tag.organizationId,\n slug: tag.slug,\n label: tag.label,\n color: tag.color ?? null,\n deletedAt: tag.deletedAt ? tag.deletedAt.toISOString() : null,\n }\n}\n\nasync function loadTagSnapshot(\n em: EntityManager,\n id: string,\n scope?: StaffSnapshotScope | null,\n): Promise<TagSnapshot | null> {\n const tag = await em.findOne(StaffTimeTag, scopedStaffSnapshotWhere(id, scope))\n return tag ? toTagSnapshot(tag) : null\n}\n\nfunction slugDuplicateError(translate: Translate, slug: string): CrudHttpError {\n const message = translate(\n 'staff.time_tracking.tags.errors.slugDuplicate',\n 'A tag with this slug already exists.',\n )\n return new CrudHttpError(409, {\n code: TAG_SLUG_DUPLICATE_CODE,\n error: message,\n slug,\n fieldErrors: { slug: message },\n })\n}\n\nfunction tagNotFoundError(translate: Translate): CrudHttpError {\n return new CrudHttpError(404, {\n error: translate('staff.time_tracking.tags.errors.notFound', 'Tag not found or not accessible.'),\n })\n}\n\nfunction unknownTagIdsError(translate: Translate, unknownIds: string[]): CrudHttpError {\n const message = translate(\n 'staff.time_tracking.tags.errors.unknownIds',\n 'Some tags do not exist in this organization.',\n )\n return new CrudHttpError(422, {\n code: TAG_UNKNOWN_IDS_CODE,\n error: message,\n unknownIds,\n fieldErrors: { tagIds: message },\n })\n}\n\nfunction targetNotFoundError(translate: Translate, target: 'task' | 'time_entry'): CrudHttpError {\n // Deliberately the same 404 for \"does not exist\" and \"not yours\": tagging must\n // not become a way to probe which task or entry ids exist (screen 17 note 1).\n const message =\n target === 'task'\n ? translate('staff.time_tracking.tags.errors.taskNotFound', 'Task not found or not accessible.')\n : translate('staff.time_tracking.tags.errors.entryNotFound', 'Time entry not found or not accessible.')\n return new CrudHttpError(404, { error: message })\n}\n\n/**\n * Persistence for one junction table. The command bodies below are shared; only\n * these closures know which table and which target column they address, so each\n * ORM call stays fully typed against its own entity.\n */\ntype TagJunctionOps = {\n target: 'task' | 'time_entry'\n resourceKind: string\n loadAssignments: (\n em: EntityManager,\n targetId: string,\n scope: StaffCommandScope,\n ) => Promise<TagAssignmentSnapshot[]>\n loadAssignmentsForSnapshot: (\n em: EntityManager,\n targetId: string,\n scope: StaffSnapshotScope | null,\n ) => Promise<TagAssignmentSnapshot[]>\n loadAssignmentsByTag: (\n em: EntityManager,\n tagId: string,\n scope: StaffCommandScope,\n ) => Promise<TagAssignmentSnapshot[]>\n createAssignment: (em: EntityManager, row: Omit<TagAssignmentSnapshot, 'id'>) => void\n deleteAssignments: (em: EntityManager, ids: string[], scope: StaffCommandScope) => Promise<number>\n deleteAssignmentsByTag: (em: EntityManager, tagId: string, scope: StaffCommandScope) => Promise<number>\n restoreAssignments: (em: EntityManager, rows: TagAssignmentSnapshot[]) => Promise<void>\n}\n\nfunction snapshotScopeWhere(scope?: StaffSnapshotScope | null): { tenantId?: string; organizationId?: string } {\n const where: { tenantId?: string; organizationId?: string } = {}\n if (scope?.tenantId) where.tenantId = scope.tenantId\n if (scope?.organizationId) where.organizationId = scope.organizationId\n return where\n}\n\nconst taskJunctionOps: TagJunctionOps = {\n target: 'task',\n resourceKind: 'staff.timesheets.task_tag',\n loadAssignments: async (em, targetId, scope) => {\n const rows = await em.find(StaffTimeTaskTag, applyScopeToWhere<StaffTimeTaskTag>({ taskId: targetId }, scope))\n return rows.map((row) => ({\n id: row.id,\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n tagId: row.tagId,\n targetId: row.taskId,\n }))\n },\n loadAssignmentsForSnapshot: async (em, targetId, scope) => {\n const rows = await em.find(StaffTimeTaskTag, { taskId: targetId, ...snapshotScopeWhere(scope) })\n return rows.map((row) => ({\n id: row.id,\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n tagId: row.tagId,\n targetId: row.taskId,\n }))\n },\n loadAssignmentsByTag: async (em, tagId, scope) => {\n const rows = await em.find(StaffTimeTaskTag, applyScopeToWhere<StaffTimeTaskTag>({ tagId }, scope), {\n limit: TAG_ASSIGNMENT_SNAPSHOT_LIMIT + 1,\n })\n return rows.map((row) => ({\n id: row.id,\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n tagId: row.tagId,\n targetId: row.taskId,\n }))\n },\n createAssignment: (em, row) => {\n em.persist(\n em.create(StaffTimeTaskTag, {\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n tagId: row.tagId,\n taskId: row.targetId,\n createdAt: new Date(),\n }),\n )\n },\n deleteAssignments: async (em, ids, scope) =>\n em.nativeDelete(StaffTimeTaskTag, applyScopeToWhere<StaffTimeTaskTag>({ id: { $in: ids } }, scope)),\n deleteAssignmentsByTag: async (em, tagId, scope) =>\n em.nativeDelete(StaffTimeTaskTag, applyScopeToWhere<StaffTimeTaskTag>({ tagId }, scope)),\n restoreAssignments: async (em, rows) => {\n for (const row of rows) {\n const existing = await em.findOne(StaffTimeTaskTag, {\n tagId: row.tagId,\n taskId: row.targetId,\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n })\n if (existing) continue\n em.persist(\n em.create(StaffTimeTaskTag, {\n id: row.id,\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n tagId: row.tagId,\n taskId: row.targetId,\n createdAt: new Date(),\n }),\n )\n }\n },\n}\n\nconst entryJunctionOps: TagJunctionOps = {\n target: 'time_entry',\n resourceKind: 'staff.timesheets.entry_tag',\n loadAssignments: async (em, targetId, scope) => {\n const rows = await em.find(\n StaffTimeEntryTag,\n applyScopeToWhere<StaffTimeEntryTag>({ timeEntryId: targetId }, scope),\n )\n return rows.map((row) => ({\n id: row.id,\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n tagId: row.tagId,\n targetId: row.timeEntryId,\n }))\n },\n loadAssignmentsForSnapshot: async (em, targetId, scope) => {\n const rows = await em.find(StaffTimeEntryTag, { timeEntryId: targetId, ...snapshotScopeWhere(scope) })\n return rows.map((row) => ({\n id: row.id,\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n tagId: row.tagId,\n targetId: row.timeEntryId,\n }))\n },\n loadAssignmentsByTag: async (em, tagId, scope) => {\n const rows = await em.find(StaffTimeEntryTag, applyScopeToWhere<StaffTimeEntryTag>({ tagId }, scope), {\n limit: TAG_ASSIGNMENT_SNAPSHOT_LIMIT + 1,\n })\n return rows.map((row) => ({\n id: row.id,\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n tagId: row.tagId,\n targetId: row.timeEntryId,\n }))\n },\n createAssignment: (em, row) => {\n em.persist(\n em.create(StaffTimeEntryTag, {\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n tagId: row.tagId,\n timeEntryId: row.targetId,\n createdAt: new Date(),\n }),\n )\n },\n deleteAssignments: async (em, ids, scope) =>\n em.nativeDelete(StaffTimeEntryTag, applyScopeToWhere<StaffTimeEntryTag>({ id: { $in: ids } }, scope)),\n deleteAssignmentsByTag: async (em, tagId, scope) =>\n em.nativeDelete(StaffTimeEntryTag, applyScopeToWhere<StaffTimeEntryTag>({ tagId }, scope)),\n restoreAssignments: async (em, rows) => {\n for (const row of rows) {\n const existing = await em.findOne(StaffTimeEntryTag, {\n tagId: row.tagId,\n timeEntryId: row.targetId,\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n })\n if (existing) continue\n em.persist(\n em.create(StaffTimeEntryTag, {\n id: row.id,\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n tagId: row.tagId,\n timeEntryId: row.targetId,\n createdAt: new Date(),\n }),\n )\n }\n },\n}\n\nasync function requireKnownTags(\n em: EntityManager,\n tagIds: string[],\n scope: StaffCommandScope,\n translate: Translate,\n): Promise<void> {\n const unique = Array.from(new Set(tagIds))\n if (unique.length === 0) return\n const tags = await em.find(\n StaffTimeTag,\n applyScopeToWhere<StaffTimeTag>({ id: { $in: unique }, deletedAt: null }, scope),\n { fields: ['id'] },\n )\n const known = new Set(tags.map((tag) => tag.id))\n const unknownIds = unique.filter((id) => !known.has(id))\n if (unknownIds.length > 0) throw unknownTagIdsError(translate, unknownIds)\n}\n\nconst createTagCommand: CommandHandler<StaffTimeTagCreateInput, { tagId: string }> = {\n id: staffTimeTagCommandIds.create,\n async prepare(rawInput, ctx) {\n const parsed = staffTimeTagCreateSchema.safeParse(rawInput)\n if (!parsed.success) return {}\n const em = ctx.container.resolve('em') as EntityManager\n const existing = await em.findOne(StaffTimeTag, {\n slug: parsed.data.slug,\n tenantId: parsed.data.tenantId,\n organizationId: parsed.data.organizationId,\n deletedAt: null,\n })\n return { before: { slugTaken: existing != null } }\n },\n async execute(rawInput, ctx) {\n const parsed = staffTimeTagCreateSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n const scope = commandInputScope(ctx, parsed.tenantId, parsed.organizationId)\n const { translate } = await resolveTranslations()\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n let created: StaffTimeTag | null = null\n\n try {\n await withAtomicFlush(\n em,\n [\n async () => {\n // The read lives in the same transaction as the insert, so a\n // concurrent create cannot slip a second row past this check; the\n // partial unique index is the backstop and is translated below.\n const duplicate = await em.findOne(\n StaffTimeTag,\n applyScopeToWhere<StaffTimeTag>({ slug: parsed.slug, deletedAt: null }, scope),\n { fields: ['id'] },\n )\n if (duplicate) throw slugDuplicateError(translate, parsed.slug)\n },\n () => {\n created = em.create(StaffTimeTag, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n slug: parsed.slug,\n label: parsed.label,\n color: parsed.color ?? null,\n createdAt: new Date(),\n updatedAt: new Date(),\n deletedAt: null,\n })\n em.persist(created)\n },\n ],\n { transaction: true, label: staffTimeTagCommandIds.create },\n )\n } catch (err) {\n if (isUniqueViolation(err)) throw slugDuplicateError(translate, parsed.slug)\n throw err\n }\n\n const record = created as StaffTimeTag | null\n if (!record) throw tagNotFoundError(translate)\n\n await emitCrudSideEffects({\n dataEngine: ctx.container.resolve('dataEngine'),\n action: 'created',\n entity: record,\n identifiers: { id: record.id, organizationId: record.organizationId, tenantId: record.tenantId },\n indexer: tagCrudIndexer,\n })\n\n return { tagId: record.id }\n },\n captureAfter: async (_input, result, ctx) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const snapshot = await loadTagSnapshot(em, result.tagId, staffSnapshotScopeFromContext(ctx))\n if (!snapshot) return null\n return { snapshot }\n },\n buildLog: async ({ result, ctx }) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const snapshot = await loadTagSnapshot(em, result.tagId, staffSnapshotScopeFromContext(ctx))\n if (!snapshot) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('staff.audit.timesheets.tags.create', 'Create tag'),\n resourceKind: 'staff.timesheets.tag',\n resourceId: snapshot.id,\n tenantId: snapshot.tenantId,\n organizationId: snapshot.organizationId,\n snapshotAfter: snapshot,\n payload: {\n undo: { after: snapshot } satisfies TagUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<TagUndoPayload>(logEntry)\n const after = payload?.after\n if (!after) return\n const snapshotScope = staffSnapshotScopeFromSnapshot(after)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const tag = await em.findOne(StaffTimeTag, scopedStaffSnapshotWhere(after.id, snapshotScope))\n if (!tag) return\n\n await withAtomicFlush(\n em,\n [\n () => {\n tag.deletedAt = new Date()\n tag.updatedAt = new Date()\n },\n ],\n { transaction: true, label: `${staffTimeTagCommandIds.create}.undo` },\n )\n\n await emitCrudUndoSideEffects({\n dataEngine: ctx.container.resolve('dataEngine'),\n action: 'deleted',\n entity: tag,\n identifiers: { id: tag.id, organizationId: tag.organizationId, tenantId: tag.tenantId },\n indexer: tagCrudIndexer,\n })\n },\n}\n\nconst updateTagCommand: CommandHandler<StaffTimeTagUpdateInput, { tagId: string }> = {\n id: staffTimeTagCommandIds.update,\n async prepare(rawInput, ctx) {\n const parsed = staffTimeTagUpdateSchema.safeParse(rawInput)\n if (!parsed.success) return {}\n const em = ctx.container.resolve('em') as EntityManager\n const before = await loadTagSnapshot(em, parsed.data.id, staffSnapshotScopeFromContext(ctx))\n if (!before) return {}\n return { before: { snapshot: before } }\n },\n async execute(rawInput, ctx) {\n const parsed = staffTimeTagUpdateSchema.parse(rawInput)\n const { translate } = await resolveTranslations()\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const scope = commandActorScope(ctx)\n\n const tag = await em.findOne(StaffTimeTag, applyScopeToWhere<StaffTimeTag>({ id: parsed.id, deletedAt: null }, scope))\n if (!tag) throw tagNotFoundError(translate)\n ensureTenantScope(ctx, tag.tenantId)\n ensureOrganizationScope(ctx, tag.organizationId)\n\n try {\n await withAtomicFlush(\n em,\n [\n async () => {\n if (parsed.slug === undefined || parsed.slug === tag.slug) return\n const duplicate = await em.findOne(\n StaffTimeTag,\n applyScopeToWhere<StaffTimeTag>({ slug: parsed.slug, deletedAt: null, id: { $ne: tag.id } }, scope),\n { fields: ['id'] },\n )\n if (duplicate) throw slugDuplicateError(translate, parsed.slug)\n },\n () => {\n if (parsed.slug !== undefined) tag.slug = parsed.slug\n if (parsed.label !== undefined) tag.label = parsed.label\n if (parsed.color !== undefined) tag.color = parsed.color ?? null\n tag.updatedAt = new Date()\n },\n ],\n { transaction: true, label: staffTimeTagCommandIds.update },\n )\n } catch (err) {\n if (isUniqueViolation(err)) throw slugDuplicateError(translate, parsed.slug ?? tag.slug)\n throw err\n }\n\n await emitCrudSideEffects({\n dataEngine: ctx.container.resolve('dataEngine'),\n action: 'updated',\n entity: tag,\n identifiers: { id: tag.id, organizationId: tag.organizationId, tenantId: tag.tenantId },\n indexer: tagCrudIndexer,\n })\n\n return { tagId: tag.id }\n },\n buildLog: async ({ snapshots, ctx }) => {\n const before = (snapshots.before as { snapshot?: TagSnapshot } | undefined)?.snapshot\n if (!before) return null\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const after = await loadTagSnapshot(em, before.id, staffSnapshotScopeFromSnapshot(before))\n if (!after) return null\n const changes = buildChanges(\n before as unknown as Record<string, unknown>,\n after as unknown as Record<string, unknown>,\n ['slug', 'label', 'color'],\n )\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('staff.audit.timesheets.tags.update', 'Update tag'),\n resourceKind: 'staff.timesheets.tag',\n resourceId: before.id,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n snapshotBefore: before,\n snapshotAfter: after,\n changes,\n payload: {\n undo: { before, after } satisfies TagUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<TagUndoPayload>(logEntry)\n const before = payload?.before\n if (!before) return\n const snapshotScope = staffSnapshotScopeFromSnapshot(before)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const tag = await em.findOne(StaffTimeTag, scopedStaffSnapshotWhere(before.id, snapshotScope))\n if (!tag) return\n\n await withAtomicFlush(\n em,\n [\n () => {\n tag.slug = before.slug\n tag.label = before.label\n tag.color = before.color ?? null\n tag.updatedAt = new Date()\n },\n ],\n { transaction: true, label: `${staffTimeTagCommandIds.update}.undo` },\n )\n\n await emitCrudUndoSideEffects({\n dataEngine: ctx.container.resolve('dataEngine'),\n action: 'updated',\n entity: tag,\n identifiers: { id: tag.id, organizationId: tag.organizationId, tenantId: tag.tenantId },\n indexer: tagCrudIndexer,\n })\n },\n}\n\ntype DeleteTagResult = {\n tagId: string\n removedTaskAssignments: number\n removedEntryAssignments: number\n}\n\nconst deleteTagCommand: CommandHandler<{ id?: string }, DeleteTagResult> = {\n id: staffTimeTagCommandIds.delete,\n async prepare(input, ctx) {\n const id = input?.id\n if (!id) return {}\n const em = ctx.container.resolve('em') as EntityManager\n const scope = staffSnapshotScopeFromContext(ctx)\n const before = await loadTagSnapshot(em, id, scope)\n if (!before) return {}\n const readScope: StaffCommandScope = {\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n requireTenant: true,\n requireOrganization: true,\n }\n const taskAssignments = await taskJunctionOps.loadAssignmentsByTag(em, id, readScope)\n const entryAssignments = await entryJunctionOps.loadAssignmentsByTag(em, id, readScope)\n const truncated =\n taskAssignments.length > TAG_ASSIGNMENT_SNAPSHOT_LIMIT || entryAssignments.length > TAG_ASSIGNMENT_SNAPSHOT_LIMIT\n return {\n before: {\n snapshot: before,\n taskAssignments: taskAssignments.slice(0, TAG_ASSIGNMENT_SNAPSHOT_LIMIT),\n entryAssignments: entryAssignments.slice(0, TAG_ASSIGNMENT_SNAPSHOT_LIMIT),\n assignmentsTruncated: truncated,\n },\n }\n },\n async execute(input, ctx) {\n const { translate } = await resolveTranslations()\n const id = input?.id\n if (!id) {\n throw new CrudHttpError(400, {\n error: translate('staff.time_tracking.tags.errors.idRequired', 'Tag id is required.'),\n })\n }\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const scope = commandActorScope(ctx)\n const tag = await em.findOne(StaffTimeTag, applyScopeToWhere<StaffTimeTag>({ id, deletedAt: null }, scope))\n if (!tag) throw tagNotFoundError(translate)\n ensureTenantScope(ctx, tag.tenantId)\n ensureOrganizationScope(ctx, tag.organizationId)\n\n let removedTaskAssignments = 0\n let removedEntryAssignments = 0\n\n await withAtomicFlush(\n em,\n [\n async () => {\n // The cascade is deliberate: the badge disappears everywhere it was\n // used, and the counts travel back so the caller sees the reach.\n removedTaskAssignments = await taskJunctionOps.deleteAssignmentsByTag(em, tag.id, scope)\n removedEntryAssignments = await entryJunctionOps.deleteAssignmentsByTag(em, tag.id, scope)\n },\n () => {\n tag.deletedAt = new Date()\n tag.updatedAt = new Date()\n },\n ],\n { transaction: true, label: staffTimeTagCommandIds.delete },\n )\n\n await emitCrudSideEffects({\n dataEngine: ctx.container.resolve('dataEngine'),\n action: 'deleted',\n entity: tag,\n identifiers: { id: tag.id, organizationId: tag.organizationId, tenantId: tag.tenantId },\n indexer: tagCrudIndexer,\n })\n\n return { tagId: tag.id, removedTaskAssignments, removedEntryAssignments }\n },\n buildLog: async ({ result, snapshots }) => {\n const prepared = snapshots.before as\n | {\n snapshot?: TagSnapshot\n taskAssignments?: TagAssignmentSnapshot[]\n entryAssignments?: TagAssignmentSnapshot[]\n assignmentsTruncated?: boolean\n }\n | undefined\n const before = prepared?.snapshot\n if (!before) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('staff.audit.timesheets.tags.delete', 'Delete tag'),\n resourceKind: 'staff.timesheets.tag',\n resourceId: before.id,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n snapshotBefore: before,\n changes: {\n removedTaskAssignments: { from: result.removedTaskAssignments, to: 0 },\n removedEntryAssignments: { from: result.removedEntryAssignments, to: 0 },\n },\n payload: {\n undo: {\n before,\n taskAssignments: prepared?.taskAssignments ?? null,\n entryAssignments: prepared?.entryAssignments ?? null,\n assignmentsTruncated: prepared?.assignmentsTruncated ?? false,\n } satisfies TagUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<TagUndoPayload>(logEntry)\n const before = payload?.before\n if (!before) return\n const snapshotScope = staffSnapshotScopeFromSnapshot(before)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n let tag = await em.findOne(StaffTimeTag, scopedStaffSnapshotWhere(before.id, snapshotScope))\n\n await withAtomicFlush(\n em,\n [\n () => {\n if (!tag) {\n tag = em.create(StaffTimeTag, {\n id: before.id,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n slug: before.slug,\n label: before.label,\n color: before.color ?? null,\n createdAt: new Date(),\n updatedAt: new Date(),\n deletedAt: null,\n })\n em.persist(tag)\n return\n }\n tag.slug = before.slug\n tag.label = before.label\n tag.color = before.color ?? null\n tag.deletedAt = null\n tag.updatedAt = new Date()\n },\n // Restoring assignments re-reads the junction rows, so it is its own phase.\n () => taskJunctionOps.restoreAssignments(em, payload?.taskAssignments ?? []),\n () => entryJunctionOps.restoreAssignments(em, payload?.entryAssignments ?? []),\n ],\n { transaction: true, label: `${staffTimeTagCommandIds.delete}.undo` },\n )\n\n await emitCrudUndoSideEffects({\n dataEngine: ctx.container.resolve('dataEngine'),\n action: 'created',\n entity: tag,\n identifiers: { id: before.id, organizationId: before.organizationId, tenantId: before.tenantId },\n indexer: tagCrudIndexer,\n })\n },\n}\n\ntype AssignmentResult = {\n targetId: string\n assignedTagIds: string[]\n alreadyAssignedTagIds: string[]\n tagIds: string[]\n}\n\ntype UnassignmentResult = {\n targetId: string\n removedTagIds: string[]\n notAssignedTagIds: string[]\n tagIds: string[]\n}\n\ntype AssignmentTarget = {\n id: string\n tenantId: string\n organizationId: string\n}\n\nasync function requireTaskTarget(\n em: EntityManager,\n taskId: string,\n scope: StaffCommandScope,\n translate: Translate,\n): Promise<AssignmentTarget> {\n const task = await em.findOne(StaffTimeTask, applyScopeToWhere<StaffTimeTask>({ id: taskId, deletedAt: null }, scope))\n if (!task) throw targetNotFoundError(translate, 'task')\n return { id: task.id, tenantId: task.tenantId, organizationId: task.organizationId }\n}\n\nasync function requireEntryTarget(\n em: EntityManager,\n timeEntryId: string,\n scope: StaffCommandScope,\n translate: Translate,\n): Promise<AssignmentTarget> {\n const entry = await em.findOne(\n StaffTimeEntry,\n applyScopeToWhere<StaffTimeEntry>({ id: timeEntryId, deletedAt: null }, scope),\n )\n if (!entry) throw targetNotFoundError(translate, 'time_entry')\n if (entry.lockedReportId) {\n // An entry frozen in a closed report is read-only, and its tags are part of\n // what the report renders.\n throw new CrudHttpError(409, {\n code: TAG_TARGET_LOCKED_CODE,\n error: translate(\n 'staff.time_tracking.tags.errors.entryLocked',\n 'This time entry is locked in a closed report and its tags cannot be changed.',\n ),\n lockedReportId: entry.lockedReportId,\n })\n }\n return { id: entry.id, tenantId: entry.tenantId, organizationId: entry.organizationId }\n}\n\ntype AssignmentCommandInput = {\n tenantId: string\n organizationId: string\n targetId: string\n tagIds: string[]\n}\n\nfunction refreshTargetSideEffects(ops: TagJunctionOps) {\n return async (ctx: { container: { resolve: (name: string) => unknown } }, target: AssignmentTarget) => {\n const dataEngine = ctx.container.resolve('dataEngine') as DataEngine\n // The target's own index document and cached lists carry its tags, so they\n // are refreshed even though no column on the target row changed.\n await emitCrudSideEffects({\n dataEngine,\n action: 'updated',\n entity: target,\n identifiers: { id: target.id, organizationId: target.organizationId, tenantId: target.tenantId },\n indexer: ops.target === 'task' ? taskCrudIndexer : timeEntryCrudIndexer,\n })\n }\n}\n\nasync function runAssignment(\n ops: TagJunctionOps,\n requireTarget: (\n em: EntityManager,\n targetId: string,\n scope: StaffCommandScope,\n translate: Translate,\n ) => Promise<AssignmentTarget>,\n parsed: AssignmentCommandInput,\n ctx: Parameters<CommandHandler<AssignmentCommandInput, AssignmentResult>['execute']>[1],\n): Promise<AssignmentResult> {\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n const scope = commandInputScope(ctx, parsed.tenantId, parsed.organizationId)\n const { translate } = await resolveTranslations()\n\n const requestedTagIds = Array.from(new Set(parsed.tagIds))\n\n let target: AssignmentTarget | null = null\n let assignedTagIds: string[] = []\n let alreadyAssignedTagIds: string[] = []\n let tagIdsAfter: string[] = []\n\n // Each attempt gets its own fork: a rolled-back unit of work must not carry\n // its persisted-but-uncommitted rows into the retry.\n const attempt = async (): Promise<void> => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n await withAtomicFlush(\n em,\n [\n async () => {\n target = await requireTarget(em, parsed.targetId, scope, translate)\n await requireKnownTags(em, requestedTagIds, scope, translate)\n const existing = await ops.loadAssignments(em, parsed.targetId, scope)\n const existingTagIds = new Set(existing.map((row) => row.tagId))\n alreadyAssignedTagIds = requestedTagIds.filter((tagId) => existingTagIds.has(tagId))\n assignedTagIds = requestedTagIds.filter((tagId) => !existingTagIds.has(tagId))\n tagIdsAfter = Array.from(new Set([...existingTagIds, ...requestedTagIds]))\n },\n () => {\n for (const tagId of assignedTagIds) {\n ops.createAssignment(em, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n tagId,\n targetId: parsed.targetId,\n })\n }\n },\n ],\n { transaction: true, label: `${ops.target}.tags.assign` },\n )\n }\n\n try {\n await attempt()\n } catch (err) {\n if (!isUniqueViolation(err)) throw err\n // A concurrent assign won the race. Re-reading turns the collision into the\n // no-op success the second caller asked for; a second violation would mean\n // the row is there either way, which is the requested end state.\n try {\n await attempt()\n } catch (retryErr) {\n if (!isUniqueViolation(retryErr)) throw retryErr\n assignedTagIds = []\n alreadyAssignedTagIds = requestedTagIds\n tagIdsAfter = requestedTagIds\n }\n }\n\n const resolvedTarget = target as AssignmentTarget | null\n if (!resolvedTarget) throw targetNotFoundError(translate, ops.target)\n\n if (assignedTagIds.length > 0) {\n await refreshTargetSideEffects(ops)(ctx, resolvedTarget)\n }\n\n return {\n targetId: parsed.targetId,\n assignedTagIds,\n alreadyAssignedTagIds,\n tagIds: tagIdsAfter,\n }\n}\n\nasync function runUnassignment(\n ops: TagJunctionOps,\n requireTarget: (\n em: EntityManager,\n targetId: string,\n scope: StaffCommandScope,\n translate: Translate,\n ) => Promise<AssignmentTarget>,\n parsed: AssignmentCommandInput,\n ctx: Parameters<CommandHandler<AssignmentCommandInput, UnassignmentResult>['execute']>[1],\n): Promise<UnassignmentResult> {\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n const scope = commandInputScope(ctx, parsed.tenantId, parsed.organizationId)\n const { translate } = await resolveTranslations()\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const requestedTagIds = Array.from(new Set(parsed.tagIds))\n\n let target: AssignmentTarget | null = null\n let removedTagIds: string[] = []\n let notAssignedTagIds: string[] = []\n let tagIdsAfter: string[] = []\n\n await withAtomicFlush(\n em,\n [\n async () => {\n target = await requireTarget(em, parsed.targetId, scope, translate)\n const existing = await ops.loadAssignments(em, parsed.targetId, scope)\n const byTagId = new Map(existing.map((row) => [row.tagId, row]))\n removedTagIds = requestedTagIds.filter((tagId) => byTagId.has(tagId))\n notAssignedTagIds = requestedTagIds.filter((tagId) => !byTagId.has(tagId))\n tagIdsAfter = existing.map((row) => row.tagId).filter((tagId) => !removedTagIds.includes(tagId))\n const removableIds = removedTagIds\n .map((tagId) => byTagId.get(tagId)?.id)\n .filter((value): value is string => typeof value === 'string')\n // Removing a tag that is not assigned is a no-op, not a 404: the caller\n // asked for an end state that already holds.\n if (removableIds.length > 0) await ops.deleteAssignments(em, removableIds, scope)\n },\n ],\n { transaction: true, label: `${ops.target}.tags.unassign` },\n )\n\n const resolvedTarget = target as AssignmentTarget | null\n if (!resolvedTarget) throw targetNotFoundError(translate, ops.target)\n\n if (removedTagIds.length > 0) {\n await refreshTargetSideEffects(ops)(ctx, resolvedTarget)\n }\n\n return {\n targetId: parsed.targetId,\n removedTagIds,\n notAssignedTagIds,\n tagIds: tagIdsAfter,\n }\n}\n\nasync function restoreAssignmentState(\n ops: TagJunctionOps,\n payload: AssignmentUndoPayload,\n ctx: { container: { resolve: (name: string) => unknown } },\n): Promise<void> {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const scope: StaffCommandScope = {\n tenantId: payload.tenantId,\n organizationId: payload.organizationId,\n requireTenant: true,\n requireOrganization: true,\n }\n\n await withAtomicFlush(\n em,\n [\n async () => {\n if (payload.added.length > 0) {\n const current = await ops.loadAssignments(em, payload.targetId, scope)\n const removableIds = current\n .filter((row) => payload.added.includes(row.tagId))\n .map((row) => row.id)\n if (removableIds.length > 0) await ops.deleteAssignments(em, removableIds, scope)\n }\n },\n () => ops.restoreAssignments(em, payload.removed),\n ],\n { transaction: true, label: `${ops.target}.tags.undo` },\n )\n}\n\nfunction assignmentLogPayload(\n ops: TagJunctionOps,\n parsed: AssignmentCommandInput,\n result: AssignmentResult,\n): AssignmentUndoPayload {\n return {\n target: ops.target,\n targetId: parsed.targetId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n tagIdsBefore: result.tagIds.filter((tagId) => !result.assignedTagIds.includes(tagId)),\n tagIdsAfter: result.tagIds,\n added: result.assignedTagIds,\n removed: [],\n }\n}\n\nfunction unassignmentLogPayload(\n ops: TagJunctionOps,\n parsed: AssignmentCommandInput,\n result: UnassignmentResult,\n removedRows: TagAssignmentSnapshot[],\n): AssignmentUndoPayload {\n return {\n target: ops.target,\n targetId: parsed.targetId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n tagIdsBefore: [...result.tagIds, ...result.removedTagIds],\n tagIdsAfter: result.tagIds,\n added: [],\n removed: removedRows,\n }\n}\n\nfunction toTaskCommandInput(parsed: StaffTimeTaskTagAssignmentInput): AssignmentCommandInput {\n return {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n targetId: parsed.taskId,\n tagIds: parsed.tagIds,\n }\n}\n\nfunction toEntryCommandInput(parsed: StaffTimeEntryTagAssignmentInput): AssignmentCommandInput {\n return {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n targetId: parsed.timeEntryId,\n tagIds: parsed.tagIds,\n }\n}\n\nconst assignTaskTagsCommand: CommandHandler<StaffTimeTaskTagAssignmentInput, AssignmentResult> = {\n id: staffTimeTagCommandIds.assignTask,\n async prepare(rawInput, ctx) {\n const parsed = staffTimeTaskTagAssignmentSchema.safeParse(rawInput)\n if (!parsed.success) return {}\n const em = ctx.container.resolve('em') as EntityManager\n const rows = await taskJunctionOps.loadAssignmentsForSnapshot(\n em,\n parsed.data.taskId,\n staffSnapshotScopeFromContext(ctx),\n )\n return { before: { tagIds: rows.map((row) => row.tagId) } }\n },\n execute: async (rawInput, ctx) => {\n const parsed = staffTimeTaskTagAssignmentSchema.parse(rawInput)\n return runAssignment(taskJunctionOps, requireTaskTarget, toTaskCommandInput(parsed), ctx)\n },\n buildLog: async ({ input, result, snapshots }) => {\n const parsed = staffTimeTaskTagAssignmentSchema.parse(input)\n const commandInput = toTaskCommandInput(parsed)\n const before = (snapshots.before as { tagIds?: string[] } | undefined)?.tagIds ?? []\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('staff.audit.timesheets.tags.assign_task', 'Assign task tags'),\n resourceKind: taskJunctionOps.resourceKind,\n resourceId: commandInput.targetId,\n tenantId: commandInput.tenantId,\n organizationId: commandInput.organizationId,\n snapshotBefore: { tagIds: before },\n snapshotAfter: { tagIds: result.tagIds },\n changes: { tagIds: { from: before, to: result.tagIds } },\n payload: {\n undo: assignmentLogPayload(taskJunctionOps, commandInput, result),\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<AssignmentUndoPayload>(logEntry)\n if (!payload) return\n await restoreAssignmentState(taskJunctionOps, payload, ctx)\n },\n}\n\nconst unassignTaskTagsCommand: CommandHandler<StaffTimeTaskTagAssignmentInput, UnassignmentResult> = {\n id: staffTimeTagCommandIds.unassignTask,\n async prepare(rawInput, ctx) {\n const parsed = staffTimeTaskTagAssignmentSchema.safeParse(rawInput)\n if (!parsed.success) return {}\n const em = ctx.container.resolve('em') as EntityManager\n const rows = await taskJunctionOps.loadAssignmentsForSnapshot(\n em,\n parsed.data.taskId,\n staffSnapshotScopeFromContext(ctx),\n )\n return { before: { rows } }\n },\n execute: async (rawInput, ctx) => {\n const parsed = staffTimeTaskTagAssignmentSchema.parse(rawInput)\n return runUnassignment(taskJunctionOps, requireTaskTarget, toTaskCommandInput(parsed), ctx)\n },\n buildLog: async ({ input, result, snapshots }) => {\n const parsed = staffTimeTaskTagAssignmentSchema.parse(input)\n const commandInput = toTaskCommandInput(parsed)\n const rows = (snapshots.before as { rows?: TagAssignmentSnapshot[] } | undefined)?.rows ?? []\n const removedRows = rows.filter((row) => result.removedTagIds.includes(row.tagId))\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('staff.audit.timesheets.tags.unassign_task', 'Unassign task tags'),\n resourceKind: taskJunctionOps.resourceKind,\n resourceId: commandInput.targetId,\n tenantId: commandInput.tenantId,\n organizationId: commandInput.organizationId,\n snapshotBefore: { tagIds: rows.map((row) => row.tagId) },\n snapshotAfter: { tagIds: result.tagIds },\n changes: { tagIds: { from: rows.map((row) => row.tagId), to: result.tagIds } },\n payload: {\n undo: unassignmentLogPayload(taskJunctionOps, commandInput, result, removedRows),\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<AssignmentUndoPayload>(logEntry)\n if (!payload) return\n await restoreAssignmentState(taskJunctionOps, payload, ctx)\n },\n}\n\nconst assignEntryTagsCommand: CommandHandler<StaffTimeEntryTagAssignmentInput, AssignmentResult> = {\n id: staffTimeTagCommandIds.assignEntry,\n async prepare(rawInput, ctx) {\n const parsed = staffTimeEntryTagAssignmentSchema.safeParse(rawInput)\n if (!parsed.success) return {}\n const em = ctx.container.resolve('em') as EntityManager\n const rows = await entryJunctionOps.loadAssignmentsForSnapshot(\n em,\n parsed.data.timeEntryId,\n staffSnapshotScopeFromContext(ctx),\n )\n return { before: { tagIds: rows.map((row) => row.tagId) } }\n },\n execute: async (rawInput, ctx) => {\n const parsed = staffTimeEntryTagAssignmentSchema.parse(rawInput)\n return runAssignment(entryJunctionOps, requireEntryTarget, toEntryCommandInput(parsed), ctx)\n },\n buildLog: async ({ input, result, snapshots }) => {\n const parsed = staffTimeEntryTagAssignmentSchema.parse(input)\n const commandInput = toEntryCommandInput(parsed)\n const before = (snapshots.before as { tagIds?: string[] } | undefined)?.tagIds ?? []\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('staff.audit.timesheets.tags.assign_entry', 'Assign time entry tags'),\n resourceKind: entryJunctionOps.resourceKind,\n resourceId: commandInput.targetId,\n tenantId: commandInput.tenantId,\n organizationId: commandInput.organizationId,\n snapshotBefore: { tagIds: before },\n snapshotAfter: { tagIds: result.tagIds },\n changes: { tagIds: { from: before, to: result.tagIds } },\n payload: {\n undo: assignmentLogPayload(entryJunctionOps, commandInput, result),\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<AssignmentUndoPayload>(logEntry)\n if (!payload) return\n await restoreAssignmentState(entryJunctionOps, payload, ctx)\n },\n}\n\nconst unassignEntryTagsCommand: CommandHandler<StaffTimeEntryTagAssignmentInput, UnassignmentResult> = {\n id: staffTimeTagCommandIds.unassignEntry,\n async prepare(rawInput, ctx) {\n const parsed = staffTimeEntryTagAssignmentSchema.safeParse(rawInput)\n if (!parsed.success) return {}\n const em = ctx.container.resolve('em') as EntityManager\n const rows = await entryJunctionOps.loadAssignmentsForSnapshot(\n em,\n parsed.data.timeEntryId,\n staffSnapshotScopeFromContext(ctx),\n )\n return { before: { rows } }\n },\n execute: async (rawInput, ctx) => {\n const parsed = staffTimeEntryTagAssignmentSchema.parse(rawInput)\n return runUnassignment(entryJunctionOps, requireEntryTarget, toEntryCommandInput(parsed), ctx)\n },\n buildLog: async ({ input, result, snapshots }) => {\n const parsed = staffTimeEntryTagAssignmentSchema.parse(input)\n const commandInput = toEntryCommandInput(parsed)\n const rows = (snapshots.before as { rows?: TagAssignmentSnapshot[] } | undefined)?.rows ?? []\n const removedRows = rows.filter((row) => result.removedTagIds.includes(row.tagId))\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('staff.audit.timesheets.tags.unassign_entry', 'Unassign time entry tags'),\n resourceKind: entryJunctionOps.resourceKind,\n resourceId: commandInput.targetId,\n tenantId: commandInput.tenantId,\n organizationId: commandInput.organizationId,\n snapshotBefore: { tagIds: rows.map((row) => row.tagId) },\n snapshotAfter: { tagIds: result.tagIds },\n changes: { tagIds: { from: rows.map((row) => row.tagId), to: result.tagIds } },\n payload: {\n undo: unassignmentLogPayload(entryJunctionOps, commandInput, result, removedRows),\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<AssignmentUndoPayload>(logEntry)\n if (!payload) return\n await restoreAssignmentState(entryJunctionOps, payload, ctx)\n },\n}\n\nregisterCommand(createTagCommand)\nregisterCommand(updateTagCommand)\nregisterCommand(deleteTagCommand)\nregisterCommand(assignTaskTagsCommand)\nregisterCommand(unassignTaskTagsCommand)\nregisterCommand(assignEntryTagsCommand)\nregisterCommand(unassignEntryTagsCommand)\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Tag commands for the consulting suite (T3.4).\n *\n * A tag is org-wide vocabulary \u2014 screen 6 renders it as a card badge, screen 7 as\n * a chip in the task drawer, screens 8 and 10 on the time entry. The tag itself is\n * therefore project-agnostic; only its *assignments* touch project-scoped rows,\n * which is why the access filtering lives on the assignment routes rather than\n * here.\n *\n * Three decisions the callers depend on:\n *\n * 1. `slug` is unique per organization+tenant (partial unique index, live rows\n * only). A collision comes back as a 409 carrying `fieldErrors.slug` so the\n * form marks the field instead of showing a dead 500.\n * 2. **Deleting a tag cascades to its assignments and reports the count.** The\n * alternatives are worse: silently orphaning 200 entry badges hides the blast\n * radius, and refusing until the caller hand-unpicks 200 rows gives them no\n * way forward. The tag is soft-deleted, the junction rows are removed, the\n * response carries `removedTaskAssignments` / `removedEntryAssignments`, and\n * undo puts both the tag and every removed assignment back.\n * 3. **Assignment is idempotent.** Both junctions carry a unique index, so\n * re-sending a tag the target already has is a no-op success, never a unique\n * violation. The already-present ids come back in `alreadyAssignedTagIds` so\n * the UI can tell \"added\" from \"was already there\". Unassigning a tag that is\n * not assigned is the same kind of no-op.\n */\n\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandHandler } from '@open-mercato/shared/lib/commands'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport { CrudHttpError, isUniqueViolation } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { buildChanges, emitCrudSideEffects, emitCrudUndoSideEffects } from '@open-mercato/shared/lib/commands/helpers'\nimport { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport type { CrudIndexerConfig } from '@open-mercato/shared/lib/crud/types'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport {\n StaffTimeEntry,\n StaffTimeEntryTag,\n StaffTimeTag,\n StaffTimeTask,\n StaffTimeTaskTag,\n} from '../data/entities'\nimport {\n staffTimeTagCreateSchema,\n staffTimeEntryTagAssignmentSchema,\n staffTimeTaskTagAssignmentSchema,\n type StaffTimeTagCreateInput,\n type StaffTimeEntryTagAssignmentInput,\n type StaffTimeTaskTagAssignmentInput,\n} from '../data/validators'\nimport {\n applyScopeToWhere,\n commandActorScope,\n commandInputScope,\n ensureOrganizationScope,\n ensureTenantScope,\n extractUndoPayload,\n scopedStaffSnapshotWhere,\n staffSnapshotScopeFromContext,\n staffSnapshotScopeFromSnapshot,\n type StaffCommandScope,\n type StaffSnapshotScope,\n} from './shared'\n\nexport const staffTimeTagCommandIds = {\n create: 'staff.timesheets.tags.create',\n update: 'staff.timesheets.tags.update',\n delete: 'staff.timesheets.tags.delete',\n assignTask: 'staff.timesheets.tags.assign_task',\n unassignTask: 'staff.timesheets.tags.unassign_task',\n assignEntry: 'staff.timesheets.tags.assign_entry',\n unassignEntry: 'staff.timesheets.tags.unassign_entry',\n} as const\n\nexport const TAG_SLUG_DUPLICATE_CODE = 'time_tag_slug_duplicate'\nexport const TAG_UNKNOWN_IDS_CODE = 'time_tag_unknown_ids'\nexport const TAG_TARGET_LOCKED_CODE = 'time_entry_locked'\n\n/**\n * An assignment snapshot is written into the audit payload so undo can put the\n * junction rows back. A tag used across thousands of rows would bloat the log, so\n * the snapshot stops here and says so; undo then restores what it captured.\n */\nexport const TAG_ASSIGNMENT_SNAPSHOT_LIMIT = 500\n\nconst tagCrudIndexer: CrudIndexerConfig<StaffTimeTag> = {\n entityType: 'staff:staff_time_tag',\n}\n\nconst taskCrudIndexer: CrudIndexerConfig<StaffTimeTask> = {\n entityType: 'staff:staff_time_task',\n}\n\nconst timeEntryCrudIndexer: CrudIndexerConfig<StaffTimeEntry> = {\n entityType: 'staff:staff_time_entry',\n}\n\n// No CRUD event ids are declared for tags in `events.ts`: a tag write is\n// vocabulary maintenance the screens re-read, so these commands queue index and\n// cache side effects only.\n\ntype Translate = (key: string, fallback: string) => string\n\n/**\n * `data/validators.ts` ships the create shape only. The update schema is derived\n * from it rather than restated so the slug/label/color rules cannot drift apart.\n */\nexport const staffTimeTagUpdateSchema = z.object({\n id: z.string().uuid(),\n slug: staffTimeTagCreateSchema.shape.slug.optional(),\n label: staffTimeTagCreateSchema.shape.label.optional(),\n color: staffTimeTagCreateSchema.shape.color,\n})\n\nexport type StaffTimeTagUpdateInput = z.infer<typeof staffTimeTagUpdateSchema>\n\ntype TagSnapshot = {\n id: string\n tenantId: string\n organizationId: string\n slug: string\n label: string\n color: string | null\n deletedAt: string | null\n}\n\ntype TagAssignmentSnapshot = {\n id: string\n tenantId: string\n organizationId: string\n tagId: string\n targetId: string\n}\n\ntype TagUndoPayload = {\n before?: TagSnapshot | null\n after?: TagSnapshot | null\n taskAssignments?: TagAssignmentSnapshot[] | null\n entryAssignments?: TagAssignmentSnapshot[] | null\n assignmentsTruncated?: boolean\n}\n\ntype AssignmentUndoPayload = {\n target: 'task' | 'time_entry'\n targetId: string\n tenantId: string\n organizationId: string\n tagIdsBefore: string[]\n tagIdsAfter: string[]\n added: string[]\n removed: TagAssignmentSnapshot[]\n}\n\nfunction toTagSnapshot(tag: StaffTimeTag): TagSnapshot {\n return {\n id: tag.id,\n tenantId: tag.tenantId,\n organizationId: tag.organizationId,\n slug: tag.slug,\n label: tag.label,\n color: tag.color ?? null,\n deletedAt: tag.deletedAt ? tag.deletedAt.toISOString() : null,\n }\n}\n\nasync function loadTagSnapshot(\n em: EntityManager,\n id: string,\n scope?: StaffSnapshotScope | null,\n): Promise<TagSnapshot | null> {\n const tag = await em.findOne(StaffTimeTag, scopedStaffSnapshotWhere(id, scope))\n return tag ? toTagSnapshot(tag) : null\n}\n\nfunction slugDuplicateError(translate: Translate, slug: string): CrudHttpError {\n const message = translate(\n 'staff.time_tracking.tags.errors.slugDuplicate',\n 'A tag with this slug already exists.',\n )\n return new CrudHttpError(409, {\n code: TAG_SLUG_DUPLICATE_CODE,\n error: message,\n slug,\n fieldErrors: { slug: message },\n })\n}\n\nfunction tagNotFoundError(translate: Translate): CrudHttpError {\n return new CrudHttpError(404, {\n error: translate('staff.time_tracking.tags.errors.notFound', 'Tag not found or not accessible.'),\n })\n}\n\nfunction unknownTagIdsError(translate: Translate, unknownIds: string[]): CrudHttpError {\n const message = translate(\n 'staff.time_tracking.tags.errors.unknownIds',\n 'Some tags do not exist in this organization.',\n )\n return new CrudHttpError(422, {\n code: TAG_UNKNOWN_IDS_CODE,\n error: message,\n unknownIds,\n fieldErrors: { tagIds: message },\n })\n}\n\nfunction targetNotFoundError(translate: Translate, target: 'task' | 'time_entry'): CrudHttpError {\n // Deliberately the same 404 for \"does not exist\" and \"not yours\": tagging must\n // not become a way to probe which task or entry ids exist (screen 17 note 1).\n const message =\n target === 'task'\n ? translate('staff.time_tracking.tags.errors.taskNotFound', 'Task not found or not accessible.')\n : translate('staff.time_tracking.tags.errors.entryNotFound', 'Time entry not found or not accessible.')\n return new CrudHttpError(404, { error: message })\n}\n\n/**\n * Persistence for one junction table. The command bodies below are shared; only\n * these closures know which table and which target column they address, so each\n * ORM call stays fully typed against its own entity.\n */\ntype TagJunctionOps = {\n target: 'task' | 'time_entry'\n resourceKind: string\n loadAssignments: (\n em: EntityManager,\n targetId: string,\n scope: StaffCommandScope,\n ) => Promise<TagAssignmentSnapshot[]>\n loadAssignmentsForSnapshot: (\n em: EntityManager,\n targetId: string,\n scope: StaffSnapshotScope | null,\n ) => Promise<TagAssignmentSnapshot[]>\n loadAssignmentsByTag: (\n em: EntityManager,\n tagId: string,\n scope: StaffCommandScope,\n ) => Promise<TagAssignmentSnapshot[]>\n createAssignment: (em: EntityManager, row: Omit<TagAssignmentSnapshot, 'id'>) => void\n deleteAssignments: (em: EntityManager, ids: string[], scope: StaffCommandScope) => Promise<number>\n deleteAssignmentsByTag: (em: EntityManager, tagId: string, scope: StaffCommandScope) => Promise<number>\n restoreAssignments: (em: EntityManager, rows: TagAssignmentSnapshot[]) => Promise<void>\n}\n\nfunction snapshotScopeWhere(scope?: StaffSnapshotScope | null): { tenantId?: string; organizationId?: string } {\n const where: { tenantId?: string; organizationId?: string } = {}\n if (scope?.tenantId) where.tenantId = scope.tenantId\n if (scope?.organizationId) where.organizationId = scope.organizationId\n return where\n}\n\nconst taskJunctionOps: TagJunctionOps = {\n target: 'task',\n resourceKind: 'staff.timesheets.task_tag',\n loadAssignments: async (em, targetId, scope) => {\n const rows = await em.find(StaffTimeTaskTag, applyScopeToWhere<StaffTimeTaskTag>({ taskId: targetId }, scope))\n return rows.map((row) => ({\n id: row.id,\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n tagId: row.tagId,\n targetId: row.taskId,\n }))\n },\n loadAssignmentsForSnapshot: async (em, targetId, scope) => {\n const rows = await em.find(StaffTimeTaskTag, { taskId: targetId, ...snapshotScopeWhere(scope) })\n return rows.map((row) => ({\n id: row.id,\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n tagId: row.tagId,\n targetId: row.taskId,\n }))\n },\n loadAssignmentsByTag: async (em, tagId, scope) => {\n const rows = await em.find(StaffTimeTaskTag, applyScopeToWhere<StaffTimeTaskTag>({ tagId }, scope), {\n limit: TAG_ASSIGNMENT_SNAPSHOT_LIMIT + 1,\n })\n return rows.map((row) => ({\n id: row.id,\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n tagId: row.tagId,\n targetId: row.taskId,\n }))\n },\n createAssignment: (em, row) => {\n em.persist(\n em.create(StaffTimeTaskTag, {\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n tagId: row.tagId,\n taskId: row.targetId,\n createdAt: new Date(),\n }),\n )\n },\n deleteAssignments: async (em, ids, scope) =>\n em.nativeDelete(StaffTimeTaskTag, applyScopeToWhere<StaffTimeTaskTag>({ id: { $in: ids } }, scope)),\n deleteAssignmentsByTag: async (em, tagId, scope) =>\n em.nativeDelete(StaffTimeTaskTag, applyScopeToWhere<StaffTimeTaskTag>({ tagId }, scope)),\n restoreAssignments: async (em, rows) => {\n for (const row of rows) {\n const existing = await em.findOne(StaffTimeTaskTag, {\n tagId: row.tagId,\n taskId: row.targetId,\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n })\n if (existing) continue\n em.persist(\n em.create(StaffTimeTaskTag, {\n id: row.id,\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n tagId: row.tagId,\n taskId: row.targetId,\n createdAt: new Date(),\n }),\n )\n }\n },\n}\n\nconst entryJunctionOps: TagJunctionOps = {\n target: 'time_entry',\n resourceKind: 'staff.timesheets.entry_tag',\n loadAssignments: async (em, targetId, scope) => {\n const rows = await em.find(\n StaffTimeEntryTag,\n applyScopeToWhere<StaffTimeEntryTag>({ timeEntryId: targetId }, scope),\n )\n return rows.map((row) => ({\n id: row.id,\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n tagId: row.tagId,\n targetId: row.timeEntryId,\n }))\n },\n loadAssignmentsForSnapshot: async (em, targetId, scope) => {\n const rows = await em.find(StaffTimeEntryTag, { timeEntryId: targetId, ...snapshotScopeWhere(scope) })\n return rows.map((row) => ({\n id: row.id,\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n tagId: row.tagId,\n targetId: row.timeEntryId,\n }))\n },\n loadAssignmentsByTag: async (em, tagId, scope) => {\n const rows = await em.find(StaffTimeEntryTag, applyScopeToWhere<StaffTimeEntryTag>({ tagId }, scope), {\n limit: TAG_ASSIGNMENT_SNAPSHOT_LIMIT + 1,\n })\n return rows.map((row) => ({\n id: row.id,\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n tagId: row.tagId,\n targetId: row.timeEntryId,\n }))\n },\n createAssignment: (em, row) => {\n em.persist(\n em.create(StaffTimeEntryTag, {\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n tagId: row.tagId,\n timeEntryId: row.targetId,\n createdAt: new Date(),\n }),\n )\n },\n deleteAssignments: async (em, ids, scope) =>\n em.nativeDelete(StaffTimeEntryTag, applyScopeToWhere<StaffTimeEntryTag>({ id: { $in: ids } }, scope)),\n deleteAssignmentsByTag: async (em, tagId, scope) =>\n em.nativeDelete(StaffTimeEntryTag, applyScopeToWhere<StaffTimeEntryTag>({ tagId }, scope)),\n restoreAssignments: async (em, rows) => {\n for (const row of rows) {\n const existing = await em.findOne(StaffTimeEntryTag, {\n tagId: row.tagId,\n timeEntryId: row.targetId,\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n })\n if (existing) continue\n em.persist(\n em.create(StaffTimeEntryTag, {\n id: row.id,\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n tagId: row.tagId,\n timeEntryId: row.targetId,\n createdAt: new Date(),\n }),\n )\n }\n },\n}\n\nasync function requireKnownTags(\n em: EntityManager,\n tagIds: string[],\n scope: StaffCommandScope,\n translate: Translate,\n): Promise<void> {\n const unique = Array.from(new Set(tagIds))\n if (unique.length === 0) return\n const tags = await em.find(\n StaffTimeTag,\n applyScopeToWhere<StaffTimeTag>({ id: { $in: unique }, deletedAt: null }, scope),\n { fields: ['id'] },\n )\n const known = new Set(tags.map((tag) => tag.id))\n const unknownIds = unique.filter((id) => !known.has(id))\n if (unknownIds.length > 0) throw unknownTagIdsError(translate, unknownIds)\n}\n\nconst createTagCommand: CommandHandler<StaffTimeTagCreateInput, { tagId: string }> = {\n id: staffTimeTagCommandIds.create,\n async prepare(rawInput, ctx) {\n const parsed = staffTimeTagCreateSchema.safeParse(rawInput)\n if (!parsed.success) return {}\n const em = ctx.container.resolve('em') as EntityManager\n const existing = await em.findOne(StaffTimeTag, {\n slug: parsed.data.slug,\n tenantId: parsed.data.tenantId,\n organizationId: parsed.data.organizationId,\n deletedAt: null,\n })\n return { before: { slugTaken: existing != null } }\n },\n async execute(rawInput, ctx) {\n const parsed = staffTimeTagCreateSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n const scope = commandInputScope(ctx, parsed.tenantId, parsed.organizationId)\n const { translate } = await resolveTranslations()\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n let created: StaffTimeTag | null = null\n\n try {\n await withAtomicFlush(\n em,\n [\n async () => {\n // The read lives in the same transaction as the insert, so a\n // concurrent create cannot slip a second row past this check; the\n // partial unique index is the backstop and is translated below.\n const duplicate = await em.findOne(\n StaffTimeTag,\n applyScopeToWhere<StaffTimeTag>({ slug: parsed.slug, deletedAt: null }, scope),\n { fields: ['id'] },\n )\n if (duplicate) throw slugDuplicateError(translate, parsed.slug)\n },\n () => {\n created = em.create(StaffTimeTag, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n slug: parsed.slug,\n label: parsed.label,\n color: parsed.color ?? null,\n createdAt: new Date(),\n updatedAt: new Date(),\n deletedAt: null,\n })\n em.persist(created)\n },\n ],\n { transaction: true, label: staffTimeTagCommandIds.create },\n )\n } catch (err) {\n if (isUniqueViolation(err)) throw slugDuplicateError(translate, parsed.slug)\n throw err\n }\n\n const record = created as StaffTimeTag | null\n if (!record) throw tagNotFoundError(translate)\n\n await emitCrudSideEffects({\n dataEngine: ctx.container.resolve('dataEngine'),\n action: 'created',\n entity: record,\n identifiers: { id: record.id, organizationId: record.organizationId, tenantId: record.tenantId },\n indexer: tagCrudIndexer,\n })\n\n return { tagId: record.id }\n },\n captureAfter: async (_input, result, ctx) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const snapshot = await loadTagSnapshot(em, result.tagId, staffSnapshotScopeFromContext(ctx))\n if (!snapshot) return null\n return { snapshot }\n },\n buildLog: async ({ result, ctx }) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const snapshot = await loadTagSnapshot(em, result.tagId, staffSnapshotScopeFromContext(ctx))\n if (!snapshot) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('staff.audit.timesheets.tags.create', 'Create tag'),\n resourceKind: 'staff.timesheets.tag',\n resourceId: snapshot.id,\n tenantId: snapshot.tenantId,\n organizationId: snapshot.organizationId,\n snapshotAfter: snapshot,\n payload: {\n undo: { after: snapshot } satisfies TagUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<TagUndoPayload>(logEntry)\n const after = payload?.after\n if (!after) return\n const snapshotScope = staffSnapshotScopeFromSnapshot(after)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const tag = await em.findOne(StaffTimeTag, scopedStaffSnapshotWhere(after.id, snapshotScope))\n if (!tag) return\n\n await withAtomicFlush(\n em,\n [\n () => {\n tag.deletedAt = new Date()\n tag.updatedAt = new Date()\n },\n ],\n { transaction: true, label: `${staffTimeTagCommandIds.create}.undo` },\n )\n\n await emitCrudUndoSideEffects({\n dataEngine: ctx.container.resolve('dataEngine'),\n action: 'deleted',\n entity: tag,\n identifiers: { id: tag.id, organizationId: tag.organizationId, tenantId: tag.tenantId },\n indexer: tagCrudIndexer,\n })\n },\n}\n\nconst updateTagCommand: CommandHandler<StaffTimeTagUpdateInput, { tagId: string }> = {\n id: staffTimeTagCommandIds.update,\n async prepare(rawInput, ctx) {\n const parsed = staffTimeTagUpdateSchema.safeParse(rawInput)\n if (!parsed.success) return {}\n const em = ctx.container.resolve('em') as EntityManager\n const before = await loadTagSnapshot(em, parsed.data.id, staffSnapshotScopeFromContext(ctx))\n if (!before) return {}\n return { before: { snapshot: before } }\n },\n async execute(rawInput, ctx) {\n const parsed = staffTimeTagUpdateSchema.parse(rawInput)\n const { translate } = await resolveTranslations()\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const scope = commandActorScope(ctx)\n\n const tag = await em.findOne(StaffTimeTag, applyScopeToWhere<StaffTimeTag>({ id: parsed.id, deletedAt: null }, scope))\n if (!tag) throw tagNotFoundError(translate)\n ensureTenantScope(ctx, tag.tenantId)\n ensureOrganizationScope(ctx, tag.organizationId)\n\n try {\n await withAtomicFlush(\n em,\n [\n async () => {\n if (parsed.slug === undefined || parsed.slug === tag.slug) return\n const duplicate = await em.findOne(\n StaffTimeTag,\n applyScopeToWhere<StaffTimeTag>({ slug: parsed.slug, deletedAt: null, id: { $ne: tag.id } }, scope),\n { fields: ['id'] },\n )\n if (duplicate) throw slugDuplicateError(translate, parsed.slug)\n },\n () => {\n if (parsed.slug !== undefined) tag.slug = parsed.slug\n if (parsed.label !== undefined) tag.label = parsed.label\n if (parsed.color !== undefined) tag.color = parsed.color ?? null\n tag.updatedAt = new Date()\n },\n ],\n { transaction: true, label: staffTimeTagCommandIds.update },\n )\n } catch (err) {\n if (isUniqueViolation(err)) throw slugDuplicateError(translate, parsed.slug ?? tag.slug)\n throw err\n }\n\n await emitCrudSideEffects({\n dataEngine: ctx.container.resolve('dataEngine'),\n action: 'updated',\n entity: tag,\n identifiers: { id: tag.id, organizationId: tag.organizationId, tenantId: tag.tenantId },\n indexer: tagCrudIndexer,\n })\n\n return { tagId: tag.id }\n },\n buildLog: async ({ snapshots, ctx }) => {\n const before = (snapshots.before as { snapshot?: TagSnapshot } | undefined)?.snapshot\n if (!before) return null\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const after = await loadTagSnapshot(em, before.id, staffSnapshotScopeFromSnapshot(before))\n if (!after) return null\n const changes = buildChanges(\n before as unknown as Record<string, unknown>,\n after as unknown as Record<string, unknown>,\n ['slug', 'label', 'color'],\n )\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('staff.audit.timesheets.tags.update', 'Update tag'),\n resourceKind: 'staff.timesheets.tag',\n resourceId: before.id,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n snapshotBefore: before,\n snapshotAfter: after,\n changes,\n payload: {\n undo: { before, after } satisfies TagUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<TagUndoPayload>(logEntry)\n const before = payload?.before\n if (!before) return\n const snapshotScope = staffSnapshotScopeFromSnapshot(before)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const tag = await em.findOne(StaffTimeTag, scopedStaffSnapshotWhere(before.id, snapshotScope))\n if (!tag) return\n\n await withAtomicFlush(\n em,\n [\n () => {\n tag.slug = before.slug\n tag.label = before.label\n tag.color = before.color ?? null\n tag.updatedAt = new Date()\n },\n ],\n { transaction: true, label: `${staffTimeTagCommandIds.update}.undo` },\n )\n\n await emitCrudUndoSideEffects({\n dataEngine: ctx.container.resolve('dataEngine'),\n action: 'updated',\n entity: tag,\n identifiers: { id: tag.id, organizationId: tag.organizationId, tenantId: tag.tenantId },\n indexer: tagCrudIndexer,\n })\n },\n}\n\ntype DeleteTagResult = {\n tagId: string\n removedTaskAssignments: number\n removedEntryAssignments: number\n}\n\nconst deleteTagCommand: CommandHandler<{ id?: string }, DeleteTagResult> = {\n id: staffTimeTagCommandIds.delete,\n async prepare(input, ctx) {\n const id = input?.id\n if (!id) return {}\n const em = ctx.container.resolve('em') as EntityManager\n const scope = staffSnapshotScopeFromContext(ctx)\n const before = await loadTagSnapshot(em, id, scope)\n if (!before) return {}\n const readScope: StaffCommandScope = {\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n requireTenant: true,\n requireOrganization: true,\n }\n const taskAssignments = await taskJunctionOps.loadAssignmentsByTag(em, id, readScope)\n const entryAssignments = await entryJunctionOps.loadAssignmentsByTag(em, id, readScope)\n const truncated =\n taskAssignments.length > TAG_ASSIGNMENT_SNAPSHOT_LIMIT || entryAssignments.length > TAG_ASSIGNMENT_SNAPSHOT_LIMIT\n return {\n before: {\n snapshot: before,\n taskAssignments: taskAssignments.slice(0, TAG_ASSIGNMENT_SNAPSHOT_LIMIT),\n entryAssignments: entryAssignments.slice(0, TAG_ASSIGNMENT_SNAPSHOT_LIMIT),\n assignmentsTruncated: truncated,\n },\n }\n },\n async execute(input, ctx) {\n const { translate } = await resolveTranslations()\n const id = input?.id\n if (!id) {\n throw new CrudHttpError(400, {\n error: translate('staff.time_tracking.tags.errors.idRequired', 'Tag id is required.'),\n })\n }\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const scope = commandActorScope(ctx)\n const tag = await em.findOne(StaffTimeTag, applyScopeToWhere<StaffTimeTag>({ id, deletedAt: null }, scope))\n if (!tag) throw tagNotFoundError(translate)\n ensureTenantScope(ctx, tag.tenantId)\n ensureOrganizationScope(ctx, tag.organizationId)\n\n let removedTaskAssignments = 0\n let removedEntryAssignments = 0\n\n await withAtomicFlush(\n em,\n [\n async () => {\n // The cascade is deliberate: the badge disappears everywhere it was\n // used, and the counts travel back so the caller sees the reach.\n removedTaskAssignments = await taskJunctionOps.deleteAssignmentsByTag(em, tag.id, scope)\n removedEntryAssignments = await entryJunctionOps.deleteAssignmentsByTag(em, tag.id, scope)\n },\n () => {\n tag.deletedAt = new Date()\n tag.updatedAt = new Date()\n },\n ],\n { transaction: true, label: staffTimeTagCommandIds.delete },\n )\n\n await emitCrudSideEffects({\n dataEngine: ctx.container.resolve('dataEngine'),\n action: 'deleted',\n entity: tag,\n identifiers: { id: tag.id, organizationId: tag.organizationId, tenantId: tag.tenantId },\n indexer: tagCrudIndexer,\n })\n\n return { tagId: tag.id, removedTaskAssignments, removedEntryAssignments }\n },\n buildLog: async ({ result, snapshots }) => {\n const prepared = snapshots.before as\n | {\n snapshot?: TagSnapshot\n taskAssignments?: TagAssignmentSnapshot[]\n entryAssignments?: TagAssignmentSnapshot[]\n assignmentsTruncated?: boolean\n }\n | undefined\n const before = prepared?.snapshot\n if (!before) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('staff.audit.timesheets.tags.delete', 'Delete tag'),\n resourceKind: 'staff.timesheets.tag',\n resourceId: before.id,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n snapshotBefore: before,\n changes: {\n removedTaskAssignments: { from: result.removedTaskAssignments, to: 0 },\n removedEntryAssignments: { from: result.removedEntryAssignments, to: 0 },\n },\n payload: {\n undo: {\n before,\n taskAssignments: prepared?.taskAssignments ?? null,\n entryAssignments: prepared?.entryAssignments ?? null,\n assignmentsTruncated: prepared?.assignmentsTruncated ?? false,\n } satisfies TagUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<TagUndoPayload>(logEntry)\n const before = payload?.before\n if (!before) return\n const snapshotScope = staffSnapshotScopeFromSnapshot(before)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n let tag = await em.findOne(StaffTimeTag, scopedStaffSnapshotWhere(before.id, snapshotScope))\n\n await withAtomicFlush(\n em,\n [\n () => {\n if (!tag) {\n tag = em.create(StaffTimeTag, {\n id: before.id,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n slug: before.slug,\n label: before.label,\n color: before.color ?? null,\n createdAt: new Date(),\n updatedAt: new Date(),\n deletedAt: null,\n })\n em.persist(tag)\n return\n }\n tag.slug = before.slug\n tag.label = before.label\n tag.color = before.color ?? null\n tag.deletedAt = null\n tag.updatedAt = new Date()\n },\n // Restoring assignments re-reads the junction rows, so it is its own phase.\n () => taskJunctionOps.restoreAssignments(em, payload?.taskAssignments ?? []),\n () => entryJunctionOps.restoreAssignments(em, payload?.entryAssignments ?? []),\n ],\n { transaction: true, label: `${staffTimeTagCommandIds.delete}.undo` },\n )\n\n await emitCrudUndoSideEffects({\n dataEngine: ctx.container.resolve('dataEngine'),\n action: 'created',\n entity: tag,\n identifiers: { id: before.id, organizationId: before.organizationId, tenantId: before.tenantId },\n indexer: tagCrudIndexer,\n })\n },\n}\n\ntype AssignmentResult = {\n targetId: string\n assignedTagIds: string[]\n alreadyAssignedTagIds: string[]\n tagIds: string[]\n}\n\ntype UnassignmentResult = {\n targetId: string\n removedTagIds: string[]\n notAssignedTagIds: string[]\n tagIds: string[]\n}\n\ntype AssignmentTarget = {\n id: string\n tenantId: string\n organizationId: string\n}\n\nasync function requireTaskTarget(\n em: EntityManager,\n taskId: string,\n scope: StaffCommandScope,\n translate: Translate,\n): Promise<AssignmentTarget> {\n const task = await em.findOne(StaffTimeTask, applyScopeToWhere<StaffTimeTask>({ id: taskId, deletedAt: null }, scope))\n if (!task) throw targetNotFoundError(translate, 'task')\n return { id: task.id, tenantId: task.tenantId, organizationId: task.organizationId }\n}\n\nasync function requireEntryTarget(\n em: EntityManager,\n timeEntryId: string,\n scope: StaffCommandScope,\n translate: Translate,\n): Promise<AssignmentTarget> {\n const entry = await em.findOne(\n StaffTimeEntry,\n applyScopeToWhere<StaffTimeEntry>({ id: timeEntryId, deletedAt: null }, scope),\n )\n if (!entry) throw targetNotFoundError(translate, 'time_entry')\n if (entry.lockedReportId) {\n // An entry frozen in a closed report is read-only, and its tags are part of\n // what the report renders.\n throw entryTagsLockedError(translate, entry.lockedReportId)\n }\n return { id: entry.id, tenantId: entry.tenantId, organizationId: entry.organizationId }\n}\n\nfunction entryTagsLockedError(translate: Translate, lockedReportId: string): CrudHttpError {\n return new CrudHttpError(409, {\n code: TAG_TARGET_LOCKED_CODE,\n error: translate(\n 'staff.time_tracking.tags.errors.entryLocked',\n 'This time entry is locked in a closed report and its tags cannot be changed.',\n ),\n lockedReportId,\n })\n}\n\n/**\n * Undo restores a tag set the entry had before a report froze it, so it is held\n * to the same lock as the forward commands. It deliberately ignores `deletedAt`\n * and a missing row: neither can carry a lock, and undo behaved that way before.\n */\nasync function assertEntryTagUndoAllowed(\n payload: AssignmentUndoPayload,\n ctx: { container: { resolve: (name: string) => unknown } },\n): Promise<void> {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const entry = await findOneWithDecryption(\n em,\n StaffTimeEntry,\n { id: payload.targetId, tenantId: payload.tenantId, organizationId: payload.organizationId },\n undefined,\n { tenantId: payload.tenantId, organizationId: payload.organizationId },\n )\n if (!entry?.lockedReportId) return\n const { translate } = await resolveTranslations()\n throw entryTagsLockedError(translate, entry.lockedReportId)\n}\n\ntype AssignmentCommandInput = {\n tenantId: string\n organizationId: string\n targetId: string\n tagIds: string[]\n}\n\nfunction refreshTargetSideEffects(ops: TagJunctionOps) {\n return async (ctx: { container: { resolve: (name: string) => unknown } }, target: AssignmentTarget) => {\n const dataEngine = ctx.container.resolve('dataEngine') as DataEngine\n // The target's own index document and cached lists carry its tags, so they\n // are refreshed even though no column on the target row changed.\n await emitCrudSideEffects({\n dataEngine,\n action: 'updated',\n entity: target,\n identifiers: { id: target.id, organizationId: target.organizationId, tenantId: target.tenantId },\n indexer: ops.target === 'task' ? taskCrudIndexer : timeEntryCrudIndexer,\n })\n }\n}\n\nasync function runAssignment(\n ops: TagJunctionOps,\n requireTarget: (\n em: EntityManager,\n targetId: string,\n scope: StaffCommandScope,\n translate: Translate,\n ) => Promise<AssignmentTarget>,\n parsed: AssignmentCommandInput,\n ctx: Parameters<CommandHandler<AssignmentCommandInput, AssignmentResult>['execute']>[1],\n): Promise<AssignmentResult> {\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n const scope = commandInputScope(ctx, parsed.tenantId, parsed.organizationId)\n const { translate } = await resolveTranslations()\n\n const requestedTagIds = Array.from(new Set(parsed.tagIds))\n\n let target: AssignmentTarget | null = null\n let assignedTagIds: string[] = []\n let alreadyAssignedTagIds: string[] = []\n let tagIdsAfter: string[] = []\n\n // Each attempt gets its own fork: a rolled-back unit of work must not carry\n // its persisted-but-uncommitted rows into the retry.\n const attempt = async (): Promise<void> => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n await withAtomicFlush(\n em,\n [\n async () => {\n target = await requireTarget(em, parsed.targetId, scope, translate)\n await requireKnownTags(em, requestedTagIds, scope, translate)\n const existing = await ops.loadAssignments(em, parsed.targetId, scope)\n const existingTagIds = new Set(existing.map((row) => row.tagId))\n alreadyAssignedTagIds = requestedTagIds.filter((tagId) => existingTagIds.has(tagId))\n assignedTagIds = requestedTagIds.filter((tagId) => !existingTagIds.has(tagId))\n tagIdsAfter = Array.from(new Set([...existingTagIds, ...requestedTagIds]))\n },\n () => {\n for (const tagId of assignedTagIds) {\n ops.createAssignment(em, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n tagId,\n targetId: parsed.targetId,\n })\n }\n },\n ],\n { transaction: true, label: `${ops.target}.tags.assign` },\n )\n }\n\n try {\n await attempt()\n } catch (err) {\n if (!isUniqueViolation(err)) throw err\n // A concurrent assign won the race. Re-reading turns the collision into the\n // no-op success the second caller asked for; a second violation would mean\n // the row is there either way, which is the requested end state.\n try {\n await attempt()\n } catch (retryErr) {\n if (!isUniqueViolation(retryErr)) throw retryErr\n assignedTagIds = []\n alreadyAssignedTagIds = requestedTagIds\n tagIdsAfter = requestedTagIds\n }\n }\n\n const resolvedTarget = target as AssignmentTarget | null\n if (!resolvedTarget) throw targetNotFoundError(translate, ops.target)\n\n if (assignedTagIds.length > 0) {\n await refreshTargetSideEffects(ops)(ctx, resolvedTarget)\n }\n\n return {\n targetId: parsed.targetId,\n assignedTagIds,\n alreadyAssignedTagIds,\n tagIds: tagIdsAfter,\n }\n}\n\nasync function runUnassignment(\n ops: TagJunctionOps,\n requireTarget: (\n em: EntityManager,\n targetId: string,\n scope: StaffCommandScope,\n translate: Translate,\n ) => Promise<AssignmentTarget>,\n parsed: AssignmentCommandInput,\n ctx: Parameters<CommandHandler<AssignmentCommandInput, UnassignmentResult>['execute']>[1],\n): Promise<UnassignmentResult> {\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n const scope = commandInputScope(ctx, parsed.tenantId, parsed.organizationId)\n const { translate } = await resolveTranslations()\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const requestedTagIds = Array.from(new Set(parsed.tagIds))\n\n let target: AssignmentTarget | null = null\n let removedTagIds: string[] = []\n let notAssignedTagIds: string[] = []\n let tagIdsAfter: string[] = []\n\n await withAtomicFlush(\n em,\n [\n async () => {\n target = await requireTarget(em, parsed.targetId, scope, translate)\n const existing = await ops.loadAssignments(em, parsed.targetId, scope)\n const byTagId = new Map(existing.map((row) => [row.tagId, row]))\n removedTagIds = requestedTagIds.filter((tagId) => byTagId.has(tagId))\n notAssignedTagIds = requestedTagIds.filter((tagId) => !byTagId.has(tagId))\n tagIdsAfter = existing.map((row) => row.tagId).filter((tagId) => !removedTagIds.includes(tagId))\n const removableIds = removedTagIds\n .map((tagId) => byTagId.get(tagId)?.id)\n .filter((value): value is string => typeof value === 'string')\n // Removing a tag that is not assigned is a no-op, not a 404: the caller\n // asked for an end state that already holds.\n if (removableIds.length > 0) await ops.deleteAssignments(em, removableIds, scope)\n },\n ],\n { transaction: true, label: `${ops.target}.tags.unassign` },\n )\n\n const resolvedTarget = target as AssignmentTarget | null\n if (!resolvedTarget) throw targetNotFoundError(translate, ops.target)\n\n if (removedTagIds.length > 0) {\n await refreshTargetSideEffects(ops)(ctx, resolvedTarget)\n }\n\n return {\n targetId: parsed.targetId,\n removedTagIds,\n notAssignedTagIds,\n tagIds: tagIdsAfter,\n }\n}\n\nasync function restoreAssignmentState(\n ops: TagJunctionOps,\n payload: AssignmentUndoPayload,\n ctx: { container: { resolve: (name: string) => unknown } },\n): Promise<void> {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const scope: StaffCommandScope = {\n tenantId: payload.tenantId,\n organizationId: payload.organizationId,\n requireTenant: true,\n requireOrganization: true,\n }\n\n await withAtomicFlush(\n em,\n [\n async () => {\n if (payload.added.length > 0) {\n const current = await ops.loadAssignments(em, payload.targetId, scope)\n const removableIds = current\n .filter((row) => payload.added.includes(row.tagId))\n .map((row) => row.id)\n if (removableIds.length > 0) await ops.deleteAssignments(em, removableIds, scope)\n }\n },\n () => ops.restoreAssignments(em, payload.removed),\n ],\n { transaction: true, label: `${ops.target}.tags.undo` },\n )\n}\n\nfunction assignmentLogPayload(\n ops: TagJunctionOps,\n parsed: AssignmentCommandInput,\n result: AssignmentResult,\n): AssignmentUndoPayload {\n return {\n target: ops.target,\n targetId: parsed.targetId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n tagIdsBefore: result.tagIds.filter((tagId) => !result.assignedTagIds.includes(tagId)),\n tagIdsAfter: result.tagIds,\n added: result.assignedTagIds,\n removed: [],\n }\n}\n\nfunction unassignmentLogPayload(\n ops: TagJunctionOps,\n parsed: AssignmentCommandInput,\n result: UnassignmentResult,\n removedRows: TagAssignmentSnapshot[],\n): AssignmentUndoPayload {\n return {\n target: ops.target,\n targetId: parsed.targetId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n tagIdsBefore: [...result.tagIds, ...result.removedTagIds],\n tagIdsAfter: result.tagIds,\n added: [],\n removed: removedRows,\n }\n}\n\nfunction toTaskCommandInput(parsed: StaffTimeTaskTagAssignmentInput): AssignmentCommandInput {\n return {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n targetId: parsed.taskId,\n tagIds: parsed.tagIds,\n }\n}\n\nfunction toEntryCommandInput(parsed: StaffTimeEntryTagAssignmentInput): AssignmentCommandInput {\n return {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n targetId: parsed.timeEntryId,\n tagIds: parsed.tagIds,\n }\n}\n\nconst assignTaskTagsCommand: CommandHandler<StaffTimeTaskTagAssignmentInput, AssignmentResult> = {\n id: staffTimeTagCommandIds.assignTask,\n async prepare(rawInput, ctx) {\n const parsed = staffTimeTaskTagAssignmentSchema.safeParse(rawInput)\n if (!parsed.success) return {}\n const em = ctx.container.resolve('em') as EntityManager\n const rows = await taskJunctionOps.loadAssignmentsForSnapshot(\n em,\n parsed.data.taskId,\n staffSnapshotScopeFromContext(ctx),\n )\n return { before: { tagIds: rows.map((row) => row.tagId) } }\n },\n execute: async (rawInput, ctx) => {\n const parsed = staffTimeTaskTagAssignmentSchema.parse(rawInput)\n return runAssignment(taskJunctionOps, requireTaskTarget, toTaskCommandInput(parsed), ctx)\n },\n buildLog: async ({ input, result, snapshots }) => {\n const parsed = staffTimeTaskTagAssignmentSchema.parse(input)\n const commandInput = toTaskCommandInput(parsed)\n const before = (snapshots.before as { tagIds?: string[] } | undefined)?.tagIds ?? []\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('staff.audit.timesheets.tags.assign_task', 'Assign task tags'),\n resourceKind: taskJunctionOps.resourceKind,\n resourceId: commandInput.targetId,\n tenantId: commandInput.tenantId,\n organizationId: commandInput.organizationId,\n snapshotBefore: { tagIds: before },\n snapshotAfter: { tagIds: result.tagIds },\n changes: { tagIds: { from: before, to: result.tagIds } },\n payload: {\n undo: assignmentLogPayload(taskJunctionOps, commandInput, result),\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<AssignmentUndoPayload>(logEntry)\n if (!payload) return\n await restoreAssignmentState(taskJunctionOps, payload, ctx)\n },\n}\n\nconst unassignTaskTagsCommand: CommandHandler<StaffTimeTaskTagAssignmentInput, UnassignmentResult> = {\n id: staffTimeTagCommandIds.unassignTask,\n async prepare(rawInput, ctx) {\n const parsed = staffTimeTaskTagAssignmentSchema.safeParse(rawInput)\n if (!parsed.success) return {}\n const em = ctx.container.resolve('em') as EntityManager\n const rows = await taskJunctionOps.loadAssignmentsForSnapshot(\n em,\n parsed.data.taskId,\n staffSnapshotScopeFromContext(ctx),\n )\n return { before: { rows } }\n },\n execute: async (rawInput, ctx) => {\n const parsed = staffTimeTaskTagAssignmentSchema.parse(rawInput)\n return runUnassignment(taskJunctionOps, requireTaskTarget, toTaskCommandInput(parsed), ctx)\n },\n buildLog: async ({ input, result, snapshots }) => {\n const parsed = staffTimeTaskTagAssignmentSchema.parse(input)\n const commandInput = toTaskCommandInput(parsed)\n const rows = (snapshots.before as { rows?: TagAssignmentSnapshot[] } | undefined)?.rows ?? []\n const removedRows = rows.filter((row) => result.removedTagIds.includes(row.tagId))\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('staff.audit.timesheets.tags.unassign_task', 'Unassign task tags'),\n resourceKind: taskJunctionOps.resourceKind,\n resourceId: commandInput.targetId,\n tenantId: commandInput.tenantId,\n organizationId: commandInput.organizationId,\n snapshotBefore: { tagIds: rows.map((row) => row.tagId) },\n snapshotAfter: { tagIds: result.tagIds },\n changes: { tagIds: { from: rows.map((row) => row.tagId), to: result.tagIds } },\n payload: {\n undo: unassignmentLogPayload(taskJunctionOps, commandInput, result, removedRows),\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<AssignmentUndoPayload>(logEntry)\n if (!payload) return\n await restoreAssignmentState(taskJunctionOps, payload, ctx)\n },\n}\n\nconst assignEntryTagsCommand: CommandHandler<StaffTimeEntryTagAssignmentInput, AssignmentResult> = {\n id: staffTimeTagCommandIds.assignEntry,\n async prepare(rawInput, ctx) {\n const parsed = staffTimeEntryTagAssignmentSchema.safeParse(rawInput)\n if (!parsed.success) return {}\n const em = ctx.container.resolve('em') as EntityManager\n const rows = await entryJunctionOps.loadAssignmentsForSnapshot(\n em,\n parsed.data.timeEntryId,\n staffSnapshotScopeFromContext(ctx),\n )\n return { before: { tagIds: rows.map((row) => row.tagId) } }\n },\n execute: async (rawInput, ctx) => {\n const parsed = staffTimeEntryTagAssignmentSchema.parse(rawInput)\n return runAssignment(entryJunctionOps, requireEntryTarget, toEntryCommandInput(parsed), ctx)\n },\n buildLog: async ({ input, result, snapshots }) => {\n const parsed = staffTimeEntryTagAssignmentSchema.parse(input)\n const commandInput = toEntryCommandInput(parsed)\n const before = (snapshots.before as { tagIds?: string[] } | undefined)?.tagIds ?? []\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('staff.audit.timesheets.tags.assign_entry', 'Assign time entry tags'),\n resourceKind: entryJunctionOps.resourceKind,\n resourceId: commandInput.targetId,\n tenantId: commandInput.tenantId,\n organizationId: commandInput.organizationId,\n snapshotBefore: { tagIds: before },\n snapshotAfter: { tagIds: result.tagIds },\n changes: { tagIds: { from: before, to: result.tagIds } },\n payload: {\n undo: assignmentLogPayload(entryJunctionOps, commandInput, result),\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<AssignmentUndoPayload>(logEntry)\n if (!payload) return\n await assertEntryTagUndoAllowed(payload, ctx)\n await restoreAssignmentState(entryJunctionOps, payload, ctx)\n },\n}\n\nconst unassignEntryTagsCommand: CommandHandler<StaffTimeEntryTagAssignmentInput, UnassignmentResult> = {\n id: staffTimeTagCommandIds.unassignEntry,\n async prepare(rawInput, ctx) {\n const parsed = staffTimeEntryTagAssignmentSchema.safeParse(rawInput)\n if (!parsed.success) return {}\n const em = ctx.container.resolve('em') as EntityManager\n const rows = await entryJunctionOps.loadAssignmentsForSnapshot(\n em,\n parsed.data.timeEntryId,\n staffSnapshotScopeFromContext(ctx),\n )\n return { before: { rows } }\n },\n execute: async (rawInput, ctx) => {\n const parsed = staffTimeEntryTagAssignmentSchema.parse(rawInput)\n return runUnassignment(entryJunctionOps, requireEntryTarget, toEntryCommandInput(parsed), ctx)\n },\n buildLog: async ({ input, result, snapshots }) => {\n const parsed = staffTimeEntryTagAssignmentSchema.parse(input)\n const commandInput = toEntryCommandInput(parsed)\n const rows = (snapshots.before as { rows?: TagAssignmentSnapshot[] } | undefined)?.rows ?? []\n const removedRows = rows.filter((row) => result.removedTagIds.includes(row.tagId))\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('staff.audit.timesheets.tags.unassign_entry', 'Unassign time entry tags'),\n resourceKind: entryJunctionOps.resourceKind,\n resourceId: commandInput.targetId,\n tenantId: commandInput.tenantId,\n organizationId: commandInput.organizationId,\n snapshotBefore: { tagIds: rows.map((row) => row.tagId) },\n snapshotAfter: { tagIds: result.tagIds },\n changes: { tagIds: { from: rows.map((row) => row.tagId), to: result.tagIds } },\n payload: {\n undo: unassignmentLogPayload(entryJunctionOps, commandInput, result, removedRows),\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<AssignmentUndoPayload>(logEntry)\n if (!payload) return\n await assertEntryTagUndoAllowed(payload, ctx)\n await restoreAssignmentState(entryJunctionOps, payload, ctx)\n },\n}\n\nregisterCommand(createTagCommand)\nregisterCommand(updateTagCommand)\nregisterCommand(deleteTagCommand)\nregisterCommand(assignTaskTagsCommand)\nregisterCommand(unassignTaskTagsCommand)\nregisterCommand(assignEntryTagsCommand)\nregisterCommand(unassignEntryTagsCommand)\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.9.1-develop.7518.1.343d9198f1",
3
+ "version": "0.9.1-develop.7519.1.38f51f8dbb",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -264,16 +264,16 @@
264
264
  "zod": "^4.4.3"
265
265
  },
266
266
  "peerDependencies": {
267
- "@open-mercato/ai-assistant": "0.9.1-develop.7518.1.343d9198f1",
268
- "@open-mercato/shared": "0.9.1-develop.7518.1.343d9198f1",
269
- "@open-mercato/ui": "0.9.1-develop.7518.1.343d9198f1",
267
+ "@open-mercato/ai-assistant": "0.9.1-develop.7519.1.38f51f8dbb",
268
+ "@open-mercato/shared": "0.9.1-develop.7519.1.38f51f8dbb",
269
+ "@open-mercato/ui": "0.9.1-develop.7519.1.38f51f8dbb",
270
270
  "react": "^19.0.0",
271
271
  "react-dom": "^19.0.0"
272
272
  },
273
273
  "devDependencies": {
274
- "@open-mercato/ai-assistant": "0.9.1-develop.7518.1.343d9198f1",
275
- "@open-mercato/shared": "0.9.1-develop.7518.1.343d9198f1",
276
- "@open-mercato/ui": "0.9.1-develop.7518.1.343d9198f1",
274
+ "@open-mercato/ai-assistant": "0.9.1-develop.7519.1.38f51f8dbb",
275
+ "@open-mercato/shared": "0.9.1-develop.7519.1.38f51f8dbb",
276
+ "@open-mercato/ui": "0.9.1-develop.7519.1.38f51f8dbb",
277
277
  "@testing-library/dom": "^10.4.1",
278
278
  "@testing-library/jest-dom": "^7.0.1",
279
279
  "@testing-library/react": "^16.3.3",
@@ -905,6 +905,7 @@ const createTimeEntryCommand: CommandHandler<StaffTimeEntryCreateInput, { timeEn
905
905
  const em = (ctx.container.resolve('em') as EntityManager).fork()
906
906
  const entry = await em.findOne(StaffTimeEntry, scopedStaffSnapshotWhere(after.id, staffSnapshotScopeFromSnapshot(after)))
907
907
  if (entry) {
908
+ assertTimeEntryUnlocked(entry, (await resolveTranslations()).translate)
908
909
  entry.deletedAt = new Date()
909
910
  await em.flush()
910
911
 
@@ -1097,6 +1098,7 @@ const startTimerCommand: CommandHandler<StaffTimeEntryStartTimerInput, { timeEnt
1097
1098
  const em = (ctx.container.resolve('em') as EntityManager).fork()
1098
1099
  const entry = await em.findOne(StaffTimeEntry, scopedStaffSnapshotWhere(after.id, staffSnapshotScopeFromSnapshot(after)))
1099
1100
  if (entry) {
1101
+ assertTimeEntryUnlocked(entry, (await resolveTranslations()).translate)
1100
1102
  entry.deletedAt = new Date()
1101
1103
  await em.flush()
1102
1104
 
@@ -1318,6 +1320,7 @@ const updateTimeEntryCommand: CommandHandler<StaffTimeEntryUpdateInput, { timeEn
1318
1320
  const em = (ctx.container.resolve('em') as EntityManager).fork()
1319
1321
  const entry = await em.findOne(StaffTimeEntry, scopedStaffSnapshotWhere(before.id, staffSnapshotScopeFromSnapshot(before)))
1320
1322
  if (!entry) return
1323
+ assertTimeEntryUnlocked(entry, (await resolveTranslations()).translate)
1321
1324
  entry.date = before.date as unknown as Date
1322
1325
  entry.durationMinutes = before.durationMinutes
1323
1326
  entry.roundedMinutes = before.roundedMinutes ?? null
@@ -1467,6 +1470,7 @@ const deleteTimeEntryCommand: CommandHandler<{ id?: string }, { timeEntryId: str
1467
1470
  })
1468
1471
  em.persist(entry)
1469
1472
  } else {
1473
+ assertTimeEntryUnlocked(entry, (await resolveTranslations()).translate)
1470
1474
  entry.staffMemberId = before.staffMemberId
1471
1475
  entry.date = before.date as unknown as Date
1472
1476
  entry.durationMinutes = before.durationMinutes
@@ -31,6 +31,7 @@ import type { CommandHandler } from '@open-mercato/shared/lib/commands'
31
31
  import { registerCommand } from '@open-mercato/shared/lib/commands'
32
32
  import { CrudHttpError, isUniqueViolation } from '@open-mercato/shared/lib/crud/errors'
33
33
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
34
+ import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
34
35
  import { buildChanges, emitCrudSideEffects, emitCrudUndoSideEffects } from '@open-mercato/shared/lib/commands/helpers'
35
36
  import { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'
36
37
  import type { CrudIndexerConfig } from '@open-mercato/shared/lib/crud/types'
@@ -868,18 +869,44 @@ async function requireEntryTarget(
868
869
  if (entry.lockedReportId) {
869
870
  // An entry frozen in a closed report is read-only, and its tags are part of
870
871
  // what the report renders.
871
- throw new CrudHttpError(409, {
872
- code: TAG_TARGET_LOCKED_CODE,
873
- error: translate(
874
- 'staff.time_tracking.tags.errors.entryLocked',
875
- 'This time entry is locked in a closed report and its tags cannot be changed.',
876
- ),
877
- lockedReportId: entry.lockedReportId,
878
- })
872
+ throw entryTagsLockedError(translate, entry.lockedReportId)
879
873
  }
880
874
  return { id: entry.id, tenantId: entry.tenantId, organizationId: entry.organizationId }
881
875
  }
882
876
 
877
+ function entryTagsLockedError(translate: Translate, lockedReportId: string): CrudHttpError {
878
+ return new CrudHttpError(409, {
879
+ code: TAG_TARGET_LOCKED_CODE,
880
+ error: translate(
881
+ 'staff.time_tracking.tags.errors.entryLocked',
882
+ 'This time entry is locked in a closed report and its tags cannot be changed.',
883
+ ),
884
+ lockedReportId,
885
+ })
886
+ }
887
+
888
+ /**
889
+ * Undo restores a tag set the entry had before a report froze it, so it is held
890
+ * to the same lock as the forward commands. It deliberately ignores `deletedAt`
891
+ * and a missing row: neither can carry a lock, and undo behaved that way before.
892
+ */
893
+ async function assertEntryTagUndoAllowed(
894
+ payload: AssignmentUndoPayload,
895
+ ctx: { container: { resolve: (name: string) => unknown } },
896
+ ): Promise<void> {
897
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
898
+ const entry = await findOneWithDecryption(
899
+ em,
900
+ StaffTimeEntry,
901
+ { id: payload.targetId, tenantId: payload.tenantId, organizationId: payload.organizationId },
902
+ undefined,
903
+ { tenantId: payload.tenantId, organizationId: payload.organizationId },
904
+ )
905
+ if (!entry?.lockedReportId) return
906
+ const { translate } = await resolveTranslations()
907
+ throw entryTagsLockedError(translate, entry.lockedReportId)
908
+ }
909
+
883
910
  type AssignmentCommandInput = {
884
911
  tenantId: string
885
912
  organizationId: string
@@ -1258,6 +1285,7 @@ const assignEntryTagsCommand: CommandHandler<StaffTimeEntryTagAssignmentInput, A
1258
1285
  undo: async ({ logEntry, ctx }) => {
1259
1286
  const payload = extractUndoPayload<AssignmentUndoPayload>(logEntry)
1260
1287
  if (!payload) return
1288
+ await assertEntryTagUndoAllowed(payload, ctx)
1261
1289
  await restoreAssignmentState(entryJunctionOps, payload, ctx)
1262
1290
  },
1263
1291
  }
@@ -1302,6 +1330,7 @@ const unassignEntryTagsCommand: CommandHandler<StaffTimeEntryTagAssignmentInput,
1302
1330
  undo: async ({ logEntry, ctx }) => {
1303
1331
  const payload = extractUndoPayload<AssignmentUndoPayload>(logEntry)
1304
1332
  if (!payload) return
1333
+ await assertEntryTagUndoAllowed(payload, ctx)
1305
1334
  await restoreAssignmentState(entryJunctionOps, payload, ctx)
1306
1335
  },
1307
1336
  }