// Application orchestration contracts for UI-facing use cases. // // This layer sits above the pure domain services and repository interfaces. It // gives future Flutter/use-case code one atomic boundary per user command // without importing widgets, MongoDB clients, or platform notification APIs. import 'backlog.dart'; import 'locked_time.dart'; import 'models.dart'; import 'project_statistics.dart'; import 'repositories.dart'; import 'scheduling_engine.dart'; import 'task_lifecycle.dart'; import 'time_contracts.dart'; /// Owner-local time context supplied to application operations. class OwnerTimeZoneContext { OwnerTimeZoneContext({ required String ownerId, required String timeZoneId, }) : ownerId = _requiredTrimmed(ownerId, 'ownerId'), timeZoneId = _requiredTrimmed(timeZoneId, 'timeZoneId'); /// Authorization-neutral owner scope. V1 does not implement accounts. final String ownerId; /// IANA-style time-zone identifier chosen by the app boundary. final String timeZoneId; } /// Per-operation application context. class ApplicationOperationContext { ApplicationOperationContext({ required String operationId, required this.now, required this.ownerTimeZone, required this.clock, required this.idGenerator, }) : operationId = _requiredTrimmed(operationId, 'operationId'); /// Convenience constructor that captures [Clock.now] once. factory ApplicationOperationContext.start({ required String operationId, required OwnerTimeZoneContext ownerTimeZone, required Clock clock, required IdGenerator idGenerator, }) { return ApplicationOperationContext( operationId: operationId, now: clock.now(), ownerTimeZone: ownerTimeZone, clock: clock, idGenerator: idGenerator, ); } /// Exactly-once key for this user command. final String operationId; /// Instant used by the operation. Use this instead of repeatedly reading the /// clock during one command. final DateTime now; /// Owner scope and time-zone data for date-sensitive rules. final OwnerTimeZoneContext ownerTimeZone; /// Injected clock available to lower-level services that need one. final Clock clock; /// Injected ID generator for records created by the operation. final IdGenerator idGenerator; } /// Stable application-layer result categories. enum ApplicationFailureCode { validation, notFound, conflict, staleRevision, noSlot, duplicateOperation, persistenceFailure, unexpected, } /// Typed failure returned by application commands and queries. class ApplicationFailure { const ApplicationFailure({ required this.code, this.entityId, this.detailCode, }); /// Stable machine-readable category. final ApplicationFailureCode code; /// Optional stable entity id related to the failure. final String? entityId; /// Optional narrower stable reason, such as a domain validation code name. final String? detailCode; } /// Result wrapper for expected application success/failure paths. class ApplicationResult { const ApplicationResult._({ this.value, this.failure, }); /// Successful result carrying [value]. factory ApplicationResult.success(T value) { return ApplicationResult._(value: value); } /// Expected failure result. factory ApplicationResult.failure(ApplicationFailure failure) { return ApplicationResult._(failure: failure); } /// Successful value, if any. final T? value; /// Typed failure, if the operation did not succeed. final ApplicationFailure? failure; /// Whether the operation succeeded. bool get isSuccess => failure == null; /// Whether the operation failed with a typed application failure. bool get isFailure => failure != null; /// Return the value or throw if this result is a failure. T get requireValue { if (failure != null) { throw StateError( 'Application result is a failure: ${failure!.code.name}'); } return value as T; } } /// Exception for expected failures raised inside a unit-of-work callback. class ApplicationFailureException implements Exception { const ApplicationFailureException(this.failure); /// Failure to return from the application boundary. final ApplicationFailure failure; } /// Exception representing persistence/driver failures at repository boundaries. class ApplicationPersistenceException implements Exception { const ApplicationPersistenceException(); } /// Persisted exactly-once operation record. class ApplicationOperationRecord { ApplicationOperationRecord({ required String operationId, required String ownerId, required String operationName, required this.committedAt, }) : operationId = _requiredTrimmed(operationId, 'operationId'), ownerId = _requiredTrimmed(ownerId, 'ownerId'), operationName = _requiredTrimmed(operationName, 'operationName'); /// Exactly-once operation id. final String operationId; /// Authorization-neutral owner scope. final String ownerId; /// Stable use-case/command label. final String operationName; /// Commit instant. final DateTime committedAt; } /// Owner-scoped acknowledgement for a one-time scheduling notice. class NoticeAcknowledgementRecord { NoticeAcknowledgementRecord({ required String noticeId, required String ownerId, required this.acknowledgedAt, }) : noticeId = _requiredTrimmed(noticeId, 'noticeId'), ownerId = _requiredTrimmed(ownerId, 'ownerId'); /// Stable notice id generated by the read/use-case boundary. final String noticeId; /// Authorization-neutral owner scope. final String ownerId; /// Instant the notice was consumed. final DateTime acknowledgedAt; } /// Owner-level settings needed by application use cases. class OwnerSettings { OwnerSettings({ required String ownerId, required String timeZoneId, WallTime? dayStart, WallTime? dayEnd, this.compactModeEnabled = false, this.backlogStalenessSettings = const BacklogStalenessSettings(), }) : ownerId = _requiredTrimmed(ownerId, 'ownerId'), timeZoneId = _requiredTrimmed(timeZoneId, 'timeZoneId'), dayStart = dayStart ?? WallTime(hour: 0, minute: 0), dayEnd = dayEnd ?? WallTime(hour: 23, minute: 59) { if (this.dayStart.compareTo(this.dayEnd) >= 0) { throw ArgumentError.value( {'dayStart': this.dayStart, 'dayEnd': this.dayEnd}, 'dayEnd', 'Day end must be after day start.', ); } } /// Authorization-neutral owner scope. final String ownerId; /// Owner time-zone id used by date-sensitive application use cases. final String timeZoneId; /// Default local day start. final WallTime dayStart; /// Default local day end. final WallTime dayEnd; /// Persisted compact-mode preference. final bool compactModeEnabled; /// Configurable backlog age thresholds. final BacklogStalenessSettings backlogStalenessSettings; /// Return a copy with selected settings changed. OwnerSettings copyWith({ String? ownerId, String? timeZoneId, WallTime? dayStart, WallTime? dayEnd, bool? compactModeEnabled, BacklogStalenessSettings? backlogStalenessSettings, }) { return OwnerSettings( ownerId: ownerId ?? this.ownerId, timeZoneId: timeZoneId ?? this.timeZoneId, dayStart: dayStart ?? this.dayStart, dayEnd: dayEnd ?? this.dayEnd, compactModeEnabled: compactModeEnabled ?? this.compactModeEnabled, backlogStalenessSettings: backlogStalenessSettings ?? this.backlogStalenessSettings, ); } } /// Repository contract for internal task activities. abstract interface class TaskActivityRepository { Future findById(String id); Future> findByOperationId(String operationId); Future> findByTaskId(String taskId); Future> findByProjectId(String projectId); Future> findAll(); Future> findByOwner({ required String ownerId, RepositoryPageRequest page = const RepositoryPageRequest(), }); Future save(TaskActivity activity); Future saveAll(Iterable activities); Future append({ required TaskActivity activity, required String ownerId, }); } /// Repository contract for project statistics aggregates. abstract interface class ProjectStatisticsRepository { Future findByProjectId(String projectId); Future?> findRecordByProjectId( String projectId, ); Future> findAll(); Future save(ProjectStatistics statistics); Future saveIfRevision({ required ProjectStatistics statistics, required String ownerId, required int expectedRevision, }); } /// Repository contract for owner settings. abstract interface class OwnerSettingsRepository { Future findByOwnerId(String ownerId); Future?> findRecordByOwnerId(String ownerId); Future save(OwnerSettings settings); Future saveIfRevision({ required OwnerSettings settings, required int expectedRevision, }); } /// Repository contract for consumed one-time notices. abstract interface class NoticeAcknowledgementRepository { Future findById({ required String ownerId, required String noticeId, }); Future> findByOwnerId(String ownerId); Future> findByOwner({ required String ownerId, RepositoryPageRequest page = const RepositoryPageRequest(), }); Future save(NoticeAcknowledgementRecord record); Future insert( NoticeAcknowledgementRecord record, ); } /// Repository contract for exactly-once operation records. abstract interface class ApplicationOperationRepository { Future findById(String operationId); Future findByIdempotencyKey({ required String ownerId, required String operationId, }); Future save(ApplicationOperationRecord operation); Future insertIfAbsent( ApplicationOperationRecord operation, ); } /// Repository set available inside one application unit of work. abstract interface class ApplicationUnitOfWorkRepositories { TaskRepository get tasks; ProjectRepository get projects; LockedBlockRepository get lockedBlocks; SchedulingSnapshotRepository get schedulingSnapshots; TaskActivityRepository get taskActivities; ProjectStatisticsRepository get projectStatistics; OwnerSettingsRepository get ownerSettings; NoticeAcknowledgementRepository get noticeAcknowledgements; ApplicationOperationRepository get operations; } /// Atomic application transaction boundary. abstract interface class ApplicationUnitOfWork { Future> run({ required ApplicationOperationContext context, required String operationName, required Future> Function( ApplicationUnitOfWorkRepositories repositories, ) action, }); Future> read({ required Future> Function( ApplicationUnitOfWorkRepositories repositories, ) action, }); } /// Repository loader for common scheduler input assembly. class ApplicationSchedulingLoader { const ApplicationSchedulingLoader(this.repositories); /// Repositories for the current application operation. final ApplicationUnitOfWorkRepositories repositories; /// Load scheduler input for [window] from the repository boundary. /// /// UI code should not persist partial [SchedulingResult] objects. Commands /// should load authoritative state here, invoke domain services, then commit /// resulting entities through a unit of work. Future loadInputForWindow({ required SchedulingWindow window, List lockedIntervals = const [], List requiredVisibleIntervals = const [], }) async { final scheduledTasks = await repositories.tasks.findScheduledInWindow( window, ); return SchedulingInput( tasks: scheduledTasks, window: window, lockedIntervals: lockedIntervals, requiredVisibleIntervals: requiredVisibleIntervals, ); } } /// In-memory unit-of-work implementation for deterministic application tests. class InMemoryApplicationUnitOfWork implements ApplicationUnitOfWork { InMemoryApplicationUnitOfWork({ Iterable initialTasks = const [], Iterable initialProjects = const [], Iterable initialLockedBlocks = const [], Iterable initialLockedOverrides = const [], Iterable initialSchedulingSnapshots = const [], Iterable initialTaskActivities = const [], Iterable initialProjectStatistics = const [], Iterable initialOwnerSettings = const [], Iterable initialNoticeAcknowledgements = const [], Iterable initialOperations = const [], }) : _state = _InMemoryApplicationState( tasksById: { for (final task in initialTasks) task.id: task, }, taskOwnersById: { for (final task in initialTasks) task.id: 'owner-1', }, taskRevisionsById: { for (final task in initialTasks) task.id: 1, }, projectsById: { for (final project in initialProjects) project.id: project, }, projectOwnersById: { for (final project in initialProjects) project.id: 'owner-1', }, projectRevisionsById: { for (final project in initialProjects) project.id: 1, }, lockedBlocksById: { for (final block in initialLockedBlocks) block.id: block, }, lockedBlockOwnersById: { for (final block in initialLockedBlocks) block.id: 'owner-1', }, lockedBlockRevisionsById: { for (final block in initialLockedBlocks) block.id: 1, }, lockedOverridesById: { for (final override in initialLockedOverrides) override.id: override, }, lockedOverrideOwnersById: { for (final override in initialLockedOverrides) override.id: 'owner-1', }, lockedOverrideRevisionsById: { for (final override in initialLockedOverrides) override.id: 1, }, schedulingSnapshotsById: { for (final snapshot in initialSchedulingSnapshots) snapshot.id: snapshot, }, taskActivitiesById: { for (final activity in initialTaskActivities) activity.id: activity, }, taskActivityOwnersById: { for (final activity in initialTaskActivities) activity.id: 'owner-1', }, projectStatisticsByProjectId: { for (final statistics in initialProjectStatistics) statistics.projectId: statistics, }, projectStatisticsOwnersByProjectId: { for (final statistics in initialProjectStatistics) statistics.projectId: 'owner-1', }, projectStatisticsRevisionsByProjectId: { for (final statistics in initialProjectStatistics) statistics.projectId: 1, }, ownerSettingsByOwnerId: { for (final settings in initialOwnerSettings) settings.ownerId: settings, }, ownerSettingsRevisionsByOwnerId: { for (final settings in initialOwnerSettings) settings.ownerId: 1, }, noticeAcknowledgementsByScopedId: { for (final acknowledgement in initialNoticeAcknowledgements) _noticeAcknowledgementKey( ownerId: acknowledgement.ownerId, noticeId: acknowledgement.noticeId, ): acknowledgement, }, noticeAcknowledgementRevisionsByScopedId: { for (final acknowledgement in initialNoticeAcknowledgements) _noticeAcknowledgementKey( ownerId: acknowledgement.ownerId, noticeId: acknowledgement.noticeId, ): 1, }, operationsById: { for (final operation in initialOperations) operation.operationId: operation, }, ); _InMemoryApplicationState _state; bool _failNextCommit = false; /// Current committed tasks, useful for contract tests. List get currentTasks => List.unmodifiable(_state.tasksById.values); /// Current committed projects, useful for contract tests. List get currentProjects { return List.unmodifiable(_state.projectsById.values); } /// Current committed locked blocks, useful for contract tests. List get currentLockedBlocks { return List.unmodifiable(_state.lockedBlocksById.values); } /// Current committed locked overrides, useful for contract tests. List get currentLockedOverrides { return List.unmodifiable( _state.lockedOverridesById.values, ); } /// Current committed scheduling snapshots, useful for contract tests. List get currentSchedulingSnapshots { return List.unmodifiable( _state.schedulingSnapshotsById.values, ); } /// Current committed activities, useful for contract tests. List get currentTaskActivities { return List.unmodifiable(_state.taskActivitiesById.values); } /// Current committed project statistics, useful for contract tests. List get currentProjectStatistics { return List.unmodifiable( _state.projectStatisticsByProjectId.values, ); } /// Current committed owner settings, useful for contract tests. List get currentOwnerSettings { return List.unmodifiable( _state.ownerSettingsByOwnerId.values); } /// Current committed notice acknowledgements, useful for contract tests. List get currentNoticeAcknowledgements { return List.unmodifiable( _state.noticeAcknowledgementsByScopedId.values, ); } /// Current committed operation records, useful for contract tests. List get currentOperations { return List.unmodifiable( _state.operationsById.values, ); } /// Cause the next successful transaction to fail at commit time. void failNextCommitForTesting() { _failNextCommit = true; } @override Future> run({ required ApplicationOperationContext context, required String operationName, required Future> Function( ApplicationUnitOfWorkRepositories repositories, ) action, }) async { if (_state.operationsById.containsKey(context.operationId)) { return ApplicationResult.failure( ApplicationFailure( code: ApplicationFailureCode.duplicateOperation, entityId: context.operationId, ), ); } final staged = _state.copy(); final repositories = _InMemoryApplicationRepositories(staged); try { final result = await action(repositories); if (result.isFailure) { return result; } staged.operationsById[context.operationId] = ApplicationOperationRecord( operationId: context.operationId, ownerId: context.ownerTimeZone.ownerId, operationName: operationName, committedAt: context.now, ); if (_failNextCommit) { _failNextCommit = false; throw const ApplicationPersistenceException(); } _state = staged; return result; } on ApplicationFailureException catch (error) { return ApplicationResult.failure(error.failure); } on DomainValidationException catch (error) { return ApplicationResult.failure( ApplicationFailure( code: ApplicationFailureCode.validation, detailCode: error.code.name, ), ); } on ArgumentError catch (error) { return ApplicationResult.failure( ApplicationFailure( code: ApplicationFailureCode.validation, detailCode: error.name, ), ); } on ApplicationPersistenceException { return ApplicationResult.failure( const ApplicationFailure( code: ApplicationFailureCode.persistenceFailure, ), ); } catch (_) { return ApplicationResult.failure( const ApplicationFailure(code: ApplicationFailureCode.unexpected), ); } } @override Future> read({ required Future> Function( ApplicationUnitOfWorkRepositories repositories, ) action, }) async { final staged = _state.copy(); final repositories = _InMemoryApplicationRepositories(staged); try { return await action(repositories); } on ApplicationFailureException catch (error) { return ApplicationResult.failure(error.failure); } on DomainValidationException catch (error) { return ApplicationResult.failure( ApplicationFailure( code: ApplicationFailureCode.validation, detailCode: error.code.name, ), ); } on ArgumentError catch (error) { return ApplicationResult.failure( ApplicationFailure( code: ApplicationFailureCode.validation, detailCode: error.name, ), ); } on ApplicationPersistenceException { return ApplicationResult.failure( const ApplicationFailure( code: ApplicationFailureCode.persistenceFailure, ), ); } catch (_) { return ApplicationResult.failure( const ApplicationFailure(code: ApplicationFailureCode.unexpected), ); } } } class _InMemoryApplicationState { _InMemoryApplicationState({ required this.tasksById, required this.taskOwnersById, required this.taskRevisionsById, required this.projectsById, required this.projectOwnersById, required this.projectRevisionsById, required this.lockedBlocksById, required this.lockedBlockOwnersById, required this.lockedBlockRevisionsById, required this.lockedOverridesById, required this.lockedOverrideOwnersById, required this.lockedOverrideRevisionsById, required this.schedulingSnapshotsById, required this.taskActivitiesById, required this.taskActivityOwnersById, required this.projectStatisticsByProjectId, required this.projectStatisticsOwnersByProjectId, required this.projectStatisticsRevisionsByProjectId, required this.ownerSettingsByOwnerId, required this.ownerSettingsRevisionsByOwnerId, required this.noticeAcknowledgementsByScopedId, required this.noticeAcknowledgementRevisionsByScopedId, required this.operationsById, }); final Map tasksById; final Map taskOwnersById; final Map taskRevisionsById; final Map projectsById; final Map projectOwnersById; final Map projectRevisionsById; final Map lockedBlocksById; final Map lockedBlockOwnersById; final Map lockedBlockRevisionsById; final Map lockedOverridesById; final Map lockedOverrideOwnersById; final Map lockedOverrideRevisionsById; final Map schedulingSnapshotsById; final Map taskActivitiesById; final Map taskActivityOwnersById; final Map projectStatisticsByProjectId; final Map projectStatisticsOwnersByProjectId; final Map projectStatisticsRevisionsByProjectId; final Map ownerSettingsByOwnerId; final Map ownerSettingsRevisionsByOwnerId; final Map noticeAcknowledgementsByScopedId; final Map noticeAcknowledgementRevisionsByScopedId; final Map operationsById; _InMemoryApplicationState copy() { return _InMemoryApplicationState( tasksById: Map.of(tasksById), taskOwnersById: Map.of(taskOwnersById), taskRevisionsById: Map.of(taskRevisionsById), projectsById: Map.of(projectsById), projectOwnersById: Map.of(projectOwnersById), projectRevisionsById: Map.of(projectRevisionsById), lockedBlocksById: Map.of(lockedBlocksById), lockedBlockOwnersById: Map.of(lockedBlockOwnersById), lockedBlockRevisionsById: Map.of( lockedBlockRevisionsById, ), lockedOverridesById: Map.of(lockedOverridesById), lockedOverrideOwnersById: Map.of( lockedOverrideOwnersById, ), lockedOverrideRevisionsById: Map.of( lockedOverrideRevisionsById, ), schedulingSnapshotsById: Map.of(schedulingSnapshotsById), taskActivitiesById: Map.of(taskActivitiesById), taskActivityOwnersById: Map.of(taskActivityOwnersById), projectStatisticsByProjectId: Map.of(projectStatisticsByProjectId), projectStatisticsOwnersByProjectId: Map.of( projectStatisticsOwnersByProjectId, ), projectStatisticsRevisionsByProjectId: Map.of( projectStatisticsRevisionsByProjectId, ), ownerSettingsByOwnerId: Map.of( ownerSettingsByOwnerId, ), ownerSettingsRevisionsByOwnerId: Map.of( ownerSettingsRevisionsByOwnerId, ), noticeAcknowledgementsByScopedId: Map.of( noticeAcknowledgementsByScopedId, ), noticeAcknowledgementRevisionsByScopedId: Map.of( noticeAcknowledgementRevisionsByScopedId, ), operationsById: Map.of( operationsById, ), ); } } class _InMemoryApplicationRepositories implements ApplicationUnitOfWorkRepositories { _InMemoryApplicationRepositories(_InMemoryApplicationState state) : tasks = _UnitOfWorkTaskRepository( tasksById: state.tasksById, ownersById: state.taskOwnersById, revisionsById: state.taskRevisionsById, ), projects = _UnitOfWorkProjectRepository( projectsById: state.projectsById, ownersById: state.projectOwnersById, revisionsById: state.projectRevisionsById, ), lockedBlocks = _UnitOfWorkLockedBlockRepository( blocksById: state.lockedBlocksById, blockOwnersById: state.lockedBlockOwnersById, blockRevisionsById: state.lockedBlockRevisionsById, overridesById: state.lockedOverridesById, overrideOwnersById: state.lockedOverrideOwnersById, overrideRevisionsById: state.lockedOverrideRevisionsById, ), schedulingSnapshots = _UnitOfWorkSchedulingSnapshotRepository( state.schedulingSnapshotsById, ), taskActivities = _UnitOfWorkTaskActivityRepository( activitiesById: state.taskActivitiesById, ownersById: state.taskActivityOwnersById, ), projectStatistics = _UnitOfWorkProjectStatisticsRepository( statisticsByProjectId: state.projectStatisticsByProjectId, ownersByProjectId: state.projectStatisticsOwnersByProjectId, revisionsByProjectId: state.projectStatisticsRevisionsByProjectId, ), ownerSettings = _UnitOfWorkOwnerSettingsRepository( settingsByOwnerId: state.ownerSettingsByOwnerId, revisionsByOwnerId: state.ownerSettingsRevisionsByOwnerId, ), noticeAcknowledgements = _UnitOfWorkNoticeAcknowledgementRepository( recordsByScopedId: state.noticeAcknowledgementsByScopedId, revisionsByScopedId: state.noticeAcknowledgementRevisionsByScopedId, ), operations = _UnitOfWorkOperationRepository(state.operationsById); @override final TaskRepository tasks; @override final ProjectRepository projects; @override final LockedBlockRepository lockedBlocks; @override final SchedulingSnapshotRepository schedulingSnapshots; @override final TaskActivityRepository taskActivities; @override final ProjectStatisticsRepository projectStatistics; @override final OwnerSettingsRepository ownerSettings; @override final NoticeAcknowledgementRepository noticeAcknowledgements; @override final ApplicationOperationRepository operations; } class _UnitOfWorkTaskRepository implements TaskRepository { _UnitOfWorkTaskRepository({ required Map tasksById, required Map ownersById, required Map revisionsById, }) : _tasksById = tasksById, _ownersById = ownersById, _revisionsById = revisionsById; final Map _tasksById; final Map _ownersById; final Map _revisionsById; @override Future findById(String id) async => _tasksById[id]; @override Future?> findRecordById(String id) async { final task = _tasksById[id]; if (task == null) { return null; } return RepositoryRecord( value: task, ownerId: _ownerFor(id), revision: _revisionFor(id), ); } @override Future> findAll() async { return List.unmodifiable(_tasksById.values); } @override Future> findByStatus(TaskStatus status) async { return List.unmodifiable( _tasksById.values.where((task) => task.status == status), ); } @override Future> findByOwner({ required String ownerId, RepositoryPageRequest page = const RepositoryPageRequest(), }) async { final tasks = _tasksById.values .where((task) => _ownerFor(task.id) == ownerId) .toList(growable: false) ..sort(_compareTaskIds); return _page(tasks, page); } @override Future> findByProjectId({ required String ownerId, required String projectId, RepositoryPageRequest page = const RepositoryPageRequest(), }) async { final tasks = _tasksById.values .where( (task) => _ownerFor(task.id) == ownerId && task.projectId == projectId, ) .toList(growable: false) ..sort(_compareTaskIds); return _page(tasks, page); } @override Future> findByParentTaskId({ required String ownerId, required String parentTaskId, RepositoryPageRequest page = const RepositoryPageRequest(), }) async { final tasks = _tasksById.values .where( (task) => _ownerFor(task.id) == ownerId && task.parentTaskId == parentTaskId, ) .toList(growable: false) ..sort(_compareTaskIds); return _page(tasks, page); } @override Future> findBacklogCandidates( BacklogCandidateQuery query, ) async { final tasks = _tasksById.values .where( (task) => _ownerFor(task.id) == query.ownerId && task.status == TaskStatus.backlog && (query.projectId == null || task.projectId == query.projectId) && query.tags.every(task.backlogTags.contains), ) .toList(growable: false) ..sort(_compareBacklogCandidates); return _page(tasks, query.page); } @override Future> findScheduledInWindow(SchedulingWindow window) async { return List.unmodifiable( _tasksById.values.where((task) { return _taskOverlapsWindow(task, window); }), ); } @override Future> findScheduledInWindowForOwner({ required String ownerId, required SchedulingWindow window, RepositoryPageRequest page = const RepositoryPageRequest(), }) async { final tasks = _tasksById.values .where( (task) => _ownerFor(task.id) == ownerId && _taskOverlapsWindow(task, window), ) .toList(growable: false) ..sort(_compareScheduledTasks); return _page(tasks, page); } @override Future> findForLocalDay({ required String ownerId, required CivilDate localDate, required SchedulingWindow window, RepositoryPageRequest page = const RepositoryPageRequest(), }) { return findScheduledInWindowForOwner( ownerId: ownerId, window: window, page: page, ); } @override Future save(Task task) async { _tasksById[task.id] = task; _ownersById.putIfAbsent(task.id, () => 'owner-1'); _revisionsById[task.id] = _revisionFor(task.id) + 1; } @override Future saveAll(Iterable tasks) async { for (final task in tasks) { await save(task); } } @override Future insert({ required Task task, required String ownerId, }) async { if (_tasksById.containsKey(task.id)) { return RepositoryMutationResult.failure( RepositoryFailure( code: RepositoryFailureCode.duplicateId, entityId: task.id, ), ); } _tasksById[task.id] = task; _ownersById[task.id] = ownerId; _revisionsById[task.id] = 1; return RepositoryMutationResult.success(revision: 1); } @override Future saveIfRevision({ required Task task, required String ownerId, required int expectedRevision, }) async { if (!_tasksById.containsKey(task.id) || _ownerFor(task.id) != ownerId) { return RepositoryMutationResult.failure( RepositoryFailure( code: RepositoryFailureCode.notFound, entityId: task.id), ); } final actualRevision = _revisionFor(task.id); if (actualRevision != expectedRevision) { return RepositoryMutationResult.failure( RepositoryFailure( code: RepositoryFailureCode.staleRevision, entityId: task.id, expectedRevision: expectedRevision, actualRevision: actualRevision, ), ); } final nextRevision = actualRevision + 1; _tasksById[task.id] = task; _ownersById[task.id] = ownerId; _revisionsById[task.id] = nextRevision; return RepositoryMutationResult.success(revision: nextRevision); } String _ownerFor(String id) => _ownersById[id] ?? 'owner-1'; int _revisionFor(String id) => _revisionsById[id] ?? 1; } class _UnitOfWorkProjectRepository implements ProjectRepository { _UnitOfWorkProjectRepository({ required Map projectsById, required Map ownersById, required Map revisionsById, }) : _projectsById = projectsById, _ownersById = ownersById, _revisionsById = revisionsById; final Map _projectsById; final Map _ownersById; final Map _revisionsById; @override Future findById(String id) async => _projectsById[id]; @override Future?> findRecordById(String id) async { final project = _projectsById[id]; if (project == null) { return null; } return RepositoryRecord( value: project, ownerId: _ownerFor(id), revision: _revisionFor(id), archivedAt: project.archivedAt, ); } @override Future> findAll() async { return List.unmodifiable(_projectsById.values); } @override Future> findByOwner({ required String ownerId, bool includeArchived = false, RepositoryPageRequest page = const RepositoryPageRequest(), }) async { final projects = _projectsById.values .where( (project) => _ownerFor(project.id) == ownerId && (includeArchived || !project.isArchived), ) .toList(growable: false) ..sort(_compareProjects); return _page(projects, page); } @override Future save(ProjectProfile project) async { _projectsById[project.id] = project; _ownersById.putIfAbsent(project.id, () => 'owner-1'); _revisionsById[project.id] = _revisionFor(project.id) + 1; } @override Future insert({ required ProjectProfile project, required String ownerId, }) async { if (_projectsById.containsKey(project.id)) { return RepositoryMutationResult.failure( RepositoryFailure( code: RepositoryFailureCode.duplicateId, entityId: project.id, ), ); } _projectsById[project.id] = project; _ownersById[project.id] = ownerId; _revisionsById[project.id] = 1; return RepositoryMutationResult.success(revision: 1); } @override Future saveIfRevision({ required ProjectProfile project, required String ownerId, required int expectedRevision, }) async { if (!_projectsById.containsKey(project.id) || _ownerFor(project.id) != ownerId) { return RepositoryMutationResult.failure( RepositoryFailure( code: RepositoryFailureCode.notFound, entityId: project.id, ), ); } final actualRevision = _revisionFor(project.id); if (actualRevision != expectedRevision) { return RepositoryMutationResult.failure( RepositoryFailure( code: RepositoryFailureCode.staleRevision, entityId: project.id, expectedRevision: expectedRevision, actualRevision: actualRevision, ), ); } final nextRevision = actualRevision + 1; _projectsById[project.id] = project; _ownersById[project.id] = ownerId; _revisionsById[project.id] = nextRevision; return RepositoryMutationResult.success(revision: nextRevision); } @override Future archive({ required String projectId, required String ownerId, required int expectedRevision, required DateTime archivedAt, }) async { final project = _projectsById[projectId]; if (project == null) { return RepositoryMutationResult.failure( RepositoryFailure( code: RepositoryFailureCode.notFound, entityId: projectId, ), ); } return saveIfRevision( project: project.copyWith(archivedAt: archivedAt), ownerId: ownerId, expectedRevision: expectedRevision, ); } String _ownerFor(String id) => _ownersById[id] ?? 'owner-1'; int _revisionFor(String id) => _revisionsById[id] ?? 1; } class _UnitOfWorkLockedBlockRepository implements LockedBlockRepository { _UnitOfWorkLockedBlockRepository({ required Map blocksById, required Map blockOwnersById, required Map blockRevisionsById, required Map overridesById, required Map overrideOwnersById, required Map overrideRevisionsById, }) : _blocksById = blocksById, _blockOwnersById = blockOwnersById, _blockRevisionsById = blockRevisionsById, _overridesById = overridesById, _overrideOwnersById = overrideOwnersById, _overrideRevisionsById = overrideRevisionsById; final Map _blocksById; final Map _blockOwnersById; final Map _blockRevisionsById; final Map _overridesById; final Map _overrideOwnersById; final Map _overrideRevisionsById; @override Future findBlockById(String id) async => _blocksById[id]; @override Future?> findBlockRecordById(String id) async { final block = _blocksById[id]; if (block == null) { return null; } return RepositoryRecord( value: block, ownerId: _blockOwnerFor(id), revision: _blockRevisionFor(id), archivedAt: block.archivedAt, ); } @override Future> findAllBlocks() async { return List.unmodifiable(_blocksById.values); } @override Future> findBlocksByOwner({ required String ownerId, bool includeArchived = false, RepositoryPageRequest page = const RepositoryPageRequest(), }) async { final blocks = _blocksById.values .where( (block) => _blockOwnerFor(block.id) == ownerId && (includeArchived || !block.isArchived), ) .toList(growable: false) ..sort(_compareLockedBlocks); return _page(blocks, page); } @override Future findOverrideById(String id) async { return _overridesById[id]; } @override Future?> findOverrideRecordById( String id, ) async { final override = _overridesById[id]; if (override == null) { return null; } return RepositoryRecord( value: override, ownerId: _overrideOwnerFor(id), revision: _overrideRevisionFor(id), ); } @override Future> findAllOverrides() async { return List.unmodifiable(_overridesById.values); } @override Future> findOverridesForDate({ required String ownerId, required CivilDate date, RepositoryPageRequest page = const RepositoryPageRequest(), }) async { final overrides = _overridesById.values .where( (override) => _overrideOwnerFor(override.id) == ownerId && override.date == date, ) .toList(growable: false) ..sort(_compareLockedOverrides); return _page(overrides, page); } @override Future saveBlock(LockedBlock block) async { _blocksById[block.id] = block; _blockOwnersById.putIfAbsent(block.id, () => 'owner-1'); _blockRevisionsById[block.id] = _blockRevisionFor(block.id) + 1; } @override Future saveOverride(LockedBlockOverride override) async { _overridesById[override.id] = override; _overrideOwnersById.putIfAbsent(override.id, () => 'owner-1'); _overrideRevisionsById[override.id] = _overrideRevisionFor(override.id) + 1; } @override Future insertBlock({ required LockedBlock block, required String ownerId, }) async { if (_blocksById.containsKey(block.id)) { return RepositoryMutationResult.failure( RepositoryFailure( code: RepositoryFailureCode.duplicateId, entityId: block.id, ), ); } _blocksById[block.id] = block; _blockOwnersById[block.id] = ownerId; _blockRevisionsById[block.id] = 1; return RepositoryMutationResult.success(revision: 1); } @override Future saveBlockIfRevision({ required LockedBlock block, required String ownerId, required int expectedRevision, }) async { if (!_blocksById.containsKey(block.id) || _blockOwnerFor(block.id) != ownerId) { return RepositoryMutationResult.failure( RepositoryFailure( code: RepositoryFailureCode.notFound, entityId: block.id, ), ); } final actualRevision = _blockRevisionFor(block.id); if (actualRevision != expectedRevision) { return RepositoryMutationResult.failure( RepositoryFailure( code: RepositoryFailureCode.staleRevision, entityId: block.id, expectedRevision: expectedRevision, actualRevision: actualRevision, ), ); } final nextRevision = actualRevision + 1; _blocksById[block.id] = block; _blockOwnersById[block.id] = ownerId; _blockRevisionsById[block.id] = nextRevision; return RepositoryMutationResult.success(revision: nextRevision); } @override Future archiveBlock({ required String blockId, required String ownerId, required int expectedRevision, required DateTime archivedAt, }) async { final block = _blocksById[blockId]; if (block == null) { return RepositoryMutationResult.failure( RepositoryFailure( code: RepositoryFailureCode.notFound, entityId: blockId, ), ); } return saveBlockIfRevision( block: block.copyWith(updatedAt: archivedAt, archivedAt: archivedAt), ownerId: ownerId, expectedRevision: expectedRevision, ); } @override Future insertOverride({ required LockedBlockOverride override, required String ownerId, }) async { if (_overridesById.containsKey(override.id)) { return RepositoryMutationResult.failure( RepositoryFailure( code: RepositoryFailureCode.duplicateId, entityId: override.id, ), ); } _overridesById[override.id] = override; _overrideOwnersById[override.id] = ownerId; _overrideRevisionsById[override.id] = 1; return RepositoryMutationResult.success(revision: 1); } @override Future saveOverrideIfRevision({ required LockedBlockOverride override, required String ownerId, required int expectedRevision, }) async { if (!_overridesById.containsKey(override.id) || _overrideOwnerFor(override.id) != ownerId) { return RepositoryMutationResult.failure( RepositoryFailure( code: RepositoryFailureCode.notFound, entityId: override.id, ), ); } final actualRevision = _overrideRevisionFor(override.id); if (actualRevision != expectedRevision) { return RepositoryMutationResult.failure( RepositoryFailure( code: RepositoryFailureCode.staleRevision, entityId: override.id, expectedRevision: expectedRevision, actualRevision: actualRevision, ), ); } final nextRevision = actualRevision + 1; _overridesById[override.id] = override; _overrideOwnersById[override.id] = ownerId; _overrideRevisionsById[override.id] = nextRevision; return RepositoryMutationResult.success(revision: nextRevision); } String _blockOwnerFor(String id) => _blockOwnersById[id] ?? 'owner-1'; int _blockRevisionFor(String id) => _blockRevisionsById[id] ?? 1; String _overrideOwnerFor(String id) => _overrideOwnersById[id] ?? 'owner-1'; int _overrideRevisionFor(String id) => _overrideRevisionsById[id] ?? 1; } class _UnitOfWorkSchedulingSnapshotRepository implements SchedulingSnapshotRepository { _UnitOfWorkSchedulingSnapshotRepository(this._snapshotsById); final Map _snapshotsById; @override Future findById(String id) async { return _snapshotsById[id]; } @override Future> findInWindow( SchedulingWindow window, ) async { return List.unmodifiable( _snapshotsById.values.where( (snapshot) => snapshot.window.interval.overlaps(window.interval), ), ); } @override Future save(SchedulingStateSnapshot snapshot) async { _snapshotsById[snapshot.id] = snapshot; } } class _UnitOfWorkTaskActivityRepository implements TaskActivityRepository { _UnitOfWorkTaskActivityRepository({ required Map activitiesById, required Map ownersById, }) : _activitiesById = activitiesById, _ownersById = ownersById; final Map _activitiesById; final Map _ownersById; @override Future findById(String id) async => _activitiesById[id]; @override Future> findByOperationId(String operationId) async { return List.unmodifiable( _activitiesById.values.where( (activity) => activity.operationId == operationId, ), ); } @override Future> findByTaskId(String taskId) async { return List.unmodifiable( _activitiesById.values.where((activity) => activity.taskId == taskId), ); } @override Future> findByProjectId(String projectId) async { return List.unmodifiable( _activitiesById.values.where( (activity) => activity.projectId == projectId, ), ); } @override Future> findAll() async { return List.unmodifiable(_activitiesById.values); } @override Future> findByOwner({ required String ownerId, RepositoryPageRequest page = const RepositoryPageRequest(), }) async { final activities = _activitiesById.values .where((activity) => _ownerFor(activity.id) == ownerId) .toList(growable: false) ..sort(_compareActivities); return _page(activities, page); } @override Future save(TaskActivity activity) async { _activitiesById[activity.id] = activity; _ownersById.putIfAbsent(activity.id, () => 'owner-1'); } @override Future saveAll(Iterable activities) async { for (final activity in activities) { _activitiesById[activity.id] = activity; _ownersById.putIfAbsent(activity.id, () => 'owner-1'); } } @override Future append({ required TaskActivity activity, required String ownerId, }) async { if (_activitiesById.containsKey(activity.id)) { return RepositoryMutationResult.failure( RepositoryFailure( code: RepositoryFailureCode.duplicateId, entityId: activity.id, ), ); } _activitiesById[activity.id] = activity; _ownersById[activity.id] = ownerId; return RepositoryMutationResult.success(revision: 1); } String _ownerFor(String id) => _ownersById[id] ?? 'owner-1'; } class _UnitOfWorkProjectStatisticsRepository implements ProjectStatisticsRepository { _UnitOfWorkProjectStatisticsRepository({ required Map statisticsByProjectId, required Map ownersByProjectId, required Map revisionsByProjectId, }) : _statisticsByProjectId = statisticsByProjectId, _ownersByProjectId = ownersByProjectId, _revisionsByProjectId = revisionsByProjectId; final Map _statisticsByProjectId; final Map _ownersByProjectId; final Map _revisionsByProjectId; @override Future findByProjectId(String projectId) async { return _statisticsByProjectId[projectId]; } @override Future?> findRecordByProjectId( String projectId, ) async { final statistics = _statisticsByProjectId[projectId]; if (statistics == null) { return null; } return RepositoryRecord( value: statistics, ownerId: _ownersByProjectId[projectId] ?? 'owner-1', revision: _revisionsByProjectId[projectId] ?? 1, ); } @override Future> findAll() async { return List.unmodifiable(_statisticsByProjectId.values); } @override Future save(ProjectStatistics statistics) async { _statisticsByProjectId[statistics.projectId] = statistics; _ownersByProjectId.putIfAbsent(statistics.projectId, () => 'owner-1'); _revisionsByProjectId[statistics.projectId] = (_revisionsByProjectId[statistics.projectId] ?? 1) + 1; } @override Future saveIfRevision({ required ProjectStatistics statistics, required String ownerId, required int expectedRevision, }) async { final projectId = statistics.projectId; if (!_statisticsByProjectId.containsKey(projectId) || (_ownersByProjectId[projectId] ?? 'owner-1') != ownerId) { return RepositoryMutationResult.failure( RepositoryFailure( code: RepositoryFailureCode.notFound, entityId: projectId, ), ); } final actualRevision = _revisionsByProjectId[projectId] ?? 1; if (actualRevision != expectedRevision) { return RepositoryMutationResult.failure( RepositoryFailure( code: RepositoryFailureCode.staleRevision, entityId: projectId, expectedRevision: expectedRevision, actualRevision: actualRevision, ), ); } final nextRevision = actualRevision + 1; _statisticsByProjectId[projectId] = statistics; _ownersByProjectId[projectId] = ownerId; _revisionsByProjectId[projectId] = nextRevision; return RepositoryMutationResult.success(revision: nextRevision); } } class _UnitOfWorkOwnerSettingsRepository implements OwnerSettingsRepository { _UnitOfWorkOwnerSettingsRepository({ required Map settingsByOwnerId, required Map revisionsByOwnerId, }) : _settingsByOwnerId = settingsByOwnerId, _revisionsByOwnerId = revisionsByOwnerId; final Map _settingsByOwnerId; final Map _revisionsByOwnerId; @override Future findByOwnerId(String ownerId) async { return _settingsByOwnerId[ownerId]; } @override Future?> findRecordByOwnerId( String ownerId, ) async { final settings = _settingsByOwnerId[ownerId]; if (settings == null) { return null; } return RepositoryRecord( value: settings, ownerId: ownerId, revision: _revisionsByOwnerId[ownerId] ?? 1, ); } @override Future save(OwnerSettings settings) async { _settingsByOwnerId[settings.ownerId] = settings; _revisionsByOwnerId[settings.ownerId] = (_revisionsByOwnerId[settings.ownerId] ?? 1) + 1; } @override Future saveIfRevision({ required OwnerSettings settings, required int expectedRevision, }) async { final ownerId = settings.ownerId; if (!_settingsByOwnerId.containsKey(ownerId)) { return RepositoryMutationResult.failure( RepositoryFailure( code: RepositoryFailureCode.notFound, entityId: ownerId, ), ); } final actualRevision = _revisionsByOwnerId[ownerId] ?? 1; if (actualRevision != expectedRevision) { return RepositoryMutationResult.failure( RepositoryFailure( code: RepositoryFailureCode.staleRevision, entityId: ownerId, expectedRevision: expectedRevision, actualRevision: actualRevision, ), ); } final nextRevision = actualRevision + 1; _settingsByOwnerId[ownerId] = settings; _revisionsByOwnerId[ownerId] = nextRevision; return RepositoryMutationResult.success(revision: nextRevision); } } class _UnitOfWorkNoticeAcknowledgementRepository implements NoticeAcknowledgementRepository { _UnitOfWorkNoticeAcknowledgementRepository({ required Map recordsByScopedId, required Map revisionsByScopedId, }) : _recordsByScopedId = recordsByScopedId, _revisionsByScopedId = revisionsByScopedId; final Map _recordsByScopedId; final Map _revisionsByScopedId; @override Future findById({ required String ownerId, required String noticeId, }) async { return _recordsByScopedId[ _noticeAcknowledgementKey(ownerId: ownerId, noticeId: noticeId)]; } @override Future> findByOwnerId( String ownerId, ) async { return List.unmodifiable( _recordsByScopedId.values.where((record) => record.ownerId == ownerId), ); } @override Future> findByOwner({ required String ownerId, RepositoryPageRequest page = const RepositoryPageRequest(), }) async { final records = _recordsByScopedId.values .where((record) => record.ownerId == ownerId) .toList(growable: false) ..sort(_compareNoticeAcknowledgements); return _page(records, page); } @override Future save(NoticeAcknowledgementRecord record) async { final key = _noticeAcknowledgementKey( ownerId: record.ownerId, noticeId: record.noticeId, ); _recordsByScopedId[key] = record; _revisionsByScopedId[key] = (_revisionsByScopedId[key] ?? 1) + 1; } @override Future insert( NoticeAcknowledgementRecord record, ) async { final key = _noticeAcknowledgementKey( ownerId: record.ownerId, noticeId: record.noticeId, ); if (_recordsByScopedId.containsKey(key)) { return RepositoryMutationResult.failure( RepositoryFailure( code: RepositoryFailureCode.duplicateId, entityId: record.noticeId, ), ); } _recordsByScopedId[key] = record; _revisionsByScopedId[key] = 1; return RepositoryMutationResult.success(revision: 1); } } class _UnitOfWorkOperationRepository implements ApplicationOperationRepository { _UnitOfWorkOperationRepository(this._operationsById); final Map _operationsById; @override Future findById(String operationId) async { return _operationsById[operationId]; } @override Future findByIdempotencyKey({ required String ownerId, required String operationId, }) async { final operation = _operationsById[operationId]; if (operation == null || operation.ownerId != ownerId) { return null; } return operation; } @override Future save(ApplicationOperationRecord operation) async { _operationsById[operation.operationId] = operation; } @override Future insertIfAbsent( ApplicationOperationRecord operation, ) async { if (_operationsById.containsKey(operation.operationId)) { return RepositoryMutationResult.failure( RepositoryFailure( code: RepositoryFailureCode.duplicateOperation, entityId: operation.operationId, ), ); } _operationsById[operation.operationId] = operation; return RepositoryMutationResult.success(revision: 1); } } String _noticeAcknowledgementKey({ required String ownerId, required String noticeId, }) { return '$ownerId::$noticeId'; } RepositoryPage _page(List sortedItems, RepositoryPageRequest request) { final offset = int.tryParse(request.cursor ?? '') ?? 0; final safeOffset = offset.clamp(0, sortedItems.length); final end = (safeOffset + request.limit).clamp(0, sortedItems.length); final items = sortedItems.sublist(safeOffset, end); final nextCursor = end < sortedItems.length ? end.toString() : null; return RepositoryPage( items: List.unmodifiable(items), nextCursor: nextCursor, ); } bool _taskOverlapsWindow(Task task, SchedulingWindow window) { final start = task.scheduledStart; final end = task.scheduledEnd; if (start == null || end == null) { return false; } return TimeInterval(start: start, end: end).overlaps(window.interval); } int _compareTaskIds(Task left, Task right) => left.id.compareTo(right.id); int _compareScheduledTasks(Task left, Task right) { final leftStart = left.scheduledStart; final rightStart = right.scheduledStart; if (leftStart != null && rightStart != null) { final startComparison = leftStart.compareTo(rightStart); if (startComparison != 0) { return startComparison; } } return left.id.compareTo(right.id); } int _compareBacklogCandidates(Task left, Task right) { final createdComparison = left.createdAt.compareTo(right.createdAt); if (createdComparison != 0) { return createdComparison; } return left.id.compareTo(right.id); } int _compareProjects(ProjectProfile left, ProjectProfile right) { final nameComparison = left.name.compareTo(right.name); if (nameComparison != 0) { return nameComparison; } return left.id.compareTo(right.id); } int _compareLockedBlocks(LockedBlock left, LockedBlock right) { final nameComparison = left.name.compareTo(right.name); if (nameComparison != 0) { return nameComparison; } return left.id.compareTo(right.id); } int _compareLockedOverrides( LockedBlockOverride left, LockedBlockOverride right, ) { final dateComparison = left.date.compareTo(right.date); if (dateComparison != 0) { return dateComparison; } final createdComparison = left.createdAt.compareTo(right.createdAt); if (createdComparison != 0) { return createdComparison; } return left.id.compareTo(right.id); } int _compareActivities(TaskActivity left, TaskActivity right) { final occurredComparison = left.occurredAt.compareTo(right.occurredAt); if (occurredComparison != 0) { return occurredComparison; } return left.id.compareTo(right.id); } int _compareNoticeAcknowledgements( NoticeAcknowledgementRecord left, NoticeAcknowledgementRecord right, ) { final acknowledgedComparison = left.acknowledgedAt.compareTo(right.acknowledgedAt); if (acknowledgedComparison != 0) { return acknowledgedComparison; } return left.noticeId.compareTo(right.noticeId); } String _requiredTrimmed(String value, String name) { final trimmed = value.trim(); if (trimmed.isEmpty) { throw ArgumentError.value(value, name, 'Value is required.'); } return trimmed; }