From a6abcfa7e62c33d2c735862bbdf00b14b2c60fa2 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 8 Jan 2026 19:07:56 +0300 Subject: [PATCH] repo --- .../test/domain/models/chat_message_test.dart | 359 -------------- .../test/domain/models/chat_session_test.dart | 255 ---------- .../domain/services/chat_service_test.dart | 345 -------------- .../domain/state/chat_state_manager_test.dart | 449 ------------------ .../lib/api/di/injector.config.dart | 134 +++--- .../lib/api/purchase/payment_manager.dart | 22 +- .../lib/api/v2/admin_analytics_api_v2.dart | 27 +- .../lib/api/v2/admin_packs_api_v2.dart | 124 ++--- .../lib/api/v2/admin_users_api_v2.dart | 80 ++-- .../lib/api/v2/packs_api_v2.dart | 53 ++- .../lib/api/v2/purchases_api_v2.dart | 21 +- .../lib/api/v2/telegram_bot_api_v2.dart | 14 +- .../lib/api/v2/users_api_v2.dart | 13 +- .../lib/cron/add_free_packs.dart | 21 +- .../lib/cron/check_admins.dart | 14 +- .../lib/discounts/discounts_manager.dart | 13 +- .../lib/packs/card_pack_drift_extension.dart | 26 +- .../lib/packs/free_packs_distributor.dart | 26 +- .../lib/packs/pack_dto_converter.dart | 7 +- .../lib/packs/pack_manager.dart | 41 +- .../lib/packs/pack_repository.dart | 238 ++++++++++ .../lib/packs/products_price_resolver.dart | 8 +- .../lib/statistics/achievement_manager.dart | 6 +- .../lib/statistics/statistics_calculator.dart | 9 +- .../lib/user/user_drift_extension.dart | 7 +- .../lib/user/user_manager.dart | 88 ++-- .../lib/user/user_manager_drift.dart | 3 +- .../lib/user/user_repository.dart | 181 +++++++ .../test/api/v2/media_api_v2_test.mocks.dart | 20 +- .../lib/src/models/card_pack_model.dart | 37 +- .../lib/src/models/card_pack_model.g.dart | 48 -- .../game_tests/test_statistics_model.dart | 14 +- .../game_tests/test_statistics_model.g.dart | 44 -- .../lib/src/models/payment.dart | 20 - .../lib/src/models/payment.g.dart | 101 ---- .../src/models/promo_code/promo_code_ext.dart | 2 +- .../models/promo_code/promo_code_model.dart | 8 +- .../models/promo_code/promo_code_model.g.dart | 45 +- .../subscription/user_subscription_model.dart | 5 - .../user_subscription_model.g.dart | 37 -- .../lib/src/models/user/user_data_model.dart | 7 - .../src/models/user/user_data_model.g.dart | 61 --- .../lib/src/models/user/user_model.dart | 13 +- .../lib/src/models/user/user_model.g.dart | 39 -- 44 files changed, 900 insertions(+), 2185 deletions(-) delete mode 100644 chat/mnemo_cards_chat/test/domain/models/chat_message_test.dart delete mode 100644 chat/mnemo_cards_chat/test/domain/models/chat_session_test.dart delete mode 100644 chat/mnemo_cards_chat/test/domain/services/chat_service_test.dart delete mode 100644 chat/mnemo_cards_chat/test/domain/state/chat_state_manager_test.dart create mode 100644 mnemo_cards_backend/lib/packs/pack_repository.dart create mode 100644 mnemo_cards_backend/lib/user/user_repository.dart diff --git a/chat/mnemo_cards_chat/test/domain/models/chat_message_test.dart b/chat/mnemo_cards_chat/test/domain/models/chat_message_test.dart deleted file mode 100644 index a1abb1c..0000000 --- a/chat/mnemo_cards_chat/test/domain/models/chat_message_test.dart +++ /dev/null @@ -1,359 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:mnemo_cards_chat/domain/models/chat_message.dart'; - -void main() { - group('ChatMessage', () { - group('MessageStatus', () { - test('has correct display names', () { - expect(MessageStatus.sending.displayName, 'Отправляется...'); - expect(MessageStatus.sent.displayName, 'Отправлено'); - expect(MessageStatus.delivered.displayName, 'Доставлено'); - expect(MessageStatus.error.displayName, 'Ошибка'); - }); - - test('has correct status checks', () { - expect(MessageStatus.sending.isSending, isTrue); - expect(MessageStatus.sent.isSending, isFalse); - expect(MessageStatus.delivered.isSending, isFalse); - expect(MessageStatus.error.isSending, isFalse); - - expect(MessageStatus.error.isError, isTrue); - expect(MessageStatus.sent.isError, isFalse); - - expect(MessageStatus.sent.isDelivered, isTrue); - expect(MessageStatus.delivered.isDelivered, isTrue); - expect(MessageStatus.sending.isDelivered, isFalse); - expect(MessageStatus.error.isDelivered, isFalse); - }); - }); - - group('ChatParticipant', () { - test('creates user participant correctly', () { - final participant = ChatParticipant.user( - id: 'user_123', - name: 'John Doe', - avatarUrl: 'https://example.com/avatar.jpg', - ); - - expect(participant, isA()); - final user = participant as ChatParticipantUser; - expect(user.id, 'user_123'); - expect(user.name, 'John Doe'); - expect(user.avatarUrl, 'https://example.com/avatar.jpg'); - }); - - test('creates assistant participant correctly', () { - final participant = ChatParticipant.assistant( - id: 'assistant_123', - name: 'AI Assistant', - avatarUrl: 'https://example.com/bot.jpg', - model: 'gpt-4', - ); - - expect(participant, isA()); - final assistant = participant as ChatParticipantAssistant; - expect(assistant.id, 'assistant_123'); - expect(assistant.name, 'AI Assistant'); - expect(assistant.avatarUrl, 'https://example.com/bot.jpg'); - expect(assistant.model, 'gpt-4'); - }); - - test('user participant supports optional avatar', () { - final participant = ChatParticipant.user( - id: 'user_123', - name: 'John Doe', - ); - - final user = participant as ChatParticipantUser; - expect(user.avatarUrl, isNull); - }); - - test('assistant participant supports optional model', () { - final participant = ChatParticipant.assistant( - id: 'assistant_123', - name: 'AI Assistant', - ); - - final assistant = participant as ChatParticipantAssistant; - expect(assistant.model, isNull); - }); - }); - - group('TextMessage', () { - test('creates text message correctly', () { - final timestamp = DateTime.now(); - final sender = ChatParticipant.user(id: 'user_123', name: 'User'); - - final message = TextMessage( - id: 'msg_123', - sessionId: 'session_123', - content: 'Hello, world!', - sender: sender, - timestamp: timestamp, - status: MessageStatus.sent, - metadata: {'source': 'test'}, - ); - - expect(message.id, 'msg_123'); - expect(message.sessionId, 'session_123'); - expect(message.content, 'Hello, world!'); - expect(message.sender, sender); - expect(message.timestamp, timestamp); - expect(message.status, MessageStatus.sent); - expect(message.metadata, {'source': 'test'}); - }); - - test('supports optional metadata', () { - final message = TextMessage( - id: 'msg_123', - sessionId: 'session_123', - content: 'Hello', - sender: ChatParticipant.user(id: 'user_123', name: 'User'), - timestamp: DateTime.now(), - ); - - expect(message.metadata, isNull); - }); - - test('has default status', () { - final message = TextMessage( - id: 'msg_123', - sessionId: 'session_123', - content: 'Hello', - sender: ChatParticipant.user(id: 'user_123', name: 'User'), - timestamp: DateTime.now(), - ); - - expect(message.status, MessageStatus.sent); - }); - }); - - group('AudioMessage', () { - test('creates audio message correctly', () { - final timestamp = DateTime.now(); - final sender = ChatParticipant.user(id: 'user_123', name: 'User'); - const duration = Duration(seconds: 30); - - final message = AudioMessage( - id: 'msg_123', - sessionId: 'session_123', - audioUrl: 'https://example.com/audio.mp3', - sender: sender, - timestamp: timestamp, - duration: duration, - fileSize: 1024000, - status: MessageStatus.sent, - transcription: 'Hello from audio', - metadata: {'format': 'mp3'}, - ); - - expect(message.id, 'msg_123'); - expect(message.sessionId, 'session_123'); - expect(message.audioUrl, 'https://example.com/audio.mp3'); - expect(message.sender, sender); - expect(message.timestamp, timestamp); - expect(message.duration, duration); - expect(message.fileSize, 1024000); - expect(message.status, MessageStatus.sent); - expect(message.transcription, 'Hello from audio'); - expect(message.metadata, {'format': 'mp3'}); - }); - - test('supports optional transcription and metadata', () { - const duration = Duration(seconds: 10); - final message = AudioMessage( - id: 'msg_123', - sessionId: 'session_123', - audioUrl: 'https://example.com/audio.wav', - sender: ChatParticipant.user(id: 'user_123', name: 'User'), - timestamp: DateTime.now(), - duration: duration, - fileSize: 512000, - ); - - expect(message.transcription, isNull); - expect(message.metadata, isNull); - }); - - test('has default status', () { - const duration = Duration(seconds: 5); - final message = AudioMessage( - id: 'msg_123', - sessionId: 'session_123', - audioUrl: 'https://example.com/audio.webm', - sender: ChatParticipant.user(id: 'user_123', name: 'User'), - timestamp: DateTime.now(), - duration: duration, - fileSize: 256000, - ); - - expect(message.status, MessageStatus.sent); - }); - }); - - group('ChatMessage union', () { - test('creates text message variant', () { - final textMessage = TextMessage( - id: 'msg_123', - sessionId: 'session_123', - content: 'Hello', - sender: ChatParticipant.user(id: 'user_123', name: 'User'), - timestamp: DateTime.now(), - ); - - final chatMessage = ChatMessage.text(textMessage); - expect(chatMessage, isA()); - }); - - test('creates audio message variant', () { - const duration = Duration(seconds: 10); - final audioMessage = AudioMessage( - id: 'msg_123', - sessionId: 'session_123', - audioUrl: 'https://example.com/audio.mp3', - sender: ChatParticipant.user(id: 'user_123', name: 'User'), - timestamp: DateTime.now(), - duration: duration, - fileSize: 1024000, - ); - - final chatMessage = ChatMessage.audio(audioMessage); - expect(chatMessage, isA()); - }); - }); - - group('JSON serialization', () { - test('ChatParticipant serializes and deserializes correctly', () { - final user = ChatParticipant.user( - id: 'user_123', - name: 'John Doe', - avatarUrl: 'https://example.com/avatar.jpg', - ); - - final json = user.toJson(); - final deserialized = ChatParticipant.fromJson(json); - - expect(deserialized, equals(user)); - }); - - test('TextMessage serializes and deserializes correctly', () { - final message = TextMessage( - id: 'msg_123', - sessionId: 'session_123', - content: 'Hello, world!', - sender: ChatParticipant.user(id: 'user_123', name: 'User'), - timestamp: DateTime.parse('2024-01-01T12:00:00Z'), - status: MessageStatus.sent, - metadata: {'source': 'test'}, - ); - - final json = message.toJson(); - final deserialized = TextMessage.fromJson(json); - - expect(deserialized, equals(message)); - }); - - test('AudioMessage serializes and deserializes correctly', () { - final message = AudioMessage( - id: 'msg_123', - sessionId: 'session_123', - audioUrl: 'https://example.com/audio.mp3', - sender: ChatParticipant.user(id: 'user_123', name: 'User'), - timestamp: DateTime.parse('2024-01-01T12:00:00Z'), - duration: const Duration(seconds: 30), - fileSize: 1024000, - status: MessageStatus.sent, - transcription: 'Hello from audio', - ); - - final json = message.toJson(); - final deserialized = AudioMessage.fromJson(json); - - expect(deserialized, equals(message)); - }); - }); - - group('Equality and hashCode', () { - test('TextMessage equality works correctly', () { - final message1 = TextMessage( - id: 'msg_123', - sessionId: 'session_123', - content: 'Hello', - sender: ChatParticipant.user(id: 'user_123', name: 'User'), - timestamp: DateTime.parse('2024-01-01T12:00:00Z'), - ); - - final message2 = TextMessage( - id: 'msg_123', - sessionId: 'session_123', - content: 'Hello', - sender: ChatParticipant.user(id: 'user_123', name: 'User'), - timestamp: DateTime.parse('2024-01-01T12:00:00Z'), - ); - - final message3 = TextMessage( - id: 'msg_456', - sessionId: 'session_123', - content: 'Hello', - sender: ChatParticipant.user(id: 'user_123', name: 'User'), - timestamp: DateTime.parse('2024-01-01T12:00:00Z'), - ); - - expect(message1, equals(message2)); - expect(message1, isNot(equals(message3))); - expect(message1.hashCode, equals(message2.hashCode)); - expect(message1.hashCode, isNot(equals(message3.hashCode))); - }); - - test('AudioMessage equality works correctly', () { - const duration = Duration(seconds: 30); - final message1 = AudioMessage( - id: 'msg_123', - sessionId: 'session_123', - audioUrl: 'https://example.com/audio.mp3', - sender: ChatParticipant.user(id: 'user_123', name: 'User'), - timestamp: DateTime.parse('2024-01-01T12:00:00Z'), - duration: duration, - fileSize: 1024000, - ); - - final message2 = AudioMessage( - id: 'msg_123', - sessionId: 'session_123', - audioUrl: 'https://example.com/audio.mp3', - sender: ChatParticipant.user(id: 'user_123', name: 'User'), - timestamp: DateTime.parse('2024-01-01T12:00:00Z'), - duration: duration, - fileSize: 1024000, - ); - - expect(message1, equals(message2)); - expect(message1.hashCode, equals(message2.hashCode)); - }); - - test('ChatParticipant equality works correctly', () { - final user1 = ChatParticipant.user( - id: 'user_123', - name: 'John', - avatarUrl: 'https://example.com/avatar.jpg', - ); - - final user2 = ChatParticipant.user( - id: 'user_123', - name: 'John', - avatarUrl: 'https://example.com/avatar.jpg', - ); - - final user3 = ChatParticipant.user( - id: 'user_456', - name: 'John', - avatarUrl: 'https://example.com/avatar.jpg', - ); - - expect(user1, equals(user2)); - expect(user1, isNot(equals(user3))); - expect(user1.hashCode, equals(user2.hashCode)); - }); - }); - }); -} diff --git a/chat/mnemo_cards_chat/test/domain/models/chat_session_test.dart b/chat/mnemo_cards_chat/test/domain/models/chat_session_test.dart deleted file mode 100644 index ae60405..0000000 --- a/chat/mnemo_cards_chat/test/domain/models/chat_session_test.dart +++ /dev/null @@ -1,255 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:mnemo_cards_chat/domain/models/chat_session.dart'; - -void main() { - group('ChatSession', () { - group('ChatSessionStatus', () { - test('has correct display names', () { - expect(ChatSessionStatus.active.displayName, 'Активный'); - expect(ChatSessionStatus.archived.displayName, 'Архивирован'); - expect(ChatSessionStatus.deleted.displayName, 'Удален'); - }); - - test('has correct status checks', () { - expect(ChatSessionStatus.active.isActive, isTrue); - expect(ChatSessionStatus.archived.isActive, isFalse); - expect(ChatSessionStatus.deleted.isActive, isFalse); - - expect(ChatSessionStatus.archived.isArchived, isTrue); - expect(ChatSessionStatus.active.isArchived, isFalse); - expect(ChatSessionStatus.deleted.isArchived, isFalse); - - expect(ChatSessionStatus.deleted.isDeleted, isTrue); - expect(ChatSessionStatus.active.isDeleted, isFalse); - expect(ChatSessionStatus.archived.isDeleted, isFalse); - }); - }); - - group('ChatSession', () { - test('creates chat session correctly', () { - final createdAt = DateTime.parse('2024-01-01T10:00:00Z'); - final updatedAt = DateTime.parse('2024-01-01T11:00:00Z'); - - final session = ChatSession( - id: 'session_123', - userId: 'user_456', - title: 'Test Chat Session', - createdAt: createdAt, - updatedAt: updatedAt, - status: ChatSessionStatus.active, - description: 'A test chat session', - tags: ['test', 'ai'], - messageCount: 42, - lastMessagePreview: 'Hello, how are you?', - lastMessageAt: DateTime.parse('2024-01-01T11:30:00Z'), - ); - - expect(session.id, 'session_123'); - expect(session.userId, 'user_456'); - expect(session.title, 'Test Chat Session'); - expect(session.createdAt, createdAt); - expect(session.updatedAt, updatedAt); - expect(session.status, ChatSessionStatus.active); - expect(session.description, 'A test chat session'); - expect(session.tags, ['test', 'ai']); - expect(session.messageCount, 42); - expect(session.lastMessagePreview, 'Hello, how are you?'); - expect(session.lastMessageAt, DateTime.parse('2024-01-01T11:30:00Z')); - }); - - test('supports optional fields', () { - final session = ChatSession( - id: 'session_123', - userId: 'user_456', - title: 'Minimal Session', - createdAt: DateTime.now(), - updatedAt: DateTime.now(), - ); - - expect(session.description, isNull); - expect(session.tags, isEmpty); - expect(session.messageCount, 0); - expect(session.lastMessagePreview, isNull); - expect(session.lastMessageAt, isNull); - expect(session.status, ChatSessionStatus.active); - }); - - test('has default values', () { - final session = ChatSession( - id: 'session_123', - userId: 'user_456', - title: 'Test', - createdAt: DateTime.now(), - updatedAt: DateTime.now(), - ); - - expect(session.status, ChatSessionStatus.active); - expect(session.tags, isEmpty); - expect(session.messageCount, 0); - }); - }); - - group('CreateChatSessionRequest', () { - test('creates request correctly', () { - final request = CreateChatSessionRequest( - title: 'New Chat Session', - description: 'A new chat', - tags: ['ai', 'assistant'], - ); - - expect(request.title, 'New Chat Session'); - expect(request.description, 'A new chat'); - expect(request.tags, ['ai', 'assistant']); - }); - - test('supports optional fields', () { - final request = CreateChatSessionRequest(title: 'Simple Chat'); - - expect(request.title, 'Simple Chat'); - expect(request.description, isNull); - expect(request.tags, isEmpty); - }); - }); - - group('UpdateChatSessionRequest', () { - test('creates update request correctly', () { - final request = UpdateChatSessionRequest( - title: 'Updated Title', - description: 'Updated description', - tags: ['updated'], - status: ChatSessionStatus.archived, - ); - - expect(request.title, 'Updated Title'); - expect(request.description, 'Updated description'); - expect(request.tags, ['updated']); - expect(request.status, ChatSessionStatus.archived); - }); - - test('supports partial updates', () { - final request = UpdateChatSessionRequest( - title: 'New Title', - // Other fields null for partial update - ); - - expect(request.title, 'New Title'); - expect(request.description, isNull); - expect(request.tags, isNull); - expect(request.status, isNull); - }); - }); - - group('JSON serialization', () { - test('ChatSession serializes and deserializes correctly', () { - final session = ChatSession( - id: 'session_123', - userId: 'user_456', - title: 'Test Session', - createdAt: DateTime.parse('2024-01-01T10:00:00Z'), - updatedAt: DateTime.parse('2024-01-01T11:00:00Z'), - status: ChatSessionStatus.active, - description: 'Test description', - tags: ['test'], - messageCount: 5, - lastMessagePreview: 'Hello', - lastMessageAt: DateTime.parse('2024-01-01T11:30:00Z'), - ); - - final json = session.toJson(); - final deserialized = ChatSession.fromJson(json); - - expect(deserialized, equals(session)); - }); - - test('CreateChatSessionRequest serializes and deserializes correctly', () { - final request = CreateChatSessionRequest( - title: 'New Session', - description: 'Description', - tags: ['tag1', 'tag2'], - ); - - final json = request.toJson(); - final deserialized = CreateChatSessionRequest.fromJson(json); - - expect(deserialized, equals(request)); - }); - - test('UpdateChatSessionRequest serializes and deserializes correctly', () { - final request = UpdateChatSessionRequest( - title: 'Updated', - status: ChatSessionStatus.archived, - ); - - final json = request.toJson(); - final deserialized = UpdateChatSessionRequest.fromJson(json); - - expect(deserialized, equals(request)); - }); - }); - - group('Equality and hashCode', () { - test('ChatSession equality works correctly', () { - final session1 = ChatSession( - id: 'session_123', - userId: 'user_456', - title: 'Test', - createdAt: DateTime.parse('2024-01-01T10:00:00Z'), - updatedAt: DateTime.parse('2024-01-01T11:00:00Z'), - status: ChatSessionStatus.active, - description: 'Desc', - tags: ['tag'], - messageCount: 1, - ); - - final session2 = ChatSession( - id: 'session_123', - userId: 'user_456', - title: 'Test', - createdAt: DateTime.parse('2024-01-01T10:00:00Z'), - updatedAt: DateTime.parse('2024-01-01T11:00:00Z'), - status: ChatSessionStatus.active, - description: 'Desc', - tags: ['tag'], - messageCount: 1, - ); - - final session3 = ChatSession( - id: 'session_456', - userId: 'user_456', - title: 'Test', - createdAt: DateTime.parse('2024-01-01T10:00:00Z'), - updatedAt: DateTime.parse('2024-01-01T11:00:00Z'), - ); - - expect(session1, equals(session2)); - expect(session1, isNot(equals(session3))); - expect(session1.hashCode, equals(session2.hashCode)); - expect(session1.hashCode, isNot(equals(session3.hashCode))); - }); - - test('CreateChatSessionRequest equality works correctly', () { - final request1 = CreateChatSessionRequest( - title: 'Test', - description: 'Desc', - tags: ['tag'], - ); - - final request2 = CreateChatSessionRequest( - title: 'Test', - description: 'Desc', - tags: ['tag'], - ); - - final request3 = CreateChatSessionRequest( - title: 'Different', - description: 'Desc', - tags: ['tag'], - ); - - expect(request1, equals(request2)); - expect(request1, isNot(equals(request3))); - expect(request1.hashCode, equals(request2.hashCode)); - }); - }); - }); -} diff --git a/chat/mnemo_cards_chat/test/domain/services/chat_service_test.dart b/chat/mnemo_cards_chat/test/domain/services/chat_service_test.dart deleted file mode 100644 index 998eb6f..0000000 --- a/chat/mnemo_cards_chat/test/domain/services/chat_service_test.dart +++ /dev/null @@ -1,345 +0,0 @@ -import 'dart:typed_data'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:mnemo_cards_chat/mnemo_cards_chat.dart'; - -// Mock classes -class MockChatRepository extends Mock implements ChatRepository {} - -// Fallback values for mocktail -class CreateChatSessionRequestFake extends Fake implements CreateChatSessionRequest {} -class SendTextMessageRequestFake extends Fake implements SendTextMessageRequest {} - -void main() { - setUpAll(() { - registerFallbackValue(CreateChatSessionRequestFake()); - registerFallbackValue(SendTextMessageRequestFake()); - }); - - group('ChatService', () { - late ChatService chatService; - late MockChatRepository mockChatRepository; - - setUp(() { - mockChatRepository = MockChatRepository(); - chatService = ChatService(chatRepository: mockChatRepository); - }); - - test('can be instantiated', () { - expect(chatService, isNotNull); - expect(chatService, isA()); - }); - - test('has proper constructor signature', () { - expect( - () => ChatService(httpRepository: mockChatRepository), - returnsNormally, - ); - }); - - group('createSession', () { - test('creates session successfully', () async { - const title = 'Test Chat'; - const sessionId = 'session_123'; - final expectedSession = ChatBasicSession( - id: sessionId, - userId: 'user_123', - title: title, - createdAt: DateTime.now(), - updatedAt: DateTime.now(), - ); - - when(() => mockChatRepository.createChatSession(any())) - .thenAnswer((_) async => expectedSession); - - final result = await chatService.createSession(title: title); - - expect(result, equals(expectedSession)); - verify(() => mockChatRepository.createChatSession( - any(that: isA() - .having((r) => r.title, 'title', title)) - )).called(1); - }); - - test('creates session with description', () async { - const title = 'Test Chat'; - const description = 'Test Description'; - - when(() => mockChatRepository.createChatSession(any())) - .thenAnswer((_) async => ChatBasicSession( - id: 'session_123', - userId: 'user_123', - title: title, - createdAt: DateTime.now(), - updatedAt: DateTime.now(), - )); - - await chatService.createSession(title: title, description: description); - - verify(() => mockChatRepository.createChatSession( - any(that: isA() - .having((r) => r.title, 'title', title)) - )).called(1); - }); - }); - - group('getSession', () { - test('gets session successfully', () async { - const sessionId = 'session_123'; - final expectedSession = ChatBasicSession( - id: sessionId, - userId: 'user_123', - title: 'Test Chat', - createdAt: DateTime.now(), - updatedAt: DateTime.now(), - ); - - when(() => mockChatRepository.getChatSession(sessionId)) - .thenAnswer((_) async => expectedSession); - - final result = await chatService.getSession(sessionId); - - expect(result, equals(expectedSession)); - verify(() => mockChatRepository.getChatSession(sessionId)).called(1); - }); - }); - - group('getSessions', () { - test('gets sessions with default parameters', () async { - final sessions = [ - ChatBasicSession( - id: 'session_1', - userId: 'user_123', - title: 'Chat 1', - createdAt: DateTime.now(), - updatedAt: DateTime.now(), - ), - ]; - - when(() => mockChatRepository.getChatSessions(limit: 20, afterSessionId: null)) - .thenAnswer((_) async => sessions); - - final result = await chatService.getSessions(); - - expect(result, equals(sessions)); - verify(() => mockChatRepository.getChatSessions(limit: 20, afterSessionId: null)).called(1); - }); - - test('gets sessions with custom parameters', () async { - const limit = 10; - const afterSessionId = 'session_123'; - final sessions = []; - - when(() => mockChatRepository.getChatSessions(limit: limit, afterSessionId: afterSessionId)) - .thenAnswer((_) async => sessions); - - final result = await chatService.getSessions( - limit: limit, - afterSessionId: afterSessionId, - ); - - expect(result, equals(sessions)); - verify(() => mockChatRepository.getChatSessions( - limit: limit, - afterSessionId: afterSessionId, - )).called(1); - }); - }); - - group('sendTextMessage', () { - test('sends text message successfully', () async { - const sessionId = 'session_123'; - const content = 'Hello, world!'; - final expectedResponse = ChatMessageResponse( - messageId: 'msg_123', - sessionId: sessionId, - content: 'Assistant response', - senderId: 'assistant', - timestamp: DateTime.now(), - ); - - when(() => mockChatRepository.sendTextMessage(any())) - .thenAnswer((_) async => expectedResponse); - - final result = await chatService.sendTextMessage(sessionId, content); - - expect(result, equals(expectedResponse)); - verify(() => mockChatRepository.sendTextMessage( - any(that: isA() - .having((r) => r.sessionId, 'sessionId', sessionId) - .having((r) => r.content, 'content', content)) - )).called(1); - }); - }); - - group('sendAudioMessage', () { - test('sends audio message successfully', () async { - const sessionId = 'session_123'; - final audioData = Uint8List(1024); - const duration = Duration(seconds: 5); - final expectedResponse = ChatMessageResponse( - messageId: 'msg_123', - sessionId: sessionId, - content: 'Audio processed', - senderId: 'assistant', - timestamp: DateTime.now(), - ); - - when(() => mockChatRepository.sendAudioMessage( - sessionId, - audioData, - duration, - fileName: null, - mimeType: null, - )).thenAnswer((_) async => expectedResponse); - - final result = await chatService.sendAudioMessage( - sessionId, - audioData, - duration, - ); - - expect(result, equals(expectedResponse)); - verify(() => mockChatRepository.sendAudioMessage( - sessionId, - audioData, - duration, - fileName: null, - mimeType: null, - )).called(1); - }); - - test('sends audio message with metadata', () async { - const sessionId = 'session_123'; - final audioData = Uint8List(2048); - const duration = Duration(seconds: 10); - const transcription = 'Hello world'; - const fileName = 'recording.webm'; - const mimeType = 'audio/webm'; - - when(() => mockChatRepository.sendAudioMessage( - sessionId, - audioData, - duration, - fileName: fileName, - mimeType: mimeType, - )).thenAnswer((_) async => ChatMessageResponse( - messageId: 'msg_123', - sessionId: sessionId, - content: 'Response', - senderId: 'assistant', - timestamp: DateTime.now(), - )); - - await chatService.sendAudioMessage( - sessionId, - audioData, - duration, - transcription: transcription, - fileName: fileName, - mimeType: mimeType, - ); - - verify(() => mockChatRepository.sendAudioMessage( - sessionId, - audioData, - duration, - fileName: fileName, - mimeType: mimeType, - )).called(1); - }); - }); - - group('getMessages', () { - test('gets messages with default parameters', () async { - const sessionId = 'session_123'; - final messages = [ - ChatMessageResponse( - messageId: 'msg_1', - sessionId: sessionId, - content: 'Hello', - senderId: 'user', - timestamp: DateTime.now(), - ), - ]; - - when(() => mockChatRepository.getChatMessages( - sessionId, - limit: 50, - beforeMessageId: null, - )).thenAnswer((_) async => messages); - - final result = await chatService.getMessages(sessionId); - - expect(result, equals(messages)); - verify(() => mockChatRepository.getChatMessages( - sessionId, - limit: 50, - beforeMessageId: null, - )).called(1); - }); - - test('gets messages with custom parameters', () async { - const sessionId = 'session_123'; - const limit = 20; - const beforeMessageId = 'msg_123'; - final messages = []; - - when(() => mockChatRepository.getChatMessages( - sessionId, - limit: limit, - beforeMessageId: beforeMessageId, - )).thenAnswer((_) async => messages); - - final result = await chatService.getMessages( - sessionId, - limit: limit, - beforeMessageId: beforeMessageId, - ); - - expect(result, equals(messages)); - verify(() => mockChatRepository.getChatMessages( - sessionId, - limit: limit, - beforeMessageId: beforeMessageId, - )).called(1); - }); - }); - - group('updateSession', () { - test('updates session successfully', () async { - const sessionId = 'session_123'; - final updates = {'title': 'New Title'}; - final expectedSession = ChatBasicSession( - id: sessionId, - userId: 'user_123', - title: 'New Title', - createdAt: DateTime.now(), - updatedAt: DateTime.now(), - ); - - when(() => mockChatRepository.updateChatSession(sessionId, updates)) - .thenAnswer((_) async => expectedSession); - - final result = await chatService.updateSession(sessionId, updates); - - expect(result, equals(expectedSession)); - verify(() => mockChatRepository.updateChatSession(sessionId, updates)).called(1); - }); - }); - - group('deleteSession', () { - test('deletes session successfully', () async { - const sessionId = 'session_123'; - - when(() => mockChatRepository.deleteChatSession(sessionId)) - .thenAnswer((_) async => {}); - - await chatService.deleteSession(sessionId); - - verify(() => mockChatRepository.deleteChatSession(sessionId)).called(1); - }); - }); - }); -} diff --git a/chat/mnemo_cards_chat/test/domain/state/chat_state_manager_test.dart b/chat/mnemo_cards_chat/test/domain/state/chat_state_manager_test.dart deleted file mode 100644 index 3eeabc9..0000000 --- a/chat/mnemo_cards_chat/test/domain/state/chat_state_manager_test.dart +++ /dev/null @@ -1,449 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:mnemo_cards_chat/domain/models/chat_api_simple.dart'; -import 'package:mnemo_cards_chat/domain/models/chat_basic.dart'; -import 'package:mnemo_cards_chat/domain/models/chat_message.dart'; -import 'package:mnemo_cards_chat/domain/services/chat_service.dart'; -import 'package:mnemo_cards_chat/domain/state/chat_state_manager.dart'; - -// Mock classes -class MockChatService extends Mock implements ChatService {} - -void main() { - group('ChatStateManager', () { - late ChatStateManager stateManager; - late MockChatService mockChatService; - - setUp(() { - mockChatService = MockChatService(); - stateManager = ChatStateManager(chatService: mockChatService); - }); - - test('should initialize with loading state', () { - expect(stateManager.state, equals(const ChatState.loading())); - }); - - test('can be instantiated with proper dependencies', () { - expect(stateManager, isNotNull); - expect(stateManager, isA()); - }); - - test('has proper constructor signature', () { - expect( - () => ChatStateManager(chatService: mockChatService), - returnsNormally, - ); - }); - - test('state is accessible', () { - expect(stateManager.state, isA()); - }); - - test('has initializeChat method', () { - expect(stateManager.initializeChat, isA()); - }); - - test('has sendTextMessage method', () { - expect(stateManager.sendTextMessage, isA()); - }); - - test('has sendAudioMessage method', () { - expect(stateManager.sendAudioMessage, isA()); - }); - - test('has loadMoreMessages method', () { - expect(stateManager.loadMoreMessages, isA()); - }); - - test('has clearError method', () { - expect(stateManager.clearError, isA()); - }); - - test('has currentSession getter', () { - expect(stateManager.currentSession, isNull); - }); - - test('has messagesCount getter', () { - expect(stateManager.messagesCount, equals(0)); - }); - - group('initializeChat', () { - test('initializes chat successfully', () async { - const sessionId = 'session_123'; - final session = ChatBasicSession( - id: sessionId, - userId: 'user_123', - title: 'Test Chat', - createdAt: DateTime.now(), - updatedAt: DateTime.now(), - ); - final messages = [ - ChatMessageResponse( - messageId: 'msg_1', - sessionId: sessionId, - content: 'Hello', - senderId: 'user', - timestamp: DateTime.now(), - ), - ]; - - when(() => mockChatService.getSession(sessionId)) - .thenAnswer((_) async => session); - when(() => mockChatService.getMessages(sessionId, limit: 50)) - .thenAnswer((_) async => messages); - - await stateManager.initializeChat(sessionId); - - expect(stateManager.state, isA()); - final loadedState = stateManager.state as ChatStateLoaded; - expect(loadedState.session, equals(session)); - expect(loadedState.messages.length, equals(1)); - expect(loadedState.isSendingMessage, isFalse); - expect(loadedState.hasMoreMessages, isFalse); - expect(stateManager.currentSession, equals(session)); - expect(stateManager.messagesCount, equals(1)); - - verify(() => mockChatService.getSession(sessionId)).called(1); - verify(() => mockChatService.getMessages(sessionId, limit: 50)).called(1); - }); - - test('handles initialization error', () async { - const sessionId = 'session_123'; - final error = Exception('Network error'); - - when(() => mockChatService.getSession(sessionId)) - .thenThrow(error); - - await stateManager.initializeChat(sessionId); - - expect(stateManager.state, isA()); - final errorState = stateManager.state as ChatStateError; - expect(errorState.message, contains('Failed to load chat')); - expect(stateManager.currentSession, isNull); - }); - }); - - group('sendTextMessage', () { - late ChatBasicSession session; - - setUp(() { - session = ChatBasicSession( - id: 'session_123', - userId: 'user_123', - title: 'Test Chat', - createdAt: DateTime.now(), - updatedAt: DateTime.now(), - ); - }); - - test('sends text message successfully when chat is initialized', () async { - // First initialize chat - when(() => mockChatService.getSession(any())) - .thenAnswer((_) async => session); - when(() => mockChatService.getMessages(any(), limit: any(named: 'limit'))) - .thenAnswer((_) async => []); - await stateManager.initializeChat(session.id); - - // Now send message - const content = 'Hello, world!'; - final response = ChatMessageResponse( - messageId: 'msg_123', - sessionId: session.id, - content: 'Assistant response', - senderId: 'assistant', - timestamp: DateTime.now(), - ); - - when(() => mockChatService.sendTextMessage(session.id, content)) - .thenAnswer((_) async => response); - - await stateManager.sendTextMessage(content); - - expect(stateManager.state, isA()); - final loadedState = stateManager.state as ChatStateLoaded; - expect(loadedState.messages.length, equals(2)); // User + Assistant messages - expect(loadedState.isSendingMessage, isFalse); - - // Check user message - final userMessage = loadedState.messages[0] as TextMessage; - expect(userMessage.sender.id, equals('current_user')); - expect(userMessage.content, equals(content)); - expect(userMessage.status, equals(MessageStatus.sent)); - - // Check assistant message - final assistantMessage = loadedState.messages[1] as TextMessage; - expect(assistantMessage.sender.id, equals('assistant')); - expect(assistantMessage.content, equals('Assistant response')); - - verify(() => mockChatService.sendTextMessage(session.id, content)).called(1); - }); - - test('handles send text message error', () async { - // Initialize chat first - when(() => mockChatService.getSession(any())) - .thenAnswer((_) async => session); - when(() => mockChatService.getMessages(any(), limit: any(named: 'limit'))) - .thenAnswer((_) async => []); - await stateManager.initializeChat(session.id); - - // Send message with error - const content = 'Hello'; - final error = Exception('Network error'); - - when(() => mockChatService.sendTextMessage(session.id, content)) - .thenThrow(error); - - await stateManager.sendTextMessage(content); - - expect(stateManager.state, isA()); - final loadedState = stateManager.state as ChatStateLoaded; - expect(loadedState.isSendingMessage, isFalse); - expect(loadedState.errorMessage, contains('Failed to send message')); - expect(loadedState.messages.length, equals(1)); // Only user message - - // Check that user message has error status - final userMessage = loadedState.messages[0] as AudioMessage; - expect(userMessage.status, equals(MessageStatus.error)); - }); - - test('does nothing if chat not initialized', () async { - await stateManager.sendTextMessage('Hello'); - - verifyNever(() => mockChatService.sendTextMessage(any(), any())); - }); - }); - - group('sendAudioMessage', () { - late ChatBasicSession session; - - setUp(() { - session = ChatBasicSession( - id: 'session_123', - userId: 'user_123', - title: 'Test Chat', - createdAt: DateTime.now(), - updatedAt: DateTime.now(), - ); - }); - - test('sends audio message successfully', () async { - // Initialize chat - when(() => mockChatService.getSession(any())) - .thenAnswer((_) async => session); - when(() => mockChatService.getMessages(any(), limit: any(named: 'limit'))) - .thenAnswer((_) async => []); - await stateManager.initializeChat(session.id); - - // Send audio message - final audioData = Uint8List(1024); - const duration = Duration(seconds: 5); - final response = ChatMessageResponse( - messageId: 'msg_123', - sessionId: session.id, - content: 'Audio processed', - senderId: 'assistant', - timestamp: DateTime.now(), - audioUrl: 'https://example.com/audio.mp3', - durationMs: 5000, - fileSize: 1024, - ); - - when(() => mockChatService.sendAudioMessage( - session.id, - audioData, - duration, - transcription: null, - )).thenAnswer((_) async => response); - - await stateManager.sendAudioMessage(audioData, duration); - - expect(stateManager.state, isA()); - final loadedState = stateManager.state as ChatStateLoaded; - expect(loadedState.messages.length, equals(2)); // User + Assistant messages - - // Check user audio message - final userMessage = loadedState.messages[0] as AudioMessage; - expect(userMessage.sender.id, equals('current_user')); - expect(userMessage.duration, equals(duration)); - expect(userMessage.status, equals(MessageStatus.sent)); - - // Check assistant message - final assistantMessage = loadedState.messages[1] as TextMessage; - expect(assistantMessage.sender.id, equals('assistant')); - expect(assistantMessage.content, equals('Audio processed')); - - verify(() => mockChatService.sendAudioMessage( - session.id, - audioData, - duration, - transcription: null, - )).called(1); - }); - - test('sends audio message with transcription', () async { - // Initialize chat - when(() => mockChatService.getSession(any())) - .thenAnswer((_) async => session); - when(() => mockChatService.getMessages(any(), limit: any(named: 'limit'))) - .thenAnswer((_) async => []); - await stateManager.initializeChat(session.id); - - // Send audio with transcription - final audioData = Uint8List(2048); - const duration = Duration(seconds: 3); - const transcription = 'Hello from audio'; - - when(() => mockChatService.sendAudioMessage( - session.id, - audioData, - duration, - transcription: transcription, - )).thenAnswer((_) async => ChatMessageResponse( - messageId: 'msg_123', - sessionId: session.id, - content: 'Response', - senderId: 'assistant', - timestamp: DateTime.now(), - )); - - await stateManager.sendAudioMessage(audioData, duration, transcription: transcription); - - verify(() => mockChatService.sendAudioMessage( - session.id, - audioData, - duration, - transcription: transcription, - )).called(1); - }); - - test('does nothing if chat not initialized', () async { - final audioData = Uint8List(1024); - await stateManager.sendAudioMessage(audioData, const Duration(seconds: 1)); - - verifyNever(() => mockChatService.sendAudioMessage(any(), any(), any())); - }); - }); - - group('loadMoreMessages', () { - late ChatBasicSession session; - - setUp(() { - session = ChatBasicSession( - id: 'session_123', - userId: 'user_123', - title: 'Test Chat', - createdAt: DateTime.now(), - updatedAt: DateTime.now(), - ); - }); - - test('loads more messages successfully', () async { - // Initialize with one message - final initialMessages = [ - ChatMessageResponse( - messageId: 'msg_1', - sessionId: session.id, - content: 'Initial message', - senderId: 'user', - timestamp: DateTime.now(), - ), - ]; - - when(() => mockChatService.getSession(any())) - .thenAnswer((_) async => session); - when(() => mockChatService.getMessages(any(), limit: 50)) - .thenAnswer((_) async => initialMessages); - await stateManager.initializeChat(session.id); - - // Load more messages - final moreMessages = [ - ChatMessageResponse( - messageId: 'msg_0', - sessionId: session.id, - content: 'Older message', - senderId: 'assistant', - timestamp: DateTime.now().subtract(const Duration(hours: 1)), - ), - ]; - - when(() => mockChatService.getMessages( - session.id, - limit: 50, - beforeMessageId: 'msg_1', - )).thenAnswer((_) async => moreMessages); - - await stateManager.loadMoreMessages(); - - expect(stateManager.state, isA()); - final loadedState = stateManager.state as ChatStateLoaded; - expect(loadedState.messages.length, equals(2)); - expect(loadedState.isLoadingMore, isFalse); - expect(loadedState.hasMoreMessages, isFalse); // Only 1 more message loaded - - // Messages should be in chronological order (oldest first) - expect(loadedState.messages[0].maybeMap( - text: (msg) => msg.content, - audio: (msg) => msg.transcription, - ), equals('Older message')); - }); - - test('does nothing if already loading more', () async { - // Initialize chat - when(() => mockChatService.getSession(any())) - .thenAnswer((_) async => session); - when(() => mockChatService.getMessages(any(), limit: any(named: 'limit'))) - .thenAnswer((_) async => []); - await stateManager.initializeChat(session.id); - - // Start loading more - stateManager.state = (stateManager.state as ChatStateLoaded).copyWith(isLoadingMore: true); - - await stateManager.loadMoreMessages(); - - verifyNever(() => mockChatService.getMessages(any(), limit: any(named: 'limit'), beforeMessageId: any(named: 'beforeMessageId'))); - }); - - test('does nothing if no more messages', () async { - // Initialize chat - when(() => mockChatService.getSession(any())) - .thenAnswer((_) async => session); - when(() => mockChatService.getMessages(any(), limit: any(named: 'limit'))) - .thenAnswer((_) async => []); - await stateManager.initializeChat(session.id); - - // Set hasMoreMessages to false - stateManager.state = (stateManager.state as ChatStateLoaded).copyWith(hasMoreMessages: false); - - await stateManager.loadMoreMessages(); - - verifyNever(() => mockChatService.getMessages(any(), limit: any(named: 'limit'), beforeMessageId: any(named: 'beforeMessageId'))); - }); - }); - - group('clearError', () { - test('clears error message from loaded state', () { - stateManager.state = ChatState.loaded( - session: ChatBasicSession( - id: 'session_123', - userId: 'user_123', - title: 'Test', - createdAt: DateTime.now(), - updatedAt: DateTime.now(), - ), - messages: [], - errorMessage: 'Some error', - ); - - stateManager.clearError(); - - final loadedState = stateManager.state as ChatStateLoaded; - expect(loadedState.errorMessage, isNull); - }); - - test('does nothing for non-loaded states', () { - stateManager.clearError(); - expect(stateManager.state, isA()); // Should not crash - }); - }); - }); -} diff --git a/mnemo_cards_backend/lib/api/di/injector.config.dart b/mnemo_cards_backend/lib/api/di/injector.config.dart index 05ff00d..3b18908 100644 --- a/mnemo_cards_backend/lib/api/di/injector.config.dart +++ b/mnemo_cards_backend/lib/api/di/injector.config.dart @@ -19,6 +19,7 @@ import '../../discounts/discounts_manager.dart' as _i891; import '../../packs/free_packs_distributor.dart' as _i1062; import '../../packs/pack_dto_converter.dart' as _i433; import '../../packs/pack_manager.dart' as _i833; +import '../../packs/pack_repository.dart' as _i258; import '../../packs/products_price_resolver.dart' as _i908; import '../../promo_codes/promo_codes_manager.dart' as _i151; import '../../statistics/achievement_manager.dart' as _i802; @@ -31,6 +32,7 @@ import '../../tasks/task_manager.dart' as _i586; import '../../tests/test_manager.dart' as _i259; import '../../user/user_manager.dart' as _i280; import '../../user/user_manager_drift.dart' as _i560; +import '../../user/user_repository.dart' as _i950; import '../ads/ads_manager.dart' as _i846; import '../mnemo_shelf.dart' as _i561; import '../purchase/payment_manager.dart' as _i1009; @@ -105,14 +107,11 @@ extension GetItInjectableX on _i174.GetIt { gh.lazySingleton<_i108.JwtService>( () => _i108.JwtService(gh<_i1072.AppDatabase>()), ); - gh.lazySingleton<_i296.TelegramBotApiV2>( - () => _i296.TelegramBotApiV2(gh<_i1072.AppDatabase>()), - ); gh.lazySingleton<_i891.DiscountsManager>( () => _i891.DiscountsManager(gh<_i1072.AppDatabase>()), ); - gh.lazySingleton<_i1062.FreePacksDistributor>( - () => _i1062.FreePacksDistributor(gh<_i1072.AppDatabase>()), + gh.lazySingleton<_i258.PackRepository>( + () => _i258.PackRepository(gh<_i1072.AppDatabase>()), ); gh.lazySingleton<_i802.AchievementManager>( () => _i802.AchievementManager(gh<_i1072.AppDatabase>()), @@ -129,46 +128,42 @@ extension GetItInjectableX on _i174.GetIt { gh.lazySingleton<_i586.TaskManager>( () => _i586.TaskManager(gh<_i1072.AppDatabase>()), ); + gh.lazySingleton<_i950.UserRepository>( + () => _i950.UserRepository(gh<_i1072.AppDatabase>()), + ); gh.factory<_i1015.AdminPacksApiV2>( () => _i1015.AdminPacksApiV2(gh<_i1072.AppDatabase>()), ); - gh.factory<_i895.AdminUsersApiV2>( - () => _i895.AdminUsersApiV2(gh<_i1072.AppDatabase>()), - ); gh.lazySingleton<_i964.SubscriptionsApiV2>( () => _i964.SubscriptionsApiV2(gh<_i377.SubscriptionManager>()), ); gh.lazySingleton<_i858.DiscountsApiV2>( () => _i858.DiscountsApiV2(gh<_i891.DiscountsManager>()), ); - gh.lazySingleton<_i1009.PaymentManager>( - () => _i1009.PaymentManager( + gh.lazySingleton<_i296.TelegramBotApiV2>( + () => _i296.TelegramBotApiV2( gh<_i1072.AppDatabase>(), - gh<_i988.YooMoneyHandler>(), - gh<_i222.RustorePurchaseHandler>(), + gh<_i950.UserRepository>(), + ), + ); + gh.factory<_i895.AdminUsersApiV2>( + () => _i895.AdminUsersApiV2( + gh<_i1072.AppDatabase>(), + gh<_i950.UserRepository>(), ), ); gh.lazySingleton<_i365.MediaApiV2>( () => _i365.MediaApiV2(gh<_i747.MinioService>()), ); - gh.lazySingleton<_i280.UserManager>( - () => _i280.UserManager( - gh<_i1072.AppDatabase>(), - gh<_i1062.FreePacksDistributor>(), - gh<_i71.SessionTracker>(), - gh<_i1029.StatisticsCalculator>(), - gh<_i802.AchievementManager>(), - gh<_i909.WordStatisticsManager>(), - ), - ); gh.lazySingleton<_i985.TasksApiV2>( () => _i985.TasksApiV2(gh<_i586.TaskManager>()), ); - gh.lazySingleton<_i483.AdminAuthApiV2>( - () => _i483.AdminAuthApiV2( - gh<_i240.TelegramAuthCodeService>(), - gh<_i280.UserManager>(), - gh<_i108.JwtService>(), + gh.lazySingleton<_i1009.PaymentManager>( + () => _i1009.PaymentManager( + gh<_i1072.AppDatabase>(), + gh<_i950.UserRepository>(), + gh<_i988.YooMoneyHandler>(), + gh<_i222.RustorePurchaseHandler>(), ), ); gh.lazySingleton<_i151.PromoCodesManager>( @@ -177,6 +172,12 @@ extension GetItInjectableX on _i174.GetIt { gh<_i1009.PaymentManager>(), ), ); + gh.lazySingleton<_i1062.FreePacksDistributor>( + () => _i1062.FreePacksDistributor( + gh<_i1072.AppDatabase>(), + gh<_i258.PackRepository>(), + ), + ); gh.lazySingleton<_i560.UserManager>( () => _i560.UserManager( gh<_i1072.AppDatabase>(), @@ -186,17 +187,6 @@ extension GetItInjectableX on _i174.GetIt { gh<_i802.AchievementManager>(), ), ); - gh.lazySingleton<_i561.MnemoShelf>( - () => _i561.MnemoShelf(gh<_i280.UserManager>()), - ); - gh.lazySingleton<_i247.UsersApiV2>( - () => _i247.UsersApiV2( - gh<_i280.UserManager>(), - gh<_i1009.PaymentManager>(), - gh<_i1029.StatisticsCalculator>(), - gh<_i1072.AppDatabase>(), - ), - ); gh.lazySingleton<_i273.PromocodesApiV2>( () => _i273.PromocodesApiV2(gh<_i151.PromoCodesManager>()), ); @@ -212,6 +202,55 @@ extension GetItInjectableX on _i174.GetIt { gh<_i1072.AppDatabase>(), ), ); + gh.lazySingleton<_i280.UserManager>( + () => _i280.UserManager( + gh<_i1072.AppDatabase>(), + gh<_i950.UserRepository>(), + gh<_i1062.FreePacksDistributor>(), + gh<_i71.SessionTracker>(), + gh<_i1029.StatisticsCalculator>(), + gh<_i802.AchievementManager>(), + gh<_i909.WordStatisticsManager>(), + ), + ); + gh.lazySingleton<_i483.AdminAuthApiV2>( + () => _i483.AdminAuthApiV2( + gh<_i240.TelegramAuthCodeService>(), + gh<_i280.UserManager>(), + gh<_i108.JwtService>(), + ), + ); + gh.lazySingleton<_i433.PackDtoConverter>( + () => _i433.PackDtoConverter( + gh<_i908.ProductsPriceResolver>(), + gh<_i846.AdsManager>(), + ), + ); + gh.lazySingleton<_i833.PackManager>( + () => _i833.PackManager( + gh<_i1072.AppDatabase>(), + gh<_i258.PackRepository>(), + gh<_i433.PackDtoConverter>(), + ), + ); + gh.lazySingleton<_i69.PurchasesApiV2>( + () => _i69.PurchasesApiV2( + gh<_i1009.PaymentManager>(), + gh<_i833.PackManager>(), + gh<_i1072.AppDatabase>(), + ), + ); + gh.lazySingleton<_i561.MnemoShelf>( + () => _i561.MnemoShelf(gh<_i280.UserManager>()), + ); + gh.lazySingleton<_i247.UsersApiV2>( + () => _i247.UsersApiV2( + gh<_i280.UserManager>(), + gh<_i1009.PaymentManager>(), + gh<_i1029.StatisticsCalculator>(), + gh<_i1072.AppDatabase>(), + ), + ); gh.lazySingleton<_i170.TestsApiV2>( () => _i170.TestsApiV2( gh<_i259.TestManager>(), @@ -228,25 +267,6 @@ extension GetItInjectableX on _i174.GetIt { gh<_i240.TelegramAuthCodeService>(), ), ); - gh.lazySingleton<_i433.PackDtoConverter>( - () => _i433.PackDtoConverter( - gh<_i908.ProductsPriceResolver>(), - gh<_i846.AdsManager>(), - ), - ); - gh.lazySingleton<_i833.PackManager>( - () => _i833.PackManager( - gh<_i1072.AppDatabase>(), - gh<_i433.PackDtoConverter>(), - ), - ); - gh.lazySingleton<_i69.PurchasesApiV2>( - () => _i69.PurchasesApiV2( - gh<_i1009.PaymentManager>(), - gh<_i833.PackManager>(), - gh<_i1072.AppDatabase>(), - ), - ); gh.lazySingleton<_i800.PacksApiV2>( () => _i800.PacksApiV2( gh<_i833.PackManager>(), diff --git a/mnemo_cards_backend/lib/api/purchase/payment_manager.dart b/mnemo_cards_backend/lib/api/purchase/payment_manager.dart index 00eb3f8..aa65a09 100644 --- a/mnemo_cards_backend/lib/api/purchase/payment_manager.dart +++ b/mnemo_cards_backend/lib/api/purchase/payment_manager.dart @@ -10,7 +10,7 @@ import 'package:drift/drift.dart' as drift; import 'payment_drift_extension.dart'; import 'rustore/rustore_purchase_handler.dart'; import 'yoo_money.dart'; -import '../../user/user_drift_extension.dart'; +import '../../user/user_repository.dart'; /// Result of creating YooKassa payment URL class YookassaPaymentResult { @@ -26,11 +26,17 @@ class YookassaPaymentResult { @lazySingleton class PaymentManager { final AppDatabase _db; + final UserRepository _userRepository; late final GooglePlayPurchaseHandler googlePurchaseHandler; final YooMoneyHandler _yooMoneyHandler; final RustorePurchaseHandler _rustorePurchaseHandler; - PaymentManager(this._db, this._yooMoneyHandler, this._rustorePurchaseHandler); + PaymentManager( + this._db, + this._userRepository, + this._yooMoneyHandler, + this._rustorePurchaseHandler, + ); /// Создать платеж в базе данных Future createPayment(PaymentDto paymentDto, String userId) async { @@ -74,7 +80,7 @@ class PaymentManager { await _db.transaction(() async { if (product.type == MnemoCardsProductType.pack && product.id != null) { final packId = product.id!; - await _db.userDao.grantPackAccess( + await _userRepository.grantPackAccess( userId: userId, packId: packId, grantType: 'promo_code', @@ -114,8 +120,8 @@ class PaymentManager { } // Получить пользователя - final user = await _db.userDao.getUserWithDataById(payment.userId); - if (user == null) { + final userModel = await _userRepository.getUserWithDataById(payment.userId); + if (userModel == null) { log('User not found: ${payment.userId}'); return; } @@ -154,7 +160,7 @@ class PaymentManager { await _db.transaction(() async { // Дать доступ к пакетам for (final packId in packIds) { - await _db.userDao.grantPackAccess( + await _userRepository.grantPackAccess( userId: payment.userId, packId: packId, grantType: 'purchase', @@ -422,11 +428,11 @@ class PaymentManager { final firstProduct = payment.products.first; final productId = firstProduct['id']?.toString(); if (productId != null && payment.externalToken != null) { - final user = await _db.userDao.getUserById(payment.userId); + final user = await _userRepository.getUserById(payment.userId); await checkRustorePayment( productId: productId, subscriptionToken: payment.externalToken!, - user: user != null ? await user.toUserModel() : null, + user: user, ); } } diff --git a/mnemo_cards_backend/lib/api/v2/admin_analytics_api_v2.dart b/mnemo_cards_backend/lib/api/v2/admin_analytics_api_v2.dart index 68908d4..9a3b221 100644 --- a/mnemo_cards_backend/lib/api/v2/admin_analytics_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/admin_analytics_api_v2.dart @@ -4,8 +4,8 @@ import 'package:injectable/injectable.dart'; import 'package:mnemo_cards_backend/api/authorize/acl_types.dart'; import 'package:mnemo_cards_backend/api/authorize/access_service.dart'; import 'package:mnemo_cards_backend/api/authorize/helpers.dart'; -import 'package:mnemo_cards_backend/main.dart' as backend_main; -import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart'; +import 'package:mnemo_cards_backend/database/database.dart'; +import 'package:mnemo_cards_backend/user/user_repository.dart'; import 'package:shelf/shelf.dart'; import 'package:shelf_router/shelf_router.dart'; @@ -14,7 +14,10 @@ part 'admin_analytics_api_v2.g.dart'; /// Admin endpoints for analytics and statistics in API v2. @lazySingleton class AdminAnalyticsApiV2 { - AdminAnalyticsApiV2(); + final UserRepository _userRepository; + final AppDatabase _db; + + AdminAnalyticsApiV2(this._userRepository, this._db); Response _json( Object? data, { @@ -51,23 +54,22 @@ class AdminAnalyticsApiV2 { } // Get basic statistics - final userCount = await backend_main.database.userDao.countUsers(); + final userCount = await _userRepository.countUsers(); - final cardCount = await backend_main.database.packDao.countCards(); + final cardCount = await _db.packDao.countCards(); - final packCount = await backend_main.database.packDao.countPacks(); + final packCount = await _db.packDao.countPacks(); // Get all packs and filter in memory - final enabledPacks = await backend_main.database.packDao.getAllPacks( + final enabledPacks = await _db.packDao.getAllPacks( enabledOnly: true, ); final enabledPackCount = enabledPacks.length; - final paymentCount = await backend_main.database.paymentDao - .countAllPayments(); + final paymentCount = await _db.paymentDao.countAllPayments(); // Get recent users (last 10) - final allUsers = await backend_main.database.userDao.getAllUsers( + final allUsers = await _userRepository.getAllUsers( limit: 10, ); final recentUsers = allUsers @@ -76,17 +78,16 @@ class AdminAnalyticsApiV2 { 'id': u.id, 'name': u.name, 'email': u.email, - 'createdAt': u.createdAt.dateTime.toIso8601String(), }, ) .toList(); // Get top packs by user count - final allPacks = await backend_main.database.packDao.getAllPacks(); + final allPacks = await _db.packDao.getAllPacks(); final topPacksList = allPacks.take(5); final topPacks = await Future.wait( topPacksList.map((p) async { - final cards = await backend_main.database.packDao.getPackCards(p.id); + final cards = await _db.packDao.getPackCards(p.id); return { 'id': p.id, 'title': p.title, diff --git a/mnemo_cards_backend/lib/api/v2/admin_packs_api_v2.dart b/mnemo_cards_backend/lib/api/v2/admin_packs_api_v2.dart index fa8a9c6..96a9f61 100644 --- a/mnemo_cards_backend/lib/api/v2/admin_packs_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/admin_packs_api_v2.dart @@ -9,7 +9,9 @@ import 'package:mnemo_cards_backend/api/authorize/access_service.dart'; import 'package:mnemo_cards_backend/api/authorize/helpers.dart'; import 'package:mnemo_cards_backend/packs/card_pack_drift_extension.dart'; import 'package:mnemo_cards_backend/packs/card_image_storage.dart'; +import 'package:mnemo_cards_backend/packs/pack_repository.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart'; +import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart'; import 'package:shelf/shelf.dart'; import 'package:shelf_router/shelf_router.dart'; @@ -19,8 +21,9 @@ part 'admin_packs_api_v2.g.dart'; @injectable class AdminPacksApiV2 { final AppDatabase _db; + final PackRepository _packRepository; - AdminPacksApiV2(this._db); + AdminPacksApiV2(this._db, this._packRepository); static final _uuidRegex = RegExp( r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', @@ -47,9 +50,9 @@ class AdminPacksApiV2 { required String cardId, }) async { try { - final card = await _db.packDao.getCardById(cardId); + final card = await _packRepository.getCardById(cardId); if (card == null) return; - await _db.packDao.addCardToPack(packId: packId, cardId: cardId); + await _packRepository.addCardToPack(packId: packId, cardId: cardId); } catch (_) { // Best-effort only. } @@ -67,6 +70,7 @@ class AdminPacksApiV2 { mnemo: const drift.Value('test_image'), ); + // Note: createCard is not in PackRepository, keeping direct DAO call for now final cardId = await _db.packDao.createCard(companion); await _tryLinkCardToPack(packId: packId, cardId: cardId); @@ -78,11 +82,16 @@ class AdminPacksApiV2 { ); if (stored == null) return null; - final created = await _db.packDao.getCardById(cardId); + final created = await _packRepository.getCardById(cardId); if (created == null) return null; + // Note: updateCard is not in PackRepository, keeping direct DAO call for now + // We need to get the Drift GameCard to update it + final driftCard = await _db.packDao.getCardById(cardId); + if (driftCard == null) return null; + await _db.packDao.updateCard( - created.copyWith( + driftCard.copyWith( image: stored.fileName, updatedAt: PgDateTime(DateTime.now()), ), @@ -278,25 +287,41 @@ class AdminPacksApiV2 { final validatedLimit = limit < 1 ? 20 : (limit > 100 ? 100 : limit); // Get all packs - final allPacks = await _db.packDao.getAllPacks( + final allPacks = await _packRepository.getAllPacks( enabledOnly: !showDisabled, orderByField: 'order', ); // Apply search filter if provided - List filteredPacks = allPacks; + List filteredPacks = allPacks; if (search.isNotEmpty) { final searchTerm = search.toLowerCase(); filteredPacks = allPacks.where((pack) { return pack.title.toLowerCase().contains(searchTerm) || pack.subtitle.toLowerCase().contains(searchTerm) || - pack.id.toLowerCase().contains(searchTerm); + (pack.id?.toLowerCase().contains(searchTerm) ?? false); }).toList(); } // Convert to preview DTOs + // Note: We need to convert CardPackModel back to CardPack for toPreviewDto + // This is a limitation - we should add a method to PackRepository for preview DTOs final previewDtos = await Future.wait( - filteredPacks.map((pack) async { + filteredPacks.where((p) => p.id != null).map((packModel) async { + // Get the Drift CardPack for toPreviewDto extension + final pack = await _db.packDao.getPackById(packModel.id!); + if (pack == null) { + return { + 'id': packModel.id, + 'title': packModel.title, + 'subtitle': packModel.subtitle, + 'color': packModel.color, + 'cover': null, + 'cards': packModel.size, + 'enabled': packModel.enabled, + 'order': packModel.order, + }; + } final dto = await pack.toPreviewDto(null); return { 'id': dto.id, @@ -357,7 +382,7 @@ class AdminPacksApiV2 { }, statusCode: 400); } - final pack = await _db.packDao.getPackById(packId); + final pack = await _packRepository.getPackById(packId); if (pack == null) { return _json({ 'error': 'Pack not found', @@ -367,13 +392,13 @@ class AdminPacksApiV2 { }, statusCode: 404); } - final cards = await _db.packDao.getPackCards(packId); + final cards = await _packRepository.getPackCards(packId); // Get cards order - cards are already sorted by order from getPackCards final cardsOrder = cards.map((c) => c.id).toList(); // Get preview cards - final previewCards = await _db.packDao.getPreviewCards(packId); + final previewCards = await _packRepository.getPreviewCards(packId); final previewCardIds = previewCards.map((c) => c.id).toList(); // Get tests for this pack @@ -447,7 +472,7 @@ class AdminPacksApiV2 { if (editDto.id != null && editDto.id!.isNotEmpty) { // Update existing pack packId = editDto.id!; - final existing = await _db.packDao.getPackById(packId); + final existing = await _packRepository.getPackById(packId); if (existing == null) { return _json({ 'error': 'Pack not found', @@ -457,7 +482,7 @@ class AdminPacksApiV2 { }, statusCode: 404); } - await _db.packDao.updatePackPartial( + await _packRepository.updatePackPartial( CardPacksCompanion( id: drift.Value(packId), title: editDto.title != null @@ -504,39 +529,26 @@ class AdminPacksApiV2 { ); } else { // Create new pack - packId = await _db.packDao.createPack( - CardPacksCompanion.insert( - title: editDto.title ?? '', - subtitle: editDto.subtitle ?? '', - color: editDto.color != null - ? drift.Value(editDto.color) - : const drift.Value.absent(), - cover: editDto.cover != null - ? drift.Value(editDto.cover) - : const drift.Value.absent(), - size: editDto.size ?? 0, - googlePlayId: editDto.googlePlayId != null - ? drift.Value(editDto.googlePlayId) - : const drift.Value.absent(), - rustoreId: editDto.rustoreId != null - ? drift.Value(editDto.rustoreId) - : const drift.Value.absent(), - appStoreId: editDto.appStoreId != null - ? drift.Value(editDto.appStoreId) - : const drift.Value.absent(), - price: editDto.price != null - ? drift.Value(editDto.price) - : const drift.Value.absent(), - description: editDto.description != null - ? drift.Value(editDto.description) - : const drift.Value.absent(), - enabled: drift.Value(editDto.enabled ?? true), - version: editDto.version != null - ? drift.Value(editDto.version) - : const drift.Value.absent(), - order: drift.Value(editDto.order ?? 0), - ), + // Note: We need to create a CardPackModel first + final packModel = CardPackModel( + id: null, + title: editDto.title ?? '', + subtitle: editDto.subtitle ?? '', + size: editDto.size ?? 0, + color: editDto.color, + version: editDto.version, + cover: editDto.cover, + description: editDto.description, + googlePlayId: editDto.googlePlayId, + rustoreId: editDto.rustoreId, + appStoreId: editDto.appStoreId, + price: editDto.price, + currency: null, + enabled: editDto.enabled ?? true, + order: editDto.order ?? 0, + cardsOrder: [], ); + packId = await _packRepository.createPack(packModel); } // Handle card associations if provided @@ -544,7 +556,7 @@ class AdminPacksApiV2 { try { for (final cardId in editDto.addCardIds!) { // Verify card exists before adding - final card = await _db.packDao.getCardById(cardId); + final card = await _packRepository.getCardById(cardId); if (card == null) { return _json({ 'error': 'Card not found', @@ -555,7 +567,7 @@ class AdminPacksApiV2 { 'Card with ID "$cardId" was not found. Please verify all card IDs before adding them to the pack.', }, statusCode: 404); } - await _db.packDao.addCardToPack(packId: packId, cardId: cardId); + await _packRepository.addCardToPack(packId: packId, cardId: cardId); } } catch (e) { // Handle duplicate or constraint errors @@ -575,7 +587,7 @@ class AdminPacksApiV2 { if (editDto.removeCardIds != null && editDto.removeCardIds!.isNotEmpty) { for (final cardId in editDto.removeCardIds!) { - await _db.packDao.removeCardFromPack(packId, cardId); + await _packRepository.removeCardFromPack(packId, cardId); } } @@ -625,15 +637,15 @@ class AdminPacksApiV2 { // Handle cards order if provided if (editDto.cardsOrder != null && editDto.cardsOrder!.isNotEmpty) { - await _db.packDao.updatePackCardsOrder(packId, editDto.cardsOrder!); + await _packRepository.updatePackCardsOrder(packId, editDto.cardsOrder!); } // Handle preview cards if (editDto.previewCards != null) { - await _db.packDao.setPreviewCards(packId, editDto.previewCards!); + await _packRepository.setPreviewCards(packId, editDto.previewCards!); } - final updatedPack = await _db.packDao.getPackById(packId); + final updatedPack = await _packRepository.getPackById(packId); if (updatedPack == null) { return _json({ 'error': 'Database error', @@ -643,9 +655,9 @@ class AdminPacksApiV2 { }, statusCode: 500); } - final updatedCards = await _db.packDao.getPackCards(packId); + final updatedCards = await _packRepository.getPackCards(packId); final cardsOrder = updatedCards.map((c) => c.id).toList(); - final previewCards = await _db.packDao.getPreviewCards(packId); + final previewCards = await _packRepository.getPreviewCards(packId); final previewCardIds = previewCards.map((c) => c.id).toList(); // Get updated tests for this pack @@ -726,7 +738,7 @@ class AdminPacksApiV2 { }, statusCode: 400); } - final pack = await _db.packDao.getPackById(packId); + final pack = await _packRepository.getPackById(packId); if (pack == null) { return _json({ 'error': 'Pack not found', @@ -736,7 +748,7 @@ class AdminPacksApiV2 { }, statusCode: 404); } - await _db.packDao.softDeletePack(packId); + await _packRepository.softDeletePack(packId); return _json({'success': true, 'message': 'Pack deleted successfully'}); } catch (e, s) { diff --git a/mnemo_cards_backend/lib/api/v2/admin_users_api_v2.dart b/mnemo_cards_backend/lib/api/v2/admin_users_api_v2.dart index 34957b9..1b53283 100644 --- a/mnemo_cards_backend/lib/api/v2/admin_users_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/admin_users_api_v2.dart @@ -7,10 +7,11 @@ import 'package:mnemo_cards_backend/api/authorize/acl_types.dart'; import 'package:mnemo_cards_backend/api/authorize/access_service.dart'; import 'package:mnemo_cards_backend/api/authorize/helpers.dart'; import 'package:mnemo_cards_backend/database/database.dart'; -import 'package:mnemo_cards_backend/user/user_drift_extension.dart'; import 'package:mnemo_cards_backend/user/user_model.dart' show UserModelExtension; +import 'package:mnemo_cards_backend/user/user_repository.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart'; +import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart'; import 'package:shelf/shelf.dart'; import 'package:shelf_router/shelf_router.dart'; @@ -20,8 +21,9 @@ part 'admin_users_api_v2.g.dart'; @injectable class AdminUsersApiV2 { final AppDatabase _db; + final UserRepository _userRepository; - AdminUsersApiV2(this._db); + AdminUsersApiV2(this._db, this._userRepository); Response _json( Object? data, { @@ -78,22 +80,22 @@ class AdminUsersApiV2 { final validatedLimit = limit < 1 ? 20 : (limit > 100 ? 100 : limit); // Get total count - final total = await _db.userDao.countUsers(includeDeleted: false); + final total = await _userRepository.countUsers(includeDeleted: false); // Calculate offset final offset = (page - 1) * validatedLimit; // Get users with pagination - final users = await _db.userDao.getAllUsers( + final userModels = await _userRepository.getAllUsers( limit: validatedLimit, offset: offset, includeDeleted: false, + includePacks: true, ); // Convert to DTOs final userDtos = >[]; - for (final user in users) { - final userModel = await user.toUserModel(); + for (final userModel in userModels) { final dto = await userModel.toDto(); userDtos.add(dto.toJson()); } @@ -158,8 +160,8 @@ class AdminUsersApiV2 { }, statusCode: 400); } - final user = await _db.userDao.getUserById(userId); - if (user == null) { + final userModel = await _userRepository.getUserById(userId, includePacks: true); + if (userModel == null) { return _json({ 'error': 'User not found', 'message': 'The requested user does not exist or has been deleted', @@ -168,7 +170,6 @@ class AdminUsersApiV2 { }, statusCode: 404); } - final userModel = await user.toUserModel(); final dto = await userModel.toDto(); return _json(dto.toJson()); @@ -218,8 +219,8 @@ class AdminUsersApiV2 { if (isUpdate) { // Update existing user - final existingUser = await _db.userDao.getUserById(userDto.id!); - if (existingUser == null) { + final existingUserModel = await _userRepository.getUserById(userDto.id!); + if (existingUserModel == null) { return _json({ 'error': 'User not found', 'message': 'The user you are trying to update does not exist', @@ -250,11 +251,11 @@ class AdminUsersApiV2 { updatedAt: drift.Value(PgDateTime(DateTime.now())), ); - await _db.userDao.updateUserPartial(updates); + await _userRepository.updateUserPartial(updates); // Get updated user - final updatedUser = await _db.userDao.getUserById(userDto.id!); - if (updatedUser == null) { + final updatedUserModel = await _userRepository.getUserById(userDto.id!, includePacks: true); + if (updatedUserModel == null) { return _json({ 'error': 'Database error', 'message': 'Failed to retrieve updated user', @@ -263,8 +264,7 @@ class AdminUsersApiV2 { }, statusCode: 500); } - final userModel = await updatedUser.toUserModel(); - final dto = await userModel.toDto(); + final dto = await updatedUserModel.toDto(); return _json({'result': true, 'user': dto.toJson()}); } else { @@ -275,10 +275,8 @@ class AdminUsersApiV2 { : 'admin_created_${DateTime.now().millisecondsSinceEpoch}'; // Check if user with this externalUserId already exists - final existingUser = await _db.userDao.getUserByExternalId( - externalUserId, - ); - if (existingUser != null) { + final existingUserModel = await _userRepository.getUserByExternalId(externalUserId); + if (existingUserModel != null) { return _json({ 'error': 'User already exists', 'message': 'A user with this email already exists', @@ -288,30 +286,29 @@ class AdminUsersApiV2 { } final now = DateTime.now(); - final userCompanion = UsersCompanion.insert( - externalUserId: externalUserId, - name: drift.Value(userDto.name), - email: drift.Value(userDto.email), - telegram: drift.Value(userDto.telegram), - admin: drift.Value(userDto.admin), - purchases: drift.Value(userDto.purchases), - createdAt: drift.Value(PgDateTime(now)), - updatedAt: drift.Value(PgDateTime(now)), - isDeleted: drift.Value(false), - ); final userDataCompanion = UserDatasCompanion.insert( userId: '', // Will be set by createUserWithData registrationDate: drift.Value(PgDateTime(now)), ); - final userId = await _db.userDao.createUserWithData( - user: userCompanion, + final userModel = UserModel( + id: null, + name: userDto.name, + email: userDto.email, + telegram: userDto.telegram, + admin: userDto.admin, + purchases: userDto.purchases, + userSettings: null, + packs: [], + ); + final userId = await _userRepository.createUserWithData( + userModel: userModel, userData: userDataCompanion, ); - final createdUser = await _db.userDao.getUserById(userId); - if (createdUser == null) { + final createdUserModel = await _userRepository.getUserById(userId, includePacks: true); + if (createdUserModel == null) { return _json({ 'error': 'Database error', 'message': 'Failed to retrieve created user', @@ -320,8 +317,7 @@ class AdminUsersApiV2 { }, statusCode: 500); } - final userModel = await createdUser.toUserModel(); - final dto = await userModel.toDto(); + final dto = await createdUserModel.toDto(); return _json({'result': true, 'user': dto.toJson()}); } @@ -355,8 +351,8 @@ class AdminUsersApiV2 { }, statusCode: 400); } - final user = await _db.userDao.getUserById(userId); - if (user == null) { + final userModel = await _userRepository.getUserById(userId); + if (userModel == null) { return _json({ 'error': 'User not found', 'message': 'The user you are trying to delete does not exist', @@ -365,7 +361,9 @@ class AdminUsersApiV2 { }, statusCode: 404); } - if (user.isDeleted) { + // Check if already deleted via DAO (isDeleted is not in UserModel) + final user = await _db.userDao.getUserById(userId); + if (user != null && user.isDeleted) { return _json({ 'error': 'User already deleted', 'message': 'The user has already been deleted', @@ -373,7 +371,7 @@ class AdminUsersApiV2 { }, statusCode: 409); } - await _db.userDao.softDeleteUser(userId); + await _userRepository.softDeleteUser(userId); return _json({'result': true}); } catch (e, s) { diff --git a/mnemo_cards_backend/lib/api/v2/packs_api_v2.dart b/mnemo_cards_backend/lib/api/v2/packs_api_v2.dart index 5cdc93c..800ca2d 100644 --- a/mnemo_cards_backend/lib/api/v2/packs_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/packs_api_v2.dart @@ -8,13 +8,15 @@ import 'package:mnemo_cards_backend/database/database.dart' hide VoiceModel; import 'package:mnemo_cards_backend/database/database.dart' as drift show VoiceModel; -import 'package:mnemo_cards_backend/packs/card_pack_drift_extension.dart'; import 'package:mnemo_cards_backend/packs/card_image_storage.dart'; +import 'package:mnemo_cards_backend/packs/card_model_extension.dart'; import 'package:mnemo_cards_backend/packs/pack_manager.dart' show PackManager; +import 'package:mnemo_cards_backend/packs/pack_repository.dart'; import 'package:mnemo_cards_backend/packs/voice_storage.dart'; import 'package:mnemo_cards_backend/storage/minio_config.dart'; import 'package:mnemo_cards_backend/storage/minio_service.dart'; import 'package:mnemo_cards_backend/tests/test_manager.dart'; +import 'package:mnemo_cards_backend/user/user_repository.dart'; import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart'; import 'package:shelf/shelf.dart'; @@ -30,12 +32,16 @@ part 'packs_api_v2.g.dart'; class PacksApiV2 { final PackManager _packManager; final TestManager _testManager; + final PackRepository _packRepository; + final UserRepository _userRepository; final AppDatabase _db; final MinioService _minioService; PacksApiV2( this._packManager, this._testManager, + this._packRepository, + this._userRepository, this._db, this._minioService, ); @@ -366,13 +372,13 @@ class PacksApiV2 { user: request.user, ); - final pack = await _db.packDao.getPackById(packId); + final pack = await _packRepository.getPackById(packId); if (pack == null) { return _notFound('Pack not found'); } // Get cards for pack - final cards = await _db.packDao.getPackCards(packId); + final cards = await _packRepository.getPackCards(packId); // Parse pagination final queryParams = request.requestedUri.queryParameters; @@ -396,7 +402,7 @@ class PacksApiV2 { final cardIds = paginatedCards.map((c) => c.id).toList(); final allVoices = []; for (final cardId in cardIds) { - final voices = await _db.packDao.getCardVoices(cardId); + final voices = await _packRepository.getCardVoices(cardId); allVoices.addAll(voices); } @@ -404,9 +410,7 @@ class PacksApiV2 { // Presigned URLs now have 7 days expiration to avoid 403 errors on cached data final cardDtos = await Future.wait( paginatedCards.map((card) async { - final dto = await card.toDto( - allVoices.where((v) => v.cardId == card.id).toList(), - ); + final dto = card.toDto(); // Generate presigned URLs for images if they are object IDs (UUIDs) final imageUrl = await _getPresignedUrlIfUuid( @@ -468,7 +472,7 @@ class PacksApiV2 { // Check if pack exists and is enabled // We allow access to images for enabled packs even without auth // to support image previews in public listings - final pack = await _db.packDao.getPackById(packId); + final pack = await _packRepository.getPackById(packId); print('Pack lookup result: pack=$pack, enabled=${pack?.enabled}'); if (pack == null || !pack.enabled) { print('Pack not found or not enabled'); @@ -476,7 +480,7 @@ class PacksApiV2 { } // Get card and verify it belongs to the pack - final card = await _db.packDao.getCardById(cardId); + final card = await _packRepository.getCardById(cardId); print('Card lookup result: card=$card, image=${card?.image}'); if (card == null) { print('Card not found'); @@ -484,7 +488,7 @@ class PacksApiV2 { } // Verify card belongs to this pack - final packCards = await _db.packDao.getPackCards(packId); + final packCards = await _packRepository.getPackCards(packId); print('Pack cards count: ${packCards.length}'); final belongsToPack = packCards.any((c) => c.id == cardId); print('Card belongs to pack: $belongsToPack'); @@ -546,19 +550,19 @@ class PacksApiV2 { // Check if pack exists and is enabled // We allow access to images for enabled packs even without auth // to support image previews in public listings - final pack = await _db.packDao.getPackById(packId); + final pack = await _packRepository.getPackById(packId); if (pack == null || !pack.enabled) { return _notFound('Pack not found or not enabled'); } // Get card and verify it belongs to the pack - final card = await _db.packDao.getCardById(cardId); + final card = await _packRepository.getCardById(cardId); if (card == null || card.imageBack == null || card.imageBack!.isEmpty) { return _notFound('Card or back image not found'); } // Verify card belongs to this pack - final packCards = await _db.packDao.getPackCards(packId); + final packCards = await _packRepository.getPackCards(packId); final belongsToPack = packCards.any((c) => c.id == cardId); if (!belongsToPack) { return _notFound('Card does not belong to this pack'); @@ -611,7 +615,7 @@ class PacksApiV2 { // Check if pack exists and is enabled // We allow access to covers for enabled packs even without auth // to support image previews in public listings - final pack = await _db.packDao.getPackById(packId); + final pack = await _packRepository.getPackById(packId); if (pack == null || !pack.enabled) { return _notFound('Pack not found or not enabled'); } @@ -665,25 +669,25 @@ class PacksApiV2 { return _badRequest('Invalid pack ID'); } - final pack = await _db.packDao.getPackById(packId); + final pack = await _packRepository.getPackById(packId); if (pack == null || !pack.enabled) { return _notFound('Pack not found or not enabled'); } - final card = await _db.packDao.getCardById(cardId); + final card = await _packRepository.getCardById(cardId); if (card == null) { return _notFound('Card not found'); } // Verify card belongs to this pack - final packCards = await _db.packDao.getPackCards(packId); + final packCards = await _packRepository.getPackCards(packId); final belongsToPack = packCards.any((c) => c.id == cardId); if (!belongsToPack) { return _notFound('Card does not belong to this pack'); } // Get voices for card - final voices = await _db.packDao.getCardVoices(cardId); + final voices = await _packRepository.getCardVoices(cardId); print('🔍 getCardVoices: Found ${voices.length} voices for card $cardId'); if (voices.isEmpty) { print('⚠️ getCardVoices: No voices found for card $cardId'); @@ -742,7 +746,7 @@ class PacksApiV2 { ); if (stored != null) { voicePath = stored.fileName; - await _db.packDao.updateVoiceUrl(voice.id, stored.fileName); + await _packRepository.updateVoiceUrl(voice.id, stored.fileName); } else { voicePath = ''; } @@ -776,20 +780,21 @@ class PacksApiV2 { return _badRequest('Invalid voice ID'); } - final voice = await _db.packDao.getVoiceById(voiceId); + final voice = await _packRepository.getVoiceById(voiceId); if (voice == null || voice.voiceUrl.isEmpty) { return _notFound('Voice not found'); } // Get cards that use this voice by checking CardVoices junction table // We need to find cards that have this voice and belong to enabled packs - final allPacks = await _db.packDao.getAllPacks(enabledOnly: true); + final allPacks = await _packRepository.getAllPacks(enabledOnly: true); bool hasEnabledPack = false; for (final pack in allPacks) { - final packCards = await _db.packDao.getPackCards(pack.id); + if (pack.id == null) continue; + final packCards = await _packRepository.getPackCards(pack.id!); for (final card in packCards) { - final cardVoices = await _db.packDao.getCardVoices(card.id); + final cardVoices = await _packRepository.getCardVoices(card.id); if (cardVoices.any((v) => v.id == voiceId)) { hasEnabledPack = true; break; @@ -831,7 +836,7 @@ class PacksApiV2 { /// Check if user purchased a pack Future _isPackPurchased(String userId, String packId) async { - return await _db.userDao.hasPackAccess(userId, packId); + return await _userRepository.hasPackAccess(userId, packId); } /// GET /api/v2/packs/{packId}/tests diff --git a/mnemo_cards_backend/lib/api/v2/purchases_api_v2.dart b/mnemo_cards_backend/lib/api/v2/purchases_api_v2.dart index f8dc02a..a6f9a2c 100644 --- a/mnemo_cards_backend/lib/api/v2/purchases_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/purchases_api_v2.dart @@ -7,6 +7,7 @@ import 'package:mnemo_cards_backend/api/purchase/payment_manager.dart'; import 'package:mnemo_cards_backend/database/database.dart'; import 'package:mnemo_cards_backend/packs/pack_manager.dart'; import 'package:mnemo_cards_backend/packs/products_price_resolver.dart'; +import 'package:mnemo_cards_backend/user/user_repository.dart'; import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart'; import 'package:shelf/shelf.dart'; @@ -21,9 +22,15 @@ part 'purchases_api_v2.g.dart'; class PurchasesApiV2 { final PaymentManager _paymentManager; final PackManager _packManager; + final UserRepository _userRepository; final AppDatabase _db; - PurchasesApiV2(this._paymentManager, this._packManager, this._db); + PurchasesApiV2( + this._paymentManager, + this._packManager, + this._userRepository, + this._db, + ); Response _ok(Object? object, {Map headers = const {}}) => Response.ok( @@ -95,7 +102,7 @@ class PurchasesApiV2 { '🔍 createPackPurchase: Checking pack access for userId=${user.id}, packId=$packId', ); // Check if already purchased - final hasAccess = await _db.userDao.hasPackAccess(user.id!, packId); + final hasAccess = await _userRepository.hasPackAccess(user.id!, packId); if (hasAccess) { print('❌ createPackPurchase: Pack already purchased'); return _badRequest('Pack is already purchased'); @@ -106,7 +113,7 @@ class PurchasesApiV2 { String? price = pack.price; if (price != null && user.id != null) { print('🔍 createPackPurchase: Getting user data for userId=${user.id}'); - final userData = await _db.userDao.getUserData(user.id!); + final userData = await _userRepository.getUserData(user.id!); if (userData != null) { print('✅ createPackPurchase: User data found'); // Apply discounts if any @@ -201,7 +208,7 @@ class PurchasesApiV2 { } // Check if already purchased - final hasAccess = await _db.userDao.hasPackAccess(user.id!, productId); + final hasAccess = await _userRepository.hasPackAccess(user.id!, productId); if (hasAccess) { return _badRequest('Pack is already purchased'); } @@ -209,7 +216,7 @@ class PurchasesApiV2 { // Get price (with discounts if applicable) price = pack.price; if (price != null && user.id != null) { - final userData = await _db.userDao.getUserData(user.id!); + final userData = await _userRepository.getUserData(user.id!); if (userData != null) { // Apply discounts if any // For now, use price as-is. Discounts can be added later if needed. @@ -252,7 +259,7 @@ class PurchasesApiV2 { // Get price (with discounts if applicable) price = plan.price; if (user.id != null) { - final userData = await _db.userDao.getUserData(user.id!); + final userData = await _userRepository.getUserData(user.id!); if (userData != null) { // Apply discounts if any // For now, use price as-is. Discounts can be added later if needed. @@ -364,7 +371,7 @@ class PurchasesApiV2 { } // Check purchase status - final isPurchased = await _db.userDao.hasPackAccess(user.id!, packId); + final isPurchased = await _userRepository.hasPackAccess(user.id!, packId); // Check subscription access final activeSubscription = await _db.subscriptionDao diff --git a/mnemo_cards_backend/lib/api/v2/telegram_bot_api_v2.dart b/mnemo_cards_backend/lib/api/v2/telegram_bot_api_v2.dart index b2fbe79..cf8b379 100644 --- a/mnemo_cards_backend/lib/api/v2/telegram_bot_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/telegram_bot_api_v2.dart @@ -5,8 +5,7 @@ import 'package:drift/drift.dart'; import 'package:drift_postgres/drift_postgres.dart'; import 'package:injectable/injectable.dart'; import 'package:mnemo_cards_backend/database/database.dart'; -import 'package:mnemo_cards_backend/main.dart' as backend_main; -import 'package:mnemo_cards_backend/user/user_drift_extension.dart'; +import 'package:mnemo_cards_backend/user/user_repository.dart'; import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart'; import 'package:shelf/shelf.dart'; import 'package:shelf_open_api/shelf_open_api.dart'; @@ -21,8 +20,9 @@ part 'telegram_bot_api_v2.g.dart'; @lazySingleton class TelegramBotApiV2 { final AppDatabase _db; + final UserRepository _userRepository; - TelegramBotApiV2(this._db); + TelegramBotApiV2(this._db, this._userRepository); Response _ok(Object? object, {Map headers = const {}}) => Response.ok( @@ -193,9 +193,9 @@ class TelegramBotApiV2 { if (userId != null && userId.isNotEmpty) { // Get specific user info final users = []; - final user = await backend_main.database.userDao.getUserById(userId); + final user = await _userRepository.getUserById(userId, includePacks: true); if (user != null) { - users.add(await user.toUserModel()); + users.add(user); } if (users.isEmpty) { @@ -224,10 +224,10 @@ class TelegramBotApiV2 { }); } else { // Get all users summary - final driftUsers = await backend_main.database.userDao.getAllUsers( + final users = await _userRepository.getAllUsers( limit: 100, + includePacks: false, ); - final users = await Future.wait(driftUsers.map((u) => u.toUserModel())); return _ok({ 'total': users.length, diff --git a/mnemo_cards_backend/lib/api/v2/users_api_v2.dart b/mnemo_cards_backend/lib/api/v2/users_api_v2.dart index f2749f8..b9439d0 100644 --- a/mnemo_cards_backend/lib/api/v2/users_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/users_api_v2.dart @@ -10,6 +10,7 @@ import 'package:mnemo_cards_backend/database/database.dart'; import 'package:mnemo_cards_backend/statistics/statistics_calculator.dart'; import 'package:mnemo_cards_backend/user/user_manager.dart'; import 'package:mnemo_cards_backend/user/user_model.dart'; +import 'package:mnemo_cards_backend/user/user_repository.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart'; import 'package:shelf/shelf.dart'; import 'package:shelf_router/shelf_router.dart'; @@ -22,12 +23,14 @@ class UsersApiV2 { final UserManager _userManager; final PaymentManager _paymentManager; final StatisticsCalculator _statisticsCalculator; + final UserRepository _userRepository; final AppDatabase _db; UsersApiV2( this._userManager, this._paymentManager, this._statisticsCalculator, + this._userRepository, this._db, ); @@ -142,7 +145,7 @@ class UsersApiV2 { }, statusCode: 400); } - await _db.userDao.updateUserPartial( + await _userRepository.updateUserPartial( UsersCompanion( id: drift.Value(user.id!), name: name != null ? drift.Value(name) : const drift.Value.absent(), @@ -249,13 +252,13 @@ class UsersApiV2 { return _json({'error': 'user_id_not_found'}, statusCode: 400); } - final userData = await _db.userDao.getUserData(user.id!); + final userData = await _userRepository.getUserData(user.id!); if (userData == null) { return _json({'error': 'user_data_not_found'}, statusCode: 404); } // Convert UserData to UserDataDto - final userDataModel = await _db.userDao.getUserWithDataById(user.id!); + final userDataModel = await _userRepository.getUserWithDataById(user.id!); if (userDataModel?.userData == null) { return _json({'error': 'user_data_not_found'}, statusCode: 404); } @@ -502,7 +505,7 @@ class UsersApiV2 { } await _db.transaction(() async { - final userData = await _db.userDao.getUserData(user.id!); + final userData = await _userRepository.getUserData(user.id!); if (userData == null) { throw StateError('User data not found'); } @@ -520,7 +523,7 @@ class UsersApiV2 { final longestStreak = max(userData.longestStreak, currentStreak); // Update user data (без studyDates - это поле удалено, рассчитывается на лету) - await _db.userDao.updateUserDataPartial( + await _userRepository.updateUserDataPartial( UserDatasCompanion( userId: drift.Value(user.id!), totalStudyTimeMinutes: drift.Value(totalStudyTime), diff --git a/mnemo_cards_backend/lib/cron/add_free_packs.dart b/mnemo_cards_backend/lib/cron/add_free_packs.dart index 79f63a4..00cd281 100644 --- a/mnemo_cards_backend/lib/cron/add_free_packs.dart +++ b/mnemo_cards_backend/lib/cron/add_free_packs.dart @@ -1,14 +1,13 @@ -import 'package:mnemo_cards_backend/cron/task.dart'; -import 'package:mnemo_cards_backend/database/database.dart'; import 'package:mnemo_cards_backend/packs/free_packs_distributor.dart'; +import 'package:mnemo_cards_backend/user/user_repository.dart'; import 'task.dart' as task; class AddFreePacks with task.Task { final FreePacksDistributor _freePacksDistributor; - final AppDatabase _db; + final UserRepository _userRepository; - AddFreePacks(this._freePacksDistributor, this._db); + AddFreePacks(this._freePacksDistributor, this._userRepository); @override String get name => 'add_free_packs'; @@ -26,17 +25,18 @@ class AddFreePacks with task.Task { final freePackIds = freePacks.map((p) => p.id).toSet(); // Получаем всех пользователей - final allUsers = await _db.userDao.getAllUsers(); + final allUsers = await _userRepository.getAllUsers(); // Фильтруем пользователей, у которых нет хотя бы одного из freePacks final usersToUpdate = []; for (final user in allUsers) { - final userPacks = await _db.userDao.getUserPacks(user.id); - final userPackIds = userPacks.map((p) => p.id).toSet(); + if (user.id == null) continue; + final userPacks = await _userRepository.getUserPacks(user.id!); + final userPackIds = userPacks.map((p) => p.id).whereType().toSet(); // Если у пользователя нет хотя бы одного из freePacks if (!freePackIds.every((packId) => userPackIds.contains(packId))) { - usersToUpdate.add(user.id); + usersToUpdate.add(user.id!); } } @@ -48,9 +48,10 @@ class AddFreePacks with task.Task { for (final userId in usersToUpdate) { try { for (final pack in freePacks) { - await _db.userDao.grantPackAccess( + if (pack.id == null) continue; + await _userRepository.grantPackAccess( userId: userId, - packId: pack.id, + packId: pack.id!, grantType: 'free', ); } diff --git a/mnemo_cards_backend/lib/cron/check_admins.dart b/mnemo_cards_backend/lib/cron/check_admins.dart index 02cb9a6..b132e5c 100644 --- a/mnemo_cards_backend/lib/cron/check_admins.dart +++ b/mnemo_cards_backend/lib/cron/check_admins.dart @@ -1,6 +1,7 @@ import 'package:drift/drift.dart'; import 'package:drift_postgres/drift_postgres.dart'; import 'package:mnemo_cards_backend/user/admin_ids_service.dart'; +import 'package:mnemo_cards_backend/user/user_repository.dart'; import '../database/database.dart'; import 'task.dart' as task; @@ -10,8 +11,9 @@ class CheckAdminsTask with task.Task { String get name => 'check_admins'; final AppDatabase _db; + final UserRepository _userRepository; - CheckAdminsTask(this._db); + CheckAdminsTask(this._db, this._userRepository); @override Future task() async { @@ -35,14 +37,14 @@ class CheckAdminsTask with task.Task { } // Получить пользователя по userId из токена - final user = await _db.userDao.getUserById(token.userId); - if (user != null) { - adminIds.add(user.id); + final user = await _userRepository.getUserById(token.userId); + if (user != null && user.id != null) { + adminIds.add(user.id!); if (!user.admin) { // Установить admin = true - await _db.userDao.updateUserPartial( + await _userRepository.updateUserPartial( UsersCompanion( - id: Value(user.id), + id: Value(user.id!), admin: const Value(true), updatedAt: Value(PgDateTime(DateTime.now())), ), diff --git a/mnemo_cards_backend/lib/discounts/discounts_manager.dart b/mnemo_cards_backend/lib/discounts/discounts_manager.dart index cb5ae45..9f5a468 100644 --- a/mnemo_cards_backend/lib/discounts/discounts_manager.dart +++ b/mnemo_cards_backend/lib/discounts/discounts_manager.dart @@ -2,9 +2,9 @@ import 'dart:developer'; import 'package:injectable/injectable.dart'; import 'package:mnemo_cards_backend/database/daos/discount_dao.dart'; -import 'package:mnemo_cards_backend/database/daos/user_dao.dart'; import 'package:mnemo_cards_backend/database/database.dart'; import 'package:mnemo_cards_backend/discounts/discount_drift_extension.dart'; +import 'package:mnemo_cards_backend/user/user_repository.dart'; import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart' hide DateTimeExt; import 'package:mnemo_cards_common/src/utils/utils.dart'; @@ -13,11 +13,10 @@ import 'package:mnemo_cards_common/src/utils/utils.dart'; class DiscountsManager { final AppDatabase _db; final DiscountDao _discountDao; - final UserDao _userDao; + final UserRepository _userRepository; - DiscountsManager(this._db) - : _discountDao = _db.discountDao, - _userDao = _db.userDao; + DiscountsManager(this._db, this._userRepository) + : _discountDao = _db.discountDao; Future applyDiscount({ required Iterable discounts, @@ -103,10 +102,10 @@ class DiscountsManager { } // Получаем активные кампании - final user = await _userDao.getUserById(userData.userId); + final user = await _userRepository.getUserById(userData.userId); if (user == null) return maxDiscount; - final userDataModel = await _userDao.getUserData(userData.userId); + final userDataModel = await _userRepository.getUserData(userData.userId); final userTags = userDataModel?.tags ?? []; final activeCampaigns = await _discountDao.getActiveCampaignsForUser( diff --git a/mnemo_cards_backend/lib/packs/card_pack_drift_extension.dart b/mnemo_cards_backend/lib/packs/card_pack_drift_extension.dart index bf7d129..09dcda5 100644 --- a/mnemo_cards_backend/lib/packs/card_pack_drift_extension.dart +++ b/mnemo_cards_backend/lib/packs/card_pack_drift_extension.dart @@ -9,7 +9,7 @@ import 'package:mnemo_cards_common/mnemo_cards_common.dart'; extension CardPackToDto on CardPack { Future toPreviewDto(UserModel? user) async { final hasAccess = - user != null && (user.purchases.contains(id.toString()) || user.admin); + user != null && (user.packs.contains(id.toString()) || user.admin); // Generate cover image URL String? coverUrl; @@ -64,6 +64,30 @@ extension CardPackToDto on CardPack { } } +extension CardPackToModel on CardPack { + + Future toModel() async { + return CardPackModel( + id: id, + title: title, + subtitle: subtitle, + size: size, + color: color, + version: version, + cover: cover, + description: description, + googlePlayId: googlePlayId, + rustoreId: rustoreId, + appStoreId: appStoreId, + price: price, + currency: currency, + enabled: enabled, + order: order, + cardsOrder: cardsOrder, + ); + } +} + /// Extension для конвертации GameCard (Drift) в DTO extension GameCardToDto on GameCard { Future toDto(List voices) async { diff --git a/mnemo_cards_backend/lib/packs/free_packs_distributor.dart b/mnemo_cards_backend/lib/packs/free_packs_distributor.dart index 8dcd115..467a345 100644 --- a/mnemo_cards_backend/lib/packs/free_packs_distributor.dart +++ b/mnemo_cards_backend/lib/packs/free_packs_distributor.dart @@ -1,18 +1,25 @@ import 'package:injectable/injectable.dart'; -import 'package:mnemo_cards_backend/database/database.dart'; +import 'package:mnemo_cards_backend/user/user_repository.dart'; import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart'; +import 'pack_repository.dart'; @lazySingleton class FreePacksDistributor { - final AppDatabase _db; + final UserRepository _userRepository; + final PackRepository _packRepository; - FreePacksDistributor(this._db); + FreePacksDistributor(this._userRepository, this._packRepository); - Future> getFreePacks() async { - final allPacks = await _db.packDao.getAllPacks(enabledOnly: true); + Future> getFreePacks() async { + final allPacks = await _packRepository.getAllPacks(enabledOnly: true); // Фильтруем паки с нулевой ценой или без цены return allPacks - .where((pack) => pack.price == null || pack.price == 0) + .where((pack) { + final priceStr = pack.price; + if (priceStr == null || priceStr.isEmpty) return true; + final price = double.tryParse(priceStr); + return price == null || price == 0; + }) .toList(); } @@ -22,13 +29,14 @@ class FreePacksDistributor { await givePacksToUser(user, packs); } - Future givePacksToUser(UserModel user, List packs) async { + Future givePacksToUser(UserModel user, List packs) async { if (user.id == null) return; for (final pack in packs) { - await _db.userDao.grantPackAccess( + if (pack.id == null) continue; + await _userRepository.grantPackAccess( userId: user.id!, - packId: pack.id, + packId: pack.id!, grantType: 'free', ); } diff --git a/mnemo_cards_backend/lib/packs/pack_dto_converter.dart b/mnemo_cards_backend/lib/packs/pack_dto_converter.dart index 4404058..c0caa7f 100644 --- a/mnemo_cards_backend/lib/packs/pack_dto_converter.dart +++ b/mnemo_cards_backend/lib/packs/pack_dto_converter.dart @@ -20,7 +20,11 @@ class PackDtoConverter { CardPackModel model, UserModel? userModel, ) async { - bool available = model.users.contains(userModel); + // Check access via purchases list (source of truth is user_packs table in DB) + // user.purchases is a cached list of pack IDs the user has access to + bool available = userModel != null && + (userModel.purchases.contains(model.id?.toString()) || + userModel.admin); if (userModel != null && !available) { available = userModel.subscriptionModel?.features.contains( @@ -214,6 +218,7 @@ class PackDtoConverter { enabled: dto.enabled ?? model?.enabled ?? false, order: dto.order ?? model?.order ?? 0, cardsOrder: dto.cardsOrder ?? model?.cardsOrder ?? const [], + currency: model?.currency ?? 'RUB', ); } } diff --git a/mnemo_cards_backend/lib/packs/pack_manager.dart b/mnemo_cards_backend/lib/packs/pack_manager.dart index f6d4491..ec8f276 100644 --- a/mnemo_cards_backend/lib/packs/pack_manager.dart +++ b/mnemo_cards_backend/lib/packs/pack_manager.dart @@ -8,24 +8,37 @@ import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart' import 'package:mnemo_cards_common/mnemo_cards_common.dart'; import 'pack_dto_converter.dart'; import 'card_pack_drift_extension.dart'; +import 'pack_repository.dart'; @lazySingleton class PackManager { final AppDatabase _db; + final PackRepository _packRepository; final PackDtoConverter packDtoConverter; - PackManager(this._db, this.packDtoConverter); + PackManager(this._db, this._packRepository, this.packDtoConverter); Future> listPacksPreviews( UserModel? userModel, Map? params, ) async { - // Get packs from Drift database - final packs = await _db.packDao.getAllPacks( + // Get packs from Repository + final packModels = await _packRepository.getAllPacks( enabledOnly: true, orderByField: 'order', ); + // Convert to Drift models for toPreviewDto (temporary, until we refactor DTO conversion) + final packs = await Future.wait( + packModels.map((packModel) async { + final pack = await _db.packDao.getPackById(packModel.id!); + if (pack == null) { + throw StateError('Pack not found: ${packModel.id}'); + } + return pack; + }), + ); + return (await Future.wait( packs.map((pack) async { final dto = await pack.toPreviewDto(userModel); @@ -37,16 +50,16 @@ class PackManager { )).toList(); } - Future getPack(String id) async { - return await _db.packDao.getPackById(id); + Future getPack(String id) async { + return await _packRepository.getPackById(id); } - Future> getCards(String packId) async { - return await _db.packDao.getPackCards(packId); + Future> getCards(String packId) async { + return await _packRepository.getPackCards(packId); } - Future getCard(String id) async { - return await _db.packDao.getCardById(id); + Future getCard(String id) async { + return await _packRepository.getCardById(id); } Future getVoice(String id) async { @@ -58,12 +71,18 @@ class PackManager { } Future getPackDto(String id, UserModel? userModel) async { - final pack = await getPack(id); + final packModel = await getPack(id); + if (packModel == null) { + throw StateError('Pack not found'); + } + + // Get pack from DAO for DTO conversion (temporary, until we refactor) + final pack = await _db.packDao.getPackById(id); if (pack == null) { throw StateError('Pack not found'); } - final cards = await getCards(id); + final cards = await _db.packDao.getPackCards(id); final voices = []; for (final card in cards) { diff --git a/mnemo_cards_backend/lib/packs/pack_repository.dart b/mnemo_cards_backend/lib/packs/pack_repository.dart new file mode 100644 index 0000000..8137491 --- /dev/null +++ b/mnemo_cards_backend/lib/packs/pack_repository.dart @@ -0,0 +1,238 @@ +import 'package:drift/drift.dart'; +import 'package:drift_postgres/drift_postgres.dart'; +import 'package:injectable/injectable.dart'; +import 'package:mnemo_cards_backend/database/database.dart'; +import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart' + hide VoiceModel; + +import 'card_pack_drift_extension.dart'; + +/// Extension для конвертации GameCard (Drift) в GameCardModel +extension GameCardToModel on GameCard { + GameCardModel toModel() { + return GameCardModel( + id: id, + image: image, + mnemo: mnemo ?? '', + original: original, + translation: translation, + transcription: transcription, + transcriptionMnemo: transcriptionMnemo, + imageBack: imageBack, + back: back, + ); + } +} + +/// Repository для работы с паками +/// Работает с доменными моделями (CardPackModel), использует DAO для доступа к БД +@lazySingleton +class PackRepository { + final AppDatabase _db; + + PackRepository(this._db); + + /// Получить пак по ID + /// [includeCards] - загружать ли связанные карточки + /// [includePreviewCards] - загружать ли превью карточки + Future getPackById( + String id, { + bool includeCards = false, + bool includePreviewCards = false, + }) async { + final pack = await _db.packDao.getPackById(id); + if (pack == null) return null; + + final packModel = await pack.toModel(); + + if (includeCards) { + final cards = await _db.packDao.getPackCards(id); + packModel.cards.addAll(cards.map((c) => c.toModel())); + } + + if (includePreviewCards) { + final previewCards = await _db.packDao.getPreviewCards(id); + packModel.previewCards.addAll(previewCards.map((c) => c.toModel())); + } + + return packModel; + } + + /// Получить все паки + Future> getAllPacks({ + bool enabledOnly = false, + String? orderByField, + bool orderDesc = false, + bool includeCards = false, + bool includePreviewCards = false, + }) async { + final packs = await _db.packDao.getAllPacks( + enabledOnly: enabledOnly, + orderByField: orderByField, + orderDesc: orderDesc, + ); + + if (includeCards || includePreviewCards) { + final packModels = []; + for (final pack in packs) { + final packModel = await pack.toModel(); + + if (includeCards) { + final cards = await _db.packDao.getPackCards(pack.id); + packModel.cards.addAll(cards.map((c) => c.toModel())); + } + + if (includePreviewCards) { + final previewCards = await _db.packDao.getPreviewCards(pack.id); + packModel.previewCards.addAll(previewCards.map((c) => c.toModel())); + } + + packModels.add(packModel); + } + return packModels; + } + + return await Future.wait(packs.map((p) => p.toModel())); + } + + /// Создать пак + Future createPack(CardPackModel packModel) async { + final companion = CardPacksCompanion.insert( + title: packModel.title, + subtitle: packModel.subtitle, + size: packModel.size, + color: Value(packModel.color), + version: Value(packModel.version), + cover: Value(packModel.cover), + description: Value(packModel.description), + googlePlayId: Value(packModel.googlePlayId), + rustoreId: Value(packModel.rustoreId), + appStoreId: Value(packModel.appStoreId), + price: Value(packModel.price), + currency: packModel.currency != null + ? Value(packModel.currency!) + : const Value.absent(), + enabled: Value(packModel.enabled), + order: Value(packModel.order), + cardsOrder: Value(packModel.cardsOrder), + ); + + return await _db.packDao.createPack(companion); + } + + /// Обновить пак + Future updatePack(CardPackModel packModel) async { + if (packModel.id == null) { + throw ArgumentError('Pack ID is required'); + } + + await _db.packDao.updatePackPartial( + CardPacksCompanion( + id: Value(packModel.id!), + title: Value(packModel.title), + subtitle: Value(packModel.subtitle), + size: Value(packModel.size), + color: Value(packModel.color), + version: Value(packModel.version), + cover: Value(packModel.cover), + description: Value(packModel.description), + googlePlayId: Value(packModel.googlePlayId), + rustoreId: Value(packModel.rustoreId), + appStoreId: Value(packModel.appStoreId), + price: Value(packModel.price), + currency: packModel.currency != null + ? Value(packModel.currency!) + : const Value.absent(), + enabled: Value(packModel.enabled), + order: Value(packModel.order), + cardsOrder: Value(packModel.cardsOrder), + updatedAt: Value(PgDateTime(DateTime.now())), + ), + ); + } + + /// Обновить пак частично + Future updatePackPartial(CardPacksCompanion updates) async { + await _db.packDao.updatePackPartial(updates); + } + + /// Удалить пак (soft delete) + Future softDeletePack(String packId) async { + await _db.packDao.softDeletePack(packId); + } + + /// Подсчитать паки + Future countPacks({bool enabledOnly = false}) async { + return await _db.packDao.countPacks(enabledOnly: enabledOnly); + } + + /// Получить карточки пака + Future> getPackCards(String packId) async { + final cards = await _db.packDao.getPackCards(packId); + return cards.map((c) => c.toModel()).toList(); + } + + /// Получить превью карточки пака + Future> getPreviewCards(String packId) async { + final cards = await _db.packDao.getPreviewCards(packId); + return cards.map((c) => c.toModel()).toList(); + } + + /// Получить карточку по ID + Future getCardById(String id) async { + final card = await _db.packDao.getCardById(id); + return card?.toModel(); + } + + /// Получить паки для карточки + Future> getPacksForCard(String cardId) async { + final packs = await _db.packDao.getPacksForCard(cardId); + return await Future.wait(packs.map((p) => p.toModel())); + } + + /// Добавить карточку в пак + Future addCardToPack({ + required String packId, + required String cardId, + int order = 0, + }) async { + await _db.packDao.addCardToPack( + packId: packId, + cardId: cardId, + order: order, + ); + } + + /// Удалить карточку из пака + Future removeCardFromPack(String packId, String cardId) async { + await _db.packDao.removeCardFromPack(packId, cardId); + } + + /// Обновить порядок карточек в паке + Future updatePackCardsOrder(String packId, List cardIds) async { + await _db.packDao.updatePackCardsOrder(packId, cardIds); + } + + /// Установить preview карточки для пака + Future setPreviewCards(String packId, List cardIds) async { + await _db.packDao.setPreviewCards(packId, cardIds); + } + + // ==================== VoiceModels ==================== + + /// Получить голосовые модели карточки + Future> getCardVoices(String cardId) async { + return await _db.packDao.getCardVoices(cardId); + } + + /// Получить голосовую модель по ID + Future getVoiceById(String id) async { + return await _db.packDao.getVoiceById(id); + } + + /// Обновить путь/URL аудиофайла голосовой модели + Future updateVoiceUrl(String voiceId, String voiceUrl) async { + await _db.packDao.updateVoiceUrl(voiceId, voiceUrl); + } +} + diff --git a/mnemo_cards_backend/lib/packs/products_price_resolver.dart b/mnemo_cards_backend/lib/packs/products_price_resolver.dart index 31fa9e5..2305d41 100644 --- a/mnemo_cards_backend/lib/packs/products_price_resolver.dart +++ b/mnemo_cards_backend/lib/packs/products_price_resolver.dart @@ -1,14 +1,14 @@ import 'package:injectable/injectable.dart'; -import 'package:mnemo_cards_backend/database/database.dart'; import 'package:mnemo_cards_backend/discounts/discounts_manager.dart'; +import 'package:mnemo_cards_backend/user/user_repository.dart'; import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart'; @LazySingleton() class ProductsPriceResolver { final DiscountsManager _discountsManager; - final AppDatabase _db; + final UserRepository _userRepository; - ProductsPriceResolver(this._discountsManager, this._db); + ProductsPriceResolver(this._discountsManager, this._userRepository); Future _userProductPrice( UserModel userModel, @@ -20,7 +20,7 @@ class ProductsPriceResolver { return price; } if (userModel.id != null) { - final userData = await _db.userDao.getUserData(userModel.id!); + final userData = await _userRepository.getUserData(userModel.id!); if (userData != null) { final maxDiscount = await _discountsManager.getProductDiscount( product, diff --git a/mnemo_cards_backend/lib/statistics/achievement_manager.dart b/mnemo_cards_backend/lib/statistics/achievement_manager.dart index ebee5a1..278b85c 100644 --- a/mnemo_cards_backend/lib/statistics/achievement_manager.dart +++ b/mnemo_cards_backend/lib/statistics/achievement_manager.dart @@ -2,14 +2,16 @@ import 'package:drift/drift.dart'; import 'package:drift_postgres/drift_postgres.dart'; import 'package:injectable/injectable.dart'; import 'package:mnemo_cards_backend/database/database.dart'; +import 'package:mnemo_cards_backend/user/user_repository.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart'; import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart'; @lazySingleton class AchievementManager { final AppDatabase _db; + final UserRepository _userRepository; - AchievementManager(this._db); + AchievementManager(this._db, this._userRepository); /// Get all available achievements with their definitions List get allAchievementDefinitions => @@ -146,7 +148,7 @@ class AchievementManager { // return userData.totalTests > 0; case AchievementType.firstPackCompleted: - final userPacks = await _db.userDao.getUserPacks(userId); + final userPacks = await _userRepository.getUserPacks(userId); return userPacks.isNotEmpty; // case AchievementType.words10Learned: diff --git a/mnemo_cards_backend/lib/statistics/statistics_calculator.dart b/mnemo_cards_backend/lib/statistics/statistics_calculator.dart index 8c3e723..3cc5758 100644 --- a/mnemo_cards_backend/lib/statistics/statistics_calculator.dart +++ b/mnemo_cards_backend/lib/statistics/statistics_calculator.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'package:injectable/injectable.dart'; +import 'package:mnemo_cards_backend/user/user_repository.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart'; import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart'; import 'package:mnemo_cards_backend/database/database.dart'; @@ -9,8 +10,9 @@ import 'package:mnemo_cards_backend/database/database.dart'; @lazySingleton class StatisticsCalculator { final AppDatabase _db; + final UserRepository _userRepository; - StatisticsCalculator(this._db); + StatisticsCalculator(this._db, this._userRepository); /// Calculate pack progress for a specific user and pack /// @@ -104,12 +106,13 @@ class StatisticsCalculator { /// Calculate pack progress for all user packs Future> calculateAllPackProgress(String userId) async { // Получить все паки пользователя - final userPacks = await _db.userDao.getUserPacks(userId); + final userPacks = await _userRepository.getUserPacks(userId); // Рассчитать прогресс для каждого пака final progressList = []; for (final pack in userPacks) { - final progress = await calculatePackProgress(userId, pack.id); + if (pack.id == null) continue; + final progress = await calculatePackProgress(userId, pack.id!); progressList.add(progress); } diff --git a/mnemo_cards_backend/lib/user/user_drift_extension.dart b/mnemo_cards_backend/lib/user/user_drift_extension.dart index 16de041..68c9867 100644 --- a/mnemo_cards_backend/lib/user/user_drift_extension.dart +++ b/mnemo_cards_backend/lib/user/user_drift_extension.dart @@ -1,10 +1,14 @@ import 'package:drift/drift.dart' as drift; import 'package:mnemo_cards_backend/database/database.dart'; +import 'package:mnemo_cards_backend/packs/card_pack_drift_extension.dart'; import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart'; /// Extension для конвертации User (Drift) в UserModel extension UserToUserModel on User { - Future toUserModel() async { + Future toUserModel([List? packs]) async { + final packModels = packs != null + ? await Future.wait(packs.map((p) => p.toModel())) + : []; final userModel = UserModel( id: id, name: name, @@ -13,6 +17,7 @@ extension UserToUserModel on User { admin: admin, purchases: purchases, userSettings: userSettings, + packs: packModels, ); return userModel; diff --git a/mnemo_cards_backend/lib/user/user_manager.dart b/mnemo_cards_backend/lib/user/user_manager.dart index 226270b..6edfbcb 100644 --- a/mnemo_cards_backend/lib/user/user_manager.dart +++ b/mnemo_cards_backend/lib/user/user_manager.dart @@ -15,13 +15,14 @@ import '../statistics/statistics_calculator.dart'; import '../statistics/achievement_manager.dart'; import '../statistics/word_statistics_manager.dart'; import 'secure.dart'; -import 'user_drift_extension.dart'; +import 'user_repository.dart'; Map _onlineUsers = {}; @lazySingleton class UserManager { final AppDatabase _db; + final UserRepository _userRepository; final FreePacksDistributor _freePacksDistributor; final SessionTracker _sessionTracker; final StatisticsCalculator _statisticsCalculator; @@ -30,6 +31,7 @@ class UserManager { UserManager( this._db, + this._userRepository, this._freePacksDistributor, this._sessionTracker, this._statisticsCalculator, @@ -38,9 +40,7 @@ class UserManager { ); Future fetchUser(String id) async { - final user = await _db.userDao.getUserById(id); - if (user == null) return null; - return await user.toUserModel(); + return await _userRepository.getUserById(id, includePacks: true); } DateTime? lastOnline(String id) => _onlineUsers[id]; @@ -119,77 +119,68 @@ class UserManager { String? name, }) async { // Check if user exists by externalId - final existingUser = await _db.userDao.getUserByExternalId(externalId); - if (existingUser != null) { + var existingUserModel = await _userRepository.getUserByExternalId(externalId); + if (existingUserModel != null) { print('User found $name $email $telegram'); // Обновляем контактные данные, если они пришли впервые/изменились final shouldUpdateEmail = - email != null && email.isNotEmpty && existingUser.email != email; + email != null && email.isNotEmpty && existingUserModel.email != email; final shouldUpdateTelegram = telegram != null && telegram.isNotEmpty && - existingUser.telegram != telegram; + existingUserModel.telegram != telegram; if (shouldUpdateEmail || shouldUpdateTelegram) { - await _db.userDao.updateUserPartial( - UsersCompanion( - id: drift.Value(existingUser.id), - email: shouldUpdateEmail - ? drift.Value(email) - : const drift.Value.absent(), - telegram: shouldUpdateTelegram - ? drift.Value(telegram) - : const drift.Value.absent(), - updatedAt: drift.Value(PgDateTime(DateTime.now())), - ), + final updatedModel = existingUserModel.copyWith( + email: shouldUpdateEmail ? email : existingUserModel.email, + telegram: shouldUpdateTelegram ? telegram : existingUserModel.telegram, ); + await _userRepository.updateUser(updatedModel); + existingUserModel = updatedModel; } - final userModel = await existingUser.toUserModel(); - final token = await createOrGetAuthToken(userModel, externalId); - return (userModel, token); + final token = await createOrGetAuthToken(existingUserModel, externalId); + return (existingUserModel, token); } print('Creating new user $name $email $telegram'); // Create new user with user data in transaction final now = DateTime.now(); - final userCompanion = UsersCompanion.insert( - externalUserId: externalId, - name: drift.Value(name), - email: drift.Value(email), - telegram: drift.Value(telegram), - admin: drift.Value(false), - purchases: drift.Value([]), - createdAt: drift.Value(PgDateTime(now)), - updatedAt: drift.Value(PgDateTime(now)), - isDeleted: drift.Value(false), - ); - final userDataCompanion = UserDatasCompanion.insert( userId: '', // Will be set by createUserWithData via copyWith registrationDate: drift.Value(PgDateTime(now)), ); - final userId = await _db.userDao.createUserWithData( - user: userCompanion, + // Создаем пользователя через UserRepository + final newUserModel = UserModel( + id: null, + name: name, + email: email, + telegram: telegram, + admin: false, + purchases: [], + userSettings: null, + packs: [], + ); + final userId = await _userRepository.createUserWithData( + userModel: newUserModel, userData: userDataCompanion, ); - final user = await _db.userDao.getUserById(userId); - if (user == null) { + final createdUserModel = await _userRepository.getUserById(userId, includePacks: true); + if (createdUserModel == null) { throw Exception('Failed to create user'); } - final userModel = await user.toUserModel(); - final token = await createOrGetAuthToken(userModel, externalId); + final token = await createOrGetAuthToken(createdUserModel, externalId); // Give free packs to new user - await _freePacksDistributor.giveFreePacksToUser(userModel); + await _freePacksDistributor.giveFreePacksToUser(createdUserModel); print('User $name $email $telegram created successfully'); - return (userModel, token); + return (createdUserModel, token); } /// Обновить настройки пользователя @@ -201,13 +192,10 @@ class UserManager { throw Exception('User ID is required'); } - await _db.userDao.updateUserPartial( - UsersCompanion( - id: drift.Value(user.id!), - userSettings: drift.Value(jsonEncode(settings.toJson())), - updatedAt: drift.Value(PgDateTime(DateTime.now())), - ), + final updatedUser = user.copyWith( + userSettings: jsonEncode(settings.toJson()), ); + await _userRepository.updateUser(updatedUser); } /// Добавить статистику теста @@ -220,12 +208,12 @@ class UserManager { } // Получить или создать UserData - var userData = await _db.userDao.getUserData(user.id!); + var userData = await _userRepository.getUserData(user.id!); if (userData == null) { await _db.userDao.createUserData( UserDatasCompanion.insert(userId: user.id!), ); - userData = await _db.userDao.getUserData(user.id!); + userData = await _userRepository.getUserData(user.id!); if (userData == null) { throw Exception('Failed to create user data'); } diff --git a/mnemo_cards_backend/lib/user/user_manager_drift.dart b/mnemo_cards_backend/lib/user/user_manager_drift.dart index 59bcc21..3d43e97 100644 --- a/mnemo_cards_backend/lib/user/user_manager_drift.dart +++ b/mnemo_cards_backend/lib/user/user_manager_drift.dart @@ -33,7 +33,8 @@ class UserManager { Future fetchUser(String id) async { final user = await _db.userDao.getUserById(id); if (user == null) return null; - return await user.toUserModel(); + final packs = await _db.userDao.getUserPacks(user.id); + return await user.toUserModel(packs); } DateTime? lastOnline(String id) => _onlineUsers[id]; diff --git a/mnemo_cards_backend/lib/user/user_repository.dart b/mnemo_cards_backend/lib/user/user_repository.dart new file mode 100644 index 0000000..3d187aa --- /dev/null +++ b/mnemo_cards_backend/lib/user/user_repository.dart @@ -0,0 +1,181 @@ +import 'package:drift/drift.dart'; +import 'package:drift_postgres/drift_postgres.dart'; +import 'package:injectable/injectable.dart'; +import 'package:mnemo_cards_backend/database/database.dart'; +import 'package:mnemo_cards_backend/packs/card_pack_drift_extension.dart'; +import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart'; + +import 'user_drift_extension.dart'; + +/// Repository для работы с пользователями +/// Работает с доменными моделями (UserModel), использует DAO для доступа к БД +@lazySingleton +class UserRepository { + final AppDatabase _db; + + UserRepository(this._db); + + /// Получить пользователя по ID + /// [includePacks] - загружать ли связанные паки пользователя + Future getUserById( + String id, { + bool includePacks = false, + }) async { + final user = await _db.userDao.getUserById(id); + if (user == null) return null; + + if (includePacks) { + final packs = await _db.userDao.getUserPacks(id); + return await user.toUserModel(packs); + } + + return await user.toUserModel(); + } + + /// Получить пользователя по email + Future getUserByEmail(String email) async { + final user = await _db.userDao.getUserByEmail(email); + if (user == null) return null; + + return await user.toUserModel(); + } + + /// Получить пользователя по externalUserId + Future getUserByExternalId(String externalId) async { + final user = await _db.userDao.getUserByExternalId(externalId); + if (user == null) return null; + + return await user.toUserModel(); + } + + /// Получить пользователя с UserData + Future getUserWithDataById(String id) async { + final userWithData = await _db.userDao.getUserWithDataById(id); + if (userWithData == null) return null; + + final packs = await _db.userDao.getUserPacks(id); + final userModel = await userWithData.user.toUserModel(packs); + + // TODO: загрузить UserDataModel если нужно + // userModel.userData = userWithData.userData?.toUserDataModel(); + + return userModel; + } + + /// Создать пользователя + Future createUser(UserModel userModel) async { + final userCompanion = userModel.toUsersCompanion(); + return await _db.userDao.createUser(userCompanion); + } + + /// Создать пользователя с UserData + Future createUserWithData({ + required UserModel userModel, + required UserDatasCompanion userData, + }) async { + final userCompanion = userModel.toUsersCompanion(); + return await _db.userDao.createUserWithData( + user: userCompanion, + userData: userData, + ); + } + + /// Обновить пользователя + Future updateUser(UserModel userModel) async { + if (userModel.id == null) { + throw ArgumentError('User ID is required'); + } + + await _db.userDao.updateUserPartial( + userModel.toUsersCompanion().copyWith( + updatedAt: Value(PgDateTime(DateTime.now())), + ), + ); + } + + /// Обновить пользователя частично + Future updateUserPartial(UsersCompanion updates) async { + await _db.userDao.updateUserPartial(updates); + } + + /// Удалить пользователя (soft delete) + Future softDeleteUser(String userId) async { + await _db.userDao.softDeleteUser(userId); + } + + /// Получить всех пользователей (для админки) + Future> getAllUsers({ + int? limit, + int? offset, + bool includeDeleted = false, + bool includePacks = false, + }) async { + final users = await _db.userDao.getAllUsers( + limit: limit, + offset: offset, + includeDeleted: includeDeleted, + ); + + if (includePacks) { + final userModels = []; + for (final user in users) { + final packs = await _db.userDao.getUserPacks(user.id); + final userModel = await user.toUserModel(packs); + userModels.add(userModel); + } + return userModels; + } + + return await Future.wait(users.map((u) => u.toUserModel())); + } + + /// Подсчитать пользователей + Future countUsers({bool includeDeleted = false}) async { + return await _db.userDao.countUsers(includeDeleted: includeDeleted); + } + + /// Получить паки пользователя + Future> getUserPacks(String userId) async { + final packs = await _db.userDao.getUserPacks(userId); + return await Future.wait(packs.map((p) => p.toModel())); + } + + /// Проверить, есть ли у пользователя доступ к паку + Future hasPackAccess(String userId, String packId) async { + return await _db.userDao.hasPackAccess(userId, packId); + } + + /// Дать пользователю доступ к паку + Future grantPackAccess({ + required String userId, + required String packId, + String grantType = 'purchase', + }) async { + await _db.userDao.grantPackAccess( + userId: userId, + packId: packId, + grantType: grantType, + ); + } + + /// Отозвать доступ к паку + Future revokePackAccess(String userId, String packId) async { + await _db.userDao.revokePackAccess(userId, packId); + } + + /// Получить UserData пользователя + Future getUserData(String userId) async { + return await _db.userDao.getUserData(userId); + } + + /// Обновить UserData частично + Future updateUserDataPartial(UserDatasCompanion updates) async { + await _db.userDao.updateUserDataPartial(updates); + } + + /// Обновить время последнего визита + Future updateLastOnline(String userId) async { + await _db.userDao.updateLastOnline(userId); + } +} + diff --git a/mnemo_cards_backend/test/api/v2/media_api_v2_test.mocks.dart b/mnemo_cards_backend/test/api/v2/media_api_v2_test.mocks.dart index a8d531e..561e5bf 100644 --- a/mnemo_cards_backend/test/api/v2/media_api_v2_test.mocks.dart +++ b/mnemo_cards_backend/test/api/v2/media_api_v2_test.mocks.dart @@ -166,30 +166,30 @@ class MockPackManager extends _i1.Mock implements _i8.PackManager { as _i5.Future>); @override - _i5.Future<_i10.CardPack?> getPack(String? id) => + _i5.Future<_i9.CardPackModel?> getPack(String? id) => (super.noSuchMethod( Invocation.method(#getPack, [id]), - returnValue: _i5.Future<_i10.CardPack?>.value(), + returnValue: _i5.Future<_i9.CardPackModel?>.value(), ) - as _i5.Future<_i10.CardPack?>); + as _i5.Future<_i9.CardPackModel?>); @override - _i5.Future> getCards(String? packId) => + _i5.Future> getCards(String? packId) => (super.noSuchMethod( Invocation.method(#getCards, [packId]), - returnValue: _i5.Future>.value( - <_i10.GameCard>[], + returnValue: _i5.Future>.value( + <_i9.GameCardModel>[], ), ) - as _i5.Future>); + as _i5.Future>); @override - _i5.Future<_i10.GameCard?> getCard(String? id) => + _i5.Future<_i9.GameCardModel?> getCard(String? id) => (super.noSuchMethod( Invocation.method(#getCard, [id]), - returnValue: _i5.Future<_i10.GameCard?>.value(), + returnValue: _i5.Future<_i9.GameCardModel?>.value(), ) - as _i5.Future<_i10.GameCard?>); + as _i5.Future<_i9.GameCardModel?>); @override _i5.Future<_i10.VoiceModel?> getVoice(String? id) => diff --git a/mnemo_cards_common_backend/lib/src/models/card_pack_model.dart b/mnemo_cards_common_backend/lib/src/models/card_pack_model.dart index bba04bd..70c0da2 100644 --- a/mnemo_cards_common_backend/lib/src/models/card_pack_model.dart +++ b/mnemo_cards_common_backend/lib/src/models/card_pack_model.dart @@ -2,13 +2,11 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:copy_with_extension/copy_with_extension.dart'; import 'package:mnemo_cards_common_backend/src/models/game_card_model.dart'; import 'package:mnemo_cards_common_backend/src/models/game_tests/test_model.dart'; -import 'package:mnemo_cards_common_backend/src/models/user/user_model.dart'; import 'product_model.dart'; part 'card_pack_model.g.dart'; -@JsonSerializable() @CopyWith() class CardPackModel implements MnemoCardsProductModel { String? id; @@ -28,37 +26,30 @@ class CardPackModel implements MnemoCardsProductModel { final bool enabled; final int order; final List cardsOrder; - // Relations - loaded separately from database + // Relations - loaded separately from database (when needed) final List previewCards = []; final List cards = []; final List tests = []; - final List users = []; CardPackModel({ required this.title, required this.subtitle, required this.size, - this.color, - this.cover, - this.description, - this.version, - this.id, - this.googlePlayId, - this.rustoreId, - this.appStoreId, - this.price, - this.cardsOrder = const [], - this.enabled = true, - this.order = 0, - this.currency = 'RUB', + required this.color, + required this.cover, + required this.description, + required this.version, + required this.id, + required this.googlePlayId, + required this.rustoreId, + required this.appStoreId, + required this.price, + required this.cardsOrder, + required this.enabled, + required this.order, + required this.currency, }); - factory CardPackModel.fromJson(Map json) => - _$CardPackModelFromJson(json); - - Map toJson() => _$CardPackModelToJson(this); - @override - @JsonKey(includeFromJson: false, includeToJson: false) final type = MnemoCardsProductModelType.pack; } diff --git a/mnemo_cards_common_backend/lib/src/models/card_pack_model.g.dart b/mnemo_cards_common_backend/lib/src/models/card_pack_model.g.dart index 01e6f9f..c5dd694 100644 --- a/mnemo_cards_common_backend/lib/src/models/card_pack_model.g.dart +++ b/mnemo_cards_common_backend/lib/src/models/card_pack_model.g.dart @@ -226,51 +226,3 @@ extension $CardPackModelCopyWith on CardPackModel { // ignore: library_private_types_in_public_api _$CardPackModelCWProxy get copyWith => _$CardPackModelCWProxyImpl(this); } - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -CardPackModel _$CardPackModelFromJson(Map json) => - CardPackModel( - title: json['title'] as String, - subtitle: json['subtitle'] as String, - size: (json['size'] as num).toInt(), - color: json['color'] as String?, - cover: json['cover'] as String?, - description: json['description'] as String?, - version: json['version'] as String?, - id: json['id'] as String?, - googlePlayId: json['googlePlayId'] as String?, - rustoreId: json['rustoreId'] as String?, - appStoreId: json['appStoreId'] as String?, - price: json['price'] as String?, - cardsOrder: - (json['cardsOrder'] as List?) - ?.map((e) => e as String) - .toList() ?? - const [], - enabled: json['enabled'] as bool? ?? true, - order: (json['order'] as num?)?.toInt() ?? 0, - currency: json['currency'] as String? ?? 'RUB', - ); - -Map _$CardPackModelToJson(CardPackModel instance) => - { - 'id': instance.id, - 'title': instance.title, - 'subtitle': instance.subtitle, - 'color': instance.color, - 'cover': instance.cover, - 'size': instance.size, - 'version': instance.version, - 'googlePlayId': instance.googlePlayId, - 'rustoreId': instance.rustoreId, - 'appStoreId': instance.appStoreId, - 'price': instance.price, - 'currency': instance.currency, - 'description': instance.description, - 'enabled': instance.enabled, - 'order': instance.order, - 'cardsOrder': instance.cardsOrder, - }; diff --git a/mnemo_cards_common_backend/lib/src/models/game_tests/test_statistics_model.dart b/mnemo_cards_common_backend/lib/src/models/game_tests/test_statistics_model.dart index 92ac20f..8bb6120 100644 --- a/mnemo_cards_common_backend/lib/src/models/game_tests/test_statistics_model.dart +++ b/mnemo_cards_common_backend/lib/src/models/game_tests/test_statistics_model.dart @@ -7,13 +7,12 @@ import '../statistics/word_statistics_model.dart'; part 'test_statistics_model.g.dart'; -@JsonSerializable() @CopyWith() class TestStatisticsModel { final String id; // Relations - loaded separately from database - TestModel? test; - UserDataModel? userData; + String? testId; + String? userId; final List attempts; TestStatisticsModel({ @@ -24,13 +23,8 @@ class TestStatisticsModel { @JsonKey(includeFromJson: false, includeToJson: false) TestAttempt? get lastAttempt => attempts.lastOrNull; - factory TestStatisticsModel.fromJson(Map json) => - _$TestStatisticsModelFromJson(json); - - Map toJson() => _$TestStatisticsModelToJson(this); } -@JsonSerializable() @CopyWith() class TestAttempt { final List words; @@ -41,8 +35,4 @@ class TestAttempt { this.sessionToken, }); - factory TestAttempt.fromJson(Map json) => - _$TestAttemptFromJson(json); - - Map toJson() => _$TestAttemptToJson(this); } diff --git a/mnemo_cards_common_backend/lib/src/models/game_tests/test_statistics_model.g.dart b/mnemo_cards_common_backend/lib/src/models/game_tests/test_statistics_model.g.dart index f1c1886..9e050d1 100644 --- a/mnemo_cards_common_backend/lib/src/models/game_tests/test_statistics_model.g.dart +++ b/mnemo_cards_common_backend/lib/src/models/game_tests/test_statistics_model.g.dart @@ -128,47 +128,3 @@ extension $TestAttemptCopyWith on TestAttempt { // ignore: library_private_types_in_public_api _$TestAttemptCWProxy get copyWith => _$TestAttemptCWProxyImpl(this); } - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -TestStatisticsModel _$TestStatisticsModelFromJson(Map json) => - TestStatisticsModel( - attempts: - (json['attempts'] as List?) - ?.map((e) => TestAttempt.fromJson(e as Map)) - .toList() ?? - const [], - id: json['id'] as String, - ) - ..test = json['test'] == null - ? null - : TestModel.fromJson(json['test'] as Map) - ..userData = json['userData'] == null - ? null - : UserDataModel.fromJson(json['userData'] as Map); - -Map _$TestStatisticsModelToJson( - TestStatisticsModel instance, -) => { - 'id': instance.id, - 'test': instance.test, - 'userData': instance.userData, - 'attempts': instance.attempts, -}; - -TestAttempt _$TestAttemptFromJson(Map json) => TestAttempt( - words: - (json['words'] as List?) - ?.map((e) => WordStatisticsModel.fromJson(e as Map)) - .toList() ?? - const [], - sessionToken: json['sessionToken'] as String?, -); - -Map _$TestAttemptToJson(TestAttempt instance) => - { - 'words': instance.words, - 'sessionToken': instance.sessionToken, - }; diff --git a/mnemo_cards_common_backend/lib/src/models/payment.dart b/mnemo_cards_common_backend/lib/src/models/payment.dart index 88735c2..873e91e 100644 --- a/mnemo_cards_common_backend/lib/src/models/payment.dart +++ b/mnemo_cards_common_backend/lib/src/models/payment.dart @@ -3,18 +3,14 @@ import 'package:copy_with_extension/copy_with_extension.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart'; import 'product_model.dart'; -import 'user/user_model.dart'; part 'payment.g.dart'; -@JsonSerializable() @CopyWith() class PaymentModel { String? id; final String amount; final String currency; - // Relations - loaded separately from database - UserModel? user; @JsonKey(unknownEnumValue: PaymentStatus.unknown) final PaymentStatus status; @JsonKey(unknownEnumValue: PaymentSystem.unknown) @@ -24,10 +20,6 @@ class PaymentModel { final DateTime date; final String userId; - @deprecated - final List packs; - @deprecated - final bool subscription; final List products; PaymentModel({ @@ -38,8 +30,6 @@ class PaymentModel { required this.userId, required this.date, required this.products, - this.packs = const [], - this.subscription = false, this.externalToken, this.meta, this.id, @@ -97,8 +87,6 @@ class PaymentModel { required this.externalToken, required this.date, required this.products, - this.packs = const [], - this.subscription = false, this.meta, this.id, }) : paymentSystem = PaymentSystem.yookassa, @@ -111,8 +99,6 @@ class PaymentModel { required this.externalToken, required this.date, required this.products, - this.packs = const [], - this.subscription = false, this.meta, this.id, this.status = PaymentStatus.created, @@ -125,15 +111,9 @@ class PaymentModel { required this.externalToken, required this.date, required this.products, - this.packs = const [], - this.subscription = false, this.meta, this.id, this.status = PaymentStatus.created, }) : paymentSystem = PaymentSystem.rustore; - factory PaymentModel.fromJson(Map json) => - _$PaymentModelFromJson(json); - - Map toJson() => _$PaymentModelToJson(this); } diff --git a/mnemo_cards_common_backend/lib/src/models/payment.g.dart b/mnemo_cards_common_backend/lib/src/models/payment.g.dart index 703cbf5..83011fe 100644 --- a/mnemo_cards_common_backend/lib/src/models/payment.g.dart +++ b/mnemo_cards_common_backend/lib/src/models/payment.g.dart @@ -21,10 +21,6 @@ abstract class _$PaymentModelCWProxy { PaymentModel products(List products); - PaymentModel packs(@deprecated List packs); - - PaymentModel subscription(@deprecated bool subscription); - PaymentModel externalToken(String? externalToken); PaymentModel meta(String? meta); @@ -46,8 +42,6 @@ abstract class _$PaymentModelCWProxy { String userId, DateTime date, List products, - @deprecated List packs, - @deprecated bool subscription, String? externalToken, String? meta, String? id, @@ -84,13 +78,6 @@ class _$PaymentModelCWProxyImpl implements _$PaymentModelCWProxy { PaymentModel products(List products) => call(products: products); - @override - PaymentModel packs(@deprecated List packs) => call(packs: packs); - - @override - PaymentModel subscription(@deprecated bool subscription) => - call(subscription: subscription); - @override PaymentModel externalToken(String? externalToken) => call(externalToken: externalToken); @@ -117,8 +104,6 @@ class _$PaymentModelCWProxyImpl implements _$PaymentModelCWProxy { Object? userId = const $CopyWithPlaceholder(), Object? date = const $CopyWithPlaceholder(), Object? products = const $CopyWithPlaceholder(), - @deprecated Object? packs = const $CopyWithPlaceholder(), - @deprecated Object? subscription = const $CopyWithPlaceholder(), Object? externalToken = const $CopyWithPlaceholder(), Object? meta = const $CopyWithPlaceholder(), Object? id = const $CopyWithPlaceholder(), @@ -153,15 +138,6 @@ class _$PaymentModelCWProxyImpl implements _$PaymentModelCWProxy { ? _value.products // ignore: cast_nullable_to_non_nullable : products as List, - packs: packs == const $CopyWithPlaceholder() || packs == null - ? _value.packs - // ignore: cast_nullable_to_non_nullable - : packs as List, - subscription: - subscription == const $CopyWithPlaceholder() || subscription == null - ? _value.subscription - // ignore: cast_nullable_to_non_nullable - : subscription as bool, externalToken: externalToken == const $CopyWithPlaceholder() ? _value.externalToken // ignore: cast_nullable_to_non_nullable @@ -184,80 +160,3 @@ extension $PaymentModelCopyWith on PaymentModel { // ignore: library_private_types_in_public_api _$PaymentModelCWProxy get copyWith => _$PaymentModelCWProxyImpl(this); } - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -PaymentModel _$PaymentModelFromJson(Map json) => - PaymentModel( - amount: json['amount'] as String, - currency: json['currency'] as String, - status: $enumDecode( - _$PaymentStatusEnumMap, - json['status'], - unknownValue: PaymentStatus.unknown, - ), - paymentSystem: $enumDecode( - _$PaymentSystemEnumMap, - json['paymentSystem'], - unknownValue: PaymentSystem.unknown, - ), - userId: json['userId'] as String, - date: DateTime.parse(json['date'] as String), - products: (json['products'] as List) - .map( - (e) => MnemoCardsProductModelBase.fromJson( - e as Map, - ), - ) - .toList(), - packs: - (json['packs'] as List?) - ?.map((e) => e as String) - .toList() ?? - const [], - subscription: json['subscription'] as bool? ?? false, - externalToken: json['externalToken'] as String?, - meta: json['meta'] as String?, - id: json['id'] as String?, - ) - ..user = json['user'] == null - ? null - : UserModel.fromJson(json['user'] as Map); - -Map _$PaymentModelToJson(PaymentModel instance) => - { - 'id': instance.id, - 'amount': instance.amount, - 'currency': instance.currency, - 'user': instance.user, - 'status': _$PaymentStatusEnumMap[instance.status]!, - 'paymentSystem': _$PaymentSystemEnumMap[instance.paymentSystem]!, - 'externalToken': instance.externalToken, - 'meta': instance.meta, - 'date': instance.date.toIso8601String(), - 'userId': instance.userId, - 'packs': instance.packs, - 'subscription': instance.subscription, - 'products': instance.products, - }; - -const _$PaymentStatusEnumMap = { - PaymentStatus.created: 'created', - PaymentStatus.waiting: 'waiting', - PaymentStatus.succeeded: 'succeeded', - PaymentStatus.canceled: 'canceled', - PaymentStatus.processed: 'processed', - PaymentStatus.unknown: 'unknown', -}; - -const _$PaymentSystemEnumMap = { - PaymentSystem.google: 'google', - PaymentSystem.apple: 'apple', - PaymentSystem.rustore: 'rustore', - PaymentSystem.yookassa: 'yookassa', - PaymentSystem.promoCode: 'promoCode', - PaymentSystem.adView: 'adView', - PaymentSystem.unknown: 'unknown', -}; diff --git a/mnemo_cards_common_backend/lib/src/models/promo_code/promo_code_ext.dart b/mnemo_cards_common_backend/lib/src/models/promo_code/promo_code_ext.dart index 16ce25a..1af214d 100644 --- a/mnemo_cards_common_backend/lib/src/models/promo_code/promo_code_ext.dart +++ b/mnemo_cards_common_backend/lib/src/models/promo_code/promo_code_ext.dart @@ -2,5 +2,5 @@ import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart'; extension PromoCodeExt on PromoCodeModel { Future isReserved() async => - userData != null; + userId != null; } diff --git a/mnemo_cards_common_backend/lib/src/models/promo_code/promo_code_model.dart b/mnemo_cards_common_backend/lib/src/models/promo_code/promo_code_model.dart index 3988a08..d740f41 100644 --- a/mnemo_cards_common_backend/lib/src/models/promo_code/promo_code_model.dart +++ b/mnemo_cards_common_backend/lib/src/models/promo_code/promo_code_model.dart @@ -6,7 +6,6 @@ import 'promo_codes_campaign_model.dart'; part 'promo_code_model.g.dart'; -@JsonSerializable() @CopyWith() class PromoCodeModel { String? id; @@ -15,16 +14,13 @@ class PromoCodeModel { // Relations - loaded separately from database PromoCodesCampaignModel? campaign; // Individual promo code - UserDataModel? userData; + String? userId; PromoCodeModel({ required this.code, this.activations = 0, this.id, + this.userId, }); - factory PromoCodeModel.fromJson(Map json) => - _$PromoCodeModelFromJson(json); - - Map toJson() => _$PromoCodeModelToJson(this); } diff --git a/mnemo_cards_common_backend/lib/src/models/promo_code/promo_code_model.g.dart b/mnemo_cards_common_backend/lib/src/models/promo_code/promo_code_model.g.dart index 5b812aa..a36b267 100644 --- a/mnemo_cards_common_backend/lib/src/models/promo_code/promo_code_model.g.dart +++ b/mnemo_cards_common_backend/lib/src/models/promo_code/promo_code_model.g.dart @@ -13,6 +13,8 @@ abstract class _$PromoCodeModelCWProxy { PromoCodeModel id(String? id); + PromoCodeModel userId(String? userId); + /// Creates a new instance with the provided field values. /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `PromoCodeModel(...).copyWith.fieldName(value)`. /// @@ -20,7 +22,12 @@ abstract class _$PromoCodeModelCWProxy { /// ```dart /// PromoCodeModel(...).copyWith(id: 12, name: "My name") /// ``` - PromoCodeModel call({String code, int activations, String? id}); + PromoCodeModel call({ + String code, + int activations, + String? id, + String? userId, + }); } /// Callable proxy for `copyWith` functionality. @@ -39,6 +46,9 @@ class _$PromoCodeModelCWProxyImpl implements _$PromoCodeModelCWProxy { @override PromoCodeModel id(String? id) => call(id: id); + @override + PromoCodeModel userId(String? userId) => call(userId: userId); + @override /// Creates a new instance with the provided field values. /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `PromoCodeModel(...).copyWith.fieldName(value)`. @@ -51,6 +61,7 @@ class _$PromoCodeModelCWProxyImpl implements _$PromoCodeModelCWProxy { Object? code = const $CopyWithPlaceholder(), Object? activations = const $CopyWithPlaceholder(), Object? id = const $CopyWithPlaceholder(), + Object? userId = const $CopyWithPlaceholder(), }) { return PromoCodeModel( code: code == const $CopyWithPlaceholder() || code == null @@ -66,6 +77,10 @@ class _$PromoCodeModelCWProxyImpl implements _$PromoCodeModelCWProxy { ? _value.id // ignore: cast_nullable_to_non_nullable : id as String?, + userId: userId == const $CopyWithPlaceholder() + ? _value.userId + // ignore: cast_nullable_to_non_nullable + : userId as String?, ); } } @@ -76,31 +91,3 @@ extension $PromoCodeModelCopyWith on PromoCodeModel { // ignore: library_private_types_in_public_api _$PromoCodeModelCWProxy get copyWith => _$PromoCodeModelCWProxyImpl(this); } - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -PromoCodeModel _$PromoCodeModelFromJson(Map json) => - PromoCodeModel( - code: json['code'] as String, - activations: (json['activations'] as num?)?.toInt() ?? 0, - id: json['id'] as String?, - ) - ..campaign = json['campaign'] == null - ? null - : PromoCodesCampaignModel.fromJson( - json['campaign'] as Map, - ) - ..userData = json['userData'] == null - ? null - : UserDataModel.fromJson(json['userData'] as Map); - -Map _$PromoCodeModelToJson(PromoCodeModel instance) => - { - 'id': instance.id, - 'code': instance.code, - 'activations': instance.activations, - 'campaign': instance.campaign, - 'userData': instance.userData, - }; diff --git a/mnemo_cards_common_backend/lib/src/models/subscription/user_subscription_model.dart b/mnemo_cards_common_backend/lib/src/models/subscription/user_subscription_model.dart index fe68692..dc119db 100644 --- a/mnemo_cards_common_backend/lib/src/models/subscription/user_subscription_model.dart +++ b/mnemo_cards_common_backend/lib/src/models/subscription/user_subscription_model.dart @@ -5,7 +5,6 @@ import 'package:mnemo_cards_common/mnemo_cards_common.dart'; part 'user_subscription_model.g.dart'; -@JsonSerializable() @CopyWith() class UserSubscriptionModel { String? id; @@ -24,10 +23,6 @@ class UserSubscriptionModel { required this.features, }); - factory UserSubscriptionModel.fromJson(Map json) => - _$UserSubscriptionModelFromJson(json); - - Map toJson() => _$UserSubscriptionModelToJson(this); } extension UserSubscriptionExt on UserSubscriptionModel { diff --git a/mnemo_cards_common_backend/lib/src/models/subscription/user_subscription_model.g.dart b/mnemo_cards_common_backend/lib/src/models/subscription/user_subscription_model.g.dart index 031331a..8a3c151 100644 --- a/mnemo_cards_common_backend/lib/src/models/subscription/user_subscription_model.g.dart +++ b/mnemo_cards_common_backend/lib/src/models/subscription/user_subscription_model.g.dart @@ -93,40 +93,3 @@ extension $UserSubscriptionModelCopyWith on UserSubscriptionModel { _$UserSubscriptionModelCWProxy get copyWith => _$UserSubscriptionModelCWProxyImpl(this); } - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -UserSubscriptionModel _$UserSubscriptionModelFromJson( - Map json, -) => - UserSubscriptionModel( - id: json['id'] as String?, - start: DateTime.parse(json['start'] as String), - finish: DateTime.parse(json['finish'] as String), - features: (json['features'] as List) - .map((e) => $enumDecode(_$SubscriptionFeatureEnumEnumMap, e)) - .toList(), - ) - ..user = json['user'] == null - ? null - : UserModel.fromJson(json['user'] as Map); - -Map _$UserSubscriptionModelToJson( - UserSubscriptionModel instance, -) => { - 'id': instance.id, - 'user': instance.user, - 'start': instance.start.toIso8601String(), - 'finish': instance.finish.toIso8601String(), - 'features': instance.features - .map((e) => _$SubscriptionFeatureEnumEnumMap[e]!) - .toList(), -}; - -const _$SubscriptionFeatureEnumEnumMap = { - SubscriptionFeatureEnum.ads: 'ads', - SubscriptionFeatureEnum.packs: 'packs', - SubscriptionFeatureEnum.unknown: 'unknown', -}; diff --git a/mnemo_cards_common_backend/lib/src/models/user/user_data_model.dart b/mnemo_cards_common_backend/lib/src/models/user/user_data_model.dart index fd077be..a44a112 100644 --- a/mnemo_cards_common_backend/lib/src/models/user/user_data_model.dart +++ b/mnemo_cards_common_backend/lib/src/models/user/user_data_model.dart @@ -1,7 +1,6 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:copy_with_extension/copy_with_extension.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart'; -import 'package:mnemo_cards_common_backend/src/models/user/user_model.dart'; import '../discount/discount_model.dart'; import '../game_tests/test_statistics_model.dart'; @@ -12,7 +11,6 @@ import '../statistics/achievement_model.dart'; part 'user_data_model.g.dart'; -@JsonSerializable() @CopyWith() class UserDataModel { String? id; @@ -21,7 +19,6 @@ class UserDataModel { final List activatedPromoCodes = []; final List individualPromoCodes = []; final List activeDiscounts = []; - UserModel? user; final List words; final String? lastTestSessionToken; @@ -68,10 +65,6 @@ class UserDataModel { this.achievements = const [], }); - factory UserDataModel.fromJson(Map json) => - _$UserDataModelFromJson(json); - - Map toJson() => _$UserDataModelToJson(this); } // Helper functions for PackProgressModel serialization diff --git a/mnemo_cards_common_backend/lib/src/models/user/user_data_model.g.dart b/mnemo_cards_common_backend/lib/src/models/user/user_data_model.g.dart index f06fe2f..e793a16 100644 --- a/mnemo_cards_common_backend/lib/src/models/user/user_data_model.g.dart +++ b/mnemo_cards_common_backend/lib/src/models/user/user_data_model.g.dart @@ -196,64 +196,3 @@ extension $UserDataModelCopyWith on UserDataModel { // ignore: library_private_types_in_public_api _$UserDataModelCWProxy get copyWith => _$UserDataModelCWProxyImpl(this); } - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -UserDataModel _$UserDataModelFromJson(Map json) => - UserDataModel( - id: json['id'] as String?, - lastTestSessionToken: json['lastTestSessionToken'] as String?, - lastTimeOnline: json['lastTimeOnline'] == null - ? null - : DateTime.parse(json['lastTimeOnline'] as String), - words: - (json['words'] as List?) - ?.map( - (e) => - WordStatisticsModel.fromJson(e as Map), - ) - .toList() ?? - const [], - tags: - (json['tags'] as List?) - ?.map((e) => e as String) - .toList() ?? - const [], - totalStudyTimeMinutes: - (json['totalStudyTimeMinutes'] as num?)?.toInt() ?? 0, - currentStreak: (json['currentStreak'] as num?)?.toInt() ?? 0, - longestStreak: (json['longestStreak'] as num?)?.toInt() ?? 0, - packProgress: json['packProgress'] == null - ? const [] - : _packProgressFromJson(json['packProgress'] as List?), - studyDates: - (json['studyDates'] as List?) - ?.map((e) => DateTime.parse(e as String)) - .toList() ?? - const [], - achievements: json['achievements'] == null - ? const [] - : _achievementsFromJson(json['achievements'] as List?), - ) - ..user = json['user'] == null - ? null - : UserModel.fromJson(json['user'] as Map); - -Map _$UserDataModelToJson( - UserDataModel instance, -) => { - 'id': instance.id, - 'user': instance.user, - 'words': instance.words, - 'lastTestSessionToken': instance.lastTestSessionToken, - 'lastTimeOnline': instance.lastTimeOnline?.toIso8601String(), - 'tags': instance.tags, - 'totalStudyTimeMinutes': instance.totalStudyTimeMinutes, - 'currentStreak': instance.currentStreak, - 'longestStreak': instance.longestStreak, - 'packProgress': _packProgressToJson(instance.packProgress), - 'studyDates': instance.studyDates.map((e) => e.toIso8601String()).toList(), - 'achievements': _achievementsToJson(instance.achievements), -}; diff --git a/mnemo_cards_common_backend/lib/src/models/user/user_model.dart b/mnemo_cards_common_backend/lib/src/models/user/user_model.dart index d3da2e8..59e080f 100644 --- a/mnemo_cards_common_backend/lib/src/models/user/user_model.dart +++ b/mnemo_cards_common_backend/lib/src/models/user/user_model.dart @@ -7,7 +7,6 @@ import '../subscription/user_subscription_model.dart'; part 'user_model.g.dart'; -@JsonSerializable() @CopyWith() class UserModel { String? id; @@ -16,11 +15,10 @@ class UserModel { final String? telegram; @JsonKey(defaultValue: false) final bool admin; - // Relations - loaded separately from database - final List packs = []; - UserDataModel? userData; @JsonKey(defaultValue: []) final List purchases; + final List packs; + UserDataModel? userData; UserSubscriptionModel? subscriptionModel; // UserSettingsDto @@ -33,15 +31,10 @@ class UserModel { this.telegram, this.admin = false, this.purchases = const [], + this.packs = const [], this.userSettings, }); - factory UserModel.fromJson(Map json) => - _$UserModelFromJson(json); - - Map toJson() => _$UserModelToJson(this); - - @JsonKey(includeFromJson: false, includeToJson: false) static UserModel get empty => UserModel( admin: false, purchases: [], diff --git a/mnemo_cards_common_backend/lib/src/models/user/user_model.g.dart b/mnemo_cards_common_backend/lib/src/models/user/user_model.g.dart index bd5b588..0e5f094 100644 --- a/mnemo_cards_common_backend/lib/src/models/user/user_model.g.dart +++ b/mnemo_cards_common_backend/lib/src/models/user/user_model.g.dart @@ -124,42 +124,3 @@ extension $UserModelCopyWith on UserModel { // ignore: library_private_types_in_public_api _$UserModelCWProxy get copyWith => _$UserModelCWProxyImpl(this); } - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -UserModel _$UserModelFromJson(Map json) => - UserModel( - id: json['id'] as String?, - name: json['name'] as String?, - email: json['email'] as String?, - telegram: json['telegram'] as String?, - admin: json['admin'] as bool? ?? false, - purchases: - (json['purchases'] as List?) - ?.map((e) => e as String) - .toList() ?? - [], - userSettings: json['userSettings'] as String?, - ) - ..userData = json['userData'] == null - ? null - : UserDataModel.fromJson(json['userData'] as Map) - ..subscriptionModel = json['subscriptionModel'] == null - ? null - : UserSubscriptionModel.fromJson( - json['subscriptionModel'] as Map, - ); - -Map _$UserModelToJson(UserModel instance) => { - 'id': instance.id, - 'name': instance.name, - 'email': instance.email, - 'telegram': instance.telegram, - 'admin': instance.admin, - 'userData': instance.userData, - 'purchases': instance.purchases, - 'subscriptionModel': instance.subscriptionModel, - 'userSettings': instance.userSettings, -};