feat(core): consolide le chainage type metier d'exercice -> strategie Health Services -> payload montre -> consommation Kotlin

Finalise #183 : modele/persistance des types metier d'exercice, mapping vers la strategie Health Services, propagation du payload dans le contrat watch bridge et consommation cote Kotlin montre. Perimetre complet QA vert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 16:17:31 +02:00
parent 30e657a67f
commit 2ae716206a
10 changed files with 444 additions and 22 deletions

View File

@ -193,6 +193,10 @@ final class DriftStarterSeedRepository
await database
.into(database.programExercises)
.insertOnConflictUpdate(_programExerciseCompanion(exercise));
await _writeProgramExerciseHealthServicesStrategySnapshot(
database,
exercise,
);
}
await database
.into(database.workoutTemplates)
@ -523,6 +527,10 @@ final class DriftLocalSyncChangeRepository
await database
.into(database.programExercises)
.insertOnConflictUpdate(_programExerciseCompanion(exercise));
await _writeProgramExerciseHealthServicesStrategySnapshot(
database,
exercise,
);
}
});
return true;
@ -996,7 +1004,7 @@ final class DriftProgramRepository implements ProgramRepository {
.get();
return _programFromRow(
row,
exercises.map(_programExerciseFromRow).toList(),
await _programExercisesFromRows(database, exercises),
isExample: await _isProgramExample(id),
);
}
@ -1021,7 +1029,7 @@ final class DriftProgramRepository implements ProgramRepository {
programs.add(
_programFromRow(
row,
exercises.map(_programExerciseFromRow).toList(),
await _programExercisesFromRows(database, exercises),
isExample: await _isProgramExample(row.id),
),
);
@ -1055,9 +1063,15 @@ final class DriftProgramRepository implements ProgramRepository {
tableName: 'program_exercises',
entityType: 'ProgramExercise',
metadata: exercise.metadata,
write: () => database
.into(database.programExercises)
.insertOnConflictUpdate(_programExerciseCompanion(exercise)),
write: () async {
await database
.into(database.programExercises)
.insertOnConflictUpdate(_programExerciseCompanion(exercise));
await _writeProgramExerciseHealthServicesStrategySnapshot(
database,
exercise,
);
},
);
}
@ -3461,6 +3475,23 @@ Future<void> _writeExerciseStarterMetadata(
);
}
Future<void> _writeProgramExerciseHealthServicesStrategySnapshot(
db.AppDatabase database,
domain.ProgramExercise exercise,
) async {
await database.customStatement(
'UPDATE program_exercises SET '
'health_services_exercise_type_strategy_snapshot_json = ? '
'WHERE id = ?',
[
_encodeHealthServicesExerciseTypes(
exercise.healthServicesExerciseTypeStrategySnapshot,
),
exercise.metadata.id,
],
);
}
Future<void> _writeProgramStarterMetadata(
db.AppDatabase database,
domain.Program program,
@ -4083,7 +4114,21 @@ db.ProgramExercisesCompanion _programExerciseCompanion(
);
}
domain.ProgramExercise _programExerciseFromRow(db.ProgramExercise row) {
Future<List<domain.ProgramExercise>> _programExercisesFromRows(
db.AppDatabase database,
List<db.ProgramExercise> rows,
) async {
final output = <domain.ProgramExercise>[];
for (final row in rows) {
output.add(await _programExerciseFromRow(database, row));
}
return output;
}
Future<domain.ProgramExercise> _programExerciseFromRow(
db.AppDatabase database,
db.ProgramExercise row,
) async {
return domain.ProgramExercise(
metadata: _metadataFromRow(row),
programId: row.programId,
@ -4102,6 +4147,8 @@ domain.ProgramExercise _programExerciseFromRow(db.ProgramExercise row) {
autoStartNextTimedStepSnapshot: row.autoStartNextTimedStepSnapshot,
autoStartNextTimedStepOverride: row.autoStartNextTimedStepOverride,
exerciseVideoMediaIdSnapshot: row.exerciseVideoMediaIdSnapshot,
healthServicesExerciseTypeStrategySnapshot:
await _programExerciseHealthServicesStrategySnapshot(database, row.id),
exerciseArchivedSnapshot: row.exerciseArchivedSnapshot,
availableTimeSnapshot: row.availableTimeSnapshot,
availableRepsSnapshot: row.availableRepsSnapshot,
@ -4121,6 +4168,25 @@ domain.ProgramExercise _programExerciseFromRow(db.ProgramExercise row) {
);
}
Future<List<domain.HealthServicesExerciseType>>
_programExerciseHealthServicesStrategySnapshot(
db.AppDatabase database,
String id,
) async {
final row = await database
.customSelect(
'SELECT health_services_exercise_type_strategy_snapshot_json '
'FROM program_exercises WHERE id = ? LIMIT 1',
variables: [Variable<String>(id)],
)
.getSingleOrNull();
return _decodeHealthServicesExerciseTypesJson(
row?.readNullable<String>(
'health_services_exercise_type_strategy_snapshot_json',
),
);
}
db.WorkoutTemplatesCompanion _workoutTemplateCompanion(
domain.WorkoutTemplate template,
) {
@ -5802,6 +5868,10 @@ List<domain.ProgramExercise> _programExercisesFromPayload(
),
exerciseVideoMediaIdSnapshot:
map['exerciseVideoMediaIdSnapshot'] as String?,
healthServicesExerciseTypeStrategySnapshot:
_healthServicesExerciseTypesFromPayload(
map['healthServicesExerciseTypeStrategy'],
),
exerciseStepsSnapshot: _stepsFromPayload(
map['exerciseStepsSnapshot'],
),
@ -6069,6 +6139,12 @@ String _encodeBusinessExerciseTypes(List<domain.BusinessExerciseType> types) {
return jsonEncode(types.map(_businessExerciseTypeToDb).toList());
}
String _encodeHealthServicesExerciseTypes(
List<domain.HealthServicesExerciseType> types,
) {
return jsonEncode(types.map((type) => type.wireName).toList());
}
List<domain.BusinessExerciseType> _decodeBusinessExerciseTypesJson(
String? encoded,
) {
@ -6083,6 +6159,20 @@ List<domain.BusinessExerciseType> _decodeBusinessExerciseTypesJson(
}
}
List<domain.HealthServicesExerciseType> _decodeHealthServicesExerciseTypesJson(
String? encoded,
) {
if (encoded == null || encoded.trim().isEmpty) {
return domain.legacyHealthServicesExerciseTypeStrategy;
}
try {
final decoded = jsonDecode(encoded);
return _healthServicesExerciseTypesFromPayload(decoded);
} on Object {
return domain.legacyHealthServicesExerciseTypeStrategy;
}
}
List<domain.BusinessExerciseType> _businessExerciseTypesFromPayload(
Object? value,
) {
@ -6099,6 +6189,24 @@ List<domain.BusinessExerciseType> _businessExerciseTypesFromPayload(
return List.unmodifiable(output);
}
List<domain.HealthServicesExerciseType> _healthServicesExerciseTypesFromPayload(
Object? value,
) {
if (value is! List) {
return domain.legacyHealthServicesExerciseTypeStrategy;
}
final output = <domain.HealthServicesExerciseType>[];
for (final raw in value.whereType<String>()) {
final type = _healthServicesExerciseTypeFromDbOrNull(raw);
if (type != null && !output.contains(type)) {
output.add(type);
}
}
return output.isEmpty
? domain.legacyHealthServicesExerciseTypeStrategy
: List.unmodifiable(output);
}
List<domain.ExerciseStep> _stepsFromPayload(Object? value) {
if (value is! List) {
return const [];
@ -6364,6 +6472,17 @@ domain.BusinessExerciseType? _businessExerciseTypeFromDbOrNull(String value) =>
_ => null,
};
domain.HealthServicesExerciseType? _healthServicesExerciseTypeFromDbOrNull(
String value,
) => switch (value) {
'RUNNING' => domain.HealthServicesExerciseType.running,
'WALKING' => domain.HealthServicesExerciseType.walking,
'HIGH_INTENSITY_INTERVAL_TRAINING' =>
domain.HealthServicesExerciseType.highIntensityIntervalTraining,
'WORKOUT' => domain.HealthServicesExerciseType.workout,
_ => null,
};
String _setResultStatusToDb(domain.SetResultStatus status) => switch (status) {
domain.SetResultStatus.completed => 'completed',
domain.SetResultStatus.skipped => 'skipped',