// 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> findAll(); Future save(TaskActivity activity); Future saveAll(Iterable activities); } /// Repository contract for project statistics aggregates. abstract interface class ProjectStatisticsRepository { Future findByProjectId(String projectId); Future> findAll(); Future save(ProjectStatistics statistics); } /// Repository contract for owner settings. abstract interface class OwnerSettingsRepository { Future findByOwnerId(String ownerId); Future save(OwnerSettings settings); } /// Repository contract for consumed one-time notices. abstract interface class NoticeAcknowledgementRepository { Future findById({ required String ownerId, required String noticeId, }); Future> findByOwnerId(String ownerId); Future save(NoticeAcknowledgementRecord record); } /// Repository contract for exactly-once operation records. abstract interface class ApplicationOperationRepository { Future findById(String operationId); Future save(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, }, projectsById: { for (final project in initialProjects) project.id: project, }, lockedBlocksById: { for (final block in initialLockedBlocks) block.id: block, }, lockedOverridesById: { for (final override in initialLockedOverrides) override.id: override, }, schedulingSnapshotsById: { for (final snapshot in initialSchedulingSnapshots) snapshot.id: snapshot, }, taskActivitiesById: { for (final activity in initialTaskActivities) activity.id: activity, }, projectStatisticsByProjectId: { for (final statistics in initialProjectStatistics) statistics.projectId: statistics, }, ownerSettingsByOwnerId: { for (final settings in initialOwnerSettings) settings.ownerId: settings, }, noticeAcknowledgementsByScopedId: { for (final acknowledgement in initialNoticeAcknowledgements) _noticeAcknowledgementKey( ownerId: acknowledgement.ownerId, noticeId: acknowledgement.noticeId, ): acknowledgement, }, 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.projectsById, required this.lockedBlocksById, required this.lockedOverridesById, required this.schedulingSnapshotsById, required this.taskActivitiesById, required this.projectStatisticsByProjectId, required this.ownerSettingsByOwnerId, required this.noticeAcknowledgementsByScopedId, required this.operationsById, }); final Map tasksById; final Map projectsById; final Map lockedBlocksById; final Map lockedOverridesById; final Map schedulingSnapshotsById; final Map taskActivitiesById; final Map projectStatisticsByProjectId; final Map ownerSettingsByOwnerId; final Map noticeAcknowledgementsByScopedId; final Map operationsById; _InMemoryApplicationState copy() { return _InMemoryApplicationState( tasksById: Map.of(tasksById), projectsById: Map.of(projectsById), lockedBlocksById: Map.of(lockedBlocksById), lockedOverridesById: Map.of(lockedOverridesById), schedulingSnapshotsById: Map.of(schedulingSnapshotsById), taskActivitiesById: Map.of(taskActivitiesById), projectStatisticsByProjectId: Map.of(projectStatisticsByProjectId), ownerSettingsByOwnerId: Map.of( ownerSettingsByOwnerId, ), noticeAcknowledgementsByScopedId: Map.of( noticeAcknowledgementsByScopedId, ), operationsById: Map.of( operationsById, ), ); } } class _InMemoryApplicationRepositories implements ApplicationUnitOfWorkRepositories { _InMemoryApplicationRepositories(_InMemoryApplicationState state) : tasks = _UnitOfWorkTaskRepository(state.tasksById), projects = _UnitOfWorkProjectRepository(state.projectsById), lockedBlocks = _UnitOfWorkLockedBlockRepository( blocksById: state.lockedBlocksById, overridesById: state.lockedOverridesById, ), schedulingSnapshots = _UnitOfWorkSchedulingSnapshotRepository( state.schedulingSnapshotsById, ), taskActivities = _UnitOfWorkTaskActivityRepository( state.taskActivitiesById, ), projectStatistics = _UnitOfWorkProjectStatisticsRepository( state.projectStatisticsByProjectId, ), ownerSettings = _UnitOfWorkOwnerSettingsRepository( state.ownerSettingsByOwnerId, ), noticeAcknowledgements = _UnitOfWorkNoticeAcknowledgementRepository( state.noticeAcknowledgementsByScopedId, ), 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(this._tasksById); final Map _tasksById; @override Future findById(String id) async => _tasksById[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> findScheduledInWindow(SchedulingWindow window) async { return List.unmodifiable( _tasksById.values.where((task) { final start = task.scheduledStart; final end = task.scheduledEnd; if (start == null || end == null) { return false; } return TimeInterval(start: start, end: end).overlaps(window.interval); }), ); } @override Future save(Task task) async { _tasksById[task.id] = task; } @override Future saveAll(Iterable tasks) async { for (final task in tasks) { _tasksById[task.id] = task; } } } class _UnitOfWorkProjectRepository implements ProjectRepository { _UnitOfWorkProjectRepository(this._projectsById); final Map _projectsById; @override Future findById(String id) async => _projectsById[id]; @override Future> findAll() async { return List.unmodifiable(_projectsById.values); } @override Future save(ProjectProfile project) async { _projectsById[project.id] = project; } } class _UnitOfWorkLockedBlockRepository implements LockedBlockRepository { _UnitOfWorkLockedBlockRepository({ required Map blocksById, required Map overridesById, }) : _blocksById = blocksById, _overridesById = overridesById; final Map _blocksById; final Map _overridesById; @override Future findBlockById(String id) async => _blocksById[id]; @override Future> findAllBlocks() async { return List.unmodifiable(_blocksById.values); } @override Future findOverrideById(String id) async { return _overridesById[id]; } @override Future> findAllOverrides() async { return List.unmodifiable(_overridesById.values); } @override Future saveBlock(LockedBlock block) async { _blocksById[block.id] = block; } @override Future saveOverride(LockedBlockOverride override) async { _overridesById[override.id] = override; } } 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(this._activitiesById); final Map _activitiesById; @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> findAll() async { return List.unmodifiable(_activitiesById.values); } @override Future save(TaskActivity activity) async { _activitiesById[activity.id] = activity; } @override Future saveAll(Iterable activities) async { for (final activity in activities) { _activitiesById[activity.id] = activity; } } } class _UnitOfWorkProjectStatisticsRepository implements ProjectStatisticsRepository { _UnitOfWorkProjectStatisticsRepository(this._statisticsByProjectId); final Map _statisticsByProjectId; @override Future findByProjectId(String projectId) async { return _statisticsByProjectId[projectId]; } @override Future> findAll() async { return List.unmodifiable(_statisticsByProjectId.values); } @override Future save(ProjectStatistics statistics) async { _statisticsByProjectId[statistics.projectId] = statistics; } } class _UnitOfWorkOwnerSettingsRepository implements OwnerSettingsRepository { _UnitOfWorkOwnerSettingsRepository(this._settingsByOwnerId); final Map _settingsByOwnerId; @override Future findByOwnerId(String ownerId) async { return _settingsByOwnerId[ownerId]; } @override Future save(OwnerSettings settings) async { _settingsByOwnerId[settings.ownerId] = settings; } } class _UnitOfWorkNoticeAcknowledgementRepository implements NoticeAcknowledgementRepository { _UnitOfWorkNoticeAcknowledgementRepository(this._recordsByScopedId); final Map _recordsByScopedId; @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 save(NoticeAcknowledgementRecord record) async { _recordsByScopedId[_noticeAcknowledgementKey( ownerId: record.ownerId, noticeId: record.noticeId, )] = record; } } class _UnitOfWorkOperationRepository implements ApplicationOperationRepository { _UnitOfWorkOperationRepository(this._operationsById); final Map _operationsById; @override Future findById(String operationId) async { return _operationsById[operationId]; } @override Future save(ApplicationOperationRecord operation) async { _operationsById[operation.operationId] = operation; } } String _noticeAcknowledgementKey({ required String ownerId, required String noticeId, }) { return '$ownerId::$noticeId'; } String _requiredTrimmed(String value, String name) { final trimmed = value.trim(); if (trimmed.isEmpty) { throw ArgumentError.value(value, name, 'Value is required.'); } return trimmed; }