repo
Some checks failed
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
Web App CI / test (push) Waiting to run
Web App CI / build (push) Blocked by required conditions
Deploy Mnemo Cards / Deploy Backend (push) Waiting to run
Deploy Mnemo Cards / Deploy Web App (push) Blocked by required conditions
Deploy Mnemo Cards / Final Verification (push) Blocked by required conditions
Mobile App CI / test (push) Has been cancelled
Deploy Telegram Bot / Deploy Telegram Bot (push) Has been cancelled
Mobile App CI / build-android (push) Has been cancelled
Mobile App CI / build-ios (push) Has been cancelled

This commit is contained in:
Dmitry 2026-01-08 19:07:56 +03:00
parent df2f5d5740
commit a6abcfa7e6
44 changed files with 900 additions and 2185 deletions

View file

@ -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<ChatParticipantUser>());
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<ChatParticipantAssistant>());
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<ChatMessageText>());
});
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<ChatMessageAudio>());
});
});
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));
});
});
});
}

View file

@ -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));
});
});
});
}

View file

@ -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<ChatService>());
});
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<CreateChatSessionRequest>()
.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<CreateChatSessionRequest>()
.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 = <ChatBasicSession>[];
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<SendTextMessageRequest>()
.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 = <ChatMessageResponse>[];
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);
});
});
});
}

View file

@ -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<ChatStateManager>());
});
test('has proper constructor signature', () {
expect(
() => ChatStateManager(chatService: mockChatService),
returnsNormally,
);
});
test('state is accessible', () {
expect(stateManager.state, isA<ChatState>());
});
test('has initializeChat method', () {
expect(stateManager.initializeChat, isA<Function>());
});
test('has sendTextMessage method', () {
expect(stateManager.sendTextMessage, isA<Function>());
});
test('has sendAudioMessage method', () {
expect(stateManager.sendAudioMessage, isA<Function>());
});
test('has loadMoreMessages method', () {
expect(stateManager.loadMoreMessages, isA<Function>());
});
test('has clearError method', () {
expect(stateManager.clearError, isA<Function>());
});
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<ChatStateLoaded>());
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<ChatStateError>());
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<ChatStateLoaded>());
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<ChatStateLoaded>());
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<ChatStateLoaded>());
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<ChatStateLoaded>());
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<ChatState>()); // Should not crash
});
});
});
}

View file

@ -19,6 +19,7 @@ import '../../discounts/discounts_manager.dart' as _i891;
import '../../packs/free_packs_distributor.dart' as _i1062; import '../../packs/free_packs_distributor.dart' as _i1062;
import '../../packs/pack_dto_converter.dart' as _i433; import '../../packs/pack_dto_converter.dart' as _i433;
import '../../packs/pack_manager.dart' as _i833; import '../../packs/pack_manager.dart' as _i833;
import '../../packs/pack_repository.dart' as _i258;
import '../../packs/products_price_resolver.dart' as _i908; import '../../packs/products_price_resolver.dart' as _i908;
import '../../promo_codes/promo_codes_manager.dart' as _i151; import '../../promo_codes/promo_codes_manager.dart' as _i151;
import '../../statistics/achievement_manager.dart' as _i802; 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 '../../tests/test_manager.dart' as _i259;
import '../../user/user_manager.dart' as _i280; import '../../user/user_manager.dart' as _i280;
import '../../user/user_manager_drift.dart' as _i560; import '../../user/user_manager_drift.dart' as _i560;
import '../../user/user_repository.dart' as _i950;
import '../ads/ads_manager.dart' as _i846; import '../ads/ads_manager.dart' as _i846;
import '../mnemo_shelf.dart' as _i561; import '../mnemo_shelf.dart' as _i561;
import '../purchase/payment_manager.dart' as _i1009; import '../purchase/payment_manager.dart' as _i1009;
@ -105,14 +107,11 @@ extension GetItInjectableX on _i174.GetIt {
gh.lazySingleton<_i108.JwtService>( gh.lazySingleton<_i108.JwtService>(
() => _i108.JwtService(gh<_i1072.AppDatabase>()), () => _i108.JwtService(gh<_i1072.AppDatabase>()),
); );
gh.lazySingleton<_i296.TelegramBotApiV2>(
() => _i296.TelegramBotApiV2(gh<_i1072.AppDatabase>()),
);
gh.lazySingleton<_i891.DiscountsManager>( gh.lazySingleton<_i891.DiscountsManager>(
() => _i891.DiscountsManager(gh<_i1072.AppDatabase>()), () => _i891.DiscountsManager(gh<_i1072.AppDatabase>()),
); );
gh.lazySingleton<_i1062.FreePacksDistributor>( gh.lazySingleton<_i258.PackRepository>(
() => _i1062.FreePacksDistributor(gh<_i1072.AppDatabase>()), () => _i258.PackRepository(gh<_i1072.AppDatabase>()),
); );
gh.lazySingleton<_i802.AchievementManager>( gh.lazySingleton<_i802.AchievementManager>(
() => _i802.AchievementManager(gh<_i1072.AppDatabase>()), () => _i802.AchievementManager(gh<_i1072.AppDatabase>()),
@ -129,46 +128,42 @@ extension GetItInjectableX on _i174.GetIt {
gh.lazySingleton<_i586.TaskManager>( gh.lazySingleton<_i586.TaskManager>(
() => _i586.TaskManager(gh<_i1072.AppDatabase>()), () => _i586.TaskManager(gh<_i1072.AppDatabase>()),
); );
gh.lazySingleton<_i950.UserRepository>(
() => _i950.UserRepository(gh<_i1072.AppDatabase>()),
);
gh.factory<_i1015.AdminPacksApiV2>( gh.factory<_i1015.AdminPacksApiV2>(
() => _i1015.AdminPacksApiV2(gh<_i1072.AppDatabase>()), () => _i1015.AdminPacksApiV2(gh<_i1072.AppDatabase>()),
); );
gh.factory<_i895.AdminUsersApiV2>(
() => _i895.AdminUsersApiV2(gh<_i1072.AppDatabase>()),
);
gh.lazySingleton<_i964.SubscriptionsApiV2>( gh.lazySingleton<_i964.SubscriptionsApiV2>(
() => _i964.SubscriptionsApiV2(gh<_i377.SubscriptionManager>()), () => _i964.SubscriptionsApiV2(gh<_i377.SubscriptionManager>()),
); );
gh.lazySingleton<_i858.DiscountsApiV2>( gh.lazySingleton<_i858.DiscountsApiV2>(
() => _i858.DiscountsApiV2(gh<_i891.DiscountsManager>()), () => _i858.DiscountsApiV2(gh<_i891.DiscountsManager>()),
); );
gh.lazySingleton<_i1009.PaymentManager>( gh.lazySingleton<_i296.TelegramBotApiV2>(
() => _i1009.PaymentManager( () => _i296.TelegramBotApiV2(
gh<_i1072.AppDatabase>(), gh<_i1072.AppDatabase>(),
gh<_i988.YooMoneyHandler>(), gh<_i950.UserRepository>(),
gh<_i222.RustorePurchaseHandler>(), ),
);
gh.factory<_i895.AdminUsersApiV2>(
() => _i895.AdminUsersApiV2(
gh<_i1072.AppDatabase>(),
gh<_i950.UserRepository>(),
), ),
); );
gh.lazySingleton<_i365.MediaApiV2>( gh.lazySingleton<_i365.MediaApiV2>(
() => _i365.MediaApiV2(gh<_i747.MinioService>()), () => _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>( gh.lazySingleton<_i985.TasksApiV2>(
() => _i985.TasksApiV2(gh<_i586.TaskManager>()), () => _i985.TasksApiV2(gh<_i586.TaskManager>()),
); );
gh.lazySingleton<_i483.AdminAuthApiV2>( gh.lazySingleton<_i1009.PaymentManager>(
() => _i483.AdminAuthApiV2( () => _i1009.PaymentManager(
gh<_i240.TelegramAuthCodeService>(), gh<_i1072.AppDatabase>(),
gh<_i280.UserManager>(), gh<_i950.UserRepository>(),
gh<_i108.JwtService>(), gh<_i988.YooMoneyHandler>(),
gh<_i222.RustorePurchaseHandler>(),
), ),
); );
gh.lazySingleton<_i151.PromoCodesManager>( gh.lazySingleton<_i151.PromoCodesManager>(
@ -177,6 +172,12 @@ extension GetItInjectableX on _i174.GetIt {
gh<_i1009.PaymentManager>(), gh<_i1009.PaymentManager>(),
), ),
); );
gh.lazySingleton<_i1062.FreePacksDistributor>(
() => _i1062.FreePacksDistributor(
gh<_i1072.AppDatabase>(),
gh<_i258.PackRepository>(),
),
);
gh.lazySingleton<_i560.UserManager>( gh.lazySingleton<_i560.UserManager>(
() => _i560.UserManager( () => _i560.UserManager(
gh<_i1072.AppDatabase>(), gh<_i1072.AppDatabase>(),
@ -186,17 +187,6 @@ extension GetItInjectableX on _i174.GetIt {
gh<_i802.AchievementManager>(), 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>( gh.lazySingleton<_i273.PromocodesApiV2>(
() => _i273.PromocodesApiV2(gh<_i151.PromoCodesManager>()), () => _i273.PromocodesApiV2(gh<_i151.PromoCodesManager>()),
); );
@ -212,6 +202,55 @@ extension GetItInjectableX on _i174.GetIt {
gh<_i1072.AppDatabase>(), 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>( gh.lazySingleton<_i170.TestsApiV2>(
() => _i170.TestsApiV2( () => _i170.TestsApiV2(
gh<_i259.TestManager>(), gh<_i259.TestManager>(),
@ -228,25 +267,6 @@ extension GetItInjectableX on _i174.GetIt {
gh<_i240.TelegramAuthCodeService>(), 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>( gh.lazySingleton<_i800.PacksApiV2>(
() => _i800.PacksApiV2( () => _i800.PacksApiV2(
gh<_i833.PackManager>(), gh<_i833.PackManager>(),

View file

@ -10,7 +10,7 @@ import 'package:drift/drift.dart' as drift;
import 'payment_drift_extension.dart'; import 'payment_drift_extension.dart';
import 'rustore/rustore_purchase_handler.dart'; import 'rustore/rustore_purchase_handler.dart';
import 'yoo_money.dart'; import 'yoo_money.dart';
import '../../user/user_drift_extension.dart'; import '../../user/user_repository.dart';
/// Result of creating YooKassa payment URL /// Result of creating YooKassa payment URL
class YookassaPaymentResult { class YookassaPaymentResult {
@ -26,11 +26,17 @@ class YookassaPaymentResult {
@lazySingleton @lazySingleton
class PaymentManager { class PaymentManager {
final AppDatabase _db; final AppDatabase _db;
final UserRepository _userRepository;
late final GooglePlayPurchaseHandler googlePurchaseHandler; late final GooglePlayPurchaseHandler googlePurchaseHandler;
final YooMoneyHandler _yooMoneyHandler; final YooMoneyHandler _yooMoneyHandler;
final RustorePurchaseHandler _rustorePurchaseHandler; final RustorePurchaseHandler _rustorePurchaseHandler;
PaymentManager(this._db, this._yooMoneyHandler, this._rustorePurchaseHandler); PaymentManager(
this._db,
this._userRepository,
this._yooMoneyHandler,
this._rustorePurchaseHandler,
);
/// Создать платеж в базе данных /// Создать платеж в базе данных
Future<PaymentDto> createPayment(PaymentDto paymentDto, String userId) async { Future<PaymentDto> createPayment(PaymentDto paymentDto, String userId) async {
@ -74,7 +80,7 @@ class PaymentManager {
await _db.transaction(() async { await _db.transaction(() async {
if (product.type == MnemoCardsProductType.pack && product.id != null) { if (product.type == MnemoCardsProductType.pack && product.id != null) {
final packId = product.id!; final packId = product.id!;
await _db.userDao.grantPackAccess( await _userRepository.grantPackAccess(
userId: userId, userId: userId,
packId: packId, packId: packId,
grantType: 'promo_code', grantType: 'promo_code',
@ -114,8 +120,8 @@ class PaymentManager {
} }
// Получить пользователя // Получить пользователя
final user = await _db.userDao.getUserWithDataById(payment.userId); final userModel = await _userRepository.getUserWithDataById(payment.userId);
if (user == null) { if (userModel == null) {
log('User not found: ${payment.userId}'); log('User not found: ${payment.userId}');
return; return;
} }
@ -154,7 +160,7 @@ class PaymentManager {
await _db.transaction(() async { await _db.transaction(() async {
// Дать доступ к пакетам // Дать доступ к пакетам
for (final packId in packIds) { for (final packId in packIds) {
await _db.userDao.grantPackAccess( await _userRepository.grantPackAccess(
userId: payment.userId, userId: payment.userId,
packId: packId, packId: packId,
grantType: 'purchase', grantType: 'purchase',
@ -422,11 +428,11 @@ class PaymentManager {
final firstProduct = payment.products.first; final firstProduct = payment.products.first;
final productId = firstProduct['id']?.toString(); final productId = firstProduct['id']?.toString();
if (productId != null && payment.externalToken != null) { if (productId != null && payment.externalToken != null) {
final user = await _db.userDao.getUserById(payment.userId); final user = await _userRepository.getUserById(payment.userId);
await checkRustorePayment( await checkRustorePayment(
productId: productId, productId: productId,
subscriptionToken: payment.externalToken!, subscriptionToken: payment.externalToken!,
user: user != null ? await user.toUserModel() : null, user: user,
); );
} }
} }

View file

@ -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/acl_types.dart';
import 'package:mnemo_cards_backend/api/authorize/access_service.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/api/authorize/helpers.dart';
import 'package:mnemo_cards_backend/main.dart' as backend_main; import 'package:mnemo_cards_backend/database/database.dart';
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart'; import 'package:mnemo_cards_backend/user/user_repository.dart';
import 'package:shelf/shelf.dart'; import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.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. /// Admin endpoints for analytics and statistics in API v2.
@lazySingleton @lazySingleton
class AdminAnalyticsApiV2 { class AdminAnalyticsApiV2 {
AdminAnalyticsApiV2(); final UserRepository _userRepository;
final AppDatabase _db;
AdminAnalyticsApiV2(this._userRepository, this._db);
Response _json( Response _json(
Object? data, { Object? data, {
@ -51,23 +54,22 @@ class AdminAnalyticsApiV2 {
} }
// Get basic statistics // 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 // Get all packs and filter in memory
final enabledPacks = await backend_main.database.packDao.getAllPacks( final enabledPacks = await _db.packDao.getAllPacks(
enabledOnly: true, enabledOnly: true,
); );
final enabledPackCount = enabledPacks.length; final enabledPackCount = enabledPacks.length;
final paymentCount = await backend_main.database.paymentDao final paymentCount = await _db.paymentDao.countAllPayments();
.countAllPayments();
// Get recent users (last 10) // Get recent users (last 10)
final allUsers = await backend_main.database.userDao.getAllUsers( final allUsers = await _userRepository.getAllUsers(
limit: 10, limit: 10,
); );
final recentUsers = allUsers final recentUsers = allUsers
@ -76,17 +78,16 @@ class AdminAnalyticsApiV2 {
'id': u.id, 'id': u.id,
'name': u.name, 'name': u.name,
'email': u.email, 'email': u.email,
'createdAt': u.createdAt.dateTime.toIso8601String(),
}, },
) )
.toList(); .toList();
// Get top packs by user count // 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 topPacksList = allPacks.take(5);
final topPacks = await Future.wait( final topPacks = await Future.wait(
topPacksList.map((p) async { topPacksList.map((p) async {
final cards = await backend_main.database.packDao.getPackCards(p.id); final cards = await _db.packDao.getPackCards(p.id);
return { return {
'id': p.id, 'id': p.id,
'title': p.title, 'title': p.title,

View file

@ -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/api/authorize/helpers.dart';
import 'package:mnemo_cards_backend/packs/card_pack_drift_extension.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/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/mnemo_cards_common.dart';
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
import 'package:shelf/shelf.dart'; import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart'; import 'package:shelf_router/shelf_router.dart';
@ -19,8 +21,9 @@ part 'admin_packs_api_v2.g.dart';
@injectable @injectable
class AdminPacksApiV2 { class AdminPacksApiV2 {
final AppDatabase _db; final AppDatabase _db;
final PackRepository _packRepository;
AdminPacksApiV2(this._db); AdminPacksApiV2(this._db, this._packRepository);
static final _uuidRegex = RegExp( 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}$', 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, required String cardId,
}) async { }) async {
try { try {
final card = await _db.packDao.getCardById(cardId); final card = await _packRepository.getCardById(cardId);
if (card == null) return; if (card == null) return;
await _db.packDao.addCardToPack(packId: packId, cardId: cardId); await _packRepository.addCardToPack(packId: packId, cardId: cardId);
} catch (_) { } catch (_) {
// Best-effort only. // Best-effort only.
} }
@ -67,6 +70,7 @@ class AdminPacksApiV2 {
mnemo: const drift.Value('test_image'), 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); final cardId = await _db.packDao.createCard(companion);
await _tryLinkCardToPack(packId: packId, cardId: cardId); await _tryLinkCardToPack(packId: packId, cardId: cardId);
@ -78,11 +82,16 @@ class AdminPacksApiV2 {
); );
if (stored == null) return null; if (stored == null) return null;
final created = await _db.packDao.getCardById(cardId); final created = await _packRepository.getCardById(cardId);
if (created == null) return null; 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( await _db.packDao.updateCard(
created.copyWith( driftCard.copyWith(
image: stored.fileName, image: stored.fileName,
updatedAt: PgDateTime(DateTime.now()), updatedAt: PgDateTime(DateTime.now()),
), ),
@ -278,25 +287,41 @@ class AdminPacksApiV2 {
final validatedLimit = limit < 1 ? 20 : (limit > 100 ? 100 : limit); final validatedLimit = limit < 1 ? 20 : (limit > 100 ? 100 : limit);
// Get all packs // Get all packs
final allPacks = await _db.packDao.getAllPacks( final allPacks = await _packRepository.getAllPacks(
enabledOnly: !showDisabled, enabledOnly: !showDisabled,
orderByField: 'order', orderByField: 'order',
); );
// Apply search filter if provided // Apply search filter if provided
List<CardPack> filteredPacks = allPacks; List<CardPackModel> filteredPacks = allPacks;
if (search.isNotEmpty) { if (search.isNotEmpty) {
final searchTerm = search.toLowerCase(); final searchTerm = search.toLowerCase();
filteredPacks = allPacks.where((pack) { filteredPacks = allPacks.where((pack) {
return pack.title.toLowerCase().contains(searchTerm) || return pack.title.toLowerCase().contains(searchTerm) ||
pack.subtitle.toLowerCase().contains(searchTerm) || pack.subtitle.toLowerCase().contains(searchTerm) ||
pack.id.toLowerCase().contains(searchTerm); (pack.id?.toLowerCase().contains(searchTerm) ?? false);
}).toList(); }).toList();
} }
// Convert to preview DTOs // 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( 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); final dto = await pack.toPreviewDto(null);
return { return {
'id': dto.id, 'id': dto.id,
@ -357,7 +382,7 @@ class AdminPacksApiV2 {
}, statusCode: 400); }, statusCode: 400);
} }
final pack = await _db.packDao.getPackById(packId); final pack = await _packRepository.getPackById(packId);
if (pack == null) { if (pack == null) {
return _json({ return _json({
'error': 'Pack not found', 'error': 'Pack not found',
@ -367,13 +392,13 @@ class AdminPacksApiV2 {
}, statusCode: 404); }, 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 // Get cards order - cards are already sorted by order from getPackCards
final cardsOrder = cards.map((c) => c.id).toList(); final cardsOrder = cards.map((c) => c.id).toList();
// Get preview cards // Get preview cards
final previewCards = await _db.packDao.getPreviewCards(packId); final previewCards = await _packRepository.getPreviewCards(packId);
final previewCardIds = previewCards.map((c) => c.id).toList(); final previewCardIds = previewCards.map((c) => c.id).toList();
// Get tests for this pack // Get tests for this pack
@ -447,7 +472,7 @@ class AdminPacksApiV2 {
if (editDto.id != null && editDto.id!.isNotEmpty) { if (editDto.id != null && editDto.id!.isNotEmpty) {
// Update existing pack // Update existing pack
packId = editDto.id!; packId = editDto.id!;
final existing = await _db.packDao.getPackById(packId); final existing = await _packRepository.getPackById(packId);
if (existing == null) { if (existing == null) {
return _json({ return _json({
'error': 'Pack not found', 'error': 'Pack not found',
@ -457,7 +482,7 @@ class AdminPacksApiV2 {
}, statusCode: 404); }, statusCode: 404);
} }
await _db.packDao.updatePackPartial( await _packRepository.updatePackPartial(
CardPacksCompanion( CardPacksCompanion(
id: drift.Value(packId), id: drift.Value(packId),
title: editDto.title != null title: editDto.title != null
@ -504,39 +529,26 @@ class AdminPacksApiV2 {
); );
} else { } else {
// Create new pack // Create new pack
packId = await _db.packDao.createPack( // Note: We need to create a CardPackModel first
CardPacksCompanion.insert( final packModel = CardPackModel(
title: editDto.title ?? '', id: null,
subtitle: editDto.subtitle ?? '', title: editDto.title ?? '',
color: editDto.color != null subtitle: editDto.subtitle ?? '',
? drift.Value(editDto.color) size: editDto.size ?? 0,
: const drift.Value.absent(), color: editDto.color,
cover: editDto.cover != null version: editDto.version,
? drift.Value(editDto.cover) cover: editDto.cover,
: const drift.Value.absent(), description: editDto.description,
size: editDto.size ?? 0, googlePlayId: editDto.googlePlayId,
googlePlayId: editDto.googlePlayId != null rustoreId: editDto.rustoreId,
? drift.Value(editDto.googlePlayId) appStoreId: editDto.appStoreId,
: const drift.Value.absent(), price: editDto.price,
rustoreId: editDto.rustoreId != null currency: null,
? drift.Value(editDto.rustoreId) enabled: editDto.enabled ?? true,
: const drift.Value.absent(), order: editDto.order ?? 0,
appStoreId: editDto.appStoreId != null cardsOrder: [],
? 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),
),
); );
packId = await _packRepository.createPack(packModel);
} }
// Handle card associations if provided // Handle card associations if provided
@ -544,7 +556,7 @@ class AdminPacksApiV2 {
try { try {
for (final cardId in editDto.addCardIds!) { for (final cardId in editDto.addCardIds!) {
// Verify card exists before adding // Verify card exists before adding
final card = await _db.packDao.getCardById(cardId); final card = await _packRepository.getCardById(cardId);
if (card == null) { if (card == null) {
return _json({ return _json({
'error': 'Card not found', '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.', 'Card with ID "$cardId" was not found. Please verify all card IDs before adding them to the pack.',
}, statusCode: 404); }, statusCode: 404);
} }
await _db.packDao.addCardToPack(packId: packId, cardId: cardId); await _packRepository.addCardToPack(packId: packId, cardId: cardId);
} }
} catch (e) { } catch (e) {
// Handle duplicate or constraint errors // Handle duplicate or constraint errors
@ -575,7 +587,7 @@ class AdminPacksApiV2 {
if (editDto.removeCardIds != null && editDto.removeCardIds!.isNotEmpty) { if (editDto.removeCardIds != null && editDto.removeCardIds!.isNotEmpty) {
for (final cardId in editDto.removeCardIds!) { 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 // Handle cards order if provided
if (editDto.cardsOrder != null && editDto.cardsOrder!.isNotEmpty) { if (editDto.cardsOrder != null && editDto.cardsOrder!.isNotEmpty) {
await _db.packDao.updatePackCardsOrder(packId, editDto.cardsOrder!); await _packRepository.updatePackCardsOrder(packId, editDto.cardsOrder!);
} }
// Handle preview cards // Handle preview cards
if (editDto.previewCards != null) { 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) { if (updatedPack == null) {
return _json({ return _json({
'error': 'Database error', 'error': 'Database error',
@ -643,9 +655,9 @@ class AdminPacksApiV2 {
}, statusCode: 500); }, statusCode: 500);
} }
final updatedCards = await _db.packDao.getPackCards(packId); final updatedCards = await _packRepository.getPackCards(packId);
final cardsOrder = updatedCards.map((c) => c.id).toList(); 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(); final previewCardIds = previewCards.map((c) => c.id).toList();
// Get updated tests for this pack // Get updated tests for this pack
@ -726,7 +738,7 @@ class AdminPacksApiV2 {
}, statusCode: 400); }, statusCode: 400);
} }
final pack = await _db.packDao.getPackById(packId); final pack = await _packRepository.getPackById(packId);
if (pack == null) { if (pack == null) {
return _json({ return _json({
'error': 'Pack not found', 'error': 'Pack not found',
@ -736,7 +748,7 @@ class AdminPacksApiV2 {
}, statusCode: 404); }, statusCode: 404);
} }
await _db.packDao.softDeletePack(packId); await _packRepository.softDeletePack(packId);
return _json({'success': true, 'message': 'Pack deleted successfully'}); return _json({'success': true, 'message': 'Pack deleted successfully'});
} catch (e, s) { } catch (e, s) {

View file

@ -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/access_service.dart';
import 'package:mnemo_cards_backend/api/authorize/helpers.dart'; import 'package:mnemo_cards_backend/api/authorize/helpers.dart';
import 'package:mnemo_cards_backend/database/database.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' import 'package:mnemo_cards_backend/user/user_model.dart'
show UserModelExtension; 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/mnemo_cards_common.dart';
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
import 'package:shelf/shelf.dart'; import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart'; import 'package:shelf_router/shelf_router.dart';
@ -20,8 +21,9 @@ part 'admin_users_api_v2.g.dart';
@injectable @injectable
class AdminUsersApiV2 { class AdminUsersApiV2 {
final AppDatabase _db; final AppDatabase _db;
final UserRepository _userRepository;
AdminUsersApiV2(this._db); AdminUsersApiV2(this._db, this._userRepository);
Response _json( Response _json(
Object? data, { Object? data, {
@ -78,22 +80,22 @@ class AdminUsersApiV2 {
final validatedLimit = limit < 1 ? 20 : (limit > 100 ? 100 : limit); final validatedLimit = limit < 1 ? 20 : (limit > 100 ? 100 : limit);
// Get total count // Get total count
final total = await _db.userDao.countUsers(includeDeleted: false); final total = await _userRepository.countUsers(includeDeleted: false);
// Calculate offset // Calculate offset
final offset = (page - 1) * validatedLimit; final offset = (page - 1) * validatedLimit;
// Get users with pagination // Get users with pagination
final users = await _db.userDao.getAllUsers( final userModels = await _userRepository.getAllUsers(
limit: validatedLimit, limit: validatedLimit,
offset: offset, offset: offset,
includeDeleted: false, includeDeleted: false,
includePacks: true,
); );
// Convert to DTOs // Convert to DTOs
final userDtos = <Map<String, dynamic>>[]; final userDtos = <Map<String, dynamic>>[];
for (final user in users) { for (final userModel in userModels) {
final userModel = await user.toUserModel();
final dto = await userModel.toDto(); final dto = await userModel.toDto();
userDtos.add(dto.toJson()); userDtos.add(dto.toJson());
} }
@ -158,8 +160,8 @@ class AdminUsersApiV2 {
}, statusCode: 400); }, statusCode: 400);
} }
final user = await _db.userDao.getUserById(userId); final userModel = await _userRepository.getUserById(userId, includePacks: true);
if (user == null) { if (userModel == null) {
return _json({ return _json({
'error': 'User not found', 'error': 'User not found',
'message': 'The requested user does not exist or has been deleted', 'message': 'The requested user does not exist or has been deleted',
@ -168,7 +170,6 @@ class AdminUsersApiV2 {
}, statusCode: 404); }, statusCode: 404);
} }
final userModel = await user.toUserModel();
final dto = await userModel.toDto(); final dto = await userModel.toDto();
return _json(dto.toJson()); return _json(dto.toJson());
@ -218,8 +219,8 @@ class AdminUsersApiV2 {
if (isUpdate) { if (isUpdate) {
// Update existing user // Update existing user
final existingUser = await _db.userDao.getUserById(userDto.id!); final existingUserModel = await _userRepository.getUserById(userDto.id!);
if (existingUser == null) { if (existingUserModel == null) {
return _json({ return _json({
'error': 'User not found', 'error': 'User not found',
'message': 'The user you are trying to update does not exist', 'message': 'The user you are trying to update does not exist',
@ -250,11 +251,11 @@ class AdminUsersApiV2 {
updatedAt: drift.Value(PgDateTime(DateTime.now())), updatedAt: drift.Value(PgDateTime(DateTime.now())),
); );
await _db.userDao.updateUserPartial(updates); await _userRepository.updateUserPartial(updates);
// Get updated user // Get updated user
final updatedUser = await _db.userDao.getUserById(userDto.id!); final updatedUserModel = await _userRepository.getUserById(userDto.id!, includePacks: true);
if (updatedUser == null) { if (updatedUserModel == null) {
return _json({ return _json({
'error': 'Database error', 'error': 'Database error',
'message': 'Failed to retrieve updated user', 'message': 'Failed to retrieve updated user',
@ -263,8 +264,7 @@ class AdminUsersApiV2 {
}, statusCode: 500); }, statusCode: 500);
} }
final userModel = await updatedUser.toUserModel(); final dto = await updatedUserModel.toDto();
final dto = await userModel.toDto();
return _json({'result': true, 'user': dto.toJson()}); return _json({'result': true, 'user': dto.toJson()});
} else { } else {
@ -275,10 +275,8 @@ class AdminUsersApiV2 {
: 'admin_created_${DateTime.now().millisecondsSinceEpoch}'; : 'admin_created_${DateTime.now().millisecondsSinceEpoch}';
// Check if user with this externalUserId already exists // Check if user with this externalUserId already exists
final existingUser = await _db.userDao.getUserByExternalId( final existingUserModel = await _userRepository.getUserByExternalId(externalUserId);
externalUserId, if (existingUserModel != null) {
);
if (existingUser != null) {
return _json({ return _json({
'error': 'User already exists', 'error': 'User already exists',
'message': 'A user with this email already exists', 'message': 'A user with this email already exists',
@ -288,30 +286,29 @@ class AdminUsersApiV2 {
} }
final now = DateTime.now(); 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( final userDataCompanion = UserDatasCompanion.insert(
userId: '', // Will be set by createUserWithData userId: '', // Will be set by createUserWithData
registrationDate: drift.Value(PgDateTime(now)), registrationDate: drift.Value(PgDateTime(now)),
); );
final userId = await _db.userDao.createUserWithData( final userModel = UserModel(
user: userCompanion, 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, userData: userDataCompanion,
); );
final createdUser = await _db.userDao.getUserById(userId); final createdUserModel = await _userRepository.getUserById(userId, includePacks: true);
if (createdUser == null) { if (createdUserModel == null) {
return _json({ return _json({
'error': 'Database error', 'error': 'Database error',
'message': 'Failed to retrieve created user', 'message': 'Failed to retrieve created user',
@ -320,8 +317,7 @@ class AdminUsersApiV2 {
}, statusCode: 500); }, statusCode: 500);
} }
final userModel = await createdUser.toUserModel(); final dto = await createdUserModel.toDto();
final dto = await userModel.toDto();
return _json({'result': true, 'user': dto.toJson()}); return _json({'result': true, 'user': dto.toJson()});
} }
@ -355,8 +351,8 @@ class AdminUsersApiV2 {
}, statusCode: 400); }, statusCode: 400);
} }
final user = await _db.userDao.getUserById(userId); final userModel = await _userRepository.getUserById(userId);
if (user == null) { if (userModel == null) {
return _json({ return _json({
'error': 'User not found', 'error': 'User not found',
'message': 'The user you are trying to delete does not exist', 'message': 'The user you are trying to delete does not exist',
@ -365,7 +361,9 @@ class AdminUsersApiV2 {
}, statusCode: 404); }, 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({ return _json({
'error': 'User already deleted', 'error': 'User already deleted',
'message': 'The user has already been deleted', 'message': 'The user has already been deleted',
@ -373,7 +371,7 @@ class AdminUsersApiV2 {
}, statusCode: 409); }, statusCode: 409);
} }
await _db.userDao.softDeleteUser(userId); await _userRepository.softDeleteUser(userId);
return _json({'result': true}); return _json({'result': true});
} catch (e, s) { } catch (e, s) {

View file

@ -8,13 +8,15 @@ import 'package:mnemo_cards_backend/database/database.dart' hide VoiceModel;
import 'package:mnemo_cards_backend/database/database.dart' import 'package:mnemo_cards_backend/database/database.dart'
as drift as drift
show VoiceModel; 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_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_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/packs/voice_storage.dart';
import 'package:mnemo_cards_backend/storage/minio_config.dart'; import 'package:mnemo_cards_backend/storage/minio_config.dart';
import 'package:mnemo_cards_backend/storage/minio_service.dart'; import 'package:mnemo_cards_backend/storage/minio_service.dart';
import 'package:mnemo_cards_backend/tests/test_manager.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_backend/mnemo_cards_common_backend.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'package:shelf/shelf.dart'; import 'package:shelf/shelf.dart';
@ -30,12 +32,16 @@ part 'packs_api_v2.g.dart';
class PacksApiV2 { class PacksApiV2 {
final PackManager _packManager; final PackManager _packManager;
final TestManager _testManager; final TestManager _testManager;
final PackRepository _packRepository;
final UserRepository _userRepository;
final AppDatabase _db; final AppDatabase _db;
final MinioService _minioService; final MinioService _minioService;
PacksApiV2( PacksApiV2(
this._packManager, this._packManager,
this._testManager, this._testManager,
this._packRepository,
this._userRepository,
this._db, this._db,
this._minioService, this._minioService,
); );
@ -366,13 +372,13 @@ class PacksApiV2 {
user: request.user, user: request.user,
); );
final pack = await _db.packDao.getPackById(packId); final pack = await _packRepository.getPackById(packId);
if (pack == null) { if (pack == null) {
return _notFound('Pack not found'); return _notFound('Pack not found');
} }
// Get cards for pack // Get cards for pack
final cards = await _db.packDao.getPackCards(packId); final cards = await _packRepository.getPackCards(packId);
// Parse pagination // Parse pagination
final queryParams = request.requestedUri.queryParameters; final queryParams = request.requestedUri.queryParameters;
@ -396,7 +402,7 @@ class PacksApiV2 {
final cardIds = paginatedCards.map((c) => c.id).toList(); final cardIds = paginatedCards.map((c) => c.id).toList();
final allVoices = <drift.VoiceModel>[]; final allVoices = <drift.VoiceModel>[];
for (final cardId in cardIds) { for (final cardId in cardIds) {
final voices = await _db.packDao.getCardVoices(cardId); final voices = await _packRepository.getCardVoices(cardId);
allVoices.addAll(voices); allVoices.addAll(voices);
} }
@ -404,9 +410,7 @@ class PacksApiV2 {
// Presigned URLs now have 7 days expiration to avoid 403 errors on cached data // Presigned URLs now have 7 days expiration to avoid 403 errors on cached data
final cardDtos = await Future.wait( final cardDtos = await Future.wait(
paginatedCards.map((card) async { paginatedCards.map((card) async {
final dto = await card.toDto( final dto = card.toDto();
allVoices.where((v) => v.cardId == card.id).toList(),
);
// Generate presigned URLs for images if they are object IDs (UUIDs) // Generate presigned URLs for images if they are object IDs (UUIDs)
final imageUrl = await _getPresignedUrlIfUuid( final imageUrl = await _getPresignedUrlIfUuid(
@ -468,7 +472,7 @@ class PacksApiV2 {
// Check if pack exists and is enabled // Check if pack exists and is enabled
// We allow access to images for enabled packs even without auth // We allow access to images for enabled packs even without auth
// to support image previews in public listings // 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}'); print('Pack lookup result: pack=$pack, enabled=${pack?.enabled}');
if (pack == null || !pack.enabled) { if (pack == null || !pack.enabled) {
print('Pack not found or not enabled'); print('Pack not found or not enabled');
@ -476,7 +480,7 @@ class PacksApiV2 {
} }
// Get card and verify it belongs to the pack // 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}'); print('Card lookup result: card=$card, image=${card?.image}');
if (card == null) { if (card == null) {
print('Card not found'); print('Card not found');
@ -484,7 +488,7 @@ class PacksApiV2 {
} }
// Verify card belongs to this pack // 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}'); print('Pack cards count: ${packCards.length}');
final belongsToPack = packCards.any((c) => c.id == cardId); final belongsToPack = packCards.any((c) => c.id == cardId);
print('Card belongs to pack: $belongsToPack'); print('Card belongs to pack: $belongsToPack');
@ -546,19 +550,19 @@ class PacksApiV2 {
// Check if pack exists and is enabled // Check if pack exists and is enabled
// We allow access to images for enabled packs even without auth // We allow access to images for enabled packs even without auth
// to support image previews in public listings // 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) { if (pack == null || !pack.enabled) {
return _notFound('Pack not found or not enabled'); return _notFound('Pack not found or not enabled');
} }
// Get card and verify it belongs to the pack // 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) { if (card == null || card.imageBack == null || card.imageBack!.isEmpty) {
return _notFound('Card or back image not found'); return _notFound('Card or back image not found');
} }
// Verify card belongs to this pack // 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); final belongsToPack = packCards.any((c) => c.id == cardId);
if (!belongsToPack) { if (!belongsToPack) {
return _notFound('Card does not belong to this pack'); return _notFound('Card does not belong to this pack');
@ -611,7 +615,7 @@ class PacksApiV2 {
// Check if pack exists and is enabled // Check if pack exists and is enabled
// We allow access to covers for enabled packs even without auth // We allow access to covers for enabled packs even without auth
// to support image previews in public listings // 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) { if (pack == null || !pack.enabled) {
return _notFound('Pack not found or not enabled'); return _notFound('Pack not found or not enabled');
} }
@ -665,25 +669,25 @@ class PacksApiV2 {
return _badRequest('Invalid pack ID'); return _badRequest('Invalid pack ID');
} }
final pack = await _db.packDao.getPackById(packId); final pack = await _packRepository.getPackById(packId);
if (pack == null || !pack.enabled) { if (pack == null || !pack.enabled) {
return _notFound('Pack not found or not 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) { if (card == null) {
return _notFound('Card not found'); return _notFound('Card not found');
} }
// Verify card belongs to this pack // 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); final belongsToPack = packCards.any((c) => c.id == cardId);
if (!belongsToPack) { if (!belongsToPack) {
return _notFound('Card does not belong to this pack'); return _notFound('Card does not belong to this pack');
} }
// Get voices for card // 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'); print('🔍 getCardVoices: Found ${voices.length} voices for card $cardId');
if (voices.isEmpty) { if (voices.isEmpty) {
print('⚠️ getCardVoices: No voices found for card $cardId'); print('⚠️ getCardVoices: No voices found for card $cardId');
@ -742,7 +746,7 @@ class PacksApiV2 {
); );
if (stored != null) { if (stored != null) {
voicePath = stored.fileName; voicePath = stored.fileName;
await _db.packDao.updateVoiceUrl(voice.id, stored.fileName); await _packRepository.updateVoiceUrl(voice.id, stored.fileName);
} else { } else {
voicePath = ''; voicePath = '';
} }
@ -776,20 +780,21 @@ class PacksApiV2 {
return _badRequest('Invalid voice ID'); return _badRequest('Invalid voice ID');
} }
final voice = await _db.packDao.getVoiceById(voiceId); final voice = await _packRepository.getVoiceById(voiceId);
if (voice == null || voice.voiceUrl.isEmpty) { if (voice == null || voice.voiceUrl.isEmpty) {
return _notFound('Voice not found'); return _notFound('Voice not found');
} }
// Get cards that use this voice by checking CardVoices junction table // 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 // 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; bool hasEnabledPack = false;
for (final pack in allPacks) { 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) { 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)) { if (cardVoices.any((v) => v.id == voiceId)) {
hasEnabledPack = true; hasEnabledPack = true;
break; break;
@ -831,7 +836,7 @@ class PacksApiV2 {
/// Check if user purchased a pack /// Check if user purchased a pack
Future<bool> _isPackPurchased(String userId, String packId) async { Future<bool> _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 /// GET /api/v2/packs/{packId}/tests

View file

@ -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/database/database.dart';
import 'package:mnemo_cards_backend/packs/pack_manager.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/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_backend/mnemo_cards_common_backend.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'package:shelf/shelf.dart'; import 'package:shelf/shelf.dart';
@ -21,9 +22,15 @@ part 'purchases_api_v2.g.dart';
class PurchasesApiV2 { class PurchasesApiV2 {
final PaymentManager _paymentManager; final PaymentManager _paymentManager;
final PackManager _packManager; final PackManager _packManager;
final UserRepository _userRepository;
final AppDatabase _db; final AppDatabase _db;
PurchasesApiV2(this._paymentManager, this._packManager, this._db); PurchasesApiV2(
this._paymentManager,
this._packManager,
this._userRepository,
this._db,
);
Response _ok(Object? object, {Map<String, String> headers = const {}}) => Response _ok(Object? object, {Map<String, String> headers = const {}}) =>
Response.ok( Response.ok(
@ -95,7 +102,7 @@ class PurchasesApiV2 {
'🔍 createPackPurchase: Checking pack access for userId=${user.id}, packId=$packId', '🔍 createPackPurchase: Checking pack access for userId=${user.id}, packId=$packId',
); );
// Check if already purchased // Check if already purchased
final hasAccess = await _db.userDao.hasPackAccess(user.id!, packId); final hasAccess = await _userRepository.hasPackAccess(user.id!, packId);
if (hasAccess) { if (hasAccess) {
print('❌ createPackPurchase: Pack already purchased'); print('❌ createPackPurchase: Pack already purchased');
return _badRequest('Pack is already purchased'); return _badRequest('Pack is already purchased');
@ -106,7 +113,7 @@ class PurchasesApiV2 {
String? price = pack.price; String? price = pack.price;
if (price != null && user.id != null) { if (price != null && user.id != null) {
print('🔍 createPackPurchase: Getting user data for userId=${user.id}'); 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) { if (userData != null) {
print('✅ createPackPurchase: User data found'); print('✅ createPackPurchase: User data found');
// Apply discounts if any // Apply discounts if any
@ -201,7 +208,7 @@ class PurchasesApiV2 {
} }
// Check if already purchased // Check if already purchased
final hasAccess = await _db.userDao.hasPackAccess(user.id!, productId); final hasAccess = await _userRepository.hasPackAccess(user.id!, productId);
if (hasAccess) { if (hasAccess) {
return _badRequest('Pack is already purchased'); return _badRequest('Pack is already purchased');
} }
@ -209,7 +216,7 @@ class PurchasesApiV2 {
// Get price (with discounts if applicable) // Get price (with discounts if applicable)
price = pack.price; price = pack.price;
if (price != null && user.id != null) { if (price != null && user.id != null) {
final userData = await _db.userDao.getUserData(user.id!); final userData = await _userRepository.getUserData(user.id!);
if (userData != null) { if (userData != null) {
// Apply discounts if any // Apply discounts if any
// For now, use price as-is. Discounts can be added later if needed. // 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) // Get price (with discounts if applicable)
price = plan.price; price = plan.price;
if (user.id != null) { if (user.id != null) {
final userData = await _db.userDao.getUserData(user.id!); final userData = await _userRepository.getUserData(user.id!);
if (userData != null) { if (userData != null) {
// Apply discounts if any // Apply discounts if any
// For now, use price as-is. Discounts can be added later if needed. // For now, use price as-is. Discounts can be added later if needed.
@ -364,7 +371,7 @@ class PurchasesApiV2 {
} }
// Check purchase status // Check purchase status
final isPurchased = await _db.userDao.hasPackAccess(user.id!, packId); final isPurchased = await _userRepository.hasPackAccess(user.id!, packId);
// Check subscription access // Check subscription access
final activeSubscription = await _db.subscriptionDao final activeSubscription = await _db.subscriptionDao

View file

@ -5,8 +5,7 @@ import 'package:drift/drift.dart';
import 'package:drift_postgres/drift_postgres.dart'; import 'package:drift_postgres/drift_postgres.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
import 'package:mnemo_cards_backend/database/database.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_repository.dart';
import 'package:mnemo_cards_backend/user/user_drift_extension.dart';
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart'; import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
import 'package:shelf/shelf.dart'; import 'package:shelf/shelf.dart';
import 'package:shelf_open_api/shelf_open_api.dart'; import 'package:shelf_open_api/shelf_open_api.dart';
@ -21,8 +20,9 @@ part 'telegram_bot_api_v2.g.dart';
@lazySingleton @lazySingleton
class TelegramBotApiV2 { class TelegramBotApiV2 {
final AppDatabase _db; final AppDatabase _db;
final UserRepository _userRepository;
TelegramBotApiV2(this._db); TelegramBotApiV2(this._db, this._userRepository);
Response _ok(Object? object, {Map<String, String> headers = const {}}) => Response _ok(Object? object, {Map<String, String> headers = const {}}) =>
Response.ok( Response.ok(
@ -193,9 +193,9 @@ class TelegramBotApiV2 {
if (userId != null && userId.isNotEmpty) { if (userId != null && userId.isNotEmpty) {
// Get specific user info // Get specific user info
final users = <UserModel>[]; final users = <UserModel>[];
final user = await backend_main.database.userDao.getUserById(userId); final user = await _userRepository.getUserById(userId, includePacks: true);
if (user != null) { if (user != null) {
users.add(await user.toUserModel()); users.add(user);
} }
if (users.isEmpty) { if (users.isEmpty) {
@ -224,10 +224,10 @@ class TelegramBotApiV2 {
}); });
} else { } else {
// Get all users summary // Get all users summary
final driftUsers = await backend_main.database.userDao.getAllUsers( final users = await _userRepository.getAllUsers(
limit: 100, limit: 100,
includePacks: false,
); );
final users = await Future.wait(driftUsers.map((u) => u.toUserModel()));
return _ok({ return _ok({
'total': users.length, 'total': users.length,

View file

@ -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/statistics/statistics_calculator.dart';
import 'package:mnemo_cards_backend/user/user_manager.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_model.dart';
import 'package:mnemo_cards_backend/user/user_repository.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'package:shelf/shelf.dart'; import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart'; import 'package:shelf_router/shelf_router.dart';
@ -22,12 +23,14 @@ class UsersApiV2 {
final UserManager _userManager; final UserManager _userManager;
final PaymentManager _paymentManager; final PaymentManager _paymentManager;
final StatisticsCalculator _statisticsCalculator; final StatisticsCalculator _statisticsCalculator;
final UserRepository _userRepository;
final AppDatabase _db; final AppDatabase _db;
UsersApiV2( UsersApiV2(
this._userManager, this._userManager,
this._paymentManager, this._paymentManager,
this._statisticsCalculator, this._statisticsCalculator,
this._userRepository,
this._db, this._db,
); );
@ -142,7 +145,7 @@ class UsersApiV2 {
}, statusCode: 400); }, statusCode: 400);
} }
await _db.userDao.updateUserPartial( await _userRepository.updateUserPartial(
UsersCompanion( UsersCompanion(
id: drift.Value(user.id!), id: drift.Value(user.id!),
name: name != null ? drift.Value(name) : const drift.Value.absent(), 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); 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) { if (userData == null) {
return _json({'error': 'user_data_not_found'}, statusCode: 404); return _json({'error': 'user_data_not_found'}, statusCode: 404);
} }
// Convert UserData to UserDataDto // Convert UserData to UserDataDto
final userDataModel = await _db.userDao.getUserWithDataById(user.id!); final userDataModel = await _userRepository.getUserWithDataById(user.id!);
if (userDataModel?.userData == null) { if (userDataModel?.userData == null) {
return _json({'error': 'user_data_not_found'}, statusCode: 404); return _json({'error': 'user_data_not_found'}, statusCode: 404);
} }
@ -502,7 +505,7 @@ class UsersApiV2 {
} }
await _db.transaction(() async { await _db.transaction(() async {
final userData = await _db.userDao.getUserData(user.id!); final userData = await _userRepository.getUserData(user.id!);
if (userData == null) { if (userData == null) {
throw StateError('User data not found'); throw StateError('User data not found');
} }
@ -520,7 +523,7 @@ class UsersApiV2 {
final longestStreak = max(userData.longestStreak, currentStreak); final longestStreak = max(userData.longestStreak, currentStreak);
// Update user data (без studyDates - это поле удалено, рассчитывается на лету) // Update user data (без studyDates - это поле удалено, рассчитывается на лету)
await _db.userDao.updateUserDataPartial( await _userRepository.updateUserDataPartial(
UserDatasCompanion( UserDatasCompanion(
userId: drift.Value(user.id!), userId: drift.Value(user.id!),
totalStudyTimeMinutes: drift.Value(totalStudyTime), totalStudyTimeMinutes: drift.Value(totalStudyTime),

View file

@ -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/packs/free_packs_distributor.dart';
import 'package:mnemo_cards_backend/user/user_repository.dart';
import 'task.dart' as task; import 'task.dart' as task;
class AddFreePacks with task.Task { class AddFreePacks with task.Task {
final FreePacksDistributor _freePacksDistributor; final FreePacksDistributor _freePacksDistributor;
final AppDatabase _db; final UserRepository _userRepository;
AddFreePacks(this._freePacksDistributor, this._db); AddFreePacks(this._freePacksDistributor, this._userRepository);
@override @override
String get name => 'add_free_packs'; String get name => 'add_free_packs';
@ -26,17 +25,18 @@ class AddFreePacks with task.Task {
final freePackIds = freePacks.map((p) => p.id).toSet(); final freePackIds = freePacks.map((p) => p.id).toSet();
// Получаем всех пользователей // Получаем всех пользователей
final allUsers = await _db.userDao.getAllUsers(); final allUsers = await _userRepository.getAllUsers();
// Фильтруем пользователей, у которых нет хотя бы одного из freePacks // Фильтруем пользователей, у которых нет хотя бы одного из freePacks
final usersToUpdate = <String>[]; final usersToUpdate = <String>[];
for (final user in allUsers) { for (final user in allUsers) {
final userPacks = await _db.userDao.getUserPacks(user.id); if (user.id == null) continue;
final userPackIds = userPacks.map((p) => p.id).toSet(); final userPacks = await _userRepository.getUserPacks(user.id!);
final userPackIds = userPacks.map((p) => p.id).whereType<String>().toSet();
// Если у пользователя нет хотя бы одного из freePacks // Если у пользователя нет хотя бы одного из freePacks
if (!freePackIds.every((packId) => userPackIds.contains(packId))) { 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) { for (final userId in usersToUpdate) {
try { try {
for (final pack in freePacks) { for (final pack in freePacks) {
await _db.userDao.grantPackAccess( if (pack.id == null) continue;
await _userRepository.grantPackAccess(
userId: userId, userId: userId,
packId: pack.id, packId: pack.id!,
grantType: 'free', grantType: 'free',
); );
} }

View file

@ -1,6 +1,7 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:drift_postgres/drift_postgres.dart'; import 'package:drift_postgres/drift_postgres.dart';
import 'package:mnemo_cards_backend/user/admin_ids_service.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 '../database/database.dart';
import 'task.dart' as task; import 'task.dart' as task;
@ -10,8 +11,9 @@ class CheckAdminsTask with task.Task {
String get name => 'check_admins'; String get name => 'check_admins';
final AppDatabase _db; final AppDatabase _db;
final UserRepository _userRepository;
CheckAdminsTask(this._db); CheckAdminsTask(this._db, this._userRepository);
@override @override
Future<void> task() async { Future<void> task() async {
@ -35,14 +37,14 @@ class CheckAdminsTask with task.Task {
} }
// Получить пользователя по userId из токена // Получить пользователя по userId из токена
final user = await _db.userDao.getUserById(token.userId); final user = await _userRepository.getUserById(token.userId);
if (user != null) { if (user != null && user.id != null) {
adminIds.add(user.id); adminIds.add(user.id!);
if (!user.admin) { if (!user.admin) {
// Установить admin = true // Установить admin = true
await _db.userDao.updateUserPartial( await _userRepository.updateUserPartial(
UsersCompanion( UsersCompanion(
id: Value(user.id), id: Value(user.id!),
admin: const Value(true), admin: const Value(true),
updatedAt: Value(PgDateTime(DateTime.now())), updatedAt: Value(PgDateTime(DateTime.now())),
), ),

View file

@ -2,9 +2,9 @@ import 'dart:developer';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
import 'package:mnemo_cards_backend/database/daos/discount_dao.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/database/database.dart';
import 'package:mnemo_cards_backend/discounts/discount_drift_extension.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_backend/mnemo_cards_common_backend.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart' hide DateTimeExt; import 'package:mnemo_cards_common/mnemo_cards_common.dart' hide DateTimeExt;
import 'package:mnemo_cards_common/src/utils/utils.dart'; import 'package:mnemo_cards_common/src/utils/utils.dart';
@ -13,11 +13,10 @@ import 'package:mnemo_cards_common/src/utils/utils.dart';
class DiscountsManager { class DiscountsManager {
final AppDatabase _db; final AppDatabase _db;
final DiscountDao _discountDao; final DiscountDao _discountDao;
final UserDao _userDao; final UserRepository _userRepository;
DiscountsManager(this._db) DiscountsManager(this._db, this._userRepository)
: _discountDao = _db.discountDao, : _discountDao = _db.discountDao;
_userDao = _db.userDao;
Future<void> applyDiscount({ Future<void> applyDiscount({
required Iterable<DiscountModel> discounts, required Iterable<DiscountModel> 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; if (user == null) return maxDiscount;
final userDataModel = await _userDao.getUserData(userData.userId); final userDataModel = await _userRepository.getUserData(userData.userId);
final userTags = userDataModel?.tags ?? []; final userTags = userDataModel?.tags ?? [];
final activeCampaigns = await _discountDao.getActiveCampaignsForUser( final activeCampaigns = await _discountDao.getActiveCampaignsForUser(

View file

@ -9,7 +9,7 @@ import 'package:mnemo_cards_common/mnemo_cards_common.dart';
extension CardPackToDto on CardPack { extension CardPackToDto on CardPack {
Future<CardPackPreviewDto> toPreviewDto(UserModel? user) async { Future<CardPackPreviewDto> toPreviewDto(UserModel? user) async {
final hasAccess = final hasAccess =
user != null && (user.purchases.contains(id.toString()) || user.admin); user != null && (user.packs.contains(id.toString()) || user.admin);
// Generate cover image URL // Generate cover image URL
String? coverUrl; String? coverUrl;
@ -64,6 +64,30 @@ extension CardPackToDto on CardPack {
} }
} }
extension CardPackToModel on CardPack {
Future<CardPackModel> 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 для конвертации GameCard (Drift) в DTO
extension GameCardToDto on GameCard { extension GameCardToDto on GameCard {
Future<GameCardDto> toDto(List<VoiceModel> voices) async { Future<GameCardDto> toDto(List<VoiceModel> voices) async {

View file

@ -1,18 +1,25 @@
import 'package:injectable/injectable.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_backend/mnemo_cards_common_backend.dart'; import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
import 'pack_repository.dart';
@lazySingleton @lazySingleton
class FreePacksDistributor { class FreePacksDistributor {
final AppDatabase _db; final UserRepository _userRepository;
final PackRepository _packRepository;
FreePacksDistributor(this._db); FreePacksDistributor(this._userRepository, this._packRepository);
Future<List<CardPack>> getFreePacks() async { Future<List<CardPackModel>> getFreePacks() async {
final allPacks = await _db.packDao.getAllPacks(enabledOnly: true); final allPacks = await _packRepository.getAllPacks(enabledOnly: true);
// Фильтруем паки с нулевой ценой или без цены // Фильтруем паки с нулевой ценой или без цены
return allPacks 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(); .toList();
} }
@ -22,13 +29,14 @@ class FreePacksDistributor {
await givePacksToUser(user, packs); await givePacksToUser(user, packs);
} }
Future<void> givePacksToUser(UserModel user, List<CardPack> packs) async { Future<void> givePacksToUser(UserModel user, List<CardPackModel> packs) async {
if (user.id == null) return; if (user.id == null) return;
for (final pack in packs) { for (final pack in packs) {
await _db.userDao.grantPackAccess( if (pack.id == null) continue;
await _userRepository.grantPackAccess(
userId: user.id!, userId: user.id!,
packId: pack.id, packId: pack.id!,
grantType: 'free', grantType: 'free',
); );
} }

View file

@ -20,7 +20,11 @@ class PackDtoConverter {
CardPackModel model, CardPackModel model,
UserModel? userModel, UserModel? userModel,
) async { ) 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) { if (userModel != null && !available) {
available = available =
userModel.subscriptionModel?.features.contains( userModel.subscriptionModel?.features.contains(
@ -214,6 +218,7 @@ class PackDtoConverter {
enabled: dto.enabled ?? model?.enabled ?? false, enabled: dto.enabled ?? model?.enabled ?? false,
order: dto.order ?? model?.order ?? 0, order: dto.order ?? model?.order ?? 0,
cardsOrder: dto.cardsOrder ?? model?.cardsOrder ?? const [], cardsOrder: dto.cardsOrder ?? model?.cardsOrder ?? const [],
currency: model?.currency ?? 'RUB',
); );
} }
} }

View file

@ -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 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'pack_dto_converter.dart'; import 'pack_dto_converter.dart';
import 'card_pack_drift_extension.dart'; import 'card_pack_drift_extension.dart';
import 'pack_repository.dart';
@lazySingleton @lazySingleton
class PackManager { class PackManager {
final AppDatabase _db; final AppDatabase _db;
final PackRepository _packRepository;
final PackDtoConverter packDtoConverter; final PackDtoConverter packDtoConverter;
PackManager(this._db, this.packDtoConverter); PackManager(this._db, this._packRepository, this.packDtoConverter);
Future<List<CardPackPreviewDto>> listPacksPreviews( Future<List<CardPackPreviewDto>> listPacksPreviews(
UserModel? userModel, UserModel? userModel,
Map<String, String>? params, Map<String, String>? params,
) async { ) async {
// Get packs from Drift database // Get packs from Repository
final packs = await _db.packDao.getAllPacks( final packModels = await _packRepository.getAllPacks(
enabledOnly: true, enabledOnly: true,
orderByField: 'order', 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( return (await Future.wait(
packs.map((pack) async { packs.map((pack) async {
final dto = await pack.toPreviewDto(userModel); final dto = await pack.toPreviewDto(userModel);
@ -37,16 +50,16 @@ class PackManager {
)).toList(); )).toList();
} }
Future<CardPack?> getPack(String id) async { Future<CardPackModel?> getPack(String id) async {
return await _db.packDao.getPackById(id); return await _packRepository.getPackById(id);
} }
Future<List<GameCard>> getCards(String packId) async { Future<List<GameCardModel>> getCards(String packId) async {
return await _db.packDao.getPackCards(packId); return await _packRepository.getPackCards(packId);
} }
Future<GameCard?> getCard(String id) async { Future<GameCardModel?> getCard(String id) async {
return await _db.packDao.getCardById(id); return await _packRepository.getCardById(id);
} }
Future<VoiceModel?> getVoice(String id) async { Future<VoiceModel?> getVoice(String id) async {
@ -58,12 +71,18 @@ class PackManager {
} }
Future<CardPackDto> getPackDto(String id, UserModel? userModel) async { Future<CardPackDto> 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) { if (pack == null) {
throw StateError('Pack not found'); throw StateError('Pack not found');
} }
final cards = await getCards(id); final cards = await _db.packDao.getPackCards(id);
final voices = <VoiceModel>[]; final voices = <VoiceModel>[];
for (final card in cards) { for (final card in cards) {

View file

@ -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<CardPackModel?> 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<List<CardPackModel>> 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 = <CardPackModel>[];
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<String> 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<void> 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<void> updatePackPartial(CardPacksCompanion updates) async {
await _db.packDao.updatePackPartial(updates);
}
/// Удалить пак (soft delete)
Future<void> softDeletePack(String packId) async {
await _db.packDao.softDeletePack(packId);
}
/// Подсчитать паки
Future<int> countPacks({bool enabledOnly = false}) async {
return await _db.packDao.countPacks(enabledOnly: enabledOnly);
}
/// Получить карточки пака
Future<List<GameCardModel>> getPackCards(String packId) async {
final cards = await _db.packDao.getPackCards(packId);
return cards.map((c) => c.toModel()).toList();
}
/// Получить превью карточки пака
Future<List<GameCardModel>> getPreviewCards(String packId) async {
final cards = await _db.packDao.getPreviewCards(packId);
return cards.map((c) => c.toModel()).toList();
}
/// Получить карточку по ID
Future<GameCardModel?> getCardById(String id) async {
final card = await _db.packDao.getCardById(id);
return card?.toModel();
}
/// Получить паки для карточки
Future<List<CardPackModel>> getPacksForCard(String cardId) async {
final packs = await _db.packDao.getPacksForCard(cardId);
return await Future.wait(packs.map((p) => p.toModel()));
}
/// Добавить карточку в пак
Future<void> addCardToPack({
required String packId,
required String cardId,
int order = 0,
}) async {
await _db.packDao.addCardToPack(
packId: packId,
cardId: cardId,
order: order,
);
}
/// Удалить карточку из пака
Future<void> removeCardFromPack(String packId, String cardId) async {
await _db.packDao.removeCardFromPack(packId, cardId);
}
/// Обновить порядок карточек в паке
Future<void> updatePackCardsOrder(String packId, List<String> cardIds) async {
await _db.packDao.updatePackCardsOrder(packId, cardIds);
}
/// Установить preview карточки для пака
Future<void> setPreviewCards(String packId, List<String> cardIds) async {
await _db.packDao.setPreviewCards(packId, cardIds);
}
// ==================== VoiceModels ====================
/// Получить голосовые модели карточки
Future<List<VoiceModel>> getCardVoices(String cardId) async {
return await _db.packDao.getCardVoices(cardId);
}
/// Получить голосовую модель по ID
Future<VoiceModel?> getVoiceById(String id) async {
return await _db.packDao.getVoiceById(id);
}
/// Обновить путь/URL аудиофайла голосовой модели
Future<void> updateVoiceUrl(String voiceId, String voiceUrl) async {
await _db.packDao.updateVoiceUrl(voiceId, voiceUrl);
}
}

View file

@ -1,14 +1,14 @@
import 'package:injectable/injectable.dart'; 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/discounts/discounts_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_backend/mnemo_cards_common_backend.dart';
@LazySingleton() @LazySingleton()
class ProductsPriceResolver { class ProductsPriceResolver {
final DiscountsManager _discountsManager; final DiscountsManager _discountsManager;
final AppDatabase _db; final UserRepository _userRepository;
ProductsPriceResolver(this._discountsManager, this._db); ProductsPriceResolver(this._discountsManager, this._userRepository);
Future<String> _userProductPrice( Future<String> _userProductPrice(
UserModel userModel, UserModel userModel,
@ -20,7 +20,7 @@ class ProductsPriceResolver {
return price; return price;
} }
if (userModel.id != null) { if (userModel.id != null) {
final userData = await _db.userDao.getUserData(userModel.id!); final userData = await _userRepository.getUserData(userModel.id!);
if (userData != null) { if (userData != null) {
final maxDiscount = await _discountsManager.getProductDiscount( final maxDiscount = await _discountsManager.getProductDiscount(
product, product,

View file

@ -2,14 +2,16 @@ import 'package:drift/drift.dart';
import 'package:drift_postgres/drift_postgres.dart'; import 'package:drift_postgres/drift_postgres.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
import 'package:mnemo_cards_backend/database/database.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/mnemo_cards_common.dart';
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart'; import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
@lazySingleton @lazySingleton
class AchievementManager { class AchievementManager {
final AppDatabase _db; final AppDatabase _db;
final UserRepository _userRepository;
AchievementManager(this._db); AchievementManager(this._db, this._userRepository);
/// Get all available achievements with their definitions /// Get all available achievements with their definitions
List<AchievementDto> get allAchievementDefinitions => List<AchievementDto> get allAchievementDefinitions =>
@ -146,7 +148,7 @@ class AchievementManager {
// return userData.totalTests > 0; // return userData.totalTests > 0;
case AchievementType.firstPackCompleted: case AchievementType.firstPackCompleted:
final userPacks = await _db.userDao.getUserPacks(userId); final userPacks = await _userRepository.getUserPacks(userId);
return userPacks.isNotEmpty; return userPacks.isNotEmpty;
// case AchievementType.words10Learned: // case AchievementType.words10Learned:

View file

@ -1,6 +1,7 @@
import 'dart:math'; import 'dart:math';
import 'package:injectable/injectable.dart'; 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/mnemo_cards_common.dart';
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart'; import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
import 'package:mnemo_cards_backend/database/database.dart'; import 'package:mnemo_cards_backend/database/database.dart';
@ -9,8 +10,9 @@ import 'package:mnemo_cards_backend/database/database.dart';
@lazySingleton @lazySingleton
class StatisticsCalculator { class StatisticsCalculator {
final AppDatabase _db; final AppDatabase _db;
final UserRepository _userRepository;
StatisticsCalculator(this._db); StatisticsCalculator(this._db, this._userRepository);
/// Calculate pack progress for a specific user and pack /// Calculate pack progress for a specific user and pack
/// ///
@ -104,12 +106,13 @@ class StatisticsCalculator {
/// Calculate pack progress for all user packs /// Calculate pack progress for all user packs
Future<List<PackProgressDto>> calculateAllPackProgress(String userId) async { Future<List<PackProgressDto>> calculateAllPackProgress(String userId) async {
// Получить все паки пользователя // Получить все паки пользователя
final userPacks = await _db.userDao.getUserPacks(userId); final userPacks = await _userRepository.getUserPacks(userId);
// Рассчитать прогресс для каждого пака // Рассчитать прогресс для каждого пака
final progressList = <PackProgressDto>[]; final progressList = <PackProgressDto>[];
for (final pack in userPacks) { 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); progressList.add(progress);
} }

View file

@ -1,10 +1,14 @@
import 'package:drift/drift.dart' as drift; import 'package:drift/drift.dart' as drift;
import 'package:mnemo_cards_backend/database/database.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 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
/// Extension для конвертации User (Drift) в UserModel /// Extension для конвертации User (Drift) в UserModel
extension UserToUserModel on User { extension UserToUserModel on User {
Future<UserModel> toUserModel() async { Future<UserModel> toUserModel([List<CardPack>? packs]) async {
final packModels = packs != null
? await Future.wait(packs.map((p) => p.toModel()))
: <CardPackModel>[];
final userModel = UserModel( final userModel = UserModel(
id: id, id: id,
name: name, name: name,
@ -13,6 +17,7 @@ extension UserToUserModel on User {
admin: admin, admin: admin,
purchases: purchases, purchases: purchases,
userSettings: userSettings, userSettings: userSettings,
packs: packModels,
); );
return userModel; return userModel;

View file

@ -15,13 +15,14 @@ import '../statistics/statistics_calculator.dart';
import '../statistics/achievement_manager.dart'; import '../statistics/achievement_manager.dart';
import '../statistics/word_statistics_manager.dart'; import '../statistics/word_statistics_manager.dart';
import 'secure.dart'; import 'secure.dart';
import 'user_drift_extension.dart'; import 'user_repository.dart';
Map<String?, DateTime> _onlineUsers = {}; Map<String?, DateTime> _onlineUsers = {};
@lazySingleton @lazySingleton
class UserManager { class UserManager {
final AppDatabase _db; final AppDatabase _db;
final UserRepository _userRepository;
final FreePacksDistributor _freePacksDistributor; final FreePacksDistributor _freePacksDistributor;
final SessionTracker _sessionTracker; final SessionTracker _sessionTracker;
final StatisticsCalculator _statisticsCalculator; final StatisticsCalculator _statisticsCalculator;
@ -30,6 +31,7 @@ class UserManager {
UserManager( UserManager(
this._db, this._db,
this._userRepository,
this._freePacksDistributor, this._freePacksDistributor,
this._sessionTracker, this._sessionTracker,
this._statisticsCalculator, this._statisticsCalculator,
@ -38,9 +40,7 @@ class UserManager {
); );
Future<UserModel?> fetchUser(String id) async { Future<UserModel?> fetchUser(String id) async {
final user = await _db.userDao.getUserById(id); return await _userRepository.getUserById(id, includePacks: true);
if (user == null) return null;
return await user.toUserModel();
} }
DateTime? lastOnline(String id) => _onlineUsers[id]; DateTime? lastOnline(String id) => _onlineUsers[id];
@ -119,77 +119,68 @@ class UserManager {
String? name, String? name,
}) async { }) async {
// Check if user exists by externalId // Check if user exists by externalId
final existingUser = await _db.userDao.getUserByExternalId(externalId); var existingUserModel = await _userRepository.getUserByExternalId(externalId);
if (existingUser != null) { if (existingUserModel != null) {
print('User found $name $email $telegram'); print('User found $name $email $telegram');
// Обновляем контактные данные, если они пришли впервые/изменились // Обновляем контактные данные, если они пришли впервые/изменились
final shouldUpdateEmail = final shouldUpdateEmail =
email != null && email.isNotEmpty && existingUser.email != email; email != null && email.isNotEmpty && existingUserModel.email != email;
final shouldUpdateTelegram = final shouldUpdateTelegram =
telegram != null && telegram != null &&
telegram.isNotEmpty && telegram.isNotEmpty &&
existingUser.telegram != telegram; existingUserModel.telegram != telegram;
if (shouldUpdateEmail || shouldUpdateTelegram) { if (shouldUpdateEmail || shouldUpdateTelegram) {
await _db.userDao.updateUserPartial( final updatedModel = existingUserModel.copyWith(
UsersCompanion( email: shouldUpdateEmail ? email : existingUserModel.email,
id: drift.Value(existingUser.id), telegram: shouldUpdateTelegram ? telegram : existingUserModel.telegram,
email: shouldUpdateEmail
? drift.Value(email)
: const drift.Value.absent(),
telegram: shouldUpdateTelegram
? drift.Value(telegram)
: const drift.Value.absent(),
updatedAt: drift.Value(PgDateTime(DateTime.now())),
),
); );
await _userRepository.updateUser(updatedModel);
existingUserModel = updatedModel;
} }
final userModel = await existingUser.toUserModel(); final token = await createOrGetAuthToken(existingUserModel, externalId);
final token = await createOrGetAuthToken(userModel, externalId); return (existingUserModel, token);
return (userModel, token);
} }
print('Creating new user $name $email $telegram'); print('Creating new user $name $email $telegram');
// Create new user with user data in transaction // Create new user with user data in transaction
final now = DateTime.now(); 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( final userDataCompanion = UserDatasCompanion.insert(
userId: '', // Will be set by createUserWithData via copyWith userId: '', // Will be set by createUserWithData via copyWith
registrationDate: drift.Value(PgDateTime(now)), registrationDate: drift.Value(PgDateTime(now)),
); );
final userId = await _db.userDao.createUserWithData( // Создаем пользователя через UserRepository
user: userCompanion, 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, userData: userDataCompanion,
); );
final user = await _db.userDao.getUserById(userId); final createdUserModel = await _userRepository.getUserById(userId, includePacks: true);
if (user == null) { if (createdUserModel == null) {
throw Exception('Failed to create user'); throw Exception('Failed to create user');
} }
final userModel = await user.toUserModel(); final token = await createOrGetAuthToken(createdUserModel, externalId);
final token = await createOrGetAuthToken(userModel, externalId);
// Give free packs to new user // Give free packs to new user
await _freePacksDistributor.giveFreePacksToUser(userModel); await _freePacksDistributor.giveFreePacksToUser(createdUserModel);
print('User $name $email $telegram created successfully'); 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'); throw Exception('User ID is required');
} }
await _db.userDao.updateUserPartial( final updatedUser = user.copyWith(
UsersCompanion( userSettings: jsonEncode(settings.toJson()),
id: drift.Value(user.id!),
userSettings: drift.Value(jsonEncode(settings.toJson())),
updatedAt: drift.Value(PgDateTime(DateTime.now())),
),
); );
await _userRepository.updateUser(updatedUser);
} }
/// Добавить статистику теста /// Добавить статистику теста
@ -220,12 +208,12 @@ class UserManager {
} }
// Получить или создать UserData // Получить или создать UserData
var userData = await _db.userDao.getUserData(user.id!); var userData = await _userRepository.getUserData(user.id!);
if (userData == null) { if (userData == null) {
await _db.userDao.createUserData( await _db.userDao.createUserData(
UserDatasCompanion.insert(userId: user.id!), UserDatasCompanion.insert(userId: user.id!),
); );
userData = await _db.userDao.getUserData(user.id!); userData = await _userRepository.getUserData(user.id!);
if (userData == null) { if (userData == null) {
throw Exception('Failed to create user data'); throw Exception('Failed to create user data');
} }

View file

@ -33,7 +33,8 @@ class UserManager {
Future<UserModel?> fetchUser(String id) async { Future<UserModel?> fetchUser(String id) async {
final user = await _db.userDao.getUserById(id); final user = await _db.userDao.getUserById(id);
if (user == null) return null; 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]; DateTime? lastOnline(String id) => _onlineUsers[id];

View file

@ -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<UserModel?> 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<UserModel?> getUserByEmail(String email) async {
final user = await _db.userDao.getUserByEmail(email);
if (user == null) return null;
return await user.toUserModel();
}
/// Получить пользователя по externalUserId
Future<UserModel?> getUserByExternalId(String externalId) async {
final user = await _db.userDao.getUserByExternalId(externalId);
if (user == null) return null;
return await user.toUserModel();
}
/// Получить пользователя с UserData
Future<UserModel?> 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<String> createUser(UserModel userModel) async {
final userCompanion = userModel.toUsersCompanion();
return await _db.userDao.createUser(userCompanion);
}
/// Создать пользователя с UserData
Future<String> createUserWithData({
required UserModel userModel,
required UserDatasCompanion userData,
}) async {
final userCompanion = userModel.toUsersCompanion();
return await _db.userDao.createUserWithData(
user: userCompanion,
userData: userData,
);
}
/// Обновить пользователя
Future<void> 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<void> updateUserPartial(UsersCompanion updates) async {
await _db.userDao.updateUserPartial(updates);
}
/// Удалить пользователя (soft delete)
Future<void> softDeleteUser(String userId) async {
await _db.userDao.softDeleteUser(userId);
}
/// Получить всех пользователей (для админки)
Future<List<UserModel>> 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 = <UserModel>[];
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<int> countUsers({bool includeDeleted = false}) async {
return await _db.userDao.countUsers(includeDeleted: includeDeleted);
}
/// Получить паки пользователя
Future<List<CardPackModel>> getUserPacks(String userId) async {
final packs = await _db.userDao.getUserPacks(userId);
return await Future.wait(packs.map((p) => p.toModel()));
}
/// Проверить, есть ли у пользователя доступ к паку
Future<bool> hasPackAccess(String userId, String packId) async {
return await _db.userDao.hasPackAccess(userId, packId);
}
/// Дать пользователю доступ к паку
Future<void> grantPackAccess({
required String userId,
required String packId,
String grantType = 'purchase',
}) async {
await _db.userDao.grantPackAccess(
userId: userId,
packId: packId,
grantType: grantType,
);
}
/// Отозвать доступ к паку
Future<void> revokePackAccess(String userId, String packId) async {
await _db.userDao.revokePackAccess(userId, packId);
}
/// Получить UserData пользователя
Future<UserData?> getUserData(String userId) async {
return await _db.userDao.getUserData(userId);
}
/// Обновить UserData частично
Future<void> updateUserDataPartial(UserDatasCompanion updates) async {
await _db.userDao.updateUserDataPartial(updates);
}
/// Обновить время последнего визита
Future<void> updateLastOnline(String userId) async {
await _db.userDao.updateLastOnline(userId);
}
}

View file

@ -166,30 +166,30 @@ class MockPackManager extends _i1.Mock implements _i8.PackManager {
as _i5.Future<List<_i3.CardPackPreviewDto>>); as _i5.Future<List<_i3.CardPackPreviewDto>>);
@override @override
_i5.Future<_i10.CardPack?> getPack(String? id) => _i5.Future<_i9.CardPackModel?> getPack(String? id) =>
(super.noSuchMethod( (super.noSuchMethod(
Invocation.method(#getPack, [id]), 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 @override
_i5.Future<List<_i10.GameCard>> getCards(String? packId) => _i5.Future<List<_i9.GameCardModel>> getCards(String? packId) =>
(super.noSuchMethod( (super.noSuchMethod(
Invocation.method(#getCards, [packId]), Invocation.method(#getCards, [packId]),
returnValue: _i5.Future<List<_i10.GameCard>>.value( returnValue: _i5.Future<List<_i9.GameCardModel>>.value(
<_i10.GameCard>[], <_i9.GameCardModel>[],
), ),
) )
as _i5.Future<List<_i10.GameCard>>); as _i5.Future<List<_i9.GameCardModel>>);
@override @override
_i5.Future<_i10.GameCard?> getCard(String? id) => _i5.Future<_i9.GameCardModel?> getCard(String? id) =>
(super.noSuchMethod( (super.noSuchMethod(
Invocation.method(#getCard, [id]), 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 @override
_i5.Future<_i10.VoiceModel?> getVoice(String? id) => _i5.Future<_i10.VoiceModel?> getVoice(String? id) =>

View file

@ -2,13 +2,11 @@ import 'package:json_annotation/json_annotation.dart';
import 'package:copy_with_extension/copy_with_extension.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_card_model.dart';
import 'package:mnemo_cards_common_backend/src/models/game_tests/test_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'; import 'product_model.dart';
part 'card_pack_model.g.dart'; part 'card_pack_model.g.dart';
@JsonSerializable()
@CopyWith() @CopyWith()
class CardPackModel implements MnemoCardsProductModel { class CardPackModel implements MnemoCardsProductModel {
String? id; String? id;
@ -28,37 +26,30 @@ class CardPackModel implements MnemoCardsProductModel {
final bool enabled; final bool enabled;
final int order; final int order;
final List<String> cardsOrder; final List<String> cardsOrder;
// Relations - loaded separately from database // Relations - loaded separately from database (when needed)
final List<GameCardModel> previewCards = []; final List<GameCardModel> previewCards = [];
final List<GameCardModel> cards = []; final List<GameCardModel> cards = [];
final List<TestModel> tests = []; final List<TestModel> tests = [];
final List<UserModel> users = [];
CardPackModel({ CardPackModel({
required this.title, required this.title,
required this.subtitle, required this.subtitle,
required this.size, required this.size,
this.color, required this.color,
this.cover, required this.cover,
this.description, required this.description,
this.version, required this.version,
this.id, required this.id,
this.googlePlayId, required this.googlePlayId,
this.rustoreId, required this.rustoreId,
this.appStoreId, required this.appStoreId,
this.price, required this.price,
this.cardsOrder = const [], required this.cardsOrder,
this.enabled = true, required this.enabled,
this.order = 0, required this.order,
this.currency = 'RUB', required this.currency,
}); });
factory CardPackModel.fromJson(Map<String, dynamic> json) =>
_$CardPackModelFromJson(json);
Map<String, Object?> toJson() => _$CardPackModelToJson(this);
@override @override
@JsonKey(includeFromJson: false, includeToJson: false)
final type = MnemoCardsProductModelType.pack; final type = MnemoCardsProductModelType.pack;
} }

View file

@ -226,51 +226,3 @@ extension $CardPackModelCopyWith on CardPackModel {
// ignore: library_private_types_in_public_api // ignore: library_private_types_in_public_api
_$CardPackModelCWProxy get copyWith => _$CardPackModelCWProxyImpl(this); _$CardPackModelCWProxy get copyWith => _$CardPackModelCWProxyImpl(this);
} }
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
CardPackModel _$CardPackModelFromJson(Map<String, dynamic> 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<dynamic>?)
?.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<String, dynamic> _$CardPackModelToJson(CardPackModel instance) =>
<String, dynamic>{
'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,
};

View file

@ -7,13 +7,12 @@ import '../statistics/word_statistics_model.dart';
part 'test_statistics_model.g.dart'; part 'test_statistics_model.g.dart';
@JsonSerializable()
@CopyWith() @CopyWith()
class TestStatisticsModel { class TestStatisticsModel {
final String id; final String id;
// Relations - loaded separately from database // Relations - loaded separately from database
TestModel? test; String? testId;
UserDataModel? userData; String? userId;
final List<TestAttempt> attempts; final List<TestAttempt> attempts;
TestStatisticsModel({ TestStatisticsModel({
@ -24,13 +23,8 @@ class TestStatisticsModel {
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
TestAttempt? get lastAttempt => attempts.lastOrNull; TestAttempt? get lastAttempt => attempts.lastOrNull;
factory TestStatisticsModel.fromJson(Map<String, dynamic> json) =>
_$TestStatisticsModelFromJson(json);
Map<String, Object?> toJson() => _$TestStatisticsModelToJson(this);
} }
@JsonSerializable()
@CopyWith() @CopyWith()
class TestAttempt { class TestAttempt {
final List<WordStatisticsModel> words; final List<WordStatisticsModel> words;
@ -41,8 +35,4 @@ class TestAttempt {
this.sessionToken, this.sessionToken,
}); });
factory TestAttempt.fromJson(Map<String, dynamic> json) =>
_$TestAttemptFromJson(json);
Map<String, Object?> toJson() => _$TestAttemptToJson(this);
} }

View file

@ -128,47 +128,3 @@ extension $TestAttemptCopyWith on TestAttempt {
// ignore: library_private_types_in_public_api // ignore: library_private_types_in_public_api
_$TestAttemptCWProxy get copyWith => _$TestAttemptCWProxyImpl(this); _$TestAttemptCWProxy get copyWith => _$TestAttemptCWProxyImpl(this);
} }
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
TestStatisticsModel _$TestStatisticsModelFromJson(Map<String, dynamic> json) =>
TestStatisticsModel(
attempts:
(json['attempts'] as List<dynamic>?)
?.map((e) => TestAttempt.fromJson(e as Map<String, dynamic>))
.toList() ??
const [],
id: json['id'] as String,
)
..test = json['test'] == null
? null
: TestModel.fromJson(json['test'] as Map<String, dynamic>)
..userData = json['userData'] == null
? null
: UserDataModel.fromJson(json['userData'] as Map<String, dynamic>);
Map<String, dynamic> _$TestStatisticsModelToJson(
TestStatisticsModel instance,
) => <String, dynamic>{
'id': instance.id,
'test': instance.test,
'userData': instance.userData,
'attempts': instance.attempts,
};
TestAttempt _$TestAttemptFromJson(Map<String, dynamic> json) => TestAttempt(
words:
(json['words'] as List<dynamic>?)
?.map((e) => WordStatisticsModel.fromJson(e as Map<String, dynamic>))
.toList() ??
const [],
sessionToken: json['sessionToken'] as String?,
);
Map<String, dynamic> _$TestAttemptToJson(TestAttempt instance) =>
<String, dynamic>{
'words': instance.words,
'sessionToken': instance.sessionToken,
};

View file

@ -3,18 +3,14 @@ import 'package:copy_with_extension/copy_with_extension.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'product_model.dart'; import 'product_model.dart';
import 'user/user_model.dart';
part 'payment.g.dart'; part 'payment.g.dart';
@JsonSerializable()
@CopyWith() @CopyWith()
class PaymentModel { class PaymentModel {
String? id; String? id;
final String amount; final String amount;
final String currency; final String currency;
// Relations - loaded separately from database
UserModel? user;
@JsonKey(unknownEnumValue: PaymentStatus.unknown) @JsonKey(unknownEnumValue: PaymentStatus.unknown)
final PaymentStatus status; final PaymentStatus status;
@JsonKey(unknownEnumValue: PaymentSystem.unknown) @JsonKey(unknownEnumValue: PaymentSystem.unknown)
@ -24,10 +20,6 @@ class PaymentModel {
final DateTime date; final DateTime date;
final String userId; final String userId;
@deprecated
final List<String> packs;
@deprecated
final bool subscription;
final List<MnemoCardsProductModelBase> products; final List<MnemoCardsProductModelBase> products;
PaymentModel({ PaymentModel({
@ -38,8 +30,6 @@ class PaymentModel {
required this.userId, required this.userId,
required this.date, required this.date,
required this.products, required this.products,
this.packs = const [],
this.subscription = false,
this.externalToken, this.externalToken,
this.meta, this.meta,
this.id, this.id,
@ -97,8 +87,6 @@ class PaymentModel {
required this.externalToken, required this.externalToken,
required this.date, required this.date,
required this.products, required this.products,
this.packs = const [],
this.subscription = false,
this.meta, this.meta,
this.id, this.id,
}) : paymentSystem = PaymentSystem.yookassa, }) : paymentSystem = PaymentSystem.yookassa,
@ -111,8 +99,6 @@ class PaymentModel {
required this.externalToken, required this.externalToken,
required this.date, required this.date,
required this.products, required this.products,
this.packs = const [],
this.subscription = false,
this.meta, this.meta,
this.id, this.id,
this.status = PaymentStatus.created, this.status = PaymentStatus.created,
@ -125,15 +111,9 @@ class PaymentModel {
required this.externalToken, required this.externalToken,
required this.date, required this.date,
required this.products, required this.products,
this.packs = const [],
this.subscription = false,
this.meta, this.meta,
this.id, this.id,
this.status = PaymentStatus.created, this.status = PaymentStatus.created,
}) : paymentSystem = PaymentSystem.rustore; }) : paymentSystem = PaymentSystem.rustore;
factory PaymentModel.fromJson(Map<String, dynamic> json) =>
_$PaymentModelFromJson(json);
Map<String, Object?> toJson() => _$PaymentModelToJson(this);
} }

View file

@ -21,10 +21,6 @@ abstract class _$PaymentModelCWProxy {
PaymentModel products(List<MnemoCardsProductModelBase> products); PaymentModel products(List<MnemoCardsProductModelBase> products);
PaymentModel packs(@deprecated List<String> packs);
PaymentModel subscription(@deprecated bool subscription);
PaymentModel externalToken(String? externalToken); PaymentModel externalToken(String? externalToken);
PaymentModel meta(String? meta); PaymentModel meta(String? meta);
@ -46,8 +42,6 @@ abstract class _$PaymentModelCWProxy {
String userId, String userId,
DateTime date, DateTime date,
List<MnemoCardsProductModelBase> products, List<MnemoCardsProductModelBase> products,
@deprecated List<String> packs,
@deprecated bool subscription,
String? externalToken, String? externalToken,
String? meta, String? meta,
String? id, String? id,
@ -84,13 +78,6 @@ class _$PaymentModelCWProxyImpl implements _$PaymentModelCWProxy {
PaymentModel products(List<MnemoCardsProductModelBase> products) => PaymentModel products(List<MnemoCardsProductModelBase> products) =>
call(products: products); call(products: products);
@override
PaymentModel packs(@deprecated List<String> packs) => call(packs: packs);
@override
PaymentModel subscription(@deprecated bool subscription) =>
call(subscription: subscription);
@override @override
PaymentModel externalToken(String? externalToken) => PaymentModel externalToken(String? externalToken) =>
call(externalToken: externalToken); call(externalToken: externalToken);
@ -117,8 +104,6 @@ class _$PaymentModelCWProxyImpl implements _$PaymentModelCWProxy {
Object? userId = const $CopyWithPlaceholder(), Object? userId = const $CopyWithPlaceholder(),
Object? date = const $CopyWithPlaceholder(), Object? date = const $CopyWithPlaceholder(),
Object? products = const $CopyWithPlaceholder(), Object? products = const $CopyWithPlaceholder(),
@deprecated Object? packs = const $CopyWithPlaceholder(),
@deprecated Object? subscription = const $CopyWithPlaceholder(),
Object? externalToken = const $CopyWithPlaceholder(), Object? externalToken = const $CopyWithPlaceholder(),
Object? meta = const $CopyWithPlaceholder(), Object? meta = const $CopyWithPlaceholder(),
Object? id = const $CopyWithPlaceholder(), Object? id = const $CopyWithPlaceholder(),
@ -153,15 +138,6 @@ class _$PaymentModelCWProxyImpl implements _$PaymentModelCWProxy {
? _value.products ? _value.products
// ignore: cast_nullable_to_non_nullable // ignore: cast_nullable_to_non_nullable
: products as List<MnemoCardsProductModelBase>, : products as List<MnemoCardsProductModelBase>,
packs: packs == const $CopyWithPlaceholder() || packs == null
? _value.packs
// ignore: cast_nullable_to_non_nullable
: packs as List<String>,
subscription:
subscription == const $CopyWithPlaceholder() || subscription == null
? _value.subscription
// ignore: cast_nullable_to_non_nullable
: subscription as bool,
externalToken: externalToken == const $CopyWithPlaceholder() externalToken: externalToken == const $CopyWithPlaceholder()
? _value.externalToken ? _value.externalToken
// ignore: cast_nullable_to_non_nullable // ignore: cast_nullable_to_non_nullable
@ -184,80 +160,3 @@ extension $PaymentModelCopyWith on PaymentModel {
// ignore: library_private_types_in_public_api // ignore: library_private_types_in_public_api
_$PaymentModelCWProxy get copyWith => _$PaymentModelCWProxyImpl(this); _$PaymentModelCWProxy get copyWith => _$PaymentModelCWProxyImpl(this);
} }
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
PaymentModel _$PaymentModelFromJson(Map<String, dynamic> 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<dynamic>)
.map(
(e) => MnemoCardsProductModelBase.fromJson(
e as Map<String, dynamic>,
),
)
.toList(),
packs:
(json['packs'] as List<dynamic>?)
?.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<String, dynamic>);
Map<String, dynamic> _$PaymentModelToJson(PaymentModel instance) =>
<String, dynamic>{
'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',
};

View file

@ -2,5 +2,5 @@ import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
extension PromoCodeExt on PromoCodeModel { extension PromoCodeExt on PromoCodeModel {
Future<bool> isReserved() async => Future<bool> isReserved() async =>
userData != null; userId != null;
} }

View file

@ -6,7 +6,6 @@ import 'promo_codes_campaign_model.dart';
part 'promo_code_model.g.dart'; part 'promo_code_model.g.dart';
@JsonSerializable()
@CopyWith() @CopyWith()
class PromoCodeModel { class PromoCodeModel {
String? id; String? id;
@ -15,16 +14,13 @@ class PromoCodeModel {
// Relations - loaded separately from database // Relations - loaded separately from database
PromoCodesCampaignModel? campaign; PromoCodesCampaignModel? campaign;
// Individual promo code // Individual promo code
UserDataModel? userData; String? userId;
PromoCodeModel({ PromoCodeModel({
required this.code, required this.code,
this.activations = 0, this.activations = 0,
this.id, this.id,
this.userId,
}); });
factory PromoCodeModel.fromJson(Map<String, dynamic> json) =>
_$PromoCodeModelFromJson(json);
Map<String, Object?> toJson() => _$PromoCodeModelToJson(this);
} }

View file

@ -13,6 +13,8 @@ abstract class _$PromoCodeModelCWProxy {
PromoCodeModel id(String? id); PromoCodeModel id(String? id);
PromoCodeModel userId(String? userId);
/// Creates a new instance with the provided field values. /// 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)`. /// 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 /// ```dart
/// PromoCodeModel(...).copyWith(id: 12, name: "My name") /// 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. /// Callable proxy for `copyWith` functionality.
@ -39,6 +46,9 @@ class _$PromoCodeModelCWProxyImpl implements _$PromoCodeModelCWProxy {
@override @override
PromoCodeModel id(String? id) => call(id: id); PromoCodeModel id(String? id) => call(id: id);
@override
PromoCodeModel userId(String? userId) => call(userId: userId);
@override @override
/// Creates a new instance with the provided field values. /// 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)`. /// 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? code = const $CopyWithPlaceholder(),
Object? activations = const $CopyWithPlaceholder(), Object? activations = const $CopyWithPlaceholder(),
Object? id = const $CopyWithPlaceholder(), Object? id = const $CopyWithPlaceholder(),
Object? userId = const $CopyWithPlaceholder(),
}) { }) {
return PromoCodeModel( return PromoCodeModel(
code: code == const $CopyWithPlaceholder() || code == null code: code == const $CopyWithPlaceholder() || code == null
@ -66,6 +77,10 @@ class _$PromoCodeModelCWProxyImpl implements _$PromoCodeModelCWProxy {
? _value.id ? _value.id
// ignore: cast_nullable_to_non_nullable // ignore: cast_nullable_to_non_nullable
: id as String?, : 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 // ignore: library_private_types_in_public_api
_$PromoCodeModelCWProxy get copyWith => _$PromoCodeModelCWProxyImpl(this); _$PromoCodeModelCWProxy get copyWith => _$PromoCodeModelCWProxyImpl(this);
} }
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
PromoCodeModel _$PromoCodeModelFromJson(Map<String, dynamic> 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<String, dynamic>,
)
..userData = json['userData'] == null
? null
: UserDataModel.fromJson(json['userData'] as Map<String, dynamic>);
Map<String, dynamic> _$PromoCodeModelToJson(PromoCodeModel instance) =>
<String, dynamic>{
'id': instance.id,
'code': instance.code,
'activations': instance.activations,
'campaign': instance.campaign,
'userData': instance.userData,
};

View file

@ -5,7 +5,6 @@ import 'package:mnemo_cards_common/mnemo_cards_common.dart';
part 'user_subscription_model.g.dart'; part 'user_subscription_model.g.dart';
@JsonSerializable()
@CopyWith() @CopyWith()
class UserSubscriptionModel { class UserSubscriptionModel {
String? id; String? id;
@ -24,10 +23,6 @@ class UserSubscriptionModel {
required this.features, required this.features,
}); });
factory UserSubscriptionModel.fromJson(Map<String, dynamic> json) =>
_$UserSubscriptionModelFromJson(json);
Map<String, Object?> toJson() => _$UserSubscriptionModelToJson(this);
} }
extension UserSubscriptionExt on UserSubscriptionModel { extension UserSubscriptionExt on UserSubscriptionModel {

View file

@ -93,40 +93,3 @@ extension $UserSubscriptionModelCopyWith on UserSubscriptionModel {
_$UserSubscriptionModelCWProxy get copyWith => _$UserSubscriptionModelCWProxy get copyWith =>
_$UserSubscriptionModelCWProxyImpl(this); _$UserSubscriptionModelCWProxyImpl(this);
} }
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
UserSubscriptionModel _$UserSubscriptionModelFromJson(
Map<String, dynamic> 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<dynamic>)
.map((e) => $enumDecode(_$SubscriptionFeatureEnumEnumMap, e))
.toList(),
)
..user = json['user'] == null
? null
: UserModel.fromJson(json['user'] as Map<String, dynamic>);
Map<String, dynamic> _$UserSubscriptionModelToJson(
UserSubscriptionModel instance,
) => <String, dynamic>{
'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',
};

View file

@ -1,7 +1,6 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:copy_with_extension/copy_with_extension.dart'; import 'package:copy_with_extension/copy_with_extension.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.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 '../discount/discount_model.dart';
import '../game_tests/test_statistics_model.dart'; import '../game_tests/test_statistics_model.dart';
@ -12,7 +11,6 @@ import '../statistics/achievement_model.dart';
part 'user_data_model.g.dart'; part 'user_data_model.g.dart';
@JsonSerializable()
@CopyWith() @CopyWith()
class UserDataModel { class UserDataModel {
String? id; String? id;
@ -21,7 +19,6 @@ class UserDataModel {
final List<PromoCodeModel> activatedPromoCodes = []; final List<PromoCodeModel> activatedPromoCodes = [];
final List<PromoCodeModel> individualPromoCodes = []; final List<PromoCodeModel> individualPromoCodes = [];
final List<DiscountModel> activeDiscounts = []; final List<DiscountModel> activeDiscounts = [];
UserModel? user;
final List<WordStatisticsModel> words; final List<WordStatisticsModel> words;
final String? lastTestSessionToken; final String? lastTestSessionToken;
@ -68,10 +65,6 @@ class UserDataModel {
this.achievements = const [], this.achievements = const [],
}); });
factory UserDataModel.fromJson(Map<String, dynamic> json) =>
_$UserDataModelFromJson(json);
Map<String, Object?> toJson() => _$UserDataModelToJson(this);
} }
// Helper functions for PackProgressModel serialization // Helper functions for PackProgressModel serialization

View file

@ -196,64 +196,3 @@ extension $UserDataModelCopyWith on UserDataModel {
// ignore: library_private_types_in_public_api // ignore: library_private_types_in_public_api
_$UserDataModelCWProxy get copyWith => _$UserDataModelCWProxyImpl(this); _$UserDataModelCWProxy get copyWith => _$UserDataModelCWProxyImpl(this);
} }
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
UserDataModel _$UserDataModelFromJson(Map<String, dynamic> 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<dynamic>?)
?.map(
(e) =>
WordStatisticsModel.fromJson(e as Map<String, dynamic>),
)
.toList() ??
const [],
tags:
(json['tags'] as List<dynamic>?)
?.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<dynamic>?)
?.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<String, dynamic>);
Map<String, dynamic> _$UserDataModelToJson(
UserDataModel instance,
) => <String, dynamic>{
'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),
};

View file

@ -7,7 +7,6 @@ import '../subscription/user_subscription_model.dart';
part 'user_model.g.dart'; part 'user_model.g.dart';
@JsonSerializable()
@CopyWith() @CopyWith()
class UserModel { class UserModel {
String? id; String? id;
@ -16,11 +15,10 @@ class UserModel {
final String? telegram; final String? telegram;
@JsonKey(defaultValue: false) @JsonKey(defaultValue: false)
final bool admin; final bool admin;
// Relations - loaded separately from database
final List<CardPackModel> packs = [];
UserDataModel? userData;
@JsonKey(defaultValue: []) @JsonKey(defaultValue: [])
final List<String> purchases; final List<String> purchases;
final List<CardPackModel> packs;
UserDataModel? userData;
UserSubscriptionModel? subscriptionModel; UserSubscriptionModel? subscriptionModel;
// UserSettingsDto // UserSettingsDto
@ -33,15 +31,10 @@ class UserModel {
this.telegram, this.telegram,
this.admin = false, this.admin = false,
this.purchases = const [], this.purchases = const [],
this.packs = const [],
this.userSettings, this.userSettings,
}); });
factory UserModel.fromJson(Map<String, dynamic> json) =>
_$UserModelFromJson(json);
Map<String, Object?> toJson() => _$UserModelToJson(this);
@JsonKey(includeFromJson: false, includeToJson: false)
static UserModel get empty => UserModel( static UserModel get empty => UserModel(
admin: false, admin: false,
purchases: [], purchases: [],

View file

@ -124,42 +124,3 @@ extension $UserModelCopyWith on UserModel {
// ignore: library_private_types_in_public_api // ignore: library_private_types_in_public_api
_$UserModelCWProxy get copyWith => _$UserModelCWProxyImpl(this); _$UserModelCWProxy get copyWith => _$UserModelCWProxyImpl(this);
} }
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
UserModel _$UserModelFromJson(Map<String, dynamic> 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<dynamic>?)
?.map((e) => e as String)
.toList() ??
[],
userSettings: json['userSettings'] as String?,
)
..userData = json['userData'] == null
? null
: UserDataModel.fromJson(json['userData'] as Map<String, dynamic>)
..subscriptionModel = json['subscriptionModel'] == null
? null
: UserSubscriptionModel.fromJson(
json['subscriptionModel'] as Map<String, dynamic>,
);
Map<String, dynamic> _$UserModelToJson(UserModel instance) => <String, dynamic>{
'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,
};