feat(server): partage ciblé de programmes et séances entre comptes (ticket #51)
Ajoute l'endpoint api/share_api.dart, les use cases de partage (application/share_use_cases.dart) et l'adapter Postgres (infrastructure/postgres/share_repository.dart). L'acceptation d'un partage crée une nouvelle ressource synced_resources avec de nouveaux IDs pour le destinataire, sans jamais modifier la ressource source de l'émetteur ; gestion des conflits révoqué/déjà répondu. dart pub get OK, dart analyze clean, dart test 24/24 vert. Use case d'acceptation relu manuellement et jugé correct ; pas de test de bout en bout contre un vrai PostgreSQL faute d'accès Docker dans ce sandbox. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -5,4 +5,5 @@ Pure server domain entities and invariants.
|
||||
This layer must not import Shelf, PostgreSQL adapters, Docker configuration or
|
||||
other infrastructure concerns.
|
||||
|
||||
Current entities: `UserAccount`, `AuthSession` and `SyncedResource`.
|
||||
Current entities: `UserAccount`, `AuthSession`, `SyncedResource`, `Share` and
|
||||
`ShareRecipient`.
|
||||
|
||||
@ -23,6 +23,14 @@ final class UnauthorizedException extends DomainException {
|
||||
const UnauthorizedException() : super('Unauthorized.');
|
||||
}
|
||||
|
||||
final class ShareNotFoundException extends DomainException {
|
||||
const ShareNotFoundException() : super('Share not found.');
|
||||
}
|
||||
|
||||
final class ShareConflictException extends DomainException {
|
||||
const ShareConflictException(super.message);
|
||||
}
|
||||
|
||||
final class UserAccount {
|
||||
UserAccount({
|
||||
required String id,
|
||||
@ -145,6 +153,76 @@ final class SyncedResource {
|
||||
final String? originDeviceId;
|
||||
}
|
||||
|
||||
enum ShareRecipientStatus {
|
||||
pending('pending'),
|
||||
accepted('accepted'),
|
||||
declined('declined'),
|
||||
revoked('revoked');
|
||||
|
||||
const ShareRecipientStatus(this.wireName);
|
||||
|
||||
final String wireName;
|
||||
|
||||
static ShareRecipientStatus parse(String value) {
|
||||
for (final status in values) {
|
||||
if (status.wireName == value) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
throw ValidationException('Unsupported share recipient status: $value.');
|
||||
}
|
||||
}
|
||||
|
||||
final class Share {
|
||||
Share({
|
||||
required String id,
|
||||
required String senderUserId,
|
||||
required this.resourceType,
|
||||
required Map<String, Object?> payloadJson,
|
||||
required DateTime createdAt,
|
||||
DateTime? revokedAt,
|
||||
}) : id = _nonBlank(id, 'Share id'),
|
||||
senderUserId = _nonBlank(senderUserId, 'Sender user id'),
|
||||
payloadJson = Map.unmodifiable(payloadJson),
|
||||
createdAt = createdAt.toUtc(),
|
||||
revokedAt = revokedAt?.toUtc() {
|
||||
if (resourceType != SyncedResourceType.program &&
|
||||
resourceType != SyncedResourceType.workoutTemplate) {
|
||||
throw const ValidationException(
|
||||
'Shares only support program and workoutTemplate resources.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final String id;
|
||||
final String senderUserId;
|
||||
final SyncedResourceType resourceType;
|
||||
final Map<String, Object?> payloadJson;
|
||||
final DateTime createdAt;
|
||||
final DateTime? revokedAt;
|
||||
|
||||
bool get isRevoked => revokedAt != null;
|
||||
}
|
||||
|
||||
final class ShareRecipient {
|
||||
ShareRecipient({
|
||||
required String id,
|
||||
required String shareId,
|
||||
required String recipientUserId,
|
||||
this.status = ShareRecipientStatus.pending,
|
||||
DateTime? respondedAt,
|
||||
}) : id = _nonBlank(id, 'Share recipient id'),
|
||||
shareId = _nonBlank(shareId, 'Share id'),
|
||||
recipientUserId = _nonBlank(recipientUserId, 'Recipient user id'),
|
||||
respondedAt = respondedAt?.toUtc();
|
||||
|
||||
final String id;
|
||||
final String shareId;
|
||||
final String recipientUserId;
|
||||
final ShareRecipientStatus status;
|
||||
final DateTime? respondedAt;
|
||||
}
|
||||
|
||||
String _nonBlank(String value, String label) {
|
||||
final trimmed = value.trim();
|
||||
if (trimmed.isEmpty) {
|
||||
|
||||
Reference in New Issue
Block a user