Skip to content

Commit b1dc47b

Browse files
committed
refactor: split schedule form orchestration
1 parent eabe037 commit b1dc47b

18 files changed

Lines changed: 957 additions & 682 deletions

CONTEXT.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,26 @@ _Avoid_: Provider login completed, credential received
5353
An allowlisted non-content value attached to a Product Usage Event.
5454
_Avoid_: Event payload, arbitrary metadata, raw detail
5555

56+
**Schedule**:
57+
A planned commitment with a place, appointment time, travel time, optional buffer time, and preparation.
58+
_Avoid_: Event, appointment record
59+
60+
**Preparation**:
61+
An ordered set of steps and durations used to get ready for a Schedule.
62+
_Avoid_: Routine, prep checklist
63+
64+
**Default Preparation**:
65+
The user's fallback Preparation for new Schedules.
66+
_Avoid_: Base preparation, global preparation
67+
68+
**Custom Preparation**:
69+
A Schedule-specific Preparation that differs from the user's fallback or selected template.
70+
_Avoid_: Changed preparation, edited default
71+
72+
**Preparation Mode**:
73+
The category describing whether a Schedule uses Default Preparation, a preparation template, or Custom Preparation.
74+
_Avoid_: Preparation type, preparation source
75+
5676
**Schedule Notification**:
5777
A user-facing notification that starts preparation for a scheduled commitment at the intended moment.
5878
_Avoid_: Schedule alarm, alarm, push
@@ -107,7 +127,11 @@ _Avoid_: Notification, native alarm
107127

108128
## Relationships
109129

110-
- 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.
130+
- 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.
131+
- A **Schedule** has one effective **Preparation** for calculating preparation timing.
132+
- A **Default Preparation** may seed a new **Schedule** before the user chooses a different preparation.
133+
- A **Custom Preparation** belongs to one **Schedule**.
134+
- **Preparation Mode** may appear as an **Analytics Event Parameter** without exposing preparation step names.
111135
- First-release **Product Usage Events** are **Workflow Milestone Events**, not every tap or raw navigation step.
112136
- First-release **Workflow Milestone Events** cover analytics preference, onboarding, authentication, schedule, notification permission, alarm, and schedule-finish outcomes.
113137
- **Provider Authentication Completed** precedes **OnTime Session Established** during Apple or Google sign-in.
@@ -165,6 +189,7 @@ _Avoid_: Notification, native alarm
165189
- "Event taxonomy" was broad; resolved: first-release analytics tracks **Workflow Milestone Events** only.
166190
- "Event payload" was too open-ended; resolved: events use allowlisted **Analytics Event Parameters** only.
167191
- "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**.
192+
- "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.
168193
- "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.
169194
- "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.
170195
- "Allowed" was ambiguous for permission requests; resolved: a request action is not the same as granted **Exact Timing Permission**.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import 'package:injectable/injectable.dart';
2+
import 'package:on_time_front/domain/use-cases/create_custom_preparation_use_case.dart';
3+
import 'package:on_time_front/domain/use-cases/create_schedule_with_place_use_case.dart';
4+
import 'package:on_time_front/domain/use-cases/schedule_analytics_tracker.dart';
5+
import 'package:on_time_front/domain/use-cases/schedule_form_submission.dart';
6+
7+
@Injectable()
8+
class CreateScheduleFormSubmissionUseCase {
9+
final CreateScheduleWithPlaceUseCase _createScheduleWithPlaceUseCase;
10+
final CreateCustomPreparationUseCase _createCustomPreparationUseCase;
11+
final ScheduleAnalyticsTracker _scheduleAnalyticsTracker;
12+
13+
CreateScheduleFormSubmissionUseCase(
14+
this._createScheduleWithPlaceUseCase,
15+
this._createCustomPreparationUseCase,
16+
this._scheduleAnalyticsTracker,
17+
);
18+
19+
Future<void> call(ScheduleFormSubmission submission) async {
20+
await _createScheduleWithPlaceUseCase(submission.schedule);
21+
if (submission.preparationChanged) {
22+
await _createCustomPreparationUseCase(
23+
submission.preparation,
24+
submission.schedule.id,
25+
);
26+
}
27+
await _scheduleAnalyticsTracker.trackScheduleCreated(
28+
schedule: submission.schedule,
29+
preparation: submission.preparation,
30+
);
31+
}
32+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import 'package:equatable/equatable.dart';
2+
import 'package:injectable/injectable.dart';
3+
import 'package:on_time_front/domain/entities/preparation_entity.dart';
4+
import 'package:on_time_front/domain/use-cases/get_default_preparation_use_case.dart';
5+
import 'package:on_time_front/domain/use-cases/get_preparation_by_schedule_id_use_case.dart';
6+
import 'package:on_time_front/domain/use-cases/get_schedule_by_id_use_case.dart';
7+
import 'package:on_time_front/domain/use-cases/load_preparation_by_schedule_id_use_case.dart';
8+
import 'package:uuid/uuid.dart';
9+
10+
class ScheduleFormDraft extends Equatable {
11+
final String id;
12+
final String? placeId;
13+
final String? placeName;
14+
final String? scheduleName;
15+
final DateTime? scheduleTime;
16+
final Duration? moveTime;
17+
final bool preparationChanged;
18+
final Duration? scheduleSpareTime;
19+
final String? scheduleNote;
20+
final PreparationEntity preparation;
21+
22+
const ScheduleFormDraft({
23+
required this.id,
24+
required this.placeId,
25+
required this.placeName,
26+
required this.scheduleName,
27+
required this.scheduleTime,
28+
required this.moveTime,
29+
required this.preparationChanged,
30+
required this.scheduleSpareTime,
31+
required this.scheduleNote,
32+
required this.preparation,
33+
});
34+
35+
@override
36+
List<Object?> get props => [
37+
id,
38+
placeId,
39+
placeName,
40+
scheduleName,
41+
scheduleTime,
42+
moveTime,
43+
preparationChanged,
44+
scheduleSpareTime,
45+
scheduleNote,
46+
preparation,
47+
];
48+
}
49+
50+
@Injectable()
51+
class LoadScheduleFormDraftUseCase {
52+
final LoadPreparationByScheduleIdUseCase _loadPreparationByScheduleIdUseCase;
53+
final GetPreparationByScheduleIdUseCase _getPreparationByScheduleIdUseCase;
54+
final GetDefaultPreparationUseCase _getDefaultPreparationUseCase;
55+
final GetScheduleByIdUseCase _getScheduleByIdUseCase;
56+
final DateTime Function() _now;
57+
final String Function() _newId;
58+
59+
LoadScheduleFormDraftUseCase(
60+
this._loadPreparationByScheduleIdUseCase,
61+
this._getPreparationByScheduleIdUseCase,
62+
this._getDefaultPreparationUseCase,
63+
this._getScheduleByIdUseCase,
64+
) : _now = DateTime.now,
65+
_newId = const Uuid().v7;
66+
67+
LoadScheduleFormDraftUseCase.withOverrides(
68+
this._loadPreparationByScheduleIdUseCase,
69+
this._getPreparationByScheduleIdUseCase,
70+
this._getDefaultPreparationUseCase,
71+
this._getScheduleByIdUseCase, {
72+
required DateTime Function() now,
73+
required String Function() newId,
74+
}) : _now = now,
75+
_newId = newId;
76+
77+
Future<ScheduleFormDraft> create({
78+
DateTime? initialDate,
79+
Duration? currentUserSpareTime,
80+
}) async {
81+
final defaultPreparation = await _getDefaultPreparationUseCase();
82+
83+
return ScheduleFormDraft(
84+
id: _newId(),
85+
placeId: _newId(),
86+
placeName: null,
87+
scheduleName: null,
88+
scheduleTime: initialDate == null
89+
? null
90+
: _initialScheduleTime(initialDate, _now()),
91+
moveTime: null,
92+
preparationChanged: false,
93+
scheduleSpareTime: currentUserSpareTime,
94+
scheduleNote: null,
95+
preparation: defaultPreparation,
96+
);
97+
}
98+
99+
Future<ScheduleFormDraft> edit(String scheduleId) async {
100+
await _loadPreparationByScheduleIdUseCase(scheduleId);
101+
final preparation = await _getPreparationByScheduleIdUseCase(scheduleId);
102+
final schedule = await _getScheduleByIdUseCase(scheduleId);
103+
104+
return ScheduleFormDraft(
105+
id: schedule.id,
106+
placeId: schedule.place.id,
107+
placeName: schedule.place.placeName,
108+
scheduleName: schedule.scheduleName,
109+
scheduleTime: schedule.scheduleTime,
110+
moveTime: schedule.moveTime,
111+
preparationChanged: schedule.isChanged,
112+
scheduleSpareTime: schedule.scheduleSpareTime,
113+
scheduleNote: schedule.scheduleNote,
114+
preparation: preparation,
115+
);
116+
}
117+
118+
DateTime _initialScheduleTime(DateTime initialDate, DateTime now) {
119+
final selectedDate = DateTime(
120+
initialDate.year,
121+
initialDate.month,
122+
initialDate.day,
123+
);
124+
final today = DateTime(now.year, now.month, now.day);
125+
final initialTime = selectedDate == today
126+
? now.add(const Duration(minutes: 1))
127+
: now;
128+
return DateTime(
129+
initialDate.year,
130+
initialDate.month,
131+
initialDate.day,
132+
initialTime.hour,
133+
initialTime.minute,
134+
);
135+
}
136+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import 'package:on_time_front/domain/entities/preparation_entity.dart';
2+
import 'package:on_time_front/domain/entities/schedule_entity.dart';
3+
4+
abstract interface class ScheduleAnalyticsTracker {
5+
Future<void> trackScheduleCreated({
6+
required ScheduleEntity schedule,
7+
required PreparationEntity preparation,
8+
});
9+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import 'package:equatable/equatable.dart';
2+
import 'package:on_time_front/domain/entities/preparation_entity.dart';
3+
import 'package:on_time_front/domain/entities/schedule_entity.dart';
4+
5+
class ScheduleFormSubmission extends Equatable {
6+
final ScheduleEntity schedule;
7+
final PreparationEntity preparation;
8+
final bool preparationChanged;
9+
10+
const ScheduleFormSubmission({
11+
required this.schedule,
12+
required this.preparation,
13+
required this.preparationChanged,
14+
});
15+
16+
@override
17+
List<Object?> get props => [schedule, preparation, preparationChanged];
18+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import 'package:injectable/injectable.dart';
2+
import 'package:on_time_front/domain/entities/preparation_entity.dart';
3+
import 'package:on_time_front/domain/entities/product_usage_event.dart';
4+
import 'package:on_time_front/domain/entities/schedule_entity.dart';
5+
import 'package:on_time_front/domain/use-cases/schedule_analytics_tracker.dart';
6+
import 'package:on_time_front/domain/use-cases/track_product_usage_event_use_case.dart';
7+
8+
@Injectable(as: ScheduleAnalyticsTracker)
9+
class TrackScheduleAnalyticsUseCase implements ScheduleAnalyticsTracker {
10+
final ProductUsageEventTracker _productUsageEventTracker;
11+
final DateTime Function() _now;
12+
13+
TrackScheduleAnalyticsUseCase(this._productUsageEventTracker)
14+
: _now = DateTime.now;
15+
16+
TrackScheduleAnalyticsUseCase.withClock(
17+
this._productUsageEventTracker, {
18+
required DateTime Function() now,
19+
}) : _now = now;
20+
21+
@override
22+
Future<void> trackScheduleCreated({
23+
required ScheduleEntity schedule,
24+
required PreparationEntity preparation,
25+
}) async {
26+
await _productUsageEventTracker.track(
27+
ProductUsageEvent(
28+
name: 'schedule_created',
29+
workflow: 'schedule',
30+
result: 'success',
31+
parameters: {
32+
'preparation_mode': schedule.preparationMode?.name ?? 'default',
33+
'preparation_step_count': preparation.preparationStepList.length,
34+
'minutes_until_schedule': schedule.scheduleTime
35+
.difference(_now())
36+
.inMinutes,
37+
},
38+
),
39+
);
40+
}
41+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import 'package:injectable/injectable.dart';
2+
import 'package:on_time_front/domain/use-cases/schedule_form_submission.dart';
3+
import 'package:on_time_front/domain/use-cases/update_preparation_by_schedule_id_use_case.dart';
4+
import 'package:on_time_front/domain/use-cases/update_schedule_use_case.dart';
5+
6+
@Injectable()
7+
class UpdateScheduleFormSubmissionUseCase {
8+
final UpdateScheduleUseCase _updateScheduleUseCase;
9+
final UpdatePreparationByScheduleIdUseCase
10+
_updatePreparationByScheduleIdUseCase;
11+
12+
UpdateScheduleFormSubmissionUseCase(
13+
this._updateScheduleUseCase,
14+
this._updatePreparationByScheduleIdUseCase,
15+
);
16+
17+
Future<void> call(ScheduleFormSubmission submission) async {
18+
await _updateScheduleUseCase(submission.schedule);
19+
if (submission.preparationChanged) {
20+
await _updatePreparationByScheduleIdUseCase(
21+
submission.preparation,
22+
submission.schedule.id,
23+
);
24+
}
25+
}
26+
}

0 commit comments

Comments
 (0)