From fbe4eb96b7a5e8bbe0f9f44c9c3db612723e82ec Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Sun, 28 Jun 2026 15:23:16 +0900 Subject: [PATCH] refactor: decouple schedule data source contracts --- CONTEXT.md | 32 +++++ .../schedule_local_data_source.dart | 41 +++---- .../schedule_remote_data_source.dart | 51 +++----- .../schedule_repository_impl.dart | 26 ++-- .../schedule_data_source_contract_test.dart | 37 ++++++ .../schedule_local_data_source_test.dart | 112 ++++++++++++------ .../schedule_remote_data_source_test.dart | 70 ++++++----- .../schedule_repository_impl_stream_test.dart | 76 +++++++++--- .../schedule_repository_impl_test.dart | 88 +++++++++++--- 9 files changed, 366 insertions(+), 167 deletions(-) create mode 100644 test/data/data_sources/schedule_data_source_contract_test.dart diff --git a/CONTEXT.md b/CONTEXT.md index 39095a5d..34031a9f 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -5,6 +5,26 @@ release, and feature discussions use the same terms. ## Language +**OnTime User**: +A person's OnTime account profile that owns schedules, places, preparation defaults, and notification preferences. +_Avoid_: User identity, analytics subject, device + +**Schedule**: +A planned commitment with an intended time, destination place, travel time, and optional preparation plan. +_Avoid_: Alarm, notification, calendar row + +**Place**: +The destination or location label attached to a Schedule. +_Avoid_: Route, address record, notification location + +**Preparation**: +An ordered plan of steps the user intends to complete before leaving for a Schedule. +_Avoid_: Reminder, notification, alarm + +**Preparation Step**: +One named action in a Preparation with an expected duration. +_Avoid_: UI row, checklist widget, notification step + **Product Usage Event**: A named record that a user performed a product-relevant action, excluding raw personal content. _Avoid_: User activity, tracking event, raw interaction log @@ -107,6 +127,12 @@ _Avoid_: Notification, native alarm ## Relationships +- An **OnTime User** may own zero or more **Schedules**. +- A **Schedule** has one **Place**. +- A **Schedule** may use one **Preparation**. +- 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. @@ -155,8 +181,14 @@ _Avoid_: Notification, native alarm > **Dev:** "Should the analytics event include the schedule note so we can understand why users are late?" > **Domain expert:** "No. A **Product Usage Event** can say a schedule was finished late, but it must not include the user's raw note." +> **Dev:** "Can a **Schedule Notification** replace the **Schedule** if the user taps it?" +> **Domain expert:** "No. The **Schedule Notification** only prompts preparation for the **Schedule**; the **Schedule** remains the planned commitment." + ## Flagged ambiguities +- "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. +- "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. +- "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. - "User activities" was used broadly; resolved: the canonical term is **Product Usage Event**, and raw personal content is out of scope. - "Analytics" was used to include all possible purposes; resolved: marketing and personalization are deferred, not first-release purposes. - "User identity" for analytics was ambiguous; resolved: analytics uses a **Pseudonymous Analytics Subject**, not directly identifying user data. diff --git a/lib/data/data_sources/schedule_local_data_source.dart b/lib/data/data_sources/schedule_local_data_source.dart index a1a345aa..b721e677 100644 --- a/lib/data/data_sources/schedule_local_data_source.dart +++ b/lib/data/data_sources/schedule_local_data_source.dart @@ -1,21 +1,20 @@ import 'package:injectable/injectable.dart'; import 'package:on_time_front/core/database/database.dart'; -import 'package:on_time_front/data/mappers/domain_persistence_mappers.dart'; -import 'package:on_time_front/domain/entities/schedule_entity.dart'; +import 'package:on_time_front/data/tables/schedule_with_place_model.dart'; abstract interface class ScheduleLocalDataSource { - Future createSchedule(ScheduleEntity scheduleEntity); + Future createSchedule(ScheduleWithPlace schedule); - Future> getSchedulesByDate( + Future> getSchedulesByDate( DateTime startDate, DateTime? endDate, ); - Future getScheduleById(String id); + Future getScheduleById(String id); - Future updateSchedule(ScheduleEntity scheduleEntity); + Future updateSchedule(Schedule schedule); - Future deleteSchedule(ScheduleEntity scheduleEntity); + Future deleteSchedule(Schedule schedule); } @Injectable(as: ScheduleLocalDataSource) @@ -25,38 +24,30 @@ class ScheduleLocalDataSourceImpl implements ScheduleLocalDataSource { ScheduleLocalDataSourceImpl({required this.appDatabase}); @override - Future createSchedule(ScheduleEntity scheduleEntity) async { - await appDatabase.scheduleDao.createSchedule( - scheduleEntity.toScheduleWithPlaceRow(), - ); + Future createSchedule(ScheduleWithPlace schedule) async { + await appDatabase.scheduleDao.createSchedule(schedule); } @override - Future deleteSchedule(ScheduleEntity schedulEntity) async { - await appDatabase.scheduleDao.deleteSchedule(schedulEntity.toScheduleRow()); + Future deleteSchedule(Schedule schedule) async { + await appDatabase.scheduleDao.deleteSchedule(schedule); } @override - Future getScheduleById(String id) async { - final scheduleWithPlaceModel = await appDatabase.scheduleDao - .getScheduleById(id); - return scheduleWithPlaceModel.toScheduleEntity(); + Future getScheduleById(String id) async { + return appDatabase.scheduleDao.getScheduleById(id); } @override - Future> getSchedulesByDate( + Future> getSchedulesByDate( DateTime startDate, DateTime? endDate, ) async { - final scheduleWithPlaceModel = await appDatabase.scheduleDao - .getSchedulesByDate(startDate, endDate); - return scheduleWithPlaceModel.map((e) => e.toScheduleEntity()).toList(); + return appDatabase.scheduleDao.getSchedulesByDate(startDate, endDate); } @override - Future updateSchedule(ScheduleEntity scheduleEntity) async { - await appDatabase.scheduleDao.updateSchedule( - scheduleEntity.toScheduleRow(), - ); + Future updateSchedule(Schedule schedule) async { + await appDatabase.scheduleDao.updateSchedule(schedule); } } diff --git a/lib/data/data_sources/schedule_remote_data_source.dart b/lib/data/data_sources/schedule_remote_data_source.dart index 1cfc806a..4c80538a 100644 --- a/lib/data/data_sources/schedule_remote_data_source.dart +++ b/lib/data/data_sources/schedule_remote_data_source.dart @@ -6,24 +6,19 @@ import 'package:on_time_front/data/models/create_schedule_request_model.dart'; import 'package:on_time_front/data/models/get_schedule_response_model.dart'; import 'package:on_time_front/data/models/update_schedule_request_model.dart'; -import 'package:on_time_front/domain/entities/schedule_entity.dart'; - abstract interface class ScheduleRemoteDataSource { - Future createSchedule(ScheduleEntity schedule); + Future createSchedule(CreateScheduleRequestModel schedule); - Future> getSchedulesByDate( + Future> getSchedulesByDate( DateTime startDate, DateTime? endDate, ); - Future getScheduleById(String id); + Future getScheduleById(String id); - Future updateSchedule( - ScheduleEntity schedule, { - bool includePreparationSource = false, - }); + Future updateSchedule(UpdateScheduleRequestModel schedule); - Future deleteSchedule(ScheduleEntity schedule); + Future deleteSchedule(String scheduleId); Future startSchedule(String scheduleId); @@ -36,13 +31,11 @@ class ScheduleRemoteDataSourceImpl implements ScheduleRemoteDataSource { ScheduleRemoteDataSourceImpl(this.dio); @override - Future createSchedule(ScheduleEntity schedule) async { + Future createSchedule(CreateScheduleRequestModel schedule) async { try { - CreateScheduleRequestModel createScheduleModel = - CreateScheduleRequestModel.fromEntity(schedule); final result = await dio.post( Endpoint.createSchedule, - data: createScheduleModel.toJson(), + data: schedule.toJson(), ); if (result.statusCode == 200) { return; @@ -55,19 +48,11 @@ class ScheduleRemoteDataSourceImpl implements ScheduleRemoteDataSource { } @override - Future updateSchedule( - ScheduleEntity schedule, { - bool includePreparationSource = false, - }) async { + Future updateSchedule(UpdateScheduleRequestModel schedule) async { try { - UpdateScheduleRequestModel updateScheduleModel = - UpdateScheduleRequestModel.fromEntity( - schedule, - includePreparationSource: includePreparationSource, - ); final result = await dio.put( - Endpoint.updateSchedule(schedule.id), - data: updateScheduleModel.toJson(), + Endpoint.updateSchedule(schedule.scheduleId), + data: schedule.toJson(), ); if (result.statusCode == 200) { return; @@ -80,9 +65,9 @@ class ScheduleRemoteDataSourceImpl implements ScheduleRemoteDataSource { } @override - Future deleteSchedule(ScheduleEntity schedule) async { + Future deleteSchedule(String scheduleId) async { try { - final result = await dio.delete(Endpoint.deleteScheduleById(schedule.id)); + final result = await dio.delete(Endpoint.deleteScheduleById(scheduleId)); if (result.statusCode == 200) { return; } else { @@ -125,13 +110,13 @@ class ScheduleRemoteDataSourceImpl implements ScheduleRemoteDataSource { } @override - Future getScheduleById(String id) async { + Future getScheduleById(String id) async { try { final result = await dio.get(Endpoint.getScheduleById(id)); if (result.statusCode == 200) { final GetScheduleResponseModel schedule = GetScheduleResponseModel.fromJson(result.data["data"]); - return schedule.toEntity(); + return schedule; } else { throw Exception('Error getting schedules'); } @@ -141,7 +126,7 @@ class ScheduleRemoteDataSourceImpl implements ScheduleRemoteDataSource { } @override - Future> getSchedulesByDate( + Future> getSchedulesByDate( DateTime startDate, DateTime? endDate, ) async { @@ -154,9 +139,9 @@ class ScheduleRemoteDataSourceImpl implements ScheduleRemoteDataSource { }, ); if (result.statusCode == 200) { - final List schedules = result.data["data"] - .map( - (e) => GetScheduleResponseModel.fromJson(e).toEntity(), + final List schedules = result.data["data"] + .map( + (e) => GetScheduleResponseModel.fromJson(e), ) .toList(); return schedules; diff --git a/lib/data/repositories/schedule_repository_impl.dart b/lib/data/repositories/schedule_repository_impl.dart index cd042372..e3667fbe 100644 --- a/lib/data/repositories/schedule_repository_impl.dart +++ b/lib/data/repositories/schedule_repository_impl.dart @@ -4,6 +4,8 @@ import 'package:collection/collection.dart'; import 'package:injectable/injectable.dart'; import 'package:on_time_front/data/data_sources/schedule_local_data_source.dart'; import 'package:on_time_front/data/data_sources/schedule_remote_data_source.dart'; +import 'package:on_time_front/data/models/create_schedule_request_model.dart'; +import 'package:on_time_front/data/models/update_schedule_request_model.dart'; import 'package:on_time_front/domain/entities/schedule_entity.dart'; import 'package:on_time_front/domain/repositories/schedule_repository.dart'; import 'package:on_time_front/domain/repositories/timed_preparation_repository.dart'; @@ -50,7 +52,9 @@ class ScheduleRepositoryImpl implements ScheduleRepository { @override Future createSchedule(ScheduleEntity schedule) async { try { - await scheduleRemoteDataSource.createSchedule(schedule); + await scheduleRemoteDataSource.createSchedule( + CreateScheduleRequestModel.fromEntity(schedule), + ); //await scheduleLocalDataSource.createSchedule(schedule); _emitUpsertedSchedule(schedule); } catch (e) { @@ -61,7 +65,7 @@ class ScheduleRepositoryImpl implements ScheduleRepository { @override Future deleteSchedule(ScheduleEntity schedule) async { try { - await scheduleRemoteDataSource.deleteSchedule(schedule); + await scheduleRemoteDataSource.deleteSchedule(schedule.id); await _clearTimedPreparationSafe(schedule.id); //await scheduleLocalDataSource.deleteSchedule(schedule); _emitScheduleSet( @@ -86,7 +90,9 @@ class ScheduleRepositoryImpl implements ScheduleRepository { @override Future getScheduleById(String id) async { try { - final schedule = await scheduleRemoteDataSource.getScheduleById(id); + final schedule = (await scheduleRemoteDataSource.getScheduleById( + id, + )).toEntity(); if (_isEnded(schedule.doneStatus)) { await _clearTimedPreparationSafe(schedule.id); } @@ -103,10 +109,10 @@ class ScheduleRepositoryImpl implements ScheduleRepository { DateTime? endDate, ) async { try { - final schedules = await scheduleRemoteDataSource.getSchedulesByDate( + final schedules = (await scheduleRemoteDataSource.getSchedulesByDate( startDate, endDate, - ); + )).map((schedule) => schedule.toEntity()).toList(); for (final schedule in schedules) { if (_isEnded(schedule.doneStatus)) { await _clearTimedPreparationSafe(schedule.id); @@ -130,13 +136,15 @@ class ScheduleRepositoryImpl implements ScheduleRepository { }) async { try { await scheduleRemoteDataSource.updateSchedule( - schedule, - includePreparationSource: includePreparationSource, + UpdateScheduleRequestModel.fromEntity( + schedule, + includePreparationSource: includePreparationSource, + ), ); await _clearTimedPreparationSafe(schedule.id); - final refreshedSchedule = await scheduleRemoteDataSource.getScheduleById( + final refreshedSchedule = (await scheduleRemoteDataSource.getScheduleById( schedule.id, - ); + )).toEntity(); if (_isEnded(refreshedSchedule.doneStatus)) { await _clearTimedPreparationSafe(refreshedSchedule.id); } diff --git a/test/data/data_sources/schedule_data_source_contract_test.dart b/test/data/data_sources/schedule_data_source_contract_test.dart new file mode 100644 index 00000000..321cea19 --- /dev/null +++ b/test/data/data_sources/schedule_data_source_contract_test.dart @@ -0,0 +1,37 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test( + 'schedule remote data source contract does not expose domain entities', + () { + final violations = _scheduleEntityViolations( + 'lib/data/data_sources/schedule_remote_data_source.dart', + ); + + expect(violations, isEmpty); + }, + ); + + test( + 'schedule local data source contract does not expose domain entities', + () { + final violations = _scheduleEntityViolations( + 'lib/data/data_sources/schedule_local_data_source.dart', + ); + + expect(violations, isEmpty); + }, + ); +} + +List _scheduleEntityViolations(String path) { + final lines = File(path).readAsLinesSync(); + return [ + for (var index = 0; index < lines.length; index++) + if (lines[index].contains('domain/entities/schedule_entity.dart') || + lines[index].contains('ScheduleEntity')) + '$path:${index + 1}: ${lines[index].trim()}', + ]; +} diff --git a/test/data/data_sources/schedule_local_data_source_test.dart b/test/data/data_sources/schedule_local_data_source_test.dart index 28b255cb..6a635c37 100644 --- a/test/data/data_sources/schedule_local_data_source_test.dart +++ b/test/data/data_sources/schedule_local_data_source_test.dart @@ -2,8 +2,7 @@ import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:on_time_front/core/database/database.dart'; import 'package:on_time_front/data/data_sources/schedule_local_data_source.dart'; -import 'package:on_time_front/domain/entities/place_entity.dart'; -import 'package:on_time_front/domain/entities/schedule_entity.dart'; +import 'package:on_time_front/data/tables/schedule_with_place_model.dart'; void main() { late AppDatabase database; @@ -19,7 +18,7 @@ void main() { }); test('creates and reads a schedule with its place', () async { - final schedule = _schedule( + final schedule = _scheduleWithPlace( id: 'schedule-1', placeId: 'place-1', placeName: 'Office', @@ -29,22 +28,22 @@ void main() { await dataSource.createSchedule(schedule); final stored = await dataSource.getScheduleById('schedule-1'); - expect(stored.id, 'schedule-1'); - expect(stored.place, const PlaceEntity(id: 'place-1', placeName: 'Office')); - expect(stored.scheduleName, 'Meeting schedule-1'); - expect(stored.scheduleTime, DateTime(2026, 5, 15, 9)); - expect(stored.moveTime, const Duration(minutes: 20)); - expect(stored.scheduleSpareTime, const Duration(minutes: 5)); + expect(stored.schedule.id, 'schedule-1'); + expect(stored.place, const Place(id: 'place-1', placeName: 'Office')); + expect(stored.schedule.scheduleName, 'Meeting schedule-1'); + expect(stored.schedule.scheduleTime, DateTime(2026, 5, 15, 9)); + expect(stored.schedule.moveTime, const Duration(minutes: 20)); + expect(stored.schedule.scheduleSpareTime, const Duration(minutes: 5)); }); test('filters schedules by date range and updates schedule fields', () async { - final inside = _schedule( + final inside = _scheduleWithPlace( id: 'inside', placeId: 'place-1', placeName: 'Office', time: DateTime(2026, 5, 15, 9), ); - final outside = _schedule( + final outside = _scheduleWithPlace( id: 'outside', placeId: 'place-2', placeName: 'Cafe', @@ -53,9 +52,9 @@ void main() { await dataSource.createSchedule(inside); await dataSource.createSchedule(outside); - final updated = ScheduleEntity( + final updated = _scheduleRow( id: 'inside', - place: const PlaceEntity(id: 'place-1', placeName: 'Ignored by update'), + placeId: 'place-1', scheduleName: 'Updated meeting', scheduleTime: DateTime(2026, 5, 15, 10), moveTime: const Duration(minutes: 45), @@ -72,27 +71,30 @@ void main() { DateTime(2026, 5, 16), ); - expect(schedules.map((schedule) => schedule.id), ['inside']); - expect(schedules.single.scheduleName, 'Updated meeting'); - expect(schedules.single.scheduleTime, DateTime(2026, 5, 15, 10)); - expect(schedules.single.moveTime, const Duration(minutes: 45)); - expect(schedules.single.isChanged, isTrue); - expect(schedules.single.isStarted, isTrue); - expect(schedules.single.scheduleSpareTime, const Duration(minutes: 15)); - expect(schedules.single.scheduleNote, 'updated note'); - expect(schedules.single.latenessTime, 2); + expect(schedules.map((schedule) => schedule.schedule.id), ['inside']); + expect(schedules.single.schedule.scheduleName, 'Updated meeting'); + expect(schedules.single.schedule.scheduleTime, DateTime(2026, 5, 15, 10)); + expect(schedules.single.schedule.moveTime, const Duration(minutes: 45)); + expect(schedules.single.schedule.isChanged, isTrue); + expect(schedules.single.schedule.isStarted, isTrue); + expect( + schedules.single.schedule.scheduleSpareTime, + const Duration(minutes: 15), + ); + expect(schedules.single.schedule.scheduleNote, 'updated note'); + expect(schedules.single.schedule.latenessTime, 2); }); test('deleteSchedule removes only the requested schedule', () async { await dataSource.createSchedule( - _schedule( + _scheduleWithPlace( id: 'schedule-1', placeId: 'place-1', placeName: 'Office', time: DateTime(2026, 5, 15, 9), ), ); - final keep = _schedule( + final keep = _scheduleWithPlace( id: 'schedule-2', placeId: 'place-2', placeName: 'Cafe', @@ -101,11 +103,17 @@ void main() { await dataSource.createSchedule(keep); await dataSource.deleteSchedule( - _schedule( + _scheduleRow( id: 'schedule-1', placeId: 'place-1', - placeName: 'Office', - time: DateTime(2026, 5, 15, 9), + scheduleName: 'Meeting schedule-1', + scheduleTime: DateTime(2026, 5, 15, 9), + moveTime: const Duration(minutes: 20), + isChanged: false, + isStarted: false, + scheduleSpareTime: const Duration(minutes: 5), + scheduleNote: 'note', + latenessTime: -1, ), ); @@ -117,21 +125,51 @@ void main() { }); } -ScheduleEntity _schedule({ +ScheduleWithPlace _scheduleWithPlace({ required String id, required String placeId, required String placeName, required DateTime time, }) { - return ScheduleEntity( + return ScheduleWithPlace( + schedule: _scheduleRow( + id: id, + placeId: placeId, + scheduleName: 'Meeting $id', + scheduleTime: time, + moveTime: const Duration(minutes: 20), + isChanged: false, + isStarted: false, + scheduleSpareTime: const Duration(minutes: 5), + scheduleNote: 'note', + latenessTime: -1, + ), + place: Place(id: placeId, placeName: placeName), + ); +} + +Schedule _scheduleRow({ + required String id, + required String placeId, + required String scheduleName, + required DateTime scheduleTime, + required Duration moveTime, + required bool isChanged, + required bool isStarted, + required Duration? scheduleSpareTime, + required String? scheduleNote, + required int latenessTime, +}) { + return Schedule( id: id, - place: PlaceEntity(id: placeId, placeName: placeName), - scheduleName: 'Meeting $id', - scheduleTime: time, - moveTime: const Duration(minutes: 20), - isChanged: false, - isStarted: false, - scheduleSpareTime: const Duration(minutes: 5), - scheduleNote: 'note', + placeId: placeId, + scheduleName: scheduleName, + scheduleTime: scheduleTime, + moveTime: moveTime, + isChanged: isChanged, + isStarted: isStarted, + scheduleSpareTime: scheduleSpareTime, + scheduleNote: scheduleNote, + latenessTime: latenessTime, ); } diff --git a/test/data/data_sources/schedule_remote_data_source_test.dart b/test/data/data_sources/schedule_remote_data_source_test.dart index 72918685..c8d4c1f6 100644 --- a/test/data/data_sources/schedule_remote_data_source_test.dart +++ b/test/data/data_sources/schedule_remote_data_source_test.dart @@ -4,8 +4,8 @@ import 'dart:typed_data'; import 'package:dio/dio.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:on_time_front/data/data_sources/schedule_remote_data_source.dart'; -import 'package:on_time_front/domain/entities/place_entity.dart'; -import 'package:on_time_front/domain/entities/schedule_entity.dart'; +import 'package:on_time_front/data/models/create_schedule_request_model.dart'; +import 'package:on_time_front/data/models/update_schedule_request_model.dart'; void main() { late _ScheduleAdapter adapter; @@ -22,11 +22,12 @@ void main() { test( 'create, update, delete, start, and finish send schedule API contracts', () async { - final schedule = _schedule('schedule-1'); + final createRequest = _createRequest('schedule-1'); + final updateRequest = _updateRequest('schedule-1'); - await dataSource.createSchedule(schedule); - await dataSource.updateSchedule(schedule); - await dataSource.deleteSchedule(schedule); + await dataSource.createSchedule(createRequest); + await dataSource.updateSchedule(updateRequest); + await dataSource.deleteSchedule('schedule-1'); await dataSource.startSchedule('schedule-1'); await dataSource.finishSchedule('schedule-1', 7); @@ -47,22 +48,17 @@ void main() { }, ); - test( - 'getScheduleById maps backend response into a schedule entity', - () async { - final schedule = await dataSource.getScheduleById('schedule-1'); + test('getScheduleById returns the backend response model', () async { + final schedule = await dataSource.getScheduleById('schedule-1'); - expect(schedule.id, 'schedule-1'); - expect( - schedule.place, - const PlaceEntity(id: 'place-1', placeName: 'Office'), - ); - expect(schedule.scheduleName, 'Morning meeting'); - expect(schedule.moveTime, const Duration(minutes: 20)); - expect(schedule.scheduleSpareTime, const Duration(minutes: 5)); - expect(schedule.doneStatus, ScheduleDoneStatus.normalEnd); - }, - ); + expect(schedule.scheduleId, 'schedule-1'); + expect(schedule.place.placeId, 'place-1'); + expect(schedule.place.placeName, 'Office'); + expect(schedule.scheduleName, 'Morning meeting'); + expect(schedule.moveTime, 20); + expect(schedule.scheduleSpareTime, 5); + expect(schedule.doneStatus, 'NORMAL'); + }); test( 'getSchedulesByDate passes date query parameters and maps list response', @@ -72,7 +68,7 @@ void main() { final schedules = await dataSource.getSchedulesByDate(start, end); - expect(schedules.map((schedule) => schedule.id), [ + expect(schedules.map((schedule) => schedule.scheduleId), [ 'schedule-1', 'schedule-2', ]); @@ -88,7 +84,7 @@ void main() { adapter.statusCode = 500; await expectLater( - dataSource.createSchedule(_schedule('schedule-1')), + dataSource.createSchedule(_createRequest('schedule-1')), throwsException, ); await expectLater( @@ -98,16 +94,30 @@ void main() { }); } -ScheduleEntity _schedule(String id) { - return ScheduleEntity( - id: id, - place: const PlaceEntity(id: 'place-1', placeName: 'Office'), +CreateScheduleRequestModel _createRequest(String id) { + return CreateScheduleRequestModel( + scheduleId: id, + placeId: 'place-1', + placeName: 'Office', scheduleName: 'Meeting $id', scheduleTime: DateTime(2026, 5, 15, 9), - moveTime: const Duration(minutes: 20), - isChanged: false, + moveTime: 20, + isChange: false, isStarted: false, - scheduleSpareTime: const Duration(minutes: 5), + scheduleSpareTime: 5, + scheduleNote: 'note', + ); +} + +UpdateScheduleRequestModel _updateRequest(String id) { + return UpdateScheduleRequestModel( + scheduleId: id, + placeId: 'place-1', + placeName: 'Office', + scheduleName: 'Meeting $id', + scheduleTime: DateTime(2026, 5, 15, 9), + moveTime: 20, + scheduleSpareTime: 5, scheduleNote: 'note', ); } diff --git a/test/data/repositories/schedule_repository_impl_stream_test.dart b/test/data/repositories/schedule_repository_impl_stream_test.dart index 6778fb26..dd8c93f1 100644 --- a/test/data/repositories/schedule_repository_impl_stream_test.dart +++ b/test/data/repositories/schedule_repository_impl_stream_test.dart @@ -1,7 +1,13 @@ import 'package:flutter_test/flutter_test.dart'; +import 'package:on_time_front/core/database/database.dart'; import 'package:on_time_front/data/data_sources/schedule_local_data_source.dart'; import 'package:on_time_front/data/data_sources/schedule_remote_data_source.dart'; +import 'package:on_time_front/data/models/create_schedule_request_model.dart'; +import 'package:on_time_front/data/models/get_place_response_model.dart'; +import 'package:on_time_front/data/models/get_schedule_response_model.dart'; +import 'package:on_time_front/data/models/update_schedule_request_model.dart'; import 'package:on_time_front/data/repositories/schedule_repository_impl.dart'; +import 'package:on_time_front/data/tables/schedule_with_place_model.dart'; import 'package:on_time_front/domain/entities/place_entity.dart'; import 'package:on_time_front/domain/entities/schedule_entity.dart'; import 'package:on_time_front/domain/entities/timed_preparation_snapshot_entity.dart'; @@ -9,18 +15,18 @@ import 'package:on_time_front/domain/repositories/timed_preparation_repository.d class FakeScheduleLocalDataSource implements ScheduleLocalDataSource { @override - Future createSchedule(ScheduleEntity scheduleEntity) async {} + Future createSchedule(ScheduleWithPlace schedule) async {} @override - Future deleteSchedule(ScheduleEntity scheduleEntity) async {} + Future deleteSchedule(Schedule schedule) async {} @override - Future getScheduleById(String id) async { + Future getScheduleById(String id) async { throw UnimplementedError(); } @override - Future> getSchedulesByDate( + Future> getSchedulesByDate( DateTime startDate, DateTime? endDate, ) async { @@ -28,14 +34,15 @@ class FakeScheduleLocalDataSource implements ScheduleLocalDataSource { } @override - Future updateSchedule(ScheduleEntity scheduleEntity) async {} + Future updateSchedule(Schedule schedule) async {} } class FakeScheduleRemoteDataSource implements ScheduleRemoteDataSource { FakeScheduleRemoteDataSource({ required this.getSchedulesByDateHandler, Future Function(String id)? getScheduleByIdHandler, - Future Function(ScheduleEntity schedule)? updateScheduleHandler, + Future Function(UpdateScheduleRequestModel schedule)? + updateScheduleHandler, }) : getScheduleByIdHandler = getScheduleByIdHandler ?? ((_) async => throw UnimplementedError()), updateScheduleHandler = updateScheduleHandler ?? ((_) async {}); @@ -43,13 +50,14 @@ class FakeScheduleRemoteDataSource implements ScheduleRemoteDataSource { Future> Function(DateTime startDate, DateTime? endDate) getSchedulesByDateHandler; Future Function(String id) getScheduleByIdHandler; - Future Function(ScheduleEntity schedule) updateScheduleHandler; + Future Function(UpdateScheduleRequestModel schedule) + updateScheduleHandler; @override - Future createSchedule(ScheduleEntity schedule) async {} + Future createSchedule(CreateScheduleRequestModel schedule) async {} @override - Future deleteSchedule(ScheduleEntity schedule) async {} + Future deleteSchedule(String scheduleId) async {} @override Future finishSchedule(String scheduleId, int latenessTime) async {} @@ -58,23 +66,21 @@ class FakeScheduleRemoteDataSource implements ScheduleRemoteDataSource { Future startSchedule(String scheduleId) async {} @override - Future getScheduleById(String id) async { - return getScheduleByIdHandler(id); + Future getScheduleById(String id) async { + return _responseFrom(await getScheduleByIdHandler(id)); } @override - Future> getSchedulesByDate( + Future> getSchedulesByDate( DateTime startDate, DateTime? endDate, - ) { - return getSchedulesByDateHandler(startDate, endDate); + ) async { + final schedules = await getSchedulesByDateHandler(startDate, endDate); + return schedules.map(_responseFrom).toList(); } @override - Future updateSchedule( - ScheduleEntity schedule, { - bool includePreparationSource = false, - }) { + Future updateSchedule(UpdateScheduleRequestModel schedule) { return updateScheduleHandler(schedule); } } @@ -338,3 +344,37 @@ ScheduleEntity _schedule({required String id, required DateTime scheduleTime}) { scheduleNote: '', ); } + +GetScheduleResponseModel _responseFrom(ScheduleEntity schedule) { + return GetScheduleResponseModel( + scheduleId: schedule.id, + place: GetPlaceResponseModel.fromEntity(schedule.place), + scheduleName: schedule.scheduleName, + scheduleTime: schedule.scheduleTime, + moveTime: schedule.moveTime.inMinutes, + scheduleSpareTime: schedule.scheduleSpareTime?.inMinutes ?? 0, + scheduleNote: schedule.scheduleNote, + latenessTime: schedule.latenessTime, + doneStatus: _serverDoneStatus(schedule.doneStatus), + startedAt: schedule.startedAt, + finishedAt: schedule.finishedAt, + preparationMode: schedule.preparationMode, + preparationTemplateId: schedule.preparationTemplateId, + preparationTemplateName: schedule.preparationTemplateName, + preparationTemplateDeleted: schedule.preparationTemplateDeleted, + preparationFrozen: schedule.preparationFrozen, + ); +} + +String _serverDoneStatus(ScheduleDoneStatus status) { + switch (status) { + case ScheduleDoneStatus.lateEnd: + return 'LATE'; + case ScheduleDoneStatus.normalEnd: + return 'NORMAL'; + case ScheduleDoneStatus.abnormalEnd: + return 'ABNORMAL'; + case ScheduleDoneStatus.notEnded: + return 'NOT_ENDED'; + } +} diff --git a/test/data/repositories/schedule_repository_impl_test.dart b/test/data/repositories/schedule_repository_impl_test.dart index 10d9ddfe..ff5ce772 100644 --- a/test/data/repositories/schedule_repository_impl_test.dart +++ b/test/data/repositories/schedule_repository_impl_test.dart @@ -1,5 +1,9 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; +import 'package:on_time_front/data/models/create_schedule_request_model.dart'; +import 'package:on_time_front/data/models/get_place_response_model.dart'; +import 'package:on_time_front/data/models/get_schedule_response_model.dart'; +import 'package:on_time_front/data/models/update_schedule_request_model.dart'; import 'package:on_time_front/data/repositories/schedule_repository_impl.dart'; import 'package:on_time_front/domain/entities/place_entity.dart'; import 'package:on_time_front/domain/entities/schedule_entity.dart'; @@ -74,12 +78,19 @@ void main() { 'when successful [createSchedule] should create a schedule with the given schedule entity', () async { when( - mockScheduleRemoteDataSource.createSchedule(tScheduleEntity), + mockScheduleRemoteDataSource.createSchedule(any), ).thenAnswer((_) async {}); await scheduleRepository.createSchedule(tScheduleEntity); - verify(mockScheduleRemoteDataSource.createSchedule(tScheduleEntity)); + final request = + verify( + mockScheduleRemoteDataSource.createSchedule(captureAny), + ).captured.single + as CreateScheduleRequestModel; + expect(request.scheduleId, scheduleEntityId); + expect(request.placeId, tPlaceEntity.id); + expect(request.scheduleName, tScheduleEntity.scheduleName); expect(fakeTimedPreparationRepository.clearedIds, isEmpty); }, ); @@ -88,7 +99,7 @@ void main() { 'when ScheduleRemoteDataSource throws an exception [createSchedule] should throw an exception', () async { when( - mockScheduleRemoteDataSource.createSchedule(tScheduleEntity), + mockScheduleRemoteDataSource.createSchedule(any), ).thenThrow(Exception()); final call = scheduleRepository.createSchedule(tScheduleEntity); @@ -104,12 +115,12 @@ void main() { 'when successful [deleteSchedule] clears timed cache for schedule id', () async { when( - mockScheduleRemoteDataSource.deleteSchedule(tScheduleEntity), + mockScheduleRemoteDataSource.deleteSchedule(scheduleEntityId), ).thenAnswer((_) async {}); await scheduleRepository.deleteSchedule(tScheduleEntity); - verify(mockScheduleRemoteDataSource.deleteSchedule(tScheduleEntity)); + verify(mockScheduleRemoteDataSource.deleteSchedule(scheduleEntityId)); expect(fakeTimedPreparationRepository.clearedIds, [scheduleEntityId]); }, ); @@ -118,7 +129,7 @@ void main() { 'when ScheduleRemoteDataSource throws an exception [deleteSchedule] should throw and not clear cache', () async { when( - mockScheduleRemoteDataSource.deleteSchedule(tScheduleEntity), + mockScheduleRemoteDataSource.deleteSchedule(scheduleEntityId), ).thenThrow(Exception()); final call = scheduleRepository.deleteSchedule(tScheduleEntity); @@ -135,7 +146,7 @@ void main() { () async { when( mockScheduleRemoteDataSource.getScheduleById(scheduleEntityId), - ).thenAnswer((_) async => Future.value(tScheduleEntity)); + ).thenAnswer((_) async => _responseFrom(tScheduleEntity)); final schedule = await scheduleRepository.getScheduleById( scheduleEntityId, @@ -154,7 +165,7 @@ void main() { ); when( mockScheduleRemoteDataSource.getScheduleById(scheduleEntityId), - ).thenAnswer((_) async => Future.value(endedSchedule)); + ).thenAnswer((_) async => _responseFrom(endedSchedule)); final schedule = await scheduleRepository.getScheduleById( scheduleEntityId, @@ -187,7 +198,7 @@ void main() { final schedules = [tScheduleEntity]; when( mockScheduleRemoteDataSource.getSchedulesByDate(tStartDate, tEndDate), - ).thenAnswer((_) async => Future.value(schedules)); + ).thenAnswer((_) async => _responsesFrom(schedules)); final result = await scheduleRepository.getSchedulesByDate( tStartDate, @@ -245,7 +256,7 @@ void main() { final schedules = [endedNormal, ongoing, endedLate, endedAbnormal]; when( mockScheduleRemoteDataSource.getSchedulesByDate(tStartDate, tEndDate), - ).thenAnswer((_) async => Future.value(schedules)); + ).thenAnswer((_) async => _responsesFrom(schedules)); await scheduleRepository.getSchedulesByDate(tStartDate, tEndDate); @@ -277,15 +288,22 @@ void main() { 'when successful [updateSchedule] clears timed cache for schedule id', () async { when( - mockScheduleRemoteDataSource.updateSchedule(tScheduleEntity), + mockScheduleRemoteDataSource.updateSchedule(any), ).thenAnswer((_) async {}); when( mockScheduleRemoteDataSource.getScheduleById(scheduleEntityId), - ).thenAnswer((_) async => tScheduleEntity); + ).thenAnswer((_) async => _responseFrom(tScheduleEntity)); await scheduleRepository.updateSchedule(tScheduleEntity); - verify(mockScheduleRemoteDataSource.updateSchedule(tScheduleEntity)); + final request = + verify( + mockScheduleRemoteDataSource.updateSchedule(captureAny), + ).captured.single + as UpdateScheduleRequestModel; + expect(request.scheduleId, scheduleEntityId); + expect(request.placeId, tPlaceEntity.id); + expect(request.scheduleName, tScheduleEntity.scheduleName); verify(mockScheduleRemoteDataSource.getScheduleById(scheduleEntityId)); expect(fakeTimedPreparationRepository.clearedIds, [scheduleEntityId]); }, @@ -295,7 +313,7 @@ void main() { 'when ScheduleRemoteDataSource throws an exception [updateSchedule] should throw and not clear cache', () async { when( - mockScheduleRemoteDataSource.updateSchedule(tScheduleEntity), + mockScheduleRemoteDataSource.updateSchedule(any), ).thenThrow(Exception()); final call = scheduleRepository.updateSchedule(tScheduleEntity); @@ -311,7 +329,7 @@ void main() { 'when successful [finishSchedule] clears timed cache for schedule id', () async { when( - mockScheduleRemoteDataSource.createSchedule(tScheduleEntity), + mockScheduleRemoteDataSource.createSchedule(any), ).thenAnswer((_) async {}); when( mockScheduleRemoteDataSource.finishSchedule(scheduleEntityId, 0), @@ -368,3 +386,43 @@ void main() { ); }); } + +List _responsesFrom( + Iterable schedules, +) { + return schedules.map(_responseFrom).toList(); +} + +GetScheduleResponseModel _responseFrom(ScheduleEntity schedule) { + return GetScheduleResponseModel( + scheduleId: schedule.id, + place: GetPlaceResponseModel.fromEntity(schedule.place), + scheduleName: schedule.scheduleName, + scheduleTime: schedule.scheduleTime, + moveTime: schedule.moveTime.inMinutes, + scheduleSpareTime: schedule.scheduleSpareTime?.inMinutes ?? 0, + scheduleNote: schedule.scheduleNote, + latenessTime: schedule.latenessTime, + doneStatus: _serverDoneStatus(schedule.doneStatus), + startedAt: schedule.startedAt, + finishedAt: schedule.finishedAt, + preparationMode: schedule.preparationMode, + preparationTemplateId: schedule.preparationTemplateId, + preparationTemplateName: schedule.preparationTemplateName, + preparationTemplateDeleted: schedule.preparationTemplateDeleted, + preparationFrozen: schedule.preparationFrozen, + ); +} + +String _serverDoneStatus(ScheduleDoneStatus status) { + switch (status) { + case ScheduleDoneStatus.lateEnd: + return 'LATE'; + case ScheduleDoneStatus.normalEnd: + return 'NORMAL'; + case ScheduleDoneStatus.abnormalEnd: + return 'ABNORMAL'; + case ScheduleDoneStatus.notEnded: + return 'NOT_ENDED'; + } +}