Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,12 @@ An allowlisted non-content value attached to a Product Usage Event.
_Avoid_: Event payload, arbitrary metadata, raw detail

**Schedule**:
A planned commitment with an appointment time and place that OnTime helps the user prepare for.
_Avoid_: Calendar event, meeting
A planned commitment with a place, appointment time, travel time, optional buffer time, and preparation.
_Avoid_: Event, appointment record

**Preparation**:
The set of user-planned steps completed before leaving for a Schedule.
_Avoid_: Prep routine, checklist
An ordered set of steps and durations used to get ready for a Schedule.
_Avoid_: Routine, prep checklist

**Preparation Duration**:
The sum of a Schedule's Preparation step durations, excluding move time and Schedule Spare Time.
Expand All @@ -101,6 +101,18 @@ _Avoid_: Total duration, travel time, buffer time
A user buffer before a Schedule's appointment time, separate from travel time and Preparation Duration.
_Avoid_: Preparation time, move time

**Default Preparation**:
The user's fallback Preparation for new Schedules.
_Avoid_: Base preparation, global preparation

**Custom Preparation**:
A Schedule-specific Preparation that differs from the user's fallback or selected template.
_Avoid_: Changed preparation, edited default

**Preparation Mode**:
The category describing whether a Schedule uses Default Preparation, a preparation template, or Custom Preparation.
_Avoid_: Preparation type, preparation source

**Schedule Notification**:
A user-facing notification that starts preparation for a scheduled commitment at the intended moment.
_Avoid_: Schedule alarm, alarm, push
Expand Down Expand Up @@ -167,6 +179,11 @@ _Avoid_: Loaded range, stream range, cached range

## Relationships

- A **Product Usage Event** may describe a **Schedule**, **Preparation**, notification, alarm, onboarding, or account action without storing the user's raw schedule names, notes, place names, credentials, tokens, or free text.
- A **Schedule** has one effective **Preparation** for calculating preparation timing.
- A **Default Preparation** may seed a new **Schedule** before the user chooses a different preparation.
- A **Custom Preparation** belongs to one **Schedule**.
- **Preparation Mode** may appear as an **Analytics Event Parameter** without exposing preparation step names.
- A **Monthly Calendar** displays **Schedules** grouped by calendar day.
- A **Calendar Month Range** starts at the first day of its first month and ends before the first day of the month after its last month.
- A **Monthly Calendar** may extend a **Calendar Month Range** when the user moves to an adjacent month.
Expand All @@ -176,7 +193,6 @@ _Avoid_: Loaded range, stream range, cached range
- A **Preparation** contains zero or more **Preparation Steps** in user-defined order.
- A **Preparation Step** belongs to exactly one **Preparation**.
- A **Schedule Notification** uses **Schedule** and **Preparation** timing, but is not itself a **Schedule** or **Preparation**.
- A **Product Usage Event** may describe a schedule, preparation, notification, alarm, onboarding, or account action without storing the user's raw schedule names, notes, place names, credentials, tokens, or free text.
- First-release **Product Usage Events** are **Workflow Milestone Events**, not every tap or raw navigation step.
- First-release **Workflow Milestone Events** cover analytics preference, onboarding, authentication, schedule, notification permission, alarm, and schedule-finish outcomes.
- **Provider Authentication Completed** precedes **OnTime Session Established** during Apple or Google sign-in.
Expand Down Expand Up @@ -245,6 +261,7 @@ _Avoid_: Loaded range, stream range, cached range
- "Event taxonomy" was broad; resolved: first-release analytics tracks **Workflow Milestone Events** only.
- "Event payload" was too open-ended; resolved: events use allowlisted **Analytics Event Parameters** only.
- "Login completed" was ambiguous for Apple and Google sign-in; resolved: external account prompt completion is **Provider Authentication Completed**, while usable OnTime sign-in is **OnTime Session Established**.
- "Preparation" was ambiguous between the user's fallback steps and a schedule-specific edited set; resolved: use **Default Preparation** for the fallback and **Custom Preparation** for the schedule-specific version.
- "Alarm permission" was ambiguous between **Exact Timing Permission** and notification permission; resolved: notification permission may enable a **Fallback Notification**, but does not mean **Exact Timing Permission** is granted.
- "Pending" was ambiguous for notification status; resolved: the canonical state is **No Scheduled Notification** when notifications are enabled but no upcoming Schedule Notification is armed.
- "Allowed" was ambiguous for permission requests; resolved: a request action is not the same as granted **Exact Timing Permission**.
Expand Down
32 changes: 32 additions & 0 deletions lib/domain/use-cases/create_schedule_form_submission_use_case.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import 'package:injectable/injectable.dart';
import 'package:on_time_front/domain/use-cases/create_custom_preparation_use_case.dart';
import 'package:on_time_front/domain/use-cases/create_schedule_with_place_use_case.dart';
import 'package:on_time_front/domain/use-cases/schedule_analytics_tracker.dart';
import 'package:on_time_front/domain/use-cases/schedule_form_submission.dart';

@Injectable()
class CreateScheduleFormSubmissionUseCase {
final CreateScheduleWithPlaceUseCase _createScheduleWithPlaceUseCase;
final CreateCustomPreparationUseCase _createCustomPreparationUseCase;
final ScheduleAnalyticsTracker _scheduleAnalyticsTracker;

CreateScheduleFormSubmissionUseCase(
this._createScheduleWithPlaceUseCase,
this._createCustomPreparationUseCase,
this._scheduleAnalyticsTracker,
);

Future<void> call(ScheduleFormSubmission submission) async {
await _createScheduleWithPlaceUseCase(submission.schedule);
if (submission.preparationChanged) {
await _createCustomPreparationUseCase(
submission.preparation,
submission.schedule.id,
);
}
await _scheduleAnalyticsTracker.trackScheduleCreated(
schedule: submission.schedule,
preparation: submission.preparation,
);
}
}
142 changes: 142 additions & 0 deletions lib/domain/use-cases/load_schedule_form_draft_use_case.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import 'package:equatable/equatable.dart';
import 'package:injectable/injectable.dart';
import 'package:on_time_front/domain/entities/preparation_entity.dart';
import 'package:on_time_front/domain/entities/schedule_preparation_mode.dart';
import 'package:on_time_front/domain/use-cases/get_default_preparation_use_case.dart';
import 'package:on_time_front/domain/use-cases/get_preparation_by_schedule_id_use_case.dart';
import 'package:on_time_front/domain/use-cases/get_schedule_by_id_use_case.dart';
import 'package:on_time_front/domain/use-cases/load_preparation_by_schedule_id_use_case.dart';
import 'package:uuid/uuid.dart';

class ScheduleFormDraft extends Equatable {
final String id;
final String? placeId;
final String? placeName;
final String? scheduleName;
final DateTime? scheduleTime;
final Duration? moveTime;
final bool preparationChanged;
final Duration? scheduleSpareTime;
final String? scheduleNote;
final PreparationEntity preparation;
final SchedulePreparationMode? originalPreparationMode;

const ScheduleFormDraft({
required this.id,
required this.placeId,
required this.placeName,
required this.scheduleName,
required this.scheduleTime,
required this.moveTime,
required this.preparationChanged,
required this.scheduleSpareTime,
required this.scheduleNote,
required this.preparation,
this.originalPreparationMode,
});

@override
List<Object?> get props => [
id,
placeId,
placeName,
scheduleName,
scheduleTime,
moveTime,
preparationChanged,
scheduleSpareTime,
scheduleNote,
preparation,
originalPreparationMode,
];
}

@Injectable()
class LoadScheduleFormDraftUseCase {
final LoadPreparationByScheduleIdUseCase _loadPreparationByScheduleIdUseCase;
final GetPreparationByScheduleIdUseCase _getPreparationByScheduleIdUseCase;
final GetDefaultPreparationUseCase _getDefaultPreparationUseCase;
final GetScheduleByIdUseCase _getScheduleByIdUseCase;
final DateTime Function() _now;
final String Function() _newId;

LoadScheduleFormDraftUseCase(
this._loadPreparationByScheduleIdUseCase,
this._getPreparationByScheduleIdUseCase,
this._getDefaultPreparationUseCase,
this._getScheduleByIdUseCase,
) : _now = DateTime.now,
_newId = const Uuid().v7;

LoadScheduleFormDraftUseCase.withOverrides(
this._loadPreparationByScheduleIdUseCase,
this._getPreparationByScheduleIdUseCase,
this._getDefaultPreparationUseCase,
this._getScheduleByIdUseCase, {
required DateTime Function() now,
required String Function() newId,
}) : _now = now,
_newId = newId;

Future<ScheduleFormDraft> create({
DateTime? initialDate,
Duration? currentUserSpareTime,
}) async {
final defaultPreparation = await _getDefaultPreparationUseCase();

return ScheduleFormDraft(
id: _newId(),
placeId: _newId(),
placeName: null,
scheduleName: null,
scheduleTime: initialDate == null
? null
: _initialScheduleTime(initialDate, _now()),
moveTime: null,
preparationChanged: false,
scheduleSpareTime: currentUserSpareTime,
scheduleNote: null,
preparation: defaultPreparation,
originalPreparationMode: null,
);
}

Future<ScheduleFormDraft> edit(String scheduleId) async {
await _loadPreparationByScheduleIdUseCase(scheduleId);
final preparation = await _getPreparationByScheduleIdUseCase(scheduleId);
final schedule = await _getScheduleByIdUseCase(scheduleId);

return ScheduleFormDraft(
id: schedule.id,
placeId: schedule.place.id,
placeName: schedule.place.placeName,
scheduleName: schedule.scheduleName,
scheduleTime: schedule.scheduleTime,
moveTime: schedule.moveTime,
preparationChanged: schedule.isChanged,
scheduleSpareTime: schedule.scheduleSpareTime,
scheduleNote: schedule.scheduleNote,
preparation: preparation,
originalPreparationMode: schedule.preparationMode,
);
}

DateTime _initialScheduleTime(DateTime initialDate, DateTime now) {
final selectedDate = DateTime(
initialDate.year,
initialDate.month,
initialDate.day,
);
final today = DateTime(now.year, now.month, now.day);
final initialTime = selectedDate == today
? now.add(const Duration(minutes: 1))
: now;
return DateTime(
initialDate.year,
initialDate.month,
initialDate.day,
initialTime.hour,
initialTime.minute,
);
}
}
9 changes: 9 additions & 0 deletions lib/domain/use-cases/schedule_analytics_tracker.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import 'package:on_time_front/domain/entities/preparation_entity.dart';
import 'package:on_time_front/domain/entities/schedule_entity.dart';

abstract interface class ScheduleAnalyticsTracker {
Future<void> trackScheduleCreated({
required ScheduleEntity schedule,
required PreparationEntity preparation,
});
}
26 changes: 26 additions & 0 deletions lib/domain/use-cases/schedule_form_submission.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import 'package:equatable/equatable.dart';
import 'package:on_time_front/domain/entities/preparation_entity.dart';
import 'package:on_time_front/domain/entities/schedule_entity.dart';
import 'package:on_time_front/domain/entities/schedule_preparation_mode.dart';

class ScheduleFormSubmission extends Equatable {
final ScheduleEntity schedule;
final PreparationEntity preparation;
final bool preparationChanged;
final SchedulePreparationMode? originalPreparationMode;

const ScheduleFormSubmission({
required this.schedule,
required this.preparation,
required this.preparationChanged,
this.originalPreparationMode,
});

@override
List<Object?> get props => [
schedule,
preparation,
preparationChanged,
originalPreparationMode,
];
}
36 changes: 36 additions & 0 deletions lib/domain/use-cases/track_schedule_analytics_use_case.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import 'package:injectable/injectable.dart';
import 'package:on_time_front/domain/entities/preparation_entity.dart';
import 'package:on_time_front/domain/entities/product_usage_event.dart';
import 'package:on_time_front/domain/entities/schedule_entity.dart';
import 'package:on_time_front/domain/use-cases/schedule_analytics_tracker.dart';
import 'package:on_time_front/domain/use-cases/track_product_usage_event_use_case.dart';

@Injectable(as: ScheduleAnalyticsTracker)
class TrackScheduleAnalyticsUseCase implements ScheduleAnalyticsTracker {
final ProductUsageEventTracker _productUsageEventTracker;
final DateTime Function() _now;

TrackScheduleAnalyticsUseCase(this._productUsageEventTracker)
: _now = DateTime.now;

TrackScheduleAnalyticsUseCase.withClock(
this._productUsageEventTracker, {
required DateTime Function() now,
}) : _now = now;

@override
Future<void> trackScheduleCreated({
required ScheduleEntity schedule,
required PreparationEntity preparation,
}) async {
await _productUsageEventTracker.track(
ProductUsageEvent.scheduleCreated(
preparationMode: schedule.preparationMode,
preparationStepCount: preparation.preparationStepList.length,
minutesUntilSchedule: schedule.scheduleTime
.difference(_now())
.inMinutes,
),
);
}
}
58 changes: 58 additions & 0 deletions lib/domain/use-cases/update_schedule_form_submission_use_case.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import 'package:injectable/injectable.dart';
import 'package:on_time_front/domain/entities/preparation_entity.dart';
import 'package:on_time_front/domain/entities/preparation_step_entity.dart';
import 'package:on_time_front/domain/entities/schedule_preparation_mode.dart';
import 'package:on_time_front/domain/use-cases/schedule_form_submission.dart';
import 'package:on_time_front/domain/use-cases/update_preparation_by_schedule_id_use_case.dart';
import 'package:on_time_front/domain/use-cases/update_schedule_use_case.dart';
import 'package:uuid/uuid.dart';

@Injectable()
class UpdateScheduleFormSubmissionUseCase {
final UpdateScheduleUseCase _updateScheduleUseCase;
final UpdatePreparationByScheduleIdUseCase
_updatePreparationByScheduleIdUseCase;
final String Function() _newId;

UpdateScheduleFormSubmissionUseCase(
this._updateScheduleUseCase,
this._updatePreparationByScheduleIdUseCase,
) : _newId = const Uuid().v7;

UpdateScheduleFormSubmissionUseCase.withIdGenerator(
this._updateScheduleUseCase,
this._updatePreparationByScheduleIdUseCase, {
required String Function() newId,
}) : _newId = newId;

Future<void> call(ScheduleFormSubmission submission) async {
await _updateScheduleUseCase(submission.schedule);
if (submission.preparationChanged) {
await _updatePreparationByScheduleIdUseCase(
_preparationForUpdate(submission),
submission.schedule.id,
);
}
}

PreparationEntity _preparationForUpdate(ScheduleFormSubmission submission) {
if (submission.originalPreparationMode !=
SchedulePreparationMode.defaultPreparation) {
return submission.preparation;
}

final orderedSteps = submission.preparation.ordered.preparationStepList;
final newIds = List.generate(orderedSteps.length, (_) => _newId());
final copiedSteps = <PreparationStepEntity>[
for (var i = 0; i < orderedSteps.length; i++)
PreparationStepEntity(
id: newIds[i],
preparationName: orderedSteps[i].preparationName,
preparationTime: orderedSteps[i].preparationTime,
nextPreparationId: i + 1 < newIds.length ? newIds[i + 1] : null,
),
];

return PreparationEntity(preparationStepList: copiedSteps);
}
}
Loading
Loading