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