import '../domain/domain.dart'; import 'ports.dart'; final class CreateShareResult { const CreateShareResult({ required this.share, required this.recipientUserIds, required this.unresolvedEmails, }); final Share share; final List recipientUserIds; final List unresolvedEmails; } final class AcceptShareResult { const AcceptShareResult({required this.createdResource}); final SyncedResource createdResource; } final class CreateShareUseCase { const CreateShareUseCase({ required this.users, required this.shares, required this.clock, required this.ids, }); final UserRepository users; final ShareRepository shares; final Clock clock; final IdGenerator ids; Future execute({ required String senderUserId, required String resourceType, required Map payloadJson, required List recipientEmails, }) async { final parsedType = _shareResourceType(resourceType); final emails = _normalizedEmails(recipientEmails); if (emails.isEmpty) { throw const ValidationException('recipientEmails must not be empty.'); } final recipientUserIds = []; final unresolvedEmails = []; for (final email in emails) { final user = await users.findByEmail(email); if (user == null) { unresolvedEmails.add(email); } else if (user.id != senderUserId && !recipientUserIds.contains(user.id)) { recipientUserIds.add(user.id); } } final now = clock.now().toUtc(); final share = Share( id: ids.newId(), senderUserId: senderUserId, resourceType: parsedType, payloadJson: payloadJson, createdAt: now, ); final recipients = [ for (final recipientUserId in recipientUserIds) ShareRecipient( id: ids.newId(), shareId: share.id, recipientUserId: recipientUserId, ), ]; await shares.insertShare(share: share, recipients: recipients); return CreateShareResult( share: share, recipientUserIds: recipientUserIds, unresolvedEmails: unresolvedEmails, ); } } final class ListInboxUseCase { const ListInboxUseCase({required this.shares}); final ShareRepository shares; Future> execute({required String recipientUserId}) { return shares.listInbox(recipientUserId); } } final class AcceptShareUseCase { const AcceptShareUseCase({ required this.shares, required this.resources, required this.clock, required this.ids, }); final ShareRepository shares; final SyncedResourceRepository resources; final Clock clock; final IdGenerator ids; Future execute({ required String shareId, required String recipientUserId, }) async { final share = await shares.findShareById(shareId); final recipient = await shares.findRecipient( shareId: shareId, recipientUserId: recipientUserId, ); if (share == null || recipient == null) { throw const ShareNotFoundException(); } if (share.isRevoked) { throw const ShareConflictException('Share has been revoked.'); } if (recipient.status != ShareRecipientStatus.pending) { throw const ShareConflictException('Share has already been answered.'); } final now = clock.now().toUtc(); final resource = SyncedResource( serverId: ids.newId(), ownerUserId: recipientUserId, resourceType: share.resourceType, clientId: ids.newId(), payloadJson: share.payloadJson, schemaVersion: _schemaVersionFromPayload(share.payloadJson), clientUpdatedAt: now, serverUpdatedAt: now, ); final written = await resources.upsertWithLww(resource); await shares.updateRecipientStatus( recipientId: recipient.id, status: ShareRecipientStatus.accepted, respondedAt: now, ); return AcceptShareResult(createdResource: written.resource); } } final class DeclineShareUseCase { const DeclineShareUseCase({required this.shares, required this.clock}); final ShareRepository shares; final Clock clock; Future execute({ required String shareId, required String recipientUserId, }) async { final share = await shares.findShareById(shareId); final recipient = await shares.findRecipient( shareId: shareId, recipientUserId: recipientUserId, ); if (share == null || recipient == null) { throw const ShareNotFoundException(); } if (recipient.status != ShareRecipientStatus.pending) { throw const ShareConflictException('Share has already been answered.'); } await shares.updateRecipientStatus( recipientId: recipient.id, status: ShareRecipientStatus.declined, respondedAt: clock.now().toUtc(), ); } } final class RevokeShareUseCase { const RevokeShareUseCase({required this.shares, required this.clock}); final ShareRepository shares; final Clock clock; Future execute({ required String shareId, required String senderUserId, }) async { final share = await shares.findShareById(shareId); if (share == null || share.senderUserId != senderUserId) { throw const ShareNotFoundException(); } if (share.isRevoked) { return; } await shares.revokeShare(shareId: shareId, revokedAt: clock.now().toUtc()); } } SyncedResourceType _shareResourceType(String value) { final type = SyncedResourceType.parse(value); if (type != SyncedResourceType.program && type != SyncedResourceType.workoutTemplate) { throw const ValidationException( 'resourceType must be program or workoutTemplate.', ); } return type; } List _normalizedEmails(List emails) { return emails .map((email) => email.trim().toLowerCase()) .where((email) => email.isNotEmpty) .toSet() .toList(growable: false); } int _schemaVersionFromPayload(Map payloadJson) { final schemaVersion = payloadJson['schemaVersion']; return schemaVersion is int && schemaVersion > 0 ? schemaVersion : 1; }