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
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:
parent
df2f5d5740
commit
a6abcfa7e6
44 changed files with 900 additions and 2185 deletions
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ import '../../discounts/discounts_manager.dart' as _i891;
|
|||
import '../../packs/free_packs_distributor.dart' as _i1062;
|
||||
import '../../packs/pack_dto_converter.dart' as _i433;
|
||||
import '../../packs/pack_manager.dart' as _i833;
|
||||
import '../../packs/pack_repository.dart' as _i258;
|
||||
import '../../packs/products_price_resolver.dart' as _i908;
|
||||
import '../../promo_codes/promo_codes_manager.dart' as _i151;
|
||||
import '../../statistics/achievement_manager.dart' as _i802;
|
||||
|
|
@ -31,6 +32,7 @@ import '../../tasks/task_manager.dart' as _i586;
|
|||
import '../../tests/test_manager.dart' as _i259;
|
||||
import '../../user/user_manager.dart' as _i280;
|
||||
import '../../user/user_manager_drift.dart' as _i560;
|
||||
import '../../user/user_repository.dart' as _i950;
|
||||
import '../ads/ads_manager.dart' as _i846;
|
||||
import '../mnemo_shelf.dart' as _i561;
|
||||
import '../purchase/payment_manager.dart' as _i1009;
|
||||
|
|
@ -105,14 +107,11 @@ extension GetItInjectableX on _i174.GetIt {
|
|||
gh.lazySingleton<_i108.JwtService>(
|
||||
() => _i108.JwtService(gh<_i1072.AppDatabase>()),
|
||||
);
|
||||
gh.lazySingleton<_i296.TelegramBotApiV2>(
|
||||
() => _i296.TelegramBotApiV2(gh<_i1072.AppDatabase>()),
|
||||
);
|
||||
gh.lazySingleton<_i891.DiscountsManager>(
|
||||
() => _i891.DiscountsManager(gh<_i1072.AppDatabase>()),
|
||||
);
|
||||
gh.lazySingleton<_i1062.FreePacksDistributor>(
|
||||
() => _i1062.FreePacksDistributor(gh<_i1072.AppDatabase>()),
|
||||
gh.lazySingleton<_i258.PackRepository>(
|
||||
() => _i258.PackRepository(gh<_i1072.AppDatabase>()),
|
||||
);
|
||||
gh.lazySingleton<_i802.AchievementManager>(
|
||||
() => _i802.AchievementManager(gh<_i1072.AppDatabase>()),
|
||||
|
|
@ -129,46 +128,42 @@ extension GetItInjectableX on _i174.GetIt {
|
|||
gh.lazySingleton<_i586.TaskManager>(
|
||||
() => _i586.TaskManager(gh<_i1072.AppDatabase>()),
|
||||
);
|
||||
gh.lazySingleton<_i950.UserRepository>(
|
||||
() => _i950.UserRepository(gh<_i1072.AppDatabase>()),
|
||||
);
|
||||
gh.factory<_i1015.AdminPacksApiV2>(
|
||||
() => _i1015.AdminPacksApiV2(gh<_i1072.AppDatabase>()),
|
||||
);
|
||||
gh.factory<_i895.AdminUsersApiV2>(
|
||||
() => _i895.AdminUsersApiV2(gh<_i1072.AppDatabase>()),
|
||||
);
|
||||
gh.lazySingleton<_i964.SubscriptionsApiV2>(
|
||||
() => _i964.SubscriptionsApiV2(gh<_i377.SubscriptionManager>()),
|
||||
);
|
||||
gh.lazySingleton<_i858.DiscountsApiV2>(
|
||||
() => _i858.DiscountsApiV2(gh<_i891.DiscountsManager>()),
|
||||
);
|
||||
gh.lazySingleton<_i1009.PaymentManager>(
|
||||
() => _i1009.PaymentManager(
|
||||
gh.lazySingleton<_i296.TelegramBotApiV2>(
|
||||
() => _i296.TelegramBotApiV2(
|
||||
gh<_i1072.AppDatabase>(),
|
||||
gh<_i988.YooMoneyHandler>(),
|
||||
gh<_i222.RustorePurchaseHandler>(),
|
||||
gh<_i950.UserRepository>(),
|
||||
),
|
||||
);
|
||||
gh.factory<_i895.AdminUsersApiV2>(
|
||||
() => _i895.AdminUsersApiV2(
|
||||
gh<_i1072.AppDatabase>(),
|
||||
gh<_i950.UserRepository>(),
|
||||
),
|
||||
);
|
||||
gh.lazySingleton<_i365.MediaApiV2>(
|
||||
() => _i365.MediaApiV2(gh<_i747.MinioService>()),
|
||||
);
|
||||
gh.lazySingleton<_i280.UserManager>(
|
||||
() => _i280.UserManager(
|
||||
gh<_i1072.AppDatabase>(),
|
||||
gh<_i1062.FreePacksDistributor>(),
|
||||
gh<_i71.SessionTracker>(),
|
||||
gh<_i1029.StatisticsCalculator>(),
|
||||
gh<_i802.AchievementManager>(),
|
||||
gh<_i909.WordStatisticsManager>(),
|
||||
),
|
||||
);
|
||||
gh.lazySingleton<_i985.TasksApiV2>(
|
||||
() => _i985.TasksApiV2(gh<_i586.TaskManager>()),
|
||||
);
|
||||
gh.lazySingleton<_i483.AdminAuthApiV2>(
|
||||
() => _i483.AdminAuthApiV2(
|
||||
gh<_i240.TelegramAuthCodeService>(),
|
||||
gh<_i280.UserManager>(),
|
||||
gh<_i108.JwtService>(),
|
||||
gh.lazySingleton<_i1009.PaymentManager>(
|
||||
() => _i1009.PaymentManager(
|
||||
gh<_i1072.AppDatabase>(),
|
||||
gh<_i950.UserRepository>(),
|
||||
gh<_i988.YooMoneyHandler>(),
|
||||
gh<_i222.RustorePurchaseHandler>(),
|
||||
),
|
||||
);
|
||||
gh.lazySingleton<_i151.PromoCodesManager>(
|
||||
|
|
@ -177,6 +172,12 @@ extension GetItInjectableX on _i174.GetIt {
|
|||
gh<_i1009.PaymentManager>(),
|
||||
),
|
||||
);
|
||||
gh.lazySingleton<_i1062.FreePacksDistributor>(
|
||||
() => _i1062.FreePacksDistributor(
|
||||
gh<_i1072.AppDatabase>(),
|
||||
gh<_i258.PackRepository>(),
|
||||
),
|
||||
);
|
||||
gh.lazySingleton<_i560.UserManager>(
|
||||
() => _i560.UserManager(
|
||||
gh<_i1072.AppDatabase>(),
|
||||
|
|
@ -186,17 +187,6 @@ extension GetItInjectableX on _i174.GetIt {
|
|||
gh<_i802.AchievementManager>(),
|
||||
),
|
||||
);
|
||||
gh.lazySingleton<_i561.MnemoShelf>(
|
||||
() => _i561.MnemoShelf(gh<_i280.UserManager>()),
|
||||
);
|
||||
gh.lazySingleton<_i247.UsersApiV2>(
|
||||
() => _i247.UsersApiV2(
|
||||
gh<_i280.UserManager>(),
|
||||
gh<_i1009.PaymentManager>(),
|
||||
gh<_i1029.StatisticsCalculator>(),
|
||||
gh<_i1072.AppDatabase>(),
|
||||
),
|
||||
);
|
||||
gh.lazySingleton<_i273.PromocodesApiV2>(
|
||||
() => _i273.PromocodesApiV2(gh<_i151.PromoCodesManager>()),
|
||||
);
|
||||
|
|
@ -212,6 +202,55 @@ extension GetItInjectableX on _i174.GetIt {
|
|||
gh<_i1072.AppDatabase>(),
|
||||
),
|
||||
);
|
||||
gh.lazySingleton<_i280.UserManager>(
|
||||
() => _i280.UserManager(
|
||||
gh<_i1072.AppDatabase>(),
|
||||
gh<_i950.UserRepository>(),
|
||||
gh<_i1062.FreePacksDistributor>(),
|
||||
gh<_i71.SessionTracker>(),
|
||||
gh<_i1029.StatisticsCalculator>(),
|
||||
gh<_i802.AchievementManager>(),
|
||||
gh<_i909.WordStatisticsManager>(),
|
||||
),
|
||||
);
|
||||
gh.lazySingleton<_i483.AdminAuthApiV2>(
|
||||
() => _i483.AdminAuthApiV2(
|
||||
gh<_i240.TelegramAuthCodeService>(),
|
||||
gh<_i280.UserManager>(),
|
||||
gh<_i108.JwtService>(),
|
||||
),
|
||||
);
|
||||
gh.lazySingleton<_i433.PackDtoConverter>(
|
||||
() => _i433.PackDtoConverter(
|
||||
gh<_i908.ProductsPriceResolver>(),
|
||||
gh<_i846.AdsManager>(),
|
||||
),
|
||||
);
|
||||
gh.lazySingleton<_i833.PackManager>(
|
||||
() => _i833.PackManager(
|
||||
gh<_i1072.AppDatabase>(),
|
||||
gh<_i258.PackRepository>(),
|
||||
gh<_i433.PackDtoConverter>(),
|
||||
),
|
||||
);
|
||||
gh.lazySingleton<_i69.PurchasesApiV2>(
|
||||
() => _i69.PurchasesApiV2(
|
||||
gh<_i1009.PaymentManager>(),
|
||||
gh<_i833.PackManager>(),
|
||||
gh<_i1072.AppDatabase>(),
|
||||
),
|
||||
);
|
||||
gh.lazySingleton<_i561.MnemoShelf>(
|
||||
() => _i561.MnemoShelf(gh<_i280.UserManager>()),
|
||||
);
|
||||
gh.lazySingleton<_i247.UsersApiV2>(
|
||||
() => _i247.UsersApiV2(
|
||||
gh<_i280.UserManager>(),
|
||||
gh<_i1009.PaymentManager>(),
|
||||
gh<_i1029.StatisticsCalculator>(),
|
||||
gh<_i1072.AppDatabase>(),
|
||||
),
|
||||
);
|
||||
gh.lazySingleton<_i170.TestsApiV2>(
|
||||
() => _i170.TestsApiV2(
|
||||
gh<_i259.TestManager>(),
|
||||
|
|
@ -228,25 +267,6 @@ extension GetItInjectableX on _i174.GetIt {
|
|||
gh<_i240.TelegramAuthCodeService>(),
|
||||
),
|
||||
);
|
||||
gh.lazySingleton<_i433.PackDtoConverter>(
|
||||
() => _i433.PackDtoConverter(
|
||||
gh<_i908.ProductsPriceResolver>(),
|
||||
gh<_i846.AdsManager>(),
|
||||
),
|
||||
);
|
||||
gh.lazySingleton<_i833.PackManager>(
|
||||
() => _i833.PackManager(
|
||||
gh<_i1072.AppDatabase>(),
|
||||
gh<_i433.PackDtoConverter>(),
|
||||
),
|
||||
);
|
||||
gh.lazySingleton<_i69.PurchasesApiV2>(
|
||||
() => _i69.PurchasesApiV2(
|
||||
gh<_i1009.PaymentManager>(),
|
||||
gh<_i833.PackManager>(),
|
||||
gh<_i1072.AppDatabase>(),
|
||||
),
|
||||
);
|
||||
gh.lazySingleton<_i800.PacksApiV2>(
|
||||
() => _i800.PacksApiV2(
|
||||
gh<_i833.PackManager>(),
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import 'package:drift/drift.dart' as drift;
|
|||
import 'payment_drift_extension.dart';
|
||||
import 'rustore/rustore_purchase_handler.dart';
|
||||
import 'yoo_money.dart';
|
||||
import '../../user/user_drift_extension.dart';
|
||||
import '../../user/user_repository.dart';
|
||||
|
||||
/// Result of creating YooKassa payment URL
|
||||
class YookassaPaymentResult {
|
||||
|
|
@ -26,11 +26,17 @@ class YookassaPaymentResult {
|
|||
@lazySingleton
|
||||
class PaymentManager {
|
||||
final AppDatabase _db;
|
||||
final UserRepository _userRepository;
|
||||
late final GooglePlayPurchaseHandler googlePurchaseHandler;
|
||||
final YooMoneyHandler _yooMoneyHandler;
|
||||
final RustorePurchaseHandler _rustorePurchaseHandler;
|
||||
|
||||
PaymentManager(this._db, this._yooMoneyHandler, this._rustorePurchaseHandler);
|
||||
PaymentManager(
|
||||
this._db,
|
||||
this._userRepository,
|
||||
this._yooMoneyHandler,
|
||||
this._rustorePurchaseHandler,
|
||||
);
|
||||
|
||||
/// Создать платеж в базе данных
|
||||
Future<PaymentDto> createPayment(PaymentDto paymentDto, String userId) async {
|
||||
|
|
@ -74,7 +80,7 @@ class PaymentManager {
|
|||
await _db.transaction(() async {
|
||||
if (product.type == MnemoCardsProductType.pack && product.id != null) {
|
||||
final packId = product.id!;
|
||||
await _db.userDao.grantPackAccess(
|
||||
await _userRepository.grantPackAccess(
|
||||
userId: userId,
|
||||
packId: packId,
|
||||
grantType: 'promo_code',
|
||||
|
|
@ -114,8 +120,8 @@ class PaymentManager {
|
|||
}
|
||||
|
||||
// Получить пользователя
|
||||
final user = await _db.userDao.getUserWithDataById(payment.userId);
|
||||
if (user == null) {
|
||||
final userModel = await _userRepository.getUserWithDataById(payment.userId);
|
||||
if (userModel == null) {
|
||||
log('User not found: ${payment.userId}');
|
||||
return;
|
||||
}
|
||||
|
|
@ -154,7 +160,7 @@ class PaymentManager {
|
|||
await _db.transaction(() async {
|
||||
// Дать доступ к пакетам
|
||||
for (final packId in packIds) {
|
||||
await _db.userDao.grantPackAccess(
|
||||
await _userRepository.grantPackAccess(
|
||||
userId: payment.userId,
|
||||
packId: packId,
|
||||
grantType: 'purchase',
|
||||
|
|
@ -422,11 +428,11 @@ class PaymentManager {
|
|||
final firstProduct = payment.products.first;
|
||||
final productId = firstProduct['id']?.toString();
|
||||
if (productId != null && payment.externalToken != null) {
|
||||
final user = await _db.userDao.getUserById(payment.userId);
|
||||
final user = await _userRepository.getUserById(payment.userId);
|
||||
await checkRustorePayment(
|
||||
productId: productId,
|
||||
subscriptionToken: payment.externalToken!,
|
||||
user: user != null ? await user.toUserModel() : null,
|
||||
user: user,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import 'package:injectable/injectable.dart';
|
|||
import 'package:mnemo_cards_backend/api/authorize/acl_types.dart';
|
||||
import 'package:mnemo_cards_backend/api/authorize/access_service.dart';
|
||||
import 'package:mnemo_cards_backend/api/authorize/helpers.dart';
|
||||
import 'package:mnemo_cards_backend/main.dart' as backend_main;
|
||||
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
import 'package:mnemo_cards_backend/user/user_repository.dart';
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shelf_router/shelf_router.dart';
|
||||
|
||||
|
|
@ -14,7 +14,10 @@ part 'admin_analytics_api_v2.g.dart';
|
|||
/// Admin endpoints for analytics and statistics in API v2.
|
||||
@lazySingleton
|
||||
class AdminAnalyticsApiV2 {
|
||||
AdminAnalyticsApiV2();
|
||||
final UserRepository _userRepository;
|
||||
final AppDatabase _db;
|
||||
|
||||
AdminAnalyticsApiV2(this._userRepository, this._db);
|
||||
|
||||
Response _json(
|
||||
Object? data, {
|
||||
|
|
@ -51,23 +54,22 @@ class AdminAnalyticsApiV2 {
|
|||
}
|
||||
|
||||
// Get basic statistics
|
||||
final userCount = await backend_main.database.userDao.countUsers();
|
||||
final userCount = await _userRepository.countUsers();
|
||||
|
||||
final cardCount = await backend_main.database.packDao.countCards();
|
||||
final cardCount = await _db.packDao.countCards();
|
||||
|
||||
final packCount = await backend_main.database.packDao.countPacks();
|
||||
final packCount = await _db.packDao.countPacks();
|
||||
|
||||
// Get all packs and filter in memory
|
||||
final enabledPacks = await backend_main.database.packDao.getAllPacks(
|
||||
final enabledPacks = await _db.packDao.getAllPacks(
|
||||
enabledOnly: true,
|
||||
);
|
||||
final enabledPackCount = enabledPacks.length;
|
||||
|
||||
final paymentCount = await backend_main.database.paymentDao
|
||||
.countAllPayments();
|
||||
final paymentCount = await _db.paymentDao.countAllPayments();
|
||||
|
||||
// Get recent users (last 10)
|
||||
final allUsers = await backend_main.database.userDao.getAllUsers(
|
||||
final allUsers = await _userRepository.getAllUsers(
|
||||
limit: 10,
|
||||
);
|
||||
final recentUsers = allUsers
|
||||
|
|
@ -76,17 +78,16 @@ class AdminAnalyticsApiV2 {
|
|||
'id': u.id,
|
||||
'name': u.name,
|
||||
'email': u.email,
|
||||
'createdAt': u.createdAt.dateTime.toIso8601String(),
|
||||
},
|
||||
)
|
||||
.toList();
|
||||
|
||||
// Get top packs by user count
|
||||
final allPacks = await backend_main.database.packDao.getAllPacks();
|
||||
final allPacks = await _db.packDao.getAllPacks();
|
||||
final topPacksList = allPacks.take(5);
|
||||
final topPacks = await Future.wait(
|
||||
topPacksList.map((p) async {
|
||||
final cards = await backend_main.database.packDao.getPackCards(p.id);
|
||||
final cards = await _db.packDao.getPackCards(p.id);
|
||||
return {
|
||||
'id': p.id,
|
||||
'title': p.title,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ import 'package:mnemo_cards_backend/api/authorize/access_service.dart';
|
|||
import 'package:mnemo_cards_backend/api/authorize/helpers.dart';
|
||||
import 'package:mnemo_cards_backend/packs/card_pack_drift_extension.dart';
|
||||
import 'package:mnemo_cards_backend/packs/card_image_storage.dart';
|
||||
import 'package:mnemo_cards_backend/packs/pack_repository.dart';
|
||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shelf_router/shelf_router.dart';
|
||||
|
||||
|
|
@ -19,8 +21,9 @@ part 'admin_packs_api_v2.g.dart';
|
|||
@injectable
|
||||
class AdminPacksApiV2 {
|
||||
final AppDatabase _db;
|
||||
final PackRepository _packRepository;
|
||||
|
||||
AdminPacksApiV2(this._db);
|
||||
AdminPacksApiV2(this._db, this._packRepository);
|
||||
|
||||
static final _uuidRegex = RegExp(
|
||||
r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$',
|
||||
|
|
@ -47,9 +50,9 @@ class AdminPacksApiV2 {
|
|||
required String cardId,
|
||||
}) async {
|
||||
try {
|
||||
final card = await _db.packDao.getCardById(cardId);
|
||||
final card = await _packRepository.getCardById(cardId);
|
||||
if (card == null) return;
|
||||
await _db.packDao.addCardToPack(packId: packId, cardId: cardId);
|
||||
await _packRepository.addCardToPack(packId: packId, cardId: cardId);
|
||||
} catch (_) {
|
||||
// Best-effort only.
|
||||
}
|
||||
|
|
@ -67,6 +70,7 @@ class AdminPacksApiV2 {
|
|||
mnemo: const drift.Value('test_image'),
|
||||
);
|
||||
|
||||
// Note: createCard is not in PackRepository, keeping direct DAO call for now
|
||||
final cardId = await _db.packDao.createCard(companion);
|
||||
await _tryLinkCardToPack(packId: packId, cardId: cardId);
|
||||
|
||||
|
|
@ -78,11 +82,16 @@ class AdminPacksApiV2 {
|
|||
);
|
||||
if (stored == null) return null;
|
||||
|
||||
final created = await _db.packDao.getCardById(cardId);
|
||||
final created = await _packRepository.getCardById(cardId);
|
||||
if (created == null) return null;
|
||||
|
||||
// Note: updateCard is not in PackRepository, keeping direct DAO call for now
|
||||
// We need to get the Drift GameCard to update it
|
||||
final driftCard = await _db.packDao.getCardById(cardId);
|
||||
if (driftCard == null) return null;
|
||||
|
||||
await _db.packDao.updateCard(
|
||||
created.copyWith(
|
||||
driftCard.copyWith(
|
||||
image: stored.fileName,
|
||||
updatedAt: PgDateTime(DateTime.now()),
|
||||
),
|
||||
|
|
@ -278,25 +287,41 @@ class AdminPacksApiV2 {
|
|||
final validatedLimit = limit < 1 ? 20 : (limit > 100 ? 100 : limit);
|
||||
|
||||
// Get all packs
|
||||
final allPacks = await _db.packDao.getAllPacks(
|
||||
final allPacks = await _packRepository.getAllPacks(
|
||||
enabledOnly: !showDisabled,
|
||||
orderByField: 'order',
|
||||
);
|
||||
|
||||
// Apply search filter if provided
|
||||
List<CardPack> filteredPacks = allPacks;
|
||||
List<CardPackModel> filteredPacks = allPacks;
|
||||
if (search.isNotEmpty) {
|
||||
final searchTerm = search.toLowerCase();
|
||||
filteredPacks = allPacks.where((pack) {
|
||||
return pack.title.toLowerCase().contains(searchTerm) ||
|
||||
pack.subtitle.toLowerCase().contains(searchTerm) ||
|
||||
pack.id.toLowerCase().contains(searchTerm);
|
||||
(pack.id?.toLowerCase().contains(searchTerm) ?? false);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// Convert to preview DTOs
|
||||
// Note: We need to convert CardPackModel back to CardPack for toPreviewDto
|
||||
// This is a limitation - we should add a method to PackRepository for preview DTOs
|
||||
final previewDtos = await Future.wait(
|
||||
filteredPacks.map((pack) async {
|
||||
filteredPacks.where((p) => p.id != null).map((packModel) async {
|
||||
// Get the Drift CardPack for toPreviewDto extension
|
||||
final pack = await _db.packDao.getPackById(packModel.id!);
|
||||
if (pack == null) {
|
||||
return {
|
||||
'id': packModel.id,
|
||||
'title': packModel.title,
|
||||
'subtitle': packModel.subtitle,
|
||||
'color': packModel.color,
|
||||
'cover': null,
|
||||
'cards': packModel.size,
|
||||
'enabled': packModel.enabled,
|
||||
'order': packModel.order,
|
||||
};
|
||||
}
|
||||
final dto = await pack.toPreviewDto(null);
|
||||
return {
|
||||
'id': dto.id,
|
||||
|
|
@ -357,7 +382,7 @@ class AdminPacksApiV2 {
|
|||
}, statusCode: 400);
|
||||
}
|
||||
|
||||
final pack = await _db.packDao.getPackById(packId);
|
||||
final pack = await _packRepository.getPackById(packId);
|
||||
if (pack == null) {
|
||||
return _json({
|
||||
'error': 'Pack not found',
|
||||
|
|
@ -367,13 +392,13 @@ class AdminPacksApiV2 {
|
|||
}, statusCode: 404);
|
||||
}
|
||||
|
||||
final cards = await _db.packDao.getPackCards(packId);
|
||||
final cards = await _packRepository.getPackCards(packId);
|
||||
|
||||
// Get cards order - cards are already sorted by order from getPackCards
|
||||
final cardsOrder = cards.map((c) => c.id).toList();
|
||||
|
||||
// Get preview cards
|
||||
final previewCards = await _db.packDao.getPreviewCards(packId);
|
||||
final previewCards = await _packRepository.getPreviewCards(packId);
|
||||
final previewCardIds = previewCards.map((c) => c.id).toList();
|
||||
|
||||
// Get tests for this pack
|
||||
|
|
@ -447,7 +472,7 @@ class AdminPacksApiV2 {
|
|||
if (editDto.id != null && editDto.id!.isNotEmpty) {
|
||||
// Update existing pack
|
||||
packId = editDto.id!;
|
||||
final existing = await _db.packDao.getPackById(packId);
|
||||
final existing = await _packRepository.getPackById(packId);
|
||||
if (existing == null) {
|
||||
return _json({
|
||||
'error': 'Pack not found',
|
||||
|
|
@ -457,7 +482,7 @@ class AdminPacksApiV2 {
|
|||
}, statusCode: 404);
|
||||
}
|
||||
|
||||
await _db.packDao.updatePackPartial(
|
||||
await _packRepository.updatePackPartial(
|
||||
CardPacksCompanion(
|
||||
id: drift.Value(packId),
|
||||
title: editDto.title != null
|
||||
|
|
@ -504,39 +529,26 @@ class AdminPacksApiV2 {
|
|||
);
|
||||
} else {
|
||||
// Create new pack
|
||||
packId = await _db.packDao.createPack(
|
||||
CardPacksCompanion.insert(
|
||||
title: editDto.title ?? '',
|
||||
subtitle: editDto.subtitle ?? '',
|
||||
color: editDto.color != null
|
||||
? drift.Value(editDto.color)
|
||||
: const drift.Value.absent(),
|
||||
cover: editDto.cover != null
|
||||
? drift.Value(editDto.cover)
|
||||
: const drift.Value.absent(),
|
||||
size: editDto.size ?? 0,
|
||||
googlePlayId: editDto.googlePlayId != null
|
||||
? drift.Value(editDto.googlePlayId)
|
||||
: const drift.Value.absent(),
|
||||
rustoreId: editDto.rustoreId != null
|
||||
? drift.Value(editDto.rustoreId)
|
||||
: const drift.Value.absent(),
|
||||
appStoreId: editDto.appStoreId != null
|
||||
? drift.Value(editDto.appStoreId)
|
||||
: const drift.Value.absent(),
|
||||
price: editDto.price != null
|
||||
? drift.Value(editDto.price)
|
||||
: const drift.Value.absent(),
|
||||
description: editDto.description != null
|
||||
? drift.Value(editDto.description)
|
||||
: const drift.Value.absent(),
|
||||
enabled: drift.Value(editDto.enabled ?? true),
|
||||
version: editDto.version != null
|
||||
? drift.Value(editDto.version)
|
||||
: const drift.Value.absent(),
|
||||
order: drift.Value(editDto.order ?? 0),
|
||||
),
|
||||
// Note: We need to create a CardPackModel first
|
||||
final packModel = CardPackModel(
|
||||
id: null,
|
||||
title: editDto.title ?? '',
|
||||
subtitle: editDto.subtitle ?? '',
|
||||
size: editDto.size ?? 0,
|
||||
color: editDto.color,
|
||||
version: editDto.version,
|
||||
cover: editDto.cover,
|
||||
description: editDto.description,
|
||||
googlePlayId: editDto.googlePlayId,
|
||||
rustoreId: editDto.rustoreId,
|
||||
appStoreId: editDto.appStoreId,
|
||||
price: editDto.price,
|
||||
currency: null,
|
||||
enabled: editDto.enabled ?? true,
|
||||
order: editDto.order ?? 0,
|
||||
cardsOrder: [],
|
||||
);
|
||||
packId = await _packRepository.createPack(packModel);
|
||||
}
|
||||
|
||||
// Handle card associations if provided
|
||||
|
|
@ -544,7 +556,7 @@ class AdminPacksApiV2 {
|
|||
try {
|
||||
for (final cardId in editDto.addCardIds!) {
|
||||
// Verify card exists before adding
|
||||
final card = await _db.packDao.getCardById(cardId);
|
||||
final card = await _packRepository.getCardById(cardId);
|
||||
if (card == null) {
|
||||
return _json({
|
||||
'error': 'Card not found',
|
||||
|
|
@ -555,7 +567,7 @@ class AdminPacksApiV2 {
|
|||
'Card with ID "$cardId" was not found. Please verify all card IDs before adding them to the pack.',
|
||||
}, statusCode: 404);
|
||||
}
|
||||
await _db.packDao.addCardToPack(packId: packId, cardId: cardId);
|
||||
await _packRepository.addCardToPack(packId: packId, cardId: cardId);
|
||||
}
|
||||
} catch (e) {
|
||||
// Handle duplicate or constraint errors
|
||||
|
|
@ -575,7 +587,7 @@ class AdminPacksApiV2 {
|
|||
|
||||
if (editDto.removeCardIds != null && editDto.removeCardIds!.isNotEmpty) {
|
||||
for (final cardId in editDto.removeCardIds!) {
|
||||
await _db.packDao.removeCardFromPack(packId, cardId);
|
||||
await _packRepository.removeCardFromPack(packId, cardId);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -625,15 +637,15 @@ class AdminPacksApiV2 {
|
|||
|
||||
// Handle cards order if provided
|
||||
if (editDto.cardsOrder != null && editDto.cardsOrder!.isNotEmpty) {
|
||||
await _db.packDao.updatePackCardsOrder(packId, editDto.cardsOrder!);
|
||||
await _packRepository.updatePackCardsOrder(packId, editDto.cardsOrder!);
|
||||
}
|
||||
|
||||
// Handle preview cards
|
||||
if (editDto.previewCards != null) {
|
||||
await _db.packDao.setPreviewCards(packId, editDto.previewCards!);
|
||||
await _packRepository.setPreviewCards(packId, editDto.previewCards!);
|
||||
}
|
||||
|
||||
final updatedPack = await _db.packDao.getPackById(packId);
|
||||
final updatedPack = await _packRepository.getPackById(packId);
|
||||
if (updatedPack == null) {
|
||||
return _json({
|
||||
'error': 'Database error',
|
||||
|
|
@ -643,9 +655,9 @@ class AdminPacksApiV2 {
|
|||
}, statusCode: 500);
|
||||
}
|
||||
|
||||
final updatedCards = await _db.packDao.getPackCards(packId);
|
||||
final updatedCards = await _packRepository.getPackCards(packId);
|
||||
final cardsOrder = updatedCards.map((c) => c.id).toList();
|
||||
final previewCards = await _db.packDao.getPreviewCards(packId);
|
||||
final previewCards = await _packRepository.getPreviewCards(packId);
|
||||
final previewCardIds = previewCards.map((c) => c.id).toList();
|
||||
|
||||
// Get updated tests for this pack
|
||||
|
|
@ -726,7 +738,7 @@ class AdminPacksApiV2 {
|
|||
}, statusCode: 400);
|
||||
}
|
||||
|
||||
final pack = await _db.packDao.getPackById(packId);
|
||||
final pack = await _packRepository.getPackById(packId);
|
||||
if (pack == null) {
|
||||
return _json({
|
||||
'error': 'Pack not found',
|
||||
|
|
@ -736,7 +748,7 @@ class AdminPacksApiV2 {
|
|||
}, statusCode: 404);
|
||||
}
|
||||
|
||||
await _db.packDao.softDeletePack(packId);
|
||||
await _packRepository.softDeletePack(packId);
|
||||
|
||||
return _json({'success': true, 'message': 'Pack deleted successfully'});
|
||||
} catch (e, s) {
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@ import 'package:mnemo_cards_backend/api/authorize/acl_types.dart';
|
|||
import 'package:mnemo_cards_backend/api/authorize/access_service.dart';
|
||||
import 'package:mnemo_cards_backend/api/authorize/helpers.dart';
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
import 'package:mnemo_cards_backend/user/user_drift_extension.dart';
|
||||
import 'package:mnemo_cards_backend/user/user_model.dart'
|
||||
show UserModelExtension;
|
||||
import 'package:mnemo_cards_backend/user/user_repository.dart';
|
||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shelf_router/shelf_router.dart';
|
||||
|
||||
|
|
@ -20,8 +21,9 @@ part 'admin_users_api_v2.g.dart';
|
|||
@injectable
|
||||
class AdminUsersApiV2 {
|
||||
final AppDatabase _db;
|
||||
final UserRepository _userRepository;
|
||||
|
||||
AdminUsersApiV2(this._db);
|
||||
AdminUsersApiV2(this._db, this._userRepository);
|
||||
|
||||
Response _json(
|
||||
Object? data, {
|
||||
|
|
@ -78,22 +80,22 @@ class AdminUsersApiV2 {
|
|||
final validatedLimit = limit < 1 ? 20 : (limit > 100 ? 100 : limit);
|
||||
|
||||
// Get total count
|
||||
final total = await _db.userDao.countUsers(includeDeleted: false);
|
||||
final total = await _userRepository.countUsers(includeDeleted: false);
|
||||
|
||||
// Calculate offset
|
||||
final offset = (page - 1) * validatedLimit;
|
||||
|
||||
// Get users with pagination
|
||||
final users = await _db.userDao.getAllUsers(
|
||||
final userModels = await _userRepository.getAllUsers(
|
||||
limit: validatedLimit,
|
||||
offset: offset,
|
||||
includeDeleted: false,
|
||||
includePacks: true,
|
||||
);
|
||||
|
||||
// Convert to DTOs
|
||||
final userDtos = <Map<String, dynamic>>[];
|
||||
for (final user in users) {
|
||||
final userModel = await user.toUserModel();
|
||||
for (final userModel in userModels) {
|
||||
final dto = await userModel.toDto();
|
||||
userDtos.add(dto.toJson());
|
||||
}
|
||||
|
|
@ -158,8 +160,8 @@ class AdminUsersApiV2 {
|
|||
}, statusCode: 400);
|
||||
}
|
||||
|
||||
final user = await _db.userDao.getUserById(userId);
|
||||
if (user == null) {
|
||||
final userModel = await _userRepository.getUserById(userId, includePacks: true);
|
||||
if (userModel == null) {
|
||||
return _json({
|
||||
'error': 'User not found',
|
||||
'message': 'The requested user does not exist or has been deleted',
|
||||
|
|
@ -168,7 +170,6 @@ class AdminUsersApiV2 {
|
|||
}, statusCode: 404);
|
||||
}
|
||||
|
||||
final userModel = await user.toUserModel();
|
||||
final dto = await userModel.toDto();
|
||||
|
||||
return _json(dto.toJson());
|
||||
|
|
@ -218,8 +219,8 @@ class AdminUsersApiV2 {
|
|||
|
||||
if (isUpdate) {
|
||||
// Update existing user
|
||||
final existingUser = await _db.userDao.getUserById(userDto.id!);
|
||||
if (existingUser == null) {
|
||||
final existingUserModel = await _userRepository.getUserById(userDto.id!);
|
||||
if (existingUserModel == null) {
|
||||
return _json({
|
||||
'error': 'User not found',
|
||||
'message': 'The user you are trying to update does not exist',
|
||||
|
|
@ -250,11 +251,11 @@ class AdminUsersApiV2 {
|
|||
updatedAt: drift.Value(PgDateTime(DateTime.now())),
|
||||
);
|
||||
|
||||
await _db.userDao.updateUserPartial(updates);
|
||||
await _userRepository.updateUserPartial(updates);
|
||||
|
||||
// Get updated user
|
||||
final updatedUser = await _db.userDao.getUserById(userDto.id!);
|
||||
if (updatedUser == null) {
|
||||
final updatedUserModel = await _userRepository.getUserById(userDto.id!, includePacks: true);
|
||||
if (updatedUserModel == null) {
|
||||
return _json({
|
||||
'error': 'Database error',
|
||||
'message': 'Failed to retrieve updated user',
|
||||
|
|
@ -263,8 +264,7 @@ class AdminUsersApiV2 {
|
|||
}, statusCode: 500);
|
||||
}
|
||||
|
||||
final userModel = await updatedUser.toUserModel();
|
||||
final dto = await userModel.toDto();
|
||||
final dto = await updatedUserModel.toDto();
|
||||
|
||||
return _json({'result': true, 'user': dto.toJson()});
|
||||
} else {
|
||||
|
|
@ -275,10 +275,8 @@ class AdminUsersApiV2 {
|
|||
: 'admin_created_${DateTime.now().millisecondsSinceEpoch}';
|
||||
|
||||
// Check if user with this externalUserId already exists
|
||||
final existingUser = await _db.userDao.getUserByExternalId(
|
||||
externalUserId,
|
||||
);
|
||||
if (existingUser != null) {
|
||||
final existingUserModel = await _userRepository.getUserByExternalId(externalUserId);
|
||||
if (existingUserModel != null) {
|
||||
return _json({
|
||||
'error': 'User already exists',
|
||||
'message': 'A user with this email already exists',
|
||||
|
|
@ -288,30 +286,29 @@ class AdminUsersApiV2 {
|
|||
}
|
||||
|
||||
final now = DateTime.now();
|
||||
final userCompanion = UsersCompanion.insert(
|
||||
externalUserId: externalUserId,
|
||||
name: drift.Value(userDto.name),
|
||||
email: drift.Value(userDto.email),
|
||||
telegram: drift.Value(userDto.telegram),
|
||||
admin: drift.Value(userDto.admin),
|
||||
purchases: drift.Value(userDto.purchases),
|
||||
createdAt: drift.Value(PgDateTime(now)),
|
||||
updatedAt: drift.Value(PgDateTime(now)),
|
||||
isDeleted: drift.Value(false),
|
||||
);
|
||||
|
||||
final userDataCompanion = UserDatasCompanion.insert(
|
||||
userId: '', // Will be set by createUserWithData
|
||||
registrationDate: drift.Value(PgDateTime(now)),
|
||||
);
|
||||
|
||||
final userId = await _db.userDao.createUserWithData(
|
||||
user: userCompanion,
|
||||
final userModel = UserModel(
|
||||
id: null,
|
||||
name: userDto.name,
|
||||
email: userDto.email,
|
||||
telegram: userDto.telegram,
|
||||
admin: userDto.admin,
|
||||
purchases: userDto.purchases,
|
||||
userSettings: null,
|
||||
packs: [],
|
||||
);
|
||||
final userId = await _userRepository.createUserWithData(
|
||||
userModel: userModel,
|
||||
userData: userDataCompanion,
|
||||
);
|
||||
|
||||
final createdUser = await _db.userDao.getUserById(userId);
|
||||
if (createdUser == null) {
|
||||
final createdUserModel = await _userRepository.getUserById(userId, includePacks: true);
|
||||
if (createdUserModel == null) {
|
||||
return _json({
|
||||
'error': 'Database error',
|
||||
'message': 'Failed to retrieve created user',
|
||||
|
|
@ -320,8 +317,7 @@ class AdminUsersApiV2 {
|
|||
}, statusCode: 500);
|
||||
}
|
||||
|
||||
final userModel = await createdUser.toUserModel();
|
||||
final dto = await userModel.toDto();
|
||||
final dto = await createdUserModel.toDto();
|
||||
|
||||
return _json({'result': true, 'user': dto.toJson()});
|
||||
}
|
||||
|
|
@ -355,8 +351,8 @@ class AdminUsersApiV2 {
|
|||
}, statusCode: 400);
|
||||
}
|
||||
|
||||
final user = await _db.userDao.getUserById(userId);
|
||||
if (user == null) {
|
||||
final userModel = await _userRepository.getUserById(userId);
|
||||
if (userModel == null) {
|
||||
return _json({
|
||||
'error': 'User not found',
|
||||
'message': 'The user you are trying to delete does not exist',
|
||||
|
|
@ -365,7 +361,9 @@ class AdminUsersApiV2 {
|
|||
}, statusCode: 404);
|
||||
}
|
||||
|
||||
if (user.isDeleted) {
|
||||
// Check if already deleted via DAO (isDeleted is not in UserModel)
|
||||
final user = await _db.userDao.getUserById(userId);
|
||||
if (user != null && user.isDeleted) {
|
||||
return _json({
|
||||
'error': 'User already deleted',
|
||||
'message': 'The user has already been deleted',
|
||||
|
|
@ -373,7 +371,7 @@ class AdminUsersApiV2 {
|
|||
}, statusCode: 409);
|
||||
}
|
||||
|
||||
await _db.userDao.softDeleteUser(userId);
|
||||
await _userRepository.softDeleteUser(userId);
|
||||
|
||||
return _json({'result': true});
|
||||
} catch (e, s) {
|
||||
|
|
|
|||
|
|
@ -8,13 +8,15 @@ import 'package:mnemo_cards_backend/database/database.dart' hide VoiceModel;
|
|||
import 'package:mnemo_cards_backend/database/database.dart'
|
||||
as drift
|
||||
show VoiceModel;
|
||||
import 'package:mnemo_cards_backend/packs/card_pack_drift_extension.dart';
|
||||
import 'package:mnemo_cards_backend/packs/card_image_storage.dart';
|
||||
import 'package:mnemo_cards_backend/packs/card_model_extension.dart';
|
||||
import 'package:mnemo_cards_backend/packs/pack_manager.dart' show PackManager;
|
||||
import 'package:mnemo_cards_backend/packs/pack_repository.dart';
|
||||
import 'package:mnemo_cards_backend/packs/voice_storage.dart';
|
||||
import 'package:mnemo_cards_backend/storage/minio_config.dart';
|
||||
import 'package:mnemo_cards_backend/storage/minio_service.dart';
|
||||
import 'package:mnemo_cards_backend/tests/test_manager.dart';
|
||||
import 'package:mnemo_cards_backend/user/user_repository.dart';
|
||||
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
|
||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||
import 'package:shelf/shelf.dart';
|
||||
|
|
@ -30,12 +32,16 @@ part 'packs_api_v2.g.dart';
|
|||
class PacksApiV2 {
|
||||
final PackManager _packManager;
|
||||
final TestManager _testManager;
|
||||
final PackRepository _packRepository;
|
||||
final UserRepository _userRepository;
|
||||
final AppDatabase _db;
|
||||
final MinioService _minioService;
|
||||
|
||||
PacksApiV2(
|
||||
this._packManager,
|
||||
this._testManager,
|
||||
this._packRepository,
|
||||
this._userRepository,
|
||||
this._db,
|
||||
this._minioService,
|
||||
);
|
||||
|
|
@ -366,13 +372,13 @@ class PacksApiV2 {
|
|||
user: request.user,
|
||||
);
|
||||
|
||||
final pack = await _db.packDao.getPackById(packId);
|
||||
final pack = await _packRepository.getPackById(packId);
|
||||
if (pack == null) {
|
||||
return _notFound('Pack not found');
|
||||
}
|
||||
|
||||
// Get cards for pack
|
||||
final cards = await _db.packDao.getPackCards(packId);
|
||||
final cards = await _packRepository.getPackCards(packId);
|
||||
|
||||
// Parse pagination
|
||||
final queryParams = request.requestedUri.queryParameters;
|
||||
|
|
@ -396,7 +402,7 @@ class PacksApiV2 {
|
|||
final cardIds = paginatedCards.map((c) => c.id).toList();
|
||||
final allVoices = <drift.VoiceModel>[];
|
||||
for (final cardId in cardIds) {
|
||||
final voices = await _db.packDao.getCardVoices(cardId);
|
||||
final voices = await _packRepository.getCardVoices(cardId);
|
||||
allVoices.addAll(voices);
|
||||
}
|
||||
|
||||
|
|
@ -404,9 +410,7 @@ class PacksApiV2 {
|
|||
// Presigned URLs now have 7 days expiration to avoid 403 errors on cached data
|
||||
final cardDtos = await Future.wait(
|
||||
paginatedCards.map((card) async {
|
||||
final dto = await card.toDto(
|
||||
allVoices.where((v) => v.cardId == card.id).toList(),
|
||||
);
|
||||
final dto = card.toDto();
|
||||
|
||||
// Generate presigned URLs for images if they are object IDs (UUIDs)
|
||||
final imageUrl = await _getPresignedUrlIfUuid(
|
||||
|
|
@ -468,7 +472,7 @@ class PacksApiV2 {
|
|||
// Check if pack exists and is enabled
|
||||
// We allow access to images for enabled packs even without auth
|
||||
// to support image previews in public listings
|
||||
final pack = await _db.packDao.getPackById(packId);
|
||||
final pack = await _packRepository.getPackById(packId);
|
||||
print('Pack lookup result: pack=$pack, enabled=${pack?.enabled}');
|
||||
if (pack == null || !pack.enabled) {
|
||||
print('Pack not found or not enabled');
|
||||
|
|
@ -476,7 +480,7 @@ class PacksApiV2 {
|
|||
}
|
||||
|
||||
// Get card and verify it belongs to the pack
|
||||
final card = await _db.packDao.getCardById(cardId);
|
||||
final card = await _packRepository.getCardById(cardId);
|
||||
print('Card lookup result: card=$card, image=${card?.image}');
|
||||
if (card == null) {
|
||||
print('Card not found');
|
||||
|
|
@ -484,7 +488,7 @@ class PacksApiV2 {
|
|||
}
|
||||
|
||||
// Verify card belongs to this pack
|
||||
final packCards = await _db.packDao.getPackCards(packId);
|
||||
final packCards = await _packRepository.getPackCards(packId);
|
||||
print('Pack cards count: ${packCards.length}');
|
||||
final belongsToPack = packCards.any((c) => c.id == cardId);
|
||||
print('Card belongs to pack: $belongsToPack');
|
||||
|
|
@ -546,19 +550,19 @@ class PacksApiV2 {
|
|||
// Check if pack exists and is enabled
|
||||
// We allow access to images for enabled packs even without auth
|
||||
// to support image previews in public listings
|
||||
final pack = await _db.packDao.getPackById(packId);
|
||||
final pack = await _packRepository.getPackById(packId);
|
||||
if (pack == null || !pack.enabled) {
|
||||
return _notFound('Pack not found or not enabled');
|
||||
}
|
||||
|
||||
// Get card and verify it belongs to the pack
|
||||
final card = await _db.packDao.getCardById(cardId);
|
||||
final card = await _packRepository.getCardById(cardId);
|
||||
if (card == null || card.imageBack == null || card.imageBack!.isEmpty) {
|
||||
return _notFound('Card or back image not found');
|
||||
}
|
||||
|
||||
// Verify card belongs to this pack
|
||||
final packCards = await _db.packDao.getPackCards(packId);
|
||||
final packCards = await _packRepository.getPackCards(packId);
|
||||
final belongsToPack = packCards.any((c) => c.id == cardId);
|
||||
if (!belongsToPack) {
|
||||
return _notFound('Card does not belong to this pack');
|
||||
|
|
@ -611,7 +615,7 @@ class PacksApiV2 {
|
|||
// Check if pack exists and is enabled
|
||||
// We allow access to covers for enabled packs even without auth
|
||||
// to support image previews in public listings
|
||||
final pack = await _db.packDao.getPackById(packId);
|
||||
final pack = await _packRepository.getPackById(packId);
|
||||
if (pack == null || !pack.enabled) {
|
||||
return _notFound('Pack not found or not enabled');
|
||||
}
|
||||
|
|
@ -665,25 +669,25 @@ class PacksApiV2 {
|
|||
return _badRequest('Invalid pack ID');
|
||||
}
|
||||
|
||||
final pack = await _db.packDao.getPackById(packId);
|
||||
final pack = await _packRepository.getPackById(packId);
|
||||
if (pack == null || !pack.enabled) {
|
||||
return _notFound('Pack not found or not enabled');
|
||||
}
|
||||
|
||||
final card = await _db.packDao.getCardById(cardId);
|
||||
final card = await _packRepository.getCardById(cardId);
|
||||
if (card == null) {
|
||||
return _notFound('Card not found');
|
||||
}
|
||||
|
||||
// Verify card belongs to this pack
|
||||
final packCards = await _db.packDao.getPackCards(packId);
|
||||
final packCards = await _packRepository.getPackCards(packId);
|
||||
final belongsToPack = packCards.any((c) => c.id == cardId);
|
||||
if (!belongsToPack) {
|
||||
return _notFound('Card does not belong to this pack');
|
||||
}
|
||||
|
||||
// Get voices for card
|
||||
final voices = await _db.packDao.getCardVoices(cardId);
|
||||
final voices = await _packRepository.getCardVoices(cardId);
|
||||
print('🔍 getCardVoices: Found ${voices.length} voices for card $cardId');
|
||||
if (voices.isEmpty) {
|
||||
print('⚠️ getCardVoices: No voices found for card $cardId');
|
||||
|
|
@ -742,7 +746,7 @@ class PacksApiV2 {
|
|||
);
|
||||
if (stored != null) {
|
||||
voicePath = stored.fileName;
|
||||
await _db.packDao.updateVoiceUrl(voice.id, stored.fileName);
|
||||
await _packRepository.updateVoiceUrl(voice.id, stored.fileName);
|
||||
} else {
|
||||
voicePath = '';
|
||||
}
|
||||
|
|
@ -776,20 +780,21 @@ class PacksApiV2 {
|
|||
return _badRequest('Invalid voice ID');
|
||||
}
|
||||
|
||||
final voice = await _db.packDao.getVoiceById(voiceId);
|
||||
final voice = await _packRepository.getVoiceById(voiceId);
|
||||
if (voice == null || voice.voiceUrl.isEmpty) {
|
||||
return _notFound('Voice not found');
|
||||
}
|
||||
|
||||
// Get cards that use this voice by checking CardVoices junction table
|
||||
// We need to find cards that have this voice and belong to enabled packs
|
||||
final allPacks = await _db.packDao.getAllPacks(enabledOnly: true);
|
||||
final allPacks = await _packRepository.getAllPacks(enabledOnly: true);
|
||||
bool hasEnabledPack = false;
|
||||
|
||||
for (final pack in allPacks) {
|
||||
final packCards = await _db.packDao.getPackCards(pack.id);
|
||||
if (pack.id == null) continue;
|
||||
final packCards = await _packRepository.getPackCards(pack.id!);
|
||||
for (final card in packCards) {
|
||||
final cardVoices = await _db.packDao.getCardVoices(card.id);
|
||||
final cardVoices = await _packRepository.getCardVoices(card.id);
|
||||
if (cardVoices.any((v) => v.id == voiceId)) {
|
||||
hasEnabledPack = true;
|
||||
break;
|
||||
|
|
@ -831,7 +836,7 @@ class PacksApiV2 {
|
|||
|
||||
/// Check if user purchased a pack
|
||||
Future<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
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:mnemo_cards_backend/api/purchase/payment_manager.dart';
|
|||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
import 'package:mnemo_cards_backend/packs/pack_manager.dart';
|
||||
import 'package:mnemo_cards_backend/packs/products_price_resolver.dart';
|
||||
import 'package:mnemo_cards_backend/user/user_repository.dart';
|
||||
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
|
||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||
import 'package:shelf/shelf.dart';
|
||||
|
|
@ -21,9 +22,15 @@ part 'purchases_api_v2.g.dart';
|
|||
class PurchasesApiV2 {
|
||||
final PaymentManager _paymentManager;
|
||||
final PackManager _packManager;
|
||||
final UserRepository _userRepository;
|
||||
final AppDatabase _db;
|
||||
|
||||
PurchasesApiV2(this._paymentManager, this._packManager, this._db);
|
||||
PurchasesApiV2(
|
||||
this._paymentManager,
|
||||
this._packManager,
|
||||
this._userRepository,
|
||||
this._db,
|
||||
);
|
||||
|
||||
Response _ok(Object? object, {Map<String, String> headers = const {}}) =>
|
||||
Response.ok(
|
||||
|
|
@ -95,7 +102,7 @@ class PurchasesApiV2 {
|
|||
'🔍 createPackPurchase: Checking pack access for userId=${user.id}, packId=$packId',
|
||||
);
|
||||
// Check if already purchased
|
||||
final hasAccess = await _db.userDao.hasPackAccess(user.id!, packId);
|
||||
final hasAccess = await _userRepository.hasPackAccess(user.id!, packId);
|
||||
if (hasAccess) {
|
||||
print('❌ createPackPurchase: Pack already purchased');
|
||||
return _badRequest('Pack is already purchased');
|
||||
|
|
@ -106,7 +113,7 @@ class PurchasesApiV2 {
|
|||
String? price = pack.price;
|
||||
if (price != null && user.id != null) {
|
||||
print('🔍 createPackPurchase: Getting user data for userId=${user.id}');
|
||||
final userData = await _db.userDao.getUserData(user.id!);
|
||||
final userData = await _userRepository.getUserData(user.id!);
|
||||
if (userData != null) {
|
||||
print('✅ createPackPurchase: User data found');
|
||||
// Apply discounts if any
|
||||
|
|
@ -201,7 +208,7 @@ class PurchasesApiV2 {
|
|||
}
|
||||
|
||||
// Check if already purchased
|
||||
final hasAccess = await _db.userDao.hasPackAccess(user.id!, productId);
|
||||
final hasAccess = await _userRepository.hasPackAccess(user.id!, productId);
|
||||
if (hasAccess) {
|
||||
return _badRequest('Pack is already purchased');
|
||||
}
|
||||
|
|
@ -209,7 +216,7 @@ class PurchasesApiV2 {
|
|||
// Get price (with discounts if applicable)
|
||||
price = pack.price;
|
||||
if (price != null && user.id != null) {
|
||||
final userData = await _db.userDao.getUserData(user.id!);
|
||||
final userData = await _userRepository.getUserData(user.id!);
|
||||
if (userData != null) {
|
||||
// Apply discounts if any
|
||||
// For now, use price as-is. Discounts can be added later if needed.
|
||||
|
|
@ -252,7 +259,7 @@ class PurchasesApiV2 {
|
|||
// Get price (with discounts if applicable)
|
||||
price = plan.price;
|
||||
if (user.id != null) {
|
||||
final userData = await _db.userDao.getUserData(user.id!);
|
||||
final userData = await _userRepository.getUserData(user.id!);
|
||||
if (userData != null) {
|
||||
// Apply discounts if any
|
||||
// For now, use price as-is. Discounts can be added later if needed.
|
||||
|
|
@ -364,7 +371,7 @@ class PurchasesApiV2 {
|
|||
}
|
||||
|
||||
// Check purchase status
|
||||
final isPurchased = await _db.userDao.hasPackAccess(user.id!, packId);
|
||||
final isPurchased = await _userRepository.hasPackAccess(user.id!, packId);
|
||||
|
||||
// Check subscription access
|
||||
final activeSubscription = await _db.subscriptionDao
|
||||
|
|
|
|||
|
|
@ -5,8 +5,7 @@ import 'package:drift/drift.dart';
|
|||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
import 'package:mnemo_cards_backend/main.dart' as backend_main;
|
||||
import 'package:mnemo_cards_backend/user/user_drift_extension.dart';
|
||||
import 'package:mnemo_cards_backend/user/user_repository.dart';
|
||||
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shelf_open_api/shelf_open_api.dart';
|
||||
|
|
@ -21,8 +20,9 @@ part 'telegram_bot_api_v2.g.dart';
|
|||
@lazySingleton
|
||||
class TelegramBotApiV2 {
|
||||
final AppDatabase _db;
|
||||
final UserRepository _userRepository;
|
||||
|
||||
TelegramBotApiV2(this._db);
|
||||
TelegramBotApiV2(this._db, this._userRepository);
|
||||
|
||||
Response _ok(Object? object, {Map<String, String> headers = const {}}) =>
|
||||
Response.ok(
|
||||
|
|
@ -193,9 +193,9 @@ class TelegramBotApiV2 {
|
|||
if (userId != null && userId.isNotEmpty) {
|
||||
// Get specific user info
|
||||
final users = <UserModel>[];
|
||||
final user = await backend_main.database.userDao.getUserById(userId);
|
||||
final user = await _userRepository.getUserById(userId, includePacks: true);
|
||||
if (user != null) {
|
||||
users.add(await user.toUserModel());
|
||||
users.add(user);
|
||||
}
|
||||
|
||||
if (users.isEmpty) {
|
||||
|
|
@ -224,10 +224,10 @@ class TelegramBotApiV2 {
|
|||
});
|
||||
} else {
|
||||
// Get all users summary
|
||||
final driftUsers = await backend_main.database.userDao.getAllUsers(
|
||||
final users = await _userRepository.getAllUsers(
|
||||
limit: 100,
|
||||
includePacks: false,
|
||||
);
|
||||
final users = await Future.wait(driftUsers.map((u) => u.toUserModel()));
|
||||
|
||||
return _ok({
|
||||
'total': users.length,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import 'package:mnemo_cards_backend/database/database.dart';
|
|||
import 'package:mnemo_cards_backend/statistics/statistics_calculator.dart';
|
||||
import 'package:mnemo_cards_backend/user/user_manager.dart';
|
||||
import 'package:mnemo_cards_backend/user/user_model.dart';
|
||||
import 'package:mnemo_cards_backend/user/user_repository.dart';
|
||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shelf_router/shelf_router.dart';
|
||||
|
|
@ -22,12 +23,14 @@ class UsersApiV2 {
|
|||
final UserManager _userManager;
|
||||
final PaymentManager _paymentManager;
|
||||
final StatisticsCalculator _statisticsCalculator;
|
||||
final UserRepository _userRepository;
|
||||
final AppDatabase _db;
|
||||
|
||||
UsersApiV2(
|
||||
this._userManager,
|
||||
this._paymentManager,
|
||||
this._statisticsCalculator,
|
||||
this._userRepository,
|
||||
this._db,
|
||||
);
|
||||
|
||||
|
|
@ -142,7 +145,7 @@ class UsersApiV2 {
|
|||
}, statusCode: 400);
|
||||
}
|
||||
|
||||
await _db.userDao.updateUserPartial(
|
||||
await _userRepository.updateUserPartial(
|
||||
UsersCompanion(
|
||||
id: drift.Value(user.id!),
|
||||
name: name != null ? drift.Value(name) : const drift.Value.absent(),
|
||||
|
|
@ -249,13 +252,13 @@ class UsersApiV2 {
|
|||
return _json({'error': 'user_id_not_found'}, statusCode: 400);
|
||||
}
|
||||
|
||||
final userData = await _db.userDao.getUserData(user.id!);
|
||||
final userData = await _userRepository.getUserData(user.id!);
|
||||
if (userData == null) {
|
||||
return _json({'error': 'user_data_not_found'}, statusCode: 404);
|
||||
}
|
||||
|
||||
// Convert UserData to UserDataDto
|
||||
final userDataModel = await _db.userDao.getUserWithDataById(user.id!);
|
||||
final userDataModel = await _userRepository.getUserWithDataById(user.id!);
|
||||
if (userDataModel?.userData == null) {
|
||||
return _json({'error': 'user_data_not_found'}, statusCode: 404);
|
||||
}
|
||||
|
|
@ -502,7 +505,7 @@ class UsersApiV2 {
|
|||
}
|
||||
|
||||
await _db.transaction(() async {
|
||||
final userData = await _db.userDao.getUserData(user.id!);
|
||||
final userData = await _userRepository.getUserData(user.id!);
|
||||
if (userData == null) {
|
||||
throw StateError('User data not found');
|
||||
}
|
||||
|
|
@ -520,7 +523,7 @@ class UsersApiV2 {
|
|||
final longestStreak = max(userData.longestStreak, currentStreak);
|
||||
|
||||
// Update user data (без studyDates - это поле удалено, рассчитывается на лету)
|
||||
await _db.userDao.updateUserDataPartial(
|
||||
await _userRepository.updateUserDataPartial(
|
||||
UserDatasCompanion(
|
||||
userId: drift.Value(user.id!),
|
||||
totalStudyTimeMinutes: drift.Value(totalStudyTime),
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
import 'package:mnemo_cards_backend/cron/task.dart';
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
import 'package:mnemo_cards_backend/packs/free_packs_distributor.dart';
|
||||
import 'package:mnemo_cards_backend/user/user_repository.dart';
|
||||
|
||||
import 'task.dart' as task;
|
||||
|
||||
class AddFreePacks with task.Task {
|
||||
final FreePacksDistributor _freePacksDistributor;
|
||||
final AppDatabase _db;
|
||||
final UserRepository _userRepository;
|
||||
|
||||
AddFreePacks(this._freePacksDistributor, this._db);
|
||||
AddFreePacks(this._freePacksDistributor, this._userRepository);
|
||||
|
||||
@override
|
||||
String get name => 'add_free_packs';
|
||||
|
|
@ -26,17 +25,18 @@ class AddFreePacks with task.Task {
|
|||
final freePackIds = freePacks.map((p) => p.id).toSet();
|
||||
|
||||
// Получаем всех пользователей
|
||||
final allUsers = await _db.userDao.getAllUsers();
|
||||
final allUsers = await _userRepository.getAllUsers();
|
||||
|
||||
// Фильтруем пользователей, у которых нет хотя бы одного из freePacks
|
||||
final usersToUpdate = <String>[];
|
||||
for (final user in allUsers) {
|
||||
final userPacks = await _db.userDao.getUserPacks(user.id);
|
||||
final userPackIds = userPacks.map((p) => p.id).toSet();
|
||||
if (user.id == null) continue;
|
||||
final userPacks = await _userRepository.getUserPacks(user.id!);
|
||||
final userPackIds = userPacks.map((p) => p.id).whereType<String>().toSet();
|
||||
|
||||
// Если у пользователя нет хотя бы одного из freePacks
|
||||
if (!freePackIds.every((packId) => userPackIds.contains(packId))) {
|
||||
usersToUpdate.add(user.id);
|
||||
usersToUpdate.add(user.id!);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -48,9 +48,10 @@ class AddFreePacks with task.Task {
|
|||
for (final userId in usersToUpdate) {
|
||||
try {
|
||||
for (final pack in freePacks) {
|
||||
await _db.userDao.grantPackAccess(
|
||||
if (pack.id == null) continue;
|
||||
await _userRepository.grantPackAccess(
|
||||
userId: userId,
|
||||
packId: pack.id,
|
||||
packId: pack.id!,
|
||||
grantType: 'free',
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import 'package:mnemo_cards_backend/user/admin_ids_service.dart';
|
||||
import 'package:mnemo_cards_backend/user/user_repository.dart';
|
||||
|
||||
import '../database/database.dart';
|
||||
import 'task.dart' as task;
|
||||
|
|
@ -10,8 +11,9 @@ class CheckAdminsTask with task.Task {
|
|||
String get name => 'check_admins';
|
||||
|
||||
final AppDatabase _db;
|
||||
final UserRepository _userRepository;
|
||||
|
||||
CheckAdminsTask(this._db);
|
||||
CheckAdminsTask(this._db, this._userRepository);
|
||||
|
||||
@override
|
||||
Future<void> task() async {
|
||||
|
|
@ -35,14 +37,14 @@ class CheckAdminsTask with task.Task {
|
|||
}
|
||||
|
||||
// Получить пользователя по userId из токена
|
||||
final user = await _db.userDao.getUserById(token.userId);
|
||||
if (user != null) {
|
||||
adminIds.add(user.id);
|
||||
final user = await _userRepository.getUserById(token.userId);
|
||||
if (user != null && user.id != null) {
|
||||
adminIds.add(user.id!);
|
||||
if (!user.admin) {
|
||||
// Установить admin = true
|
||||
await _db.userDao.updateUserPartial(
|
||||
await _userRepository.updateUserPartial(
|
||||
UsersCompanion(
|
||||
id: Value(user.id),
|
||||
id: Value(user.id!),
|
||||
admin: const Value(true),
|
||||
updatedAt: Value(PgDateTime(DateTime.now())),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ import 'dart:developer';
|
|||
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:mnemo_cards_backend/database/daos/discount_dao.dart';
|
||||
import 'package:mnemo_cards_backend/database/daos/user_dao.dart';
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
import 'package:mnemo_cards_backend/discounts/discount_drift_extension.dart';
|
||||
import 'package:mnemo_cards_backend/user/user_repository.dart';
|
||||
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
|
||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart' hide DateTimeExt;
|
||||
import 'package:mnemo_cards_common/src/utils/utils.dart';
|
||||
|
|
@ -13,11 +13,10 @@ import 'package:mnemo_cards_common/src/utils/utils.dart';
|
|||
class DiscountsManager {
|
||||
final AppDatabase _db;
|
||||
final DiscountDao _discountDao;
|
||||
final UserDao _userDao;
|
||||
final UserRepository _userRepository;
|
||||
|
||||
DiscountsManager(this._db)
|
||||
: _discountDao = _db.discountDao,
|
||||
_userDao = _db.userDao;
|
||||
DiscountsManager(this._db, this._userRepository)
|
||||
: _discountDao = _db.discountDao;
|
||||
|
||||
Future<void> applyDiscount({
|
||||
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;
|
||||
|
||||
final userDataModel = await _userDao.getUserData(userData.userId);
|
||||
final userDataModel = await _userRepository.getUserData(userData.userId);
|
||||
final userTags = userDataModel?.tags ?? [];
|
||||
|
||||
final activeCampaigns = await _discountDao.getActiveCampaignsForUser(
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
|||
extension CardPackToDto on CardPack {
|
||||
Future<CardPackPreviewDto> toPreviewDto(UserModel? user) async {
|
||||
final hasAccess =
|
||||
user != null && (user.purchases.contains(id.toString()) || user.admin);
|
||||
user != null && (user.packs.contains(id.toString()) || user.admin);
|
||||
|
||||
// Generate cover image URL
|
||||
String? coverUrl;
|
||||
|
|
@ -64,6 +64,30 @@ extension CardPackToDto on CardPack {
|
|||
}
|
||||
}
|
||||
|
||||
extension CardPackToModel on CardPack {
|
||||
|
||||
Future<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 GameCardToDto on GameCard {
|
||||
Future<GameCardDto> toDto(List<VoiceModel> voices) async {
|
||||
|
|
|
|||
|
|
@ -1,18 +1,25 @@
|
|||
import 'package:injectable/injectable.dart';
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
import 'package:mnemo_cards_backend/user/user_repository.dart';
|
||||
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
|
||||
import 'pack_repository.dart';
|
||||
|
||||
@lazySingleton
|
||||
class FreePacksDistributor {
|
||||
final AppDatabase _db;
|
||||
final UserRepository _userRepository;
|
||||
final PackRepository _packRepository;
|
||||
|
||||
FreePacksDistributor(this._db);
|
||||
FreePacksDistributor(this._userRepository, this._packRepository);
|
||||
|
||||
Future<List<CardPack>> getFreePacks() async {
|
||||
final allPacks = await _db.packDao.getAllPacks(enabledOnly: true);
|
||||
Future<List<CardPackModel>> getFreePacks() async {
|
||||
final allPacks = await _packRepository.getAllPacks(enabledOnly: true);
|
||||
// Фильтруем паки с нулевой ценой или без цены
|
||||
return allPacks
|
||||
.where((pack) => pack.price == null || pack.price == 0)
|
||||
.where((pack) {
|
||||
final priceStr = pack.price;
|
||||
if (priceStr == null || priceStr.isEmpty) return true;
|
||||
final price = double.tryParse(priceStr);
|
||||
return price == null || price == 0;
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
|
@ -22,13 +29,14 @@ class FreePacksDistributor {
|
|||
await givePacksToUser(user, packs);
|
||||
}
|
||||
|
||||
Future<void> givePacksToUser(UserModel user, List<CardPack> packs) async {
|
||||
Future<void> givePacksToUser(UserModel user, List<CardPackModel> packs) async {
|
||||
if (user.id == null) return;
|
||||
|
||||
for (final pack in packs) {
|
||||
await _db.userDao.grantPackAccess(
|
||||
if (pack.id == null) continue;
|
||||
await _userRepository.grantPackAccess(
|
||||
userId: user.id!,
|
||||
packId: pack.id,
|
||||
packId: pack.id!,
|
||||
grantType: 'free',
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,11 @@ class PackDtoConverter {
|
|||
CardPackModel model,
|
||||
UserModel? userModel,
|
||||
) async {
|
||||
bool available = model.users.contains(userModel);
|
||||
// Check access via purchases list (source of truth is user_packs table in DB)
|
||||
// user.purchases is a cached list of pack IDs the user has access to
|
||||
bool available = userModel != null &&
|
||||
(userModel.purchases.contains(model.id?.toString()) ||
|
||||
userModel.admin);
|
||||
if (userModel != null && !available) {
|
||||
available =
|
||||
userModel.subscriptionModel?.features.contains(
|
||||
|
|
@ -214,6 +218,7 @@ class PackDtoConverter {
|
|||
enabled: dto.enabled ?? model?.enabled ?? false,
|
||||
order: dto.order ?? model?.order ?? 0,
|
||||
cardsOrder: dto.cardsOrder ?? model?.cardsOrder ?? const [],
|
||||
currency: model?.currency ?? 'RUB',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,24 +8,37 @@ import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart'
|
|||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||
import 'pack_dto_converter.dart';
|
||||
import 'card_pack_drift_extension.dart';
|
||||
import 'pack_repository.dart';
|
||||
|
||||
@lazySingleton
|
||||
class PackManager {
|
||||
final AppDatabase _db;
|
||||
final PackRepository _packRepository;
|
||||
final PackDtoConverter packDtoConverter;
|
||||
|
||||
PackManager(this._db, this.packDtoConverter);
|
||||
PackManager(this._db, this._packRepository, this.packDtoConverter);
|
||||
|
||||
Future<List<CardPackPreviewDto>> listPacksPreviews(
|
||||
UserModel? userModel,
|
||||
Map<String, String>? params,
|
||||
) async {
|
||||
// Get packs from Drift database
|
||||
final packs = await _db.packDao.getAllPacks(
|
||||
// Get packs from Repository
|
||||
final packModels = await _packRepository.getAllPacks(
|
||||
enabledOnly: true,
|
||||
orderByField: 'order',
|
||||
);
|
||||
|
||||
// Convert to Drift models for toPreviewDto (temporary, until we refactor DTO conversion)
|
||||
final packs = await Future.wait(
|
||||
packModels.map((packModel) async {
|
||||
final pack = await _db.packDao.getPackById(packModel.id!);
|
||||
if (pack == null) {
|
||||
throw StateError('Pack not found: ${packModel.id}');
|
||||
}
|
||||
return pack;
|
||||
}),
|
||||
);
|
||||
|
||||
return (await Future.wait(
|
||||
packs.map((pack) async {
|
||||
final dto = await pack.toPreviewDto(userModel);
|
||||
|
|
@ -37,16 +50,16 @@ class PackManager {
|
|||
)).toList();
|
||||
}
|
||||
|
||||
Future<CardPack?> getPack(String id) async {
|
||||
return await _db.packDao.getPackById(id);
|
||||
Future<CardPackModel?> getPack(String id) async {
|
||||
return await _packRepository.getPackById(id);
|
||||
}
|
||||
|
||||
Future<List<GameCard>> getCards(String packId) async {
|
||||
return await _db.packDao.getPackCards(packId);
|
||||
Future<List<GameCardModel>> getCards(String packId) async {
|
||||
return await _packRepository.getPackCards(packId);
|
||||
}
|
||||
|
||||
Future<GameCard?> getCard(String id) async {
|
||||
return await _db.packDao.getCardById(id);
|
||||
Future<GameCardModel?> getCard(String id) async {
|
||||
return await _packRepository.getCardById(id);
|
||||
}
|
||||
|
||||
Future<VoiceModel?> getVoice(String id) async {
|
||||
|
|
@ -58,12 +71,18 @@ class PackManager {
|
|||
}
|
||||
|
||||
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) {
|
||||
throw StateError('Pack not found');
|
||||
}
|
||||
|
||||
final cards = await getCards(id);
|
||||
final cards = await _db.packDao.getPackCards(id);
|
||||
final voices = <VoiceModel>[];
|
||||
|
||||
for (final card in cards) {
|
||||
|
|
|
|||
238
mnemo_cards_backend/lib/packs/pack_repository.dart
Normal file
238
mnemo_cards_backend/lib/packs/pack_repository.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
import 'package:injectable/injectable.dart';
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
import 'package:mnemo_cards_backend/discounts/discounts_manager.dart';
|
||||
import 'package:mnemo_cards_backend/user/user_repository.dart';
|
||||
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
|
||||
|
||||
@LazySingleton()
|
||||
class ProductsPriceResolver {
|
||||
final DiscountsManager _discountsManager;
|
||||
final AppDatabase _db;
|
||||
final UserRepository _userRepository;
|
||||
|
||||
ProductsPriceResolver(this._discountsManager, this._db);
|
||||
ProductsPriceResolver(this._discountsManager, this._userRepository);
|
||||
|
||||
Future<String> _userProductPrice(
|
||||
UserModel userModel,
|
||||
|
|
@ -20,7 +20,7 @@ class ProductsPriceResolver {
|
|||
return price;
|
||||
}
|
||||
if (userModel.id != null) {
|
||||
final userData = await _db.userDao.getUserData(userModel.id!);
|
||||
final userData = await _userRepository.getUserData(userModel.id!);
|
||||
if (userData != null) {
|
||||
final maxDiscount = await _discountsManager.getProductDiscount(
|
||||
product,
|
||||
|
|
|
|||
|
|
@ -2,14 +2,16 @@ import 'package:drift/drift.dart';
|
|||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
import 'package:mnemo_cards_backend/user/user_repository.dart';
|
||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
|
||||
|
||||
@lazySingleton
|
||||
class AchievementManager {
|
||||
final AppDatabase _db;
|
||||
final UserRepository _userRepository;
|
||||
|
||||
AchievementManager(this._db);
|
||||
AchievementManager(this._db, this._userRepository);
|
||||
|
||||
/// Get all available achievements with their definitions
|
||||
List<AchievementDto> get allAchievementDefinitions =>
|
||||
|
|
@ -146,7 +148,7 @@ class AchievementManager {
|
|||
// return userData.totalTests > 0;
|
||||
|
||||
case AchievementType.firstPackCompleted:
|
||||
final userPacks = await _db.userDao.getUserPacks(userId);
|
||||
final userPacks = await _userRepository.getUserPacks(userId);
|
||||
return userPacks.isNotEmpty;
|
||||
|
||||
// case AchievementType.words10Learned:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:mnemo_cards_backend/user/user_repository.dart';
|
||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
|
|
@ -9,8 +10,9 @@ import 'package:mnemo_cards_backend/database/database.dart';
|
|||
@lazySingleton
|
||||
class StatisticsCalculator {
|
||||
final AppDatabase _db;
|
||||
final UserRepository _userRepository;
|
||||
|
||||
StatisticsCalculator(this._db);
|
||||
StatisticsCalculator(this._db, this._userRepository);
|
||||
|
||||
/// Calculate pack progress for a specific user and pack
|
||||
///
|
||||
|
|
@ -104,12 +106,13 @@ class StatisticsCalculator {
|
|||
/// Calculate pack progress for all user packs
|
||||
Future<List<PackProgressDto>> calculateAllPackProgress(String userId) async {
|
||||
// Получить все паки пользователя
|
||||
final userPacks = await _db.userDao.getUserPacks(userId);
|
||||
final userPacks = await _userRepository.getUserPacks(userId);
|
||||
|
||||
// Рассчитать прогресс для каждого пака
|
||||
final progressList = <PackProgressDto>[];
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
import 'package:drift/drift.dart' as drift;
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
import 'package:mnemo_cards_backend/packs/card_pack_drift_extension.dart';
|
||||
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
|
||||
|
||||
/// Extension для конвертации User (Drift) в UserModel
|
||||
extension UserToUserModel on User {
|
||||
Future<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(
|
||||
id: id,
|
||||
name: name,
|
||||
|
|
@ -13,6 +17,7 @@ extension UserToUserModel on User {
|
|||
admin: admin,
|
||||
purchases: purchases,
|
||||
userSettings: userSettings,
|
||||
packs: packModels,
|
||||
);
|
||||
|
||||
return userModel;
|
||||
|
|
|
|||
|
|
@ -15,13 +15,14 @@ import '../statistics/statistics_calculator.dart';
|
|||
import '../statistics/achievement_manager.dart';
|
||||
import '../statistics/word_statistics_manager.dart';
|
||||
import 'secure.dart';
|
||||
import 'user_drift_extension.dart';
|
||||
import 'user_repository.dart';
|
||||
|
||||
Map<String?, DateTime> _onlineUsers = {};
|
||||
|
||||
@lazySingleton
|
||||
class UserManager {
|
||||
final AppDatabase _db;
|
||||
final UserRepository _userRepository;
|
||||
final FreePacksDistributor _freePacksDistributor;
|
||||
final SessionTracker _sessionTracker;
|
||||
final StatisticsCalculator _statisticsCalculator;
|
||||
|
|
@ -30,6 +31,7 @@ class UserManager {
|
|||
|
||||
UserManager(
|
||||
this._db,
|
||||
this._userRepository,
|
||||
this._freePacksDistributor,
|
||||
this._sessionTracker,
|
||||
this._statisticsCalculator,
|
||||
|
|
@ -38,9 +40,7 @@ class UserManager {
|
|||
);
|
||||
|
||||
Future<UserModel?> fetchUser(String id) async {
|
||||
final user = await _db.userDao.getUserById(id);
|
||||
if (user == null) return null;
|
||||
return await user.toUserModel();
|
||||
return await _userRepository.getUserById(id, includePacks: true);
|
||||
}
|
||||
|
||||
DateTime? lastOnline(String id) => _onlineUsers[id];
|
||||
|
|
@ -119,77 +119,68 @@ class UserManager {
|
|||
String? name,
|
||||
}) async {
|
||||
// Check if user exists by externalId
|
||||
final existingUser = await _db.userDao.getUserByExternalId(externalId);
|
||||
if (existingUser != null) {
|
||||
var existingUserModel = await _userRepository.getUserByExternalId(externalId);
|
||||
if (existingUserModel != null) {
|
||||
print('User found $name $email $telegram');
|
||||
|
||||
// Обновляем контактные данные, если они пришли впервые/изменились
|
||||
final shouldUpdateEmail =
|
||||
email != null && email.isNotEmpty && existingUser.email != email;
|
||||
email != null && email.isNotEmpty && existingUserModel.email != email;
|
||||
final shouldUpdateTelegram =
|
||||
telegram != null &&
|
||||
telegram.isNotEmpty &&
|
||||
existingUser.telegram != telegram;
|
||||
existingUserModel.telegram != telegram;
|
||||
|
||||
if (shouldUpdateEmail || shouldUpdateTelegram) {
|
||||
await _db.userDao.updateUserPartial(
|
||||
UsersCompanion(
|
||||
id: drift.Value(existingUser.id),
|
||||
email: shouldUpdateEmail
|
||||
? drift.Value(email)
|
||||
: const drift.Value.absent(),
|
||||
telegram: shouldUpdateTelegram
|
||||
? drift.Value(telegram)
|
||||
: const drift.Value.absent(),
|
||||
updatedAt: drift.Value(PgDateTime(DateTime.now())),
|
||||
),
|
||||
final updatedModel = existingUserModel.copyWith(
|
||||
email: shouldUpdateEmail ? email : existingUserModel.email,
|
||||
telegram: shouldUpdateTelegram ? telegram : existingUserModel.telegram,
|
||||
);
|
||||
await _userRepository.updateUser(updatedModel);
|
||||
existingUserModel = updatedModel;
|
||||
}
|
||||
|
||||
final userModel = await existingUser.toUserModel();
|
||||
final token = await createOrGetAuthToken(userModel, externalId);
|
||||
return (userModel, token);
|
||||
final token = await createOrGetAuthToken(existingUserModel, externalId);
|
||||
return (existingUserModel, token);
|
||||
}
|
||||
|
||||
print('Creating new user $name $email $telegram');
|
||||
|
||||
// Create new user with user data in transaction
|
||||
final now = DateTime.now();
|
||||
final userCompanion = UsersCompanion.insert(
|
||||
externalUserId: externalId,
|
||||
name: drift.Value(name),
|
||||
email: drift.Value(email),
|
||||
telegram: drift.Value(telegram),
|
||||
admin: drift.Value(false),
|
||||
purchases: drift.Value([]),
|
||||
createdAt: drift.Value(PgDateTime(now)),
|
||||
updatedAt: drift.Value(PgDateTime(now)),
|
||||
isDeleted: drift.Value(false),
|
||||
);
|
||||
|
||||
final userDataCompanion = UserDatasCompanion.insert(
|
||||
userId: '', // Will be set by createUserWithData via copyWith
|
||||
registrationDate: drift.Value(PgDateTime(now)),
|
||||
);
|
||||
|
||||
final userId = await _db.userDao.createUserWithData(
|
||||
user: userCompanion,
|
||||
// Создаем пользователя через UserRepository
|
||||
final newUserModel = UserModel(
|
||||
id: null,
|
||||
name: name,
|
||||
email: email,
|
||||
telegram: telegram,
|
||||
admin: false,
|
||||
purchases: [],
|
||||
userSettings: null,
|
||||
packs: [],
|
||||
);
|
||||
final userId = await _userRepository.createUserWithData(
|
||||
userModel: newUserModel,
|
||||
userData: userDataCompanion,
|
||||
);
|
||||
|
||||
final user = await _db.userDao.getUserById(userId);
|
||||
if (user == null) {
|
||||
final createdUserModel = await _userRepository.getUserById(userId, includePacks: true);
|
||||
if (createdUserModel == null) {
|
||||
throw Exception('Failed to create user');
|
||||
}
|
||||
|
||||
final userModel = await user.toUserModel();
|
||||
final token = await createOrGetAuthToken(userModel, externalId);
|
||||
final token = await createOrGetAuthToken(createdUserModel, externalId);
|
||||
|
||||
// Give free packs to new user
|
||||
await _freePacksDistributor.giveFreePacksToUser(userModel);
|
||||
await _freePacksDistributor.giveFreePacksToUser(createdUserModel);
|
||||
|
||||
print('User $name $email $telegram created successfully');
|
||||
return (userModel, token);
|
||||
return (createdUserModel, token);
|
||||
}
|
||||
|
||||
/// Обновить настройки пользователя
|
||||
|
|
@ -201,13 +192,10 @@ class UserManager {
|
|||
throw Exception('User ID is required');
|
||||
}
|
||||
|
||||
await _db.userDao.updateUserPartial(
|
||||
UsersCompanion(
|
||||
id: drift.Value(user.id!),
|
||||
userSettings: drift.Value(jsonEncode(settings.toJson())),
|
||||
updatedAt: drift.Value(PgDateTime(DateTime.now())),
|
||||
),
|
||||
final updatedUser = user.copyWith(
|
||||
userSettings: jsonEncode(settings.toJson()),
|
||||
);
|
||||
await _userRepository.updateUser(updatedUser);
|
||||
}
|
||||
|
||||
/// Добавить статистику теста
|
||||
|
|
@ -220,12 +208,12 @@ class UserManager {
|
|||
}
|
||||
|
||||
// Получить или создать UserData
|
||||
var userData = await _db.userDao.getUserData(user.id!);
|
||||
var userData = await _userRepository.getUserData(user.id!);
|
||||
if (userData == null) {
|
||||
await _db.userDao.createUserData(
|
||||
UserDatasCompanion.insert(userId: user.id!),
|
||||
);
|
||||
userData = await _db.userDao.getUserData(user.id!);
|
||||
userData = await _userRepository.getUserData(user.id!);
|
||||
if (userData == null) {
|
||||
throw Exception('Failed to create user data');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,8 @@ class UserManager {
|
|||
Future<UserModel?> fetchUser(String id) async {
|
||||
final user = await _db.userDao.getUserById(id);
|
||||
if (user == null) return null;
|
||||
return await user.toUserModel();
|
||||
final packs = await _db.userDao.getUserPacks(user.id);
|
||||
return await user.toUserModel(packs);
|
||||
}
|
||||
|
||||
DateTime? lastOnline(String id) => _onlineUsers[id];
|
||||
|
|
|
|||
181
mnemo_cards_backend/lib/user/user_repository.dart
Normal file
181
mnemo_cards_backend/lib/user/user_repository.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -166,30 +166,30 @@ class MockPackManager extends _i1.Mock implements _i8.PackManager {
|
|||
as _i5.Future<List<_i3.CardPackPreviewDto>>);
|
||||
|
||||
@override
|
||||
_i5.Future<_i10.CardPack?> getPack(String? id) =>
|
||||
_i5.Future<_i9.CardPackModel?> getPack(String? id) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#getPack, [id]),
|
||||
returnValue: _i5.Future<_i10.CardPack?>.value(),
|
||||
returnValue: _i5.Future<_i9.CardPackModel?>.value(),
|
||||
)
|
||||
as _i5.Future<_i10.CardPack?>);
|
||||
as _i5.Future<_i9.CardPackModel?>);
|
||||
|
||||
@override
|
||||
_i5.Future<List<_i10.GameCard>> getCards(String? packId) =>
|
||||
_i5.Future<List<_i9.GameCardModel>> getCards(String? packId) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#getCards, [packId]),
|
||||
returnValue: _i5.Future<List<_i10.GameCard>>.value(
|
||||
<_i10.GameCard>[],
|
||||
returnValue: _i5.Future<List<_i9.GameCardModel>>.value(
|
||||
<_i9.GameCardModel>[],
|
||||
),
|
||||
)
|
||||
as _i5.Future<List<_i10.GameCard>>);
|
||||
as _i5.Future<List<_i9.GameCardModel>>);
|
||||
|
||||
@override
|
||||
_i5.Future<_i10.GameCard?> getCard(String? id) =>
|
||||
_i5.Future<_i9.GameCardModel?> getCard(String? id) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#getCard, [id]),
|
||||
returnValue: _i5.Future<_i10.GameCard?>.value(),
|
||||
returnValue: _i5.Future<_i9.GameCardModel?>.value(),
|
||||
)
|
||||
as _i5.Future<_i10.GameCard?>);
|
||||
as _i5.Future<_i9.GameCardModel?>);
|
||||
|
||||
@override
|
||||
_i5.Future<_i10.VoiceModel?> getVoice(String? id) =>
|
||||
|
|
|
|||
|
|
@ -2,13 +2,11 @@ import 'package:json_annotation/json_annotation.dart';
|
|||
import 'package:copy_with_extension/copy_with_extension.dart';
|
||||
import 'package:mnemo_cards_common_backend/src/models/game_card_model.dart';
|
||||
import 'package:mnemo_cards_common_backend/src/models/game_tests/test_model.dart';
|
||||
import 'package:mnemo_cards_common_backend/src/models/user/user_model.dart';
|
||||
|
||||
import 'product_model.dart';
|
||||
|
||||
part 'card_pack_model.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
@CopyWith()
|
||||
class CardPackModel implements MnemoCardsProductModel {
|
||||
String? id;
|
||||
|
|
@ -28,37 +26,30 @@ class CardPackModel implements MnemoCardsProductModel {
|
|||
final bool enabled;
|
||||
final int order;
|
||||
final List<String> cardsOrder;
|
||||
// Relations - loaded separately from database
|
||||
// Relations - loaded separately from database (when needed)
|
||||
final List<GameCardModel> previewCards = [];
|
||||
final List<GameCardModel> cards = [];
|
||||
final List<TestModel> tests = [];
|
||||
final List<UserModel> users = [];
|
||||
|
||||
CardPackModel({
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.size,
|
||||
this.color,
|
||||
this.cover,
|
||||
this.description,
|
||||
this.version,
|
||||
this.id,
|
||||
this.googlePlayId,
|
||||
this.rustoreId,
|
||||
this.appStoreId,
|
||||
this.price,
|
||||
this.cardsOrder = const [],
|
||||
this.enabled = true,
|
||||
this.order = 0,
|
||||
this.currency = 'RUB',
|
||||
required this.color,
|
||||
required this.cover,
|
||||
required this.description,
|
||||
required this.version,
|
||||
required this.id,
|
||||
required this.googlePlayId,
|
||||
required this.rustoreId,
|
||||
required this.appStoreId,
|
||||
required this.price,
|
||||
required this.cardsOrder,
|
||||
required this.enabled,
|
||||
required this.order,
|
||||
required this.currency,
|
||||
});
|
||||
|
||||
factory CardPackModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$CardPackModelFromJson(json);
|
||||
|
||||
Map<String, Object?> toJson() => _$CardPackModelToJson(this);
|
||||
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
final type = MnemoCardsProductModelType.pack;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -226,51 +226,3 @@ extension $CardPackModelCopyWith on CardPackModel {
|
|||
// ignore: library_private_types_in_public_api
|
||||
_$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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,13 +7,12 @@ import '../statistics/word_statistics_model.dart';
|
|||
|
||||
part 'test_statistics_model.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
@CopyWith()
|
||||
class TestStatisticsModel {
|
||||
final String id;
|
||||
// Relations - loaded separately from database
|
||||
TestModel? test;
|
||||
UserDataModel? userData;
|
||||
String? testId;
|
||||
String? userId;
|
||||
final List<TestAttempt> attempts;
|
||||
|
||||
TestStatisticsModel({
|
||||
|
|
@ -24,13 +23,8 @@ class TestStatisticsModel {
|
|||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
TestAttempt? get lastAttempt => attempts.lastOrNull;
|
||||
|
||||
factory TestStatisticsModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$TestStatisticsModelFromJson(json);
|
||||
|
||||
Map<String, Object?> toJson() => _$TestStatisticsModelToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
@CopyWith()
|
||||
class TestAttempt {
|
||||
final List<WordStatisticsModel> words;
|
||||
|
|
@ -41,8 +35,4 @@ class TestAttempt {
|
|||
this.sessionToken,
|
||||
});
|
||||
|
||||
factory TestAttempt.fromJson(Map<String, dynamic> json) =>
|
||||
_$TestAttemptFromJson(json);
|
||||
|
||||
Map<String, Object?> toJson() => _$TestAttemptToJson(this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,47 +128,3 @@ extension $TestAttemptCopyWith on TestAttempt {
|
|||
// ignore: library_private_types_in_public_api
|
||||
_$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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,18 +3,14 @@ import 'package:copy_with_extension/copy_with_extension.dart';
|
|||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||
|
||||
import 'product_model.dart';
|
||||
import 'user/user_model.dart';
|
||||
|
||||
part 'payment.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
@CopyWith()
|
||||
class PaymentModel {
|
||||
String? id;
|
||||
final String amount;
|
||||
final String currency;
|
||||
// Relations - loaded separately from database
|
||||
UserModel? user;
|
||||
@JsonKey(unknownEnumValue: PaymentStatus.unknown)
|
||||
final PaymentStatus status;
|
||||
@JsonKey(unknownEnumValue: PaymentSystem.unknown)
|
||||
|
|
@ -24,10 +20,6 @@ class PaymentModel {
|
|||
final DateTime date;
|
||||
|
||||
final String userId;
|
||||
@deprecated
|
||||
final List<String> packs;
|
||||
@deprecated
|
||||
final bool subscription;
|
||||
final List<MnemoCardsProductModelBase> products;
|
||||
|
||||
PaymentModel({
|
||||
|
|
@ -38,8 +30,6 @@ class PaymentModel {
|
|||
required this.userId,
|
||||
required this.date,
|
||||
required this.products,
|
||||
this.packs = const [],
|
||||
this.subscription = false,
|
||||
this.externalToken,
|
||||
this.meta,
|
||||
this.id,
|
||||
|
|
@ -97,8 +87,6 @@ class PaymentModel {
|
|||
required this.externalToken,
|
||||
required this.date,
|
||||
required this.products,
|
||||
this.packs = const [],
|
||||
this.subscription = false,
|
||||
this.meta,
|
||||
this.id,
|
||||
}) : paymentSystem = PaymentSystem.yookassa,
|
||||
|
|
@ -111,8 +99,6 @@ class PaymentModel {
|
|||
required this.externalToken,
|
||||
required this.date,
|
||||
required this.products,
|
||||
this.packs = const [],
|
||||
this.subscription = false,
|
||||
this.meta,
|
||||
this.id,
|
||||
this.status = PaymentStatus.created,
|
||||
|
|
@ -125,15 +111,9 @@ class PaymentModel {
|
|||
required this.externalToken,
|
||||
required this.date,
|
||||
required this.products,
|
||||
this.packs = const [],
|
||||
this.subscription = false,
|
||||
this.meta,
|
||||
this.id,
|
||||
this.status = PaymentStatus.created,
|
||||
}) : paymentSystem = PaymentSystem.rustore;
|
||||
|
||||
factory PaymentModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$PaymentModelFromJson(json);
|
||||
|
||||
Map<String, Object?> toJson() => _$PaymentModelToJson(this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,10 +21,6 @@ abstract class _$PaymentModelCWProxy {
|
|||
|
||||
PaymentModel products(List<MnemoCardsProductModelBase> products);
|
||||
|
||||
PaymentModel packs(@deprecated List<String> packs);
|
||||
|
||||
PaymentModel subscription(@deprecated bool subscription);
|
||||
|
||||
PaymentModel externalToken(String? externalToken);
|
||||
|
||||
PaymentModel meta(String? meta);
|
||||
|
|
@ -46,8 +42,6 @@ abstract class _$PaymentModelCWProxy {
|
|||
String userId,
|
||||
DateTime date,
|
||||
List<MnemoCardsProductModelBase> products,
|
||||
@deprecated List<String> packs,
|
||||
@deprecated bool subscription,
|
||||
String? externalToken,
|
||||
String? meta,
|
||||
String? id,
|
||||
|
|
@ -84,13 +78,6 @@ class _$PaymentModelCWProxyImpl implements _$PaymentModelCWProxy {
|
|||
PaymentModel products(List<MnemoCardsProductModelBase> products) =>
|
||||
call(products: products);
|
||||
|
||||
@override
|
||||
PaymentModel packs(@deprecated List<String> packs) => call(packs: packs);
|
||||
|
||||
@override
|
||||
PaymentModel subscription(@deprecated bool subscription) =>
|
||||
call(subscription: subscription);
|
||||
|
||||
@override
|
||||
PaymentModel externalToken(String? externalToken) =>
|
||||
call(externalToken: externalToken);
|
||||
|
|
@ -117,8 +104,6 @@ class _$PaymentModelCWProxyImpl implements _$PaymentModelCWProxy {
|
|||
Object? userId = const $CopyWithPlaceholder(),
|
||||
Object? date = const $CopyWithPlaceholder(),
|
||||
Object? products = const $CopyWithPlaceholder(),
|
||||
@deprecated Object? packs = const $CopyWithPlaceholder(),
|
||||
@deprecated Object? subscription = const $CopyWithPlaceholder(),
|
||||
Object? externalToken = const $CopyWithPlaceholder(),
|
||||
Object? meta = const $CopyWithPlaceholder(),
|
||||
Object? id = const $CopyWithPlaceholder(),
|
||||
|
|
@ -153,15 +138,6 @@ class _$PaymentModelCWProxyImpl implements _$PaymentModelCWProxy {
|
|||
? _value.products
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: products as List<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()
|
||||
? _value.externalToken
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
|
|
@ -184,80 +160,3 @@ extension $PaymentModelCopyWith on PaymentModel {
|
|||
// ignore: library_private_types_in_public_api
|
||||
_$PaymentModelCWProxy get copyWith => _$PaymentModelCWProxyImpl(this);
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
PaymentModel _$PaymentModelFromJson(Map<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',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,5 +2,5 @@ import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
|
|||
|
||||
extension PromoCodeExt on PromoCodeModel {
|
||||
Future<bool> isReserved() async =>
|
||||
userData != null;
|
||||
userId != null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import 'promo_codes_campaign_model.dart';
|
|||
|
||||
part 'promo_code_model.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
@CopyWith()
|
||||
class PromoCodeModel {
|
||||
String? id;
|
||||
|
|
@ -15,16 +14,13 @@ class PromoCodeModel {
|
|||
// Relations - loaded separately from database
|
||||
PromoCodesCampaignModel? campaign;
|
||||
// Individual promo code
|
||||
UserDataModel? userData;
|
||||
String? userId;
|
||||
|
||||
PromoCodeModel({
|
||||
required this.code,
|
||||
this.activations = 0,
|
||||
this.id,
|
||||
this.userId,
|
||||
});
|
||||
|
||||
factory PromoCodeModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$PromoCodeModelFromJson(json);
|
||||
|
||||
Map<String, Object?> toJson() => _$PromoCodeModelToJson(this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ abstract class _$PromoCodeModelCWProxy {
|
|||
|
||||
PromoCodeModel id(String? id);
|
||||
|
||||
PromoCodeModel userId(String? userId);
|
||||
|
||||
/// Creates a new instance with the provided field values.
|
||||
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `PromoCodeModel(...).copyWith.fieldName(value)`.
|
||||
///
|
||||
|
|
@ -20,7 +22,12 @@ abstract class _$PromoCodeModelCWProxy {
|
|||
/// ```dart
|
||||
/// PromoCodeModel(...).copyWith(id: 12, name: "My name")
|
||||
/// ```
|
||||
PromoCodeModel call({String code, int activations, String? id});
|
||||
PromoCodeModel call({
|
||||
String code,
|
||||
int activations,
|
||||
String? id,
|
||||
String? userId,
|
||||
});
|
||||
}
|
||||
|
||||
/// Callable proxy for `copyWith` functionality.
|
||||
|
|
@ -39,6 +46,9 @@ class _$PromoCodeModelCWProxyImpl implements _$PromoCodeModelCWProxy {
|
|||
@override
|
||||
PromoCodeModel id(String? id) => call(id: id);
|
||||
|
||||
@override
|
||||
PromoCodeModel userId(String? userId) => call(userId: userId);
|
||||
|
||||
@override
|
||||
/// Creates a new instance with the provided field values.
|
||||
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `PromoCodeModel(...).copyWith.fieldName(value)`.
|
||||
|
|
@ -51,6 +61,7 @@ class _$PromoCodeModelCWProxyImpl implements _$PromoCodeModelCWProxy {
|
|||
Object? code = const $CopyWithPlaceholder(),
|
||||
Object? activations = const $CopyWithPlaceholder(),
|
||||
Object? id = const $CopyWithPlaceholder(),
|
||||
Object? userId = const $CopyWithPlaceholder(),
|
||||
}) {
|
||||
return PromoCodeModel(
|
||||
code: code == const $CopyWithPlaceholder() || code == null
|
||||
|
|
@ -66,6 +77,10 @@ class _$PromoCodeModelCWProxyImpl implements _$PromoCodeModelCWProxy {
|
|||
? _value.id
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: id as String?,
|
||||
userId: userId == const $CopyWithPlaceholder()
|
||||
? _value.userId
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: userId as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -76,31 +91,3 @@ extension $PromoCodeModelCopyWith on PromoCodeModel {
|
|||
// ignore: library_private_types_in_public_api
|
||||
_$PromoCodeModelCWProxy get copyWith => _$PromoCodeModelCWProxyImpl(this);
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
PromoCodeModel _$PromoCodeModelFromJson(Map<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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
|||
|
||||
part 'user_subscription_model.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
@CopyWith()
|
||||
class UserSubscriptionModel {
|
||||
String? id;
|
||||
|
|
@ -24,10 +23,6 @@ class UserSubscriptionModel {
|
|||
required this.features,
|
||||
});
|
||||
|
||||
factory UserSubscriptionModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$UserSubscriptionModelFromJson(json);
|
||||
|
||||
Map<String, Object?> toJson() => _$UserSubscriptionModelToJson(this);
|
||||
}
|
||||
|
||||
extension UserSubscriptionExt on UserSubscriptionModel {
|
||||
|
|
|
|||
|
|
@ -93,40 +93,3 @@ extension $UserSubscriptionModelCopyWith on UserSubscriptionModel {
|
|||
_$UserSubscriptionModelCWProxy get copyWith =>
|
||||
_$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',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:copy_with_extension/copy_with_extension.dart';
|
||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||
import 'package:mnemo_cards_common_backend/src/models/user/user_model.dart';
|
||||
|
||||
import '../discount/discount_model.dart';
|
||||
import '../game_tests/test_statistics_model.dart';
|
||||
|
|
@ -12,7 +11,6 @@ import '../statistics/achievement_model.dart';
|
|||
|
||||
part 'user_data_model.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
@CopyWith()
|
||||
class UserDataModel {
|
||||
String? id;
|
||||
|
|
@ -21,7 +19,6 @@ class UserDataModel {
|
|||
final List<PromoCodeModel> activatedPromoCodes = [];
|
||||
final List<PromoCodeModel> individualPromoCodes = [];
|
||||
final List<DiscountModel> activeDiscounts = [];
|
||||
UserModel? user;
|
||||
|
||||
final List<WordStatisticsModel> words;
|
||||
final String? lastTestSessionToken;
|
||||
|
|
@ -68,10 +65,6 @@ class UserDataModel {
|
|||
this.achievements = const [],
|
||||
});
|
||||
|
||||
factory UserDataModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$UserDataModelFromJson(json);
|
||||
|
||||
Map<String, Object?> toJson() => _$UserDataModelToJson(this);
|
||||
}
|
||||
|
||||
// Helper functions for PackProgressModel serialization
|
||||
|
|
|
|||
|
|
@ -196,64 +196,3 @@ extension $UserDataModelCopyWith on UserDataModel {
|
|||
// ignore: library_private_types_in_public_api
|
||||
_$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),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import '../subscription/user_subscription_model.dart';
|
|||
|
||||
part 'user_model.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
@CopyWith()
|
||||
class UserModel {
|
||||
String? id;
|
||||
|
|
@ -16,11 +15,10 @@ class UserModel {
|
|||
final String? telegram;
|
||||
@JsonKey(defaultValue: false)
|
||||
final bool admin;
|
||||
// Relations - loaded separately from database
|
||||
final List<CardPackModel> packs = [];
|
||||
UserDataModel? userData;
|
||||
@JsonKey(defaultValue: [])
|
||||
final List<String> purchases;
|
||||
final List<CardPackModel> packs;
|
||||
UserDataModel? userData;
|
||||
UserSubscriptionModel? subscriptionModel;
|
||||
|
||||
// UserSettingsDto
|
||||
|
|
@ -33,15 +31,10 @@ class UserModel {
|
|||
this.telegram,
|
||||
this.admin = false,
|
||||
this.purchases = const [],
|
||||
this.packs = const [],
|
||||
this.userSettings,
|
||||
});
|
||||
|
||||
factory UserModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$UserModelFromJson(json);
|
||||
|
||||
Map<String, Object?> toJson() => _$UserModelToJson(this);
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
static UserModel get empty => UserModel(
|
||||
admin: false,
|
||||
purchases: [],
|
||||
|
|
|
|||
|
|
@ -124,42 +124,3 @@ extension $UserModelCopyWith on UserModel {
|
|||
// ignore: library_private_types_in_public_api
|
||||
_$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,
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue