Skip to content

Commit fbe4eb9

Browse files
committed
refactor: decouple schedule data source contracts
1 parent eabe037 commit fbe4eb9

9 files changed

Lines changed: 366 additions & 167 deletions

CONTEXT.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,26 @@ release, and feature discussions use the same terms.
55

66
## Language
77

8+
**OnTime User**:
9+
A person's OnTime account profile that owns schedules, places, preparation defaults, and notification preferences.
10+
_Avoid_: User identity, analytics subject, device
11+
12+
**Schedule**:
13+
A planned commitment with an intended time, destination place, travel time, and optional preparation plan.
14+
_Avoid_: Alarm, notification, calendar row
15+
16+
**Place**:
17+
The destination or location label attached to a Schedule.
18+
_Avoid_: Route, address record, notification location
19+
20+
**Preparation**:
21+
An ordered plan of steps the user intends to complete before leaving for a Schedule.
22+
_Avoid_: Reminder, notification, alarm
23+
24+
**Preparation Step**:
25+
One named action in a Preparation with an expected duration.
26+
_Avoid_: UI row, checklist widget, notification step
27+
828
**Product Usage Event**:
929
A named record that a user performed a product-relevant action, excluding raw personal content.
1030
_Avoid_: User activity, tracking event, raw interaction log
@@ -107,6 +127,12 @@ _Avoid_: Notification, native alarm
107127

108128
## Relationships
109129

130+
- An **OnTime User** may own zero or more **Schedules**.
131+
- A **Schedule** has one **Place**.
132+
- A **Schedule** may use one **Preparation**.
133+
- A **Preparation** contains zero or more **Preparation Steps** in user-defined order.
134+
- A **Preparation Step** belongs to exactly one **Preparation**.
135+
- A **Schedule Notification** uses **Schedule** and **Preparation** timing, but is not itself a **Schedule** or **Preparation**.
110136
- 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.
111137
- First-release **Product Usage Events** are **Workflow Milestone Events**, not every tap or raw navigation step.
112138
- First-release **Workflow Milestone Events** cover analytics preference, onboarding, authentication, schedule, notification permission, alarm, and schedule-finish outcomes.
@@ -155,8 +181,14 @@ _Avoid_: Notification, native alarm
155181
> **Dev:** "Should the analytics event include the schedule note so we can understand why users are late?"
156182
> **Domain expert:** "No. A **Product Usage Event** can say a schedule was finished late, but it must not include the user's raw note."
157183
184+
> **Dev:** "Can a **Schedule Notification** replace the **Schedule** if the user taps it?"
185+
> **Domain expert:** "No. The **Schedule Notification** only prompts preparation for the **Schedule**; the **Schedule** remains the planned commitment."
186+
158187
## Flagged ambiguities
159188

189+
- "User" was overloaded as both the human actor and stored profile; resolved: use **OnTime User** for the account/profile concept and plain user only for the person performing an action.
190+
- "Schedule" was used near notification and alarm flows; resolved: a **Schedule** is the planned commitment, while notifications and alarms are delivery experiences for preparation timing.
191+
- "Preparation state" was ambiguous between step progress and display styling; resolved: **Preparation Step** progress belongs to preparation language, while visual labels should not redefine the domain.
160192
- "User activities" was used broadly; resolved: the canonical term is **Product Usage Event**, and raw personal content is out of scope.
161193
- "Analytics" was used to include all possible purposes; resolved: marketing and personalization are deferred, not first-release purposes.
162194
- "User identity" for analytics was ambiguous; resolved: analytics uses a **Pseudonymous Analytics Subject**, not directly identifying user data.
Lines changed: 16 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,20 @@
11
import 'package:injectable/injectable.dart';
22
import 'package:on_time_front/core/database/database.dart';
3-
import 'package:on_time_front/data/mappers/domain_persistence_mappers.dart';
4-
import 'package:on_time_front/domain/entities/schedule_entity.dart';
3+
import 'package:on_time_front/data/tables/schedule_with_place_model.dart';
54

65
abstract interface class ScheduleLocalDataSource {
7-
Future<void> createSchedule(ScheduleEntity scheduleEntity);
6+
Future<void> createSchedule(ScheduleWithPlace schedule);
87

9-
Future<List<ScheduleEntity>> getSchedulesByDate(
8+
Future<List<ScheduleWithPlace>> getSchedulesByDate(
109
DateTime startDate,
1110
DateTime? endDate,
1211
);
1312

14-
Future<ScheduleEntity> getScheduleById(String id);
13+
Future<ScheduleWithPlace> getScheduleById(String id);
1514

16-
Future<void> updateSchedule(ScheduleEntity scheduleEntity);
15+
Future<void> updateSchedule(Schedule schedule);
1716

18-
Future<void> deleteSchedule(ScheduleEntity scheduleEntity);
17+
Future<void> deleteSchedule(Schedule schedule);
1918
}
2019

2120
@Injectable(as: ScheduleLocalDataSource)
@@ -25,38 +24,30 @@ class ScheduleLocalDataSourceImpl implements ScheduleLocalDataSource {
2524
ScheduleLocalDataSourceImpl({required this.appDatabase});
2625

2726
@override
28-
Future<void> createSchedule(ScheduleEntity scheduleEntity) async {
29-
await appDatabase.scheduleDao.createSchedule(
30-
scheduleEntity.toScheduleWithPlaceRow(),
31-
);
27+
Future<void> createSchedule(ScheduleWithPlace schedule) async {
28+
await appDatabase.scheduleDao.createSchedule(schedule);
3229
}
3330

3431
@override
35-
Future<void> deleteSchedule(ScheduleEntity schedulEntity) async {
36-
await appDatabase.scheduleDao.deleteSchedule(schedulEntity.toScheduleRow());
32+
Future<void> deleteSchedule(Schedule schedule) async {
33+
await appDatabase.scheduleDao.deleteSchedule(schedule);
3734
}
3835

3936
@override
40-
Future<ScheduleEntity> getScheduleById(String id) async {
41-
final scheduleWithPlaceModel = await appDatabase.scheduleDao
42-
.getScheduleById(id);
43-
return scheduleWithPlaceModel.toScheduleEntity();
37+
Future<ScheduleWithPlace> getScheduleById(String id) async {
38+
return appDatabase.scheduleDao.getScheduleById(id);
4439
}
4540

4641
@override
47-
Future<List<ScheduleEntity>> getSchedulesByDate(
42+
Future<List<ScheduleWithPlace>> getSchedulesByDate(
4843
DateTime startDate,
4944
DateTime? endDate,
5045
) async {
51-
final scheduleWithPlaceModel = await appDatabase.scheduleDao
52-
.getSchedulesByDate(startDate, endDate);
53-
return scheduleWithPlaceModel.map((e) => e.toScheduleEntity()).toList();
46+
return appDatabase.scheduleDao.getSchedulesByDate(startDate, endDate);
5447
}
5548

5649
@override
57-
Future<void> updateSchedule(ScheduleEntity scheduleEntity) async {
58-
await appDatabase.scheduleDao.updateSchedule(
59-
scheduleEntity.toScheduleRow(),
60-
);
50+
Future<void> updateSchedule(Schedule schedule) async {
51+
await appDatabase.scheduleDao.updateSchedule(schedule);
6152
}
6253
}

lib/data/data_sources/schedule_remote_data_source.dart

Lines changed: 18 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,19 @@ import 'package:on_time_front/data/models/create_schedule_request_model.dart';
66
import 'package:on_time_front/data/models/get_schedule_response_model.dart';
77
import 'package:on_time_front/data/models/update_schedule_request_model.dart';
88

9-
import 'package:on_time_front/domain/entities/schedule_entity.dart';
10-
119
abstract interface class ScheduleRemoteDataSource {
12-
Future<void> createSchedule(ScheduleEntity schedule);
10+
Future<void> createSchedule(CreateScheduleRequestModel schedule);
1311

14-
Future<List<ScheduleEntity>> getSchedulesByDate(
12+
Future<List<GetScheduleResponseModel>> getSchedulesByDate(
1513
DateTime startDate,
1614
DateTime? endDate,
1715
);
1816

19-
Future<ScheduleEntity> getScheduleById(String id);
17+
Future<GetScheduleResponseModel> getScheduleById(String id);
2018

21-
Future<void> updateSchedule(
22-
ScheduleEntity schedule, {
23-
bool includePreparationSource = false,
24-
});
19+
Future<void> updateSchedule(UpdateScheduleRequestModel schedule);
2520

26-
Future<void> deleteSchedule(ScheduleEntity schedule);
21+
Future<void> deleteSchedule(String scheduleId);
2722

2823
Future<void> startSchedule(String scheduleId);
2924

@@ -36,13 +31,11 @@ class ScheduleRemoteDataSourceImpl implements ScheduleRemoteDataSource {
3631
ScheduleRemoteDataSourceImpl(this.dio);
3732

3833
@override
39-
Future<void> createSchedule(ScheduleEntity schedule) async {
34+
Future<void> createSchedule(CreateScheduleRequestModel schedule) async {
4035
try {
41-
CreateScheduleRequestModel createScheduleModel =
42-
CreateScheduleRequestModel.fromEntity(schedule);
4336
final result = await dio.post(
4437
Endpoint.createSchedule,
45-
data: createScheduleModel.toJson(),
38+
data: schedule.toJson(),
4639
);
4740
if (result.statusCode == 200) {
4841
return;
@@ -55,19 +48,11 @@ class ScheduleRemoteDataSourceImpl implements ScheduleRemoteDataSource {
5548
}
5649

5750
@override
58-
Future<void> updateSchedule(
59-
ScheduleEntity schedule, {
60-
bool includePreparationSource = false,
61-
}) async {
51+
Future<void> updateSchedule(UpdateScheduleRequestModel schedule) async {
6252
try {
63-
UpdateScheduleRequestModel updateScheduleModel =
64-
UpdateScheduleRequestModel.fromEntity(
65-
schedule,
66-
includePreparationSource: includePreparationSource,
67-
);
6853
final result = await dio.put(
69-
Endpoint.updateSchedule(schedule.id),
70-
data: updateScheduleModel.toJson(),
54+
Endpoint.updateSchedule(schedule.scheduleId),
55+
data: schedule.toJson(),
7156
);
7257
if (result.statusCode == 200) {
7358
return;
@@ -80,9 +65,9 @@ class ScheduleRemoteDataSourceImpl implements ScheduleRemoteDataSource {
8065
}
8166

8267
@override
83-
Future<void> deleteSchedule(ScheduleEntity schedule) async {
68+
Future<void> deleteSchedule(String scheduleId) async {
8469
try {
85-
final result = await dio.delete(Endpoint.deleteScheduleById(schedule.id));
70+
final result = await dio.delete(Endpoint.deleteScheduleById(scheduleId));
8671
if (result.statusCode == 200) {
8772
return;
8873
} else {
@@ -125,13 +110,13 @@ class ScheduleRemoteDataSourceImpl implements ScheduleRemoteDataSource {
125110
}
126111

127112
@override
128-
Future<ScheduleEntity> getScheduleById(String id) async {
113+
Future<GetScheduleResponseModel> getScheduleById(String id) async {
129114
try {
130115
final result = await dio.get(Endpoint.getScheduleById(id));
131116
if (result.statusCode == 200) {
132117
final GetScheduleResponseModel schedule =
133118
GetScheduleResponseModel.fromJson(result.data["data"]);
134-
return schedule.toEntity();
119+
return schedule;
135120
} else {
136121
throw Exception('Error getting schedules');
137122
}
@@ -141,7 +126,7 @@ class ScheduleRemoteDataSourceImpl implements ScheduleRemoteDataSource {
141126
}
142127

143128
@override
144-
Future<List<ScheduleEntity>> getSchedulesByDate(
129+
Future<List<GetScheduleResponseModel>> getSchedulesByDate(
145130
DateTime startDate,
146131
DateTime? endDate,
147132
) async {
@@ -154,9 +139,9 @@ class ScheduleRemoteDataSourceImpl implements ScheduleRemoteDataSource {
154139
},
155140
);
156141
if (result.statusCode == 200) {
157-
final List<ScheduleEntity> schedules = result.data["data"]
158-
.map<ScheduleEntity>(
159-
(e) => GetScheduleResponseModel.fromJson(e).toEntity(),
142+
final List<GetScheduleResponseModel> schedules = result.data["data"]
143+
.map<GetScheduleResponseModel>(
144+
(e) => GetScheduleResponseModel.fromJson(e),
160145
)
161146
.toList();
162147
return schedules;

lib/data/repositories/schedule_repository_impl.dart

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import 'package:collection/collection.dart';
44
import 'package:injectable/injectable.dart';
55
import 'package:on_time_front/data/data_sources/schedule_local_data_source.dart';
66
import 'package:on_time_front/data/data_sources/schedule_remote_data_source.dart';
7+
import 'package:on_time_front/data/models/create_schedule_request_model.dart';
8+
import 'package:on_time_front/data/models/update_schedule_request_model.dart';
79
import 'package:on_time_front/domain/entities/schedule_entity.dart';
810
import 'package:on_time_front/domain/repositories/schedule_repository.dart';
911
import 'package:on_time_front/domain/repositories/timed_preparation_repository.dart';
@@ -50,7 +52,9 @@ class ScheduleRepositoryImpl implements ScheduleRepository {
5052
@override
5153
Future<void> createSchedule(ScheduleEntity schedule) async {
5254
try {
53-
await scheduleRemoteDataSource.createSchedule(schedule);
55+
await scheduleRemoteDataSource.createSchedule(
56+
CreateScheduleRequestModel.fromEntity(schedule),
57+
);
5458
//await scheduleLocalDataSource.createSchedule(schedule);
5559
_emitUpsertedSchedule(schedule);
5660
} catch (e) {
@@ -61,7 +65,7 @@ class ScheduleRepositoryImpl implements ScheduleRepository {
6165
@override
6266
Future<void> deleteSchedule(ScheduleEntity schedule) async {
6367
try {
64-
await scheduleRemoteDataSource.deleteSchedule(schedule);
68+
await scheduleRemoteDataSource.deleteSchedule(schedule.id);
6569
await _clearTimedPreparationSafe(schedule.id);
6670
//await scheduleLocalDataSource.deleteSchedule(schedule);
6771
_emitScheduleSet(
@@ -86,7 +90,9 @@ class ScheduleRepositoryImpl implements ScheduleRepository {
8690
@override
8791
Future<ScheduleEntity> getScheduleById(String id) async {
8892
try {
89-
final schedule = await scheduleRemoteDataSource.getScheduleById(id);
93+
final schedule = (await scheduleRemoteDataSource.getScheduleById(
94+
id,
95+
)).toEntity();
9096
if (_isEnded(schedule.doneStatus)) {
9197
await _clearTimedPreparationSafe(schedule.id);
9298
}
@@ -103,10 +109,10 @@ class ScheduleRepositoryImpl implements ScheduleRepository {
103109
DateTime? endDate,
104110
) async {
105111
try {
106-
final schedules = await scheduleRemoteDataSource.getSchedulesByDate(
112+
final schedules = (await scheduleRemoteDataSource.getSchedulesByDate(
107113
startDate,
108114
endDate,
109-
);
115+
)).map((schedule) => schedule.toEntity()).toList();
110116
for (final schedule in schedules) {
111117
if (_isEnded(schedule.doneStatus)) {
112118
await _clearTimedPreparationSafe(schedule.id);
@@ -130,13 +136,15 @@ class ScheduleRepositoryImpl implements ScheduleRepository {
130136
}) async {
131137
try {
132138
await scheduleRemoteDataSource.updateSchedule(
133-
schedule,
134-
includePreparationSource: includePreparationSource,
139+
UpdateScheduleRequestModel.fromEntity(
140+
schedule,
141+
includePreparationSource: includePreparationSource,
142+
),
135143
);
136144
await _clearTimedPreparationSafe(schedule.id);
137-
final refreshedSchedule = await scheduleRemoteDataSource.getScheduleById(
145+
final refreshedSchedule = (await scheduleRemoteDataSource.getScheduleById(
138146
schedule.id,
139-
);
147+
)).toEntity();
140148
if (_isEnded(refreshedSchedule.doneStatus)) {
141149
await _clearTimedPreparationSafe(refreshedSchedule.id);
142150
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import 'dart:io';
2+
3+
import 'package:flutter_test/flutter_test.dart';
4+
5+
void main() {
6+
test(
7+
'schedule remote data source contract does not expose domain entities',
8+
() {
9+
final violations = _scheduleEntityViolations(
10+
'lib/data/data_sources/schedule_remote_data_source.dart',
11+
);
12+
13+
expect(violations, isEmpty);
14+
},
15+
);
16+
17+
test(
18+
'schedule local data source contract does not expose domain entities',
19+
() {
20+
final violations = _scheduleEntityViolations(
21+
'lib/data/data_sources/schedule_local_data_source.dart',
22+
);
23+
24+
expect(violations, isEmpty);
25+
},
26+
);
27+
}
28+
29+
List<String> _scheduleEntityViolations(String path) {
30+
final lines = File(path).readAsLinesSync();
31+
return [
32+
for (var index = 0; index < lines.length; index++)
33+
if (lines[index].contains('domain/entities/schedule_entity.dart') ||
34+
lines[index].contains('ScheduleEntity'))
35+
'$path:${index + 1}: ${lines[index].trim()}',
36+
];
37+
}

0 commit comments

Comments
 (0)