vack tasks
Some checks are pending
Backend CI / test (push) Waiting to run
Backend 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
Some checks are pending
Backend CI / test (push) Waiting to run
Backend 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
This commit is contained in:
parent
336bafc600
commit
99100deb20
20 changed files with 2219 additions and 0 deletions
489
mnemo_cards_backend/lib/api/v2/admin_tasks_api_v2.dart
Normal file
489
mnemo_cards_backend/lib/api/v2/admin_tasks_api_v2.dart
Normal file
|
|
@ -0,0 +1,489 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
|
import 'package:drift/drift.dart' as drift;
|
||||||
|
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/api/authorize/acl_types.dart';
|
||||||
|
import 'package:mnemo_cards_backend/api/authorize/access_service.dart';
|
||||||
|
import 'package:mnemo_cards_backend/api/authorize/helpers.dart';
|
||||||
|
import 'package:mnemo_cards_backend/repository/export.dart';
|
||||||
|
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
|
||||||
|
import 'package:shelf/shelf.dart';
|
||||||
|
import 'package:shelf_router/shelf_router.dart';
|
||||||
|
|
||||||
|
part 'admin_tasks_api_v2.g.dart';
|
||||||
|
|
||||||
|
/// Admin endpoints for managing user tasks in API v2.
|
||||||
|
@injectable
|
||||||
|
class AdminTasksApiV2 {
|
||||||
|
final AppDatabase _db;
|
||||||
|
final TaskRepository _taskRepository;
|
||||||
|
|
||||||
|
AdminTasksApiV2(this._db, this._taskRepository);
|
||||||
|
|
||||||
|
Response _json(
|
||||||
|
Object? data, {
|
||||||
|
int statusCode = 200,
|
||||||
|
Map<String, String> headers = const {},
|
||||||
|
}) {
|
||||||
|
return Response(
|
||||||
|
statusCode,
|
||||||
|
body: data == null ? null : jsonEncode(data),
|
||||||
|
headers: {'Content-Type': 'application/json', ...headers},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Response> _ensureAdmin(Request request) async {
|
||||||
|
try {
|
||||||
|
await request.access!.requireAdmin(
|
||||||
|
AdminAction.access,
|
||||||
|
user: request.user,
|
||||||
|
);
|
||||||
|
return Response.ok(null);
|
||||||
|
} on AccessDenied catch (e) {
|
||||||
|
return Response(e.status, body: e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert Drift UserTask to UserTaskModel for JSON serialization
|
||||||
|
UserTaskModel _userTaskToModel(UserTask task) {
|
||||||
|
// Parse rewards from JSON
|
||||||
|
final rewardsJson = task.rewards as List<dynamic>? ?? [];
|
||||||
|
final rewards = rewardsJson
|
||||||
|
.map(
|
||||||
|
(r) => TaskRewardModel.fromJson(Map<String, dynamic>.from(r as Map)),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return UserTaskModel(
|
||||||
|
id: task.id,
|
||||||
|
title: task.title,
|
||||||
|
description: task.description,
|
||||||
|
type: task.type,
|
||||||
|
difficulty: task.difficulty,
|
||||||
|
status: task.status,
|
||||||
|
rewards: rewards,
|
||||||
|
createdAt: task.createdAt.dateTime,
|
||||||
|
expiresAt: task.expiresAt.dateTime,
|
||||||
|
completedAt: task.completedAt?.dateTime,
|
||||||
|
proofUrl: task.proofUrl,
|
||||||
|
instructions: task.instructions,
|
||||||
|
tags: task.tags,
|
||||||
|
imageUrl: task.imageUrl,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert UserTaskModel to UserTasksCompanion for database operations
|
||||||
|
UserTasksCompanion _modelToCompanion(
|
||||||
|
UserTaskModel model, {
|
||||||
|
bool isUpdate = false,
|
||||||
|
}) {
|
||||||
|
final rewardsJson = model.rewards.map((r) => r.toJson()).toList();
|
||||||
|
final now = PgDateTime(DateTime.now());
|
||||||
|
|
||||||
|
return UserTasksCompanion(
|
||||||
|
id: isUpdate && model.id != null
|
||||||
|
? drift.Value(model.id!)
|
||||||
|
: const drift.Value.absent(),
|
||||||
|
title: drift.Value(model.title),
|
||||||
|
description: drift.Value(model.description),
|
||||||
|
type: drift.Value(model.type),
|
||||||
|
difficulty: drift.Value(model.difficulty),
|
||||||
|
status: drift.Value(model.status),
|
||||||
|
rewards: drift.Value(rewardsJson),
|
||||||
|
createdAt: isUpdate
|
||||||
|
? const drift.Value.absent()
|
||||||
|
: drift.Value(PgDateTime(model.createdAt)),
|
||||||
|
expiresAt: drift.Value(PgDateTime(model.expiresAt)),
|
||||||
|
completedAt: model.completedAt != null
|
||||||
|
? drift.Value(PgDateTime(model.completedAt!))
|
||||||
|
: const drift.Value.absent(),
|
||||||
|
proofUrl: model.proofUrl != null
|
||||||
|
? drift.Value(model.proofUrl!)
|
||||||
|
: const drift.Value.absent(),
|
||||||
|
instructions: model.instructions != null
|
||||||
|
? drift.Value(model.instructions!)
|
||||||
|
: const drift.Value.absent(),
|
||||||
|
tags: drift.Value(model.tags),
|
||||||
|
imageUrl: model.imageUrl != null
|
||||||
|
? drift.Value(model.imageUrl!)
|
||||||
|
: const drift.Value.absent(),
|
||||||
|
updatedAt: drift.Value(now),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GET /api/v2/admin/tasks
|
||||||
|
/// Get all tasks with pagination and filters
|
||||||
|
@Route.get('/admin/tasks')
|
||||||
|
Future<Response> getTasks(Request request) async {
|
||||||
|
try {
|
||||||
|
final auth = await _ensureAdmin(request);
|
||||||
|
if (auth.statusCode != 200) {
|
||||||
|
return auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
final queryParams = request.url.queryParameters;
|
||||||
|
|
||||||
|
// Parse pagination parameters
|
||||||
|
final page = int.tryParse(queryParams['page'] ?? '1') ?? 1;
|
||||||
|
final limit = int.tryParse(queryParams['limit'] ?? '20') ?? 20;
|
||||||
|
final type = queryParams['type'];
|
||||||
|
final difficulty = queryParams['difficulty'];
|
||||||
|
final status = queryParams['status'];
|
||||||
|
final search = queryParams['search'] ?? '';
|
||||||
|
|
||||||
|
// Validate pagination
|
||||||
|
if (page < 1) {
|
||||||
|
return _json({
|
||||||
|
'error': 'Invalid page parameter',
|
||||||
|
'message': 'Page must be greater than 0. Received: $page',
|
||||||
|
'field': 'page',
|
||||||
|
}, statusCode: 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit validation: cap at 100
|
||||||
|
final validatedLimit = limit < 1 ? 20 : (limit > 100 ? 100 : limit);
|
||||||
|
|
||||||
|
// Get all tasks
|
||||||
|
List<UserTask> allTasks = await _taskRepository.getAllUserTasks();
|
||||||
|
|
||||||
|
// Apply filters
|
||||||
|
if (type != null && type.isNotEmpty) {
|
||||||
|
allTasks = allTasks.where((t) => t.type == type).toList();
|
||||||
|
}
|
||||||
|
if (difficulty != null && difficulty.isNotEmpty) {
|
||||||
|
allTasks = allTasks.where((t) => t.difficulty == difficulty).toList();
|
||||||
|
}
|
||||||
|
if (status != null && status.isNotEmpty) {
|
||||||
|
allTasks = allTasks.where((t) => t.status == status).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply search filter
|
||||||
|
if (search.isNotEmpty) {
|
||||||
|
final searchTerm = search.toLowerCase();
|
||||||
|
allTasks = allTasks.where((task) {
|
||||||
|
return task.title.toLowerCase().contains(searchTerm) ||
|
||||||
|
task.description.toLowerCase().contains(searchTerm) ||
|
||||||
|
task.id.toLowerCase().contains(searchTerm);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to models
|
||||||
|
final taskModels = allTasks.map(_userTaskToModel).toList();
|
||||||
|
|
||||||
|
// Calculate pagination
|
||||||
|
final total = taskModels.length;
|
||||||
|
final totalPages = (total / validatedLimit).ceil();
|
||||||
|
final offset = (page - 1) * validatedLimit;
|
||||||
|
final paginatedTasks = taskModels
|
||||||
|
.skip(offset)
|
||||||
|
.take(validatedLimit)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return _json({
|
||||||
|
'items': paginatedTasks.map((t) => t.toJson()).toList(),
|
||||||
|
'total': total,
|
||||||
|
'page': page,
|
||||||
|
'limit': validatedLimit,
|
||||||
|
'totalPages': totalPages,
|
||||||
|
});
|
||||||
|
} catch (e, s) {
|
||||||
|
log('Error in getTasks: $e\n$s');
|
||||||
|
return _json({
|
||||||
|
'error': 'Internal server error',
|
||||||
|
'message': 'Failed to fetch tasks',
|
||||||
|
}, statusCode: 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GET /api/v2/admin/tasks/{taskId}
|
||||||
|
/// Get task details by ID
|
||||||
|
@Route.get('/admin/tasks/<taskId>')
|
||||||
|
Future<Response> getTask(Request request, String taskId) async {
|
||||||
|
try {
|
||||||
|
final auth = await _ensureAdmin(request);
|
||||||
|
if (auth.statusCode != 200) {
|
||||||
|
return auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (taskId.isEmpty) {
|
||||||
|
return _json({
|
||||||
|
'error': 'Invalid task ID',
|
||||||
|
'message': 'Task ID cannot be empty',
|
||||||
|
'field': 'taskId',
|
||||||
|
}, statusCode: 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
final task = await _taskRepository.getUserTaskById(taskId);
|
||||||
|
if (task == null) {
|
||||||
|
return _json({
|
||||||
|
'error': 'Task not found',
|
||||||
|
'message': 'The requested task does not exist or has been deleted',
|
||||||
|
}, statusCode: 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
final model = _userTaskToModel(task);
|
||||||
|
return _json(model.toJson());
|
||||||
|
} catch (e, s) {
|
||||||
|
log('Error in getTask: $e\n$s');
|
||||||
|
return _json({
|
||||||
|
'error': 'Internal server error',
|
||||||
|
'message': 'Failed to fetch task',
|
||||||
|
}, statusCode: 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// POST /api/v2/admin/tasks
|
||||||
|
/// Create a new task
|
||||||
|
@Route.post('/admin/tasks')
|
||||||
|
Future<Response> createTask(Request request) async {
|
||||||
|
try {
|
||||||
|
final auth = await _ensureAdmin(request);
|
||||||
|
if (auth.statusCode != 200) {
|
||||||
|
return auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
final body = await request.readAsString();
|
||||||
|
if (body.isEmpty) {
|
||||||
|
return _json({
|
||||||
|
'error': 'bad_request',
|
||||||
|
'message': 'Task payload is required',
|
||||||
|
}, statusCode: 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
late final UserTaskModel taskModel;
|
||||||
|
try {
|
||||||
|
taskModel = UserTaskModel.fromJson(
|
||||||
|
jsonDecode(body) as Map<String, dynamic>,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return _json({
|
||||||
|
'error': 'bad_request',
|
||||||
|
'message': 'Invalid task payload: $e',
|
||||||
|
}, statusCode: 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (taskModel.title.trim().isEmpty) {
|
||||||
|
return _json({
|
||||||
|
'error': 'Validation error',
|
||||||
|
'message': 'Title is required',
|
||||||
|
'field': 'title',
|
||||||
|
}, statusCode: 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (taskModel.description.trim().isEmpty) {
|
||||||
|
return _json({
|
||||||
|
'error': 'Validation error',
|
||||||
|
'message': 'Description is required',
|
||||||
|
'field': 'description',
|
||||||
|
}, statusCode: 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create task
|
||||||
|
final companion = _modelToCompanion(taskModel, isUpdate: false);
|
||||||
|
final taskId = await _taskRepository.createUserTask(companion);
|
||||||
|
|
||||||
|
// Fetch created task
|
||||||
|
final createdTask = await _taskRepository.getUserTaskById(taskId);
|
||||||
|
if (createdTask == null) {
|
||||||
|
return _json({
|
||||||
|
'error': 'Internal server error',
|
||||||
|
'message': 'Failed to retrieve created task',
|
||||||
|
}, statusCode: 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
final model = _userTaskToModel(createdTask);
|
||||||
|
return _json({'success': true, 'task': model.toJson()});
|
||||||
|
} catch (e, s) {
|
||||||
|
log('Error in createTask: $e\n$s');
|
||||||
|
return _json({
|
||||||
|
'error': 'Internal server error',
|
||||||
|
'message': 'Failed to create task',
|
||||||
|
}, statusCode: 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PUT /api/v2/admin/tasks/{taskId}
|
||||||
|
/// Update an existing task
|
||||||
|
@Route.put('/admin/tasks/<taskId>')
|
||||||
|
Future<Response> updateTask(Request request, String taskId) async {
|
||||||
|
try {
|
||||||
|
final auth = await _ensureAdmin(request);
|
||||||
|
if (auth.statusCode != 200) {
|
||||||
|
return auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (taskId.isEmpty) {
|
||||||
|
return _json({
|
||||||
|
'error': 'Invalid task ID',
|
||||||
|
'message': 'Task ID cannot be empty',
|
||||||
|
'field': 'taskId',
|
||||||
|
}, statusCode: 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if task exists
|
||||||
|
final existing = await _taskRepository.getUserTaskById(taskId);
|
||||||
|
if (existing == null) {
|
||||||
|
return _json({
|
||||||
|
'error': 'Task not found',
|
||||||
|
'message': 'The task you are trying to update does not exist',
|
||||||
|
}, statusCode: 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
final body = await request.readAsString();
|
||||||
|
if (body.isEmpty) {
|
||||||
|
return _json({
|
||||||
|
'error': 'bad_request',
|
||||||
|
'message': 'Task payload is required',
|
||||||
|
}, statusCode: 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
late final Map<String, dynamic> updateData;
|
||||||
|
try {
|
||||||
|
updateData = jsonDecode(body) as Map<String, dynamic>;
|
||||||
|
} catch (e) {
|
||||||
|
return _json({
|
||||||
|
'error': 'bad_request',
|
||||||
|
'message': 'Invalid task payload: $e',
|
||||||
|
}, statusCode: 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert existing task to model
|
||||||
|
final existingModel = _userTaskToModel(existing);
|
||||||
|
|
||||||
|
// Apply updates (partial update)
|
||||||
|
final updatedModel = existingModel.copyWith(
|
||||||
|
title: updateData['title'] as String? ?? existingModel.title,
|
||||||
|
description:
|
||||||
|
updateData['description'] as String? ?? existingModel.description,
|
||||||
|
type: updateData['type'] as String? ?? existingModel.type,
|
||||||
|
difficulty:
|
||||||
|
updateData['difficulty'] as String? ?? existingModel.difficulty,
|
||||||
|
status: updateData['status'] as String? ?? existingModel.status,
|
||||||
|
rewards: updateData['rewards'] != null
|
||||||
|
? (updateData['rewards'] as List)
|
||||||
|
.map(
|
||||||
|
(r) => TaskRewardModel.fromJson(
|
||||||
|
Map<String, dynamic>.from(r as Map),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList()
|
||||||
|
: existingModel.rewards,
|
||||||
|
expiresAt: updateData['expiresAt'] != null
|
||||||
|
? DateTime.parse(updateData['expiresAt'] as String)
|
||||||
|
: existingModel.expiresAt,
|
||||||
|
completedAt: updateData['completedAt'] != null
|
||||||
|
? DateTime.parse(updateData['completedAt'] as String)
|
||||||
|
: existingModel.completedAt,
|
||||||
|
proofUrl: updateData['proofUrl'] as String? ?? existingModel.proofUrl,
|
||||||
|
instructions:
|
||||||
|
updateData['instructions'] as String? ?? existingModel.instructions,
|
||||||
|
tags: updateData['tags'] != null
|
||||||
|
? List<String>.from(updateData['tags'] as List)
|
||||||
|
: existingModel.tags,
|
||||||
|
imageUrl: updateData['imageUrl'] as String? ?? existingModel.imageUrl,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Convert to Drift UserTask for update using update().write() for better null handling
|
||||||
|
final rewardsJson = updatedModel.rewards.map((r) => r.toJson()).toList();
|
||||||
|
final now = PgDateTime(DateTime.now());
|
||||||
|
|
||||||
|
await (_db.update(
|
||||||
|
_db.userTasks,
|
||||||
|
)..where((ut) => ut.id.equals(taskId))).write(
|
||||||
|
UserTasksCompanion(
|
||||||
|
title: drift.Value(updatedModel.title),
|
||||||
|
description: drift.Value(updatedModel.description),
|
||||||
|
type: drift.Value(updatedModel.type),
|
||||||
|
difficulty: drift.Value(updatedModel.difficulty),
|
||||||
|
status: drift.Value(updatedModel.status),
|
||||||
|
rewards: drift.Value(rewardsJson),
|
||||||
|
expiresAt: drift.Value(PgDateTime(updatedModel.expiresAt)),
|
||||||
|
completedAt: updatedModel.completedAt != null
|
||||||
|
? drift.Value(PgDateTime(updatedModel.completedAt!))
|
||||||
|
: const drift.Value.absent(),
|
||||||
|
proofUrl: updatedModel.proofUrl != null
|
||||||
|
? drift.Value(updatedModel.proofUrl!)
|
||||||
|
: const drift.Value.absent(),
|
||||||
|
instructions: updatedModel.instructions != null
|
||||||
|
? drift.Value(updatedModel.instructions!)
|
||||||
|
: const drift.Value.absent(),
|
||||||
|
tags: drift.Value(updatedModel.tags),
|
||||||
|
imageUrl: updatedModel.imageUrl != null
|
||||||
|
? drift.Value(updatedModel.imageUrl!)
|
||||||
|
: const drift.Value.absent(),
|
||||||
|
updatedAt: drift.Value(now),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fetch updated task
|
||||||
|
final updatedTask = await _taskRepository.getUserTaskById(taskId);
|
||||||
|
if (updatedTask == null) {
|
||||||
|
return _json({
|
||||||
|
'error': 'Internal server error',
|
||||||
|
'message': 'Failed to retrieve updated task',
|
||||||
|
}, statusCode: 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
final model = _userTaskToModel(updatedTask);
|
||||||
|
return _json({'success': true, 'task': model.toJson()});
|
||||||
|
} catch (e, s) {
|
||||||
|
log('Error in updateTask: $e\n$s');
|
||||||
|
return _json({
|
||||||
|
'error': 'Internal server error',
|
||||||
|
'message': 'Failed to update task',
|
||||||
|
}, statusCode: 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// DELETE /api/v2/admin/tasks/{taskId}
|
||||||
|
/// Delete a task
|
||||||
|
@Route.delete('/admin/tasks/<taskId>')
|
||||||
|
Future<Response> deleteTask(Request request, String taskId) async {
|
||||||
|
try {
|
||||||
|
final auth = await _ensureAdmin(request);
|
||||||
|
if (auth.statusCode != 200) {
|
||||||
|
return auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (taskId.isEmpty) {
|
||||||
|
return _json({
|
||||||
|
'error': 'Invalid task ID',
|
||||||
|
'message': 'Task ID cannot be empty',
|
||||||
|
'field': 'taskId',
|
||||||
|
}, statusCode: 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if task exists
|
||||||
|
final task = await _taskRepository.getUserTaskById(taskId);
|
||||||
|
if (task == null) {
|
||||||
|
return _json({
|
||||||
|
'error': 'Task not found',
|
||||||
|
'message': 'The task you are trying to delete does not exist',
|
||||||
|
}, statusCode: 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete task
|
||||||
|
final deleted = await _taskRepository.deleteUserTask(taskId);
|
||||||
|
if (!deleted) {
|
||||||
|
return _json({
|
||||||
|
'error': 'Internal server error',
|
||||||
|
'message': 'Failed to delete task',
|
||||||
|
}, statusCode: 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _json({'success': true, 'message': 'Task deleted successfully'});
|
||||||
|
} catch (e, s) {
|
||||||
|
log('Error in deleteTask: $e\n$s');
|
||||||
|
return _json({
|
||||||
|
'error': 'Internal server error',
|
||||||
|
'message': 'Failed to delete task',
|
||||||
|
}, statusCode: 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Router get router => _$AdminTasksApiV2Router(this);
|
||||||
|
}
|
||||||
17
mnemo_cards_backend/lib/api/v2/admin_tasks_api_v2.g.dart
Normal file
17
mnemo_cards_backend/lib/api/v2/admin_tasks_api_v2.g.dart
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'admin_tasks_api_v2.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// ShelfRouterGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
Router _$AdminTasksApiV2Router(AdminTasksApiV2 service) {
|
||||||
|
final router = Router();
|
||||||
|
router.add('GET', r'/admin/tasks', service.getTasks);
|
||||||
|
router.add('GET', r'/admin/tasks/<taskId>', service.getTask);
|
||||||
|
router.add('POST', r'/admin/tasks', service.createTask);
|
||||||
|
router.add('PUT', r'/admin/tasks/<taskId>', service.updateTask);
|
||||||
|
router.add('DELETE', r'/admin/tasks/<taskId>', service.deleteTask);
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
import 'package:injectable/injectable.dart';
|
||||||
|
import 'package:mnemo_cards_backend/database/database.dart';
|
||||||
|
|
||||||
|
/// Repository для работы с достижениями
|
||||||
|
/// Работает с Drift моделями (UserAchievement), использует DAO для доступа к БД
|
||||||
|
@lazySingleton
|
||||||
|
class AchievementRepository {
|
||||||
|
final AppDatabase _db;
|
||||||
|
|
||||||
|
AchievementRepository(this._db);
|
||||||
|
|
||||||
|
/// Получить все достижения пользователя
|
||||||
|
Future<List<UserAchievement>> getUserAchievements(String userId) async {
|
||||||
|
return await _db.achievementDao.getUserAchievements(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить достижение пользователя
|
||||||
|
Future<UserAchievement?> getUserAchievement(
|
||||||
|
String userId,
|
||||||
|
String achievementId,
|
||||||
|
) async {
|
||||||
|
return await _db.achievementDao.getUserAchievement(userId, achievementId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Проверить, есть ли у пользователя достижение
|
||||||
|
Future<bool> hasAchievement(String userId, String achievementId) async {
|
||||||
|
return await _db.achievementDao.hasAchievement(userId, achievementId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Создать достижение пользователя
|
||||||
|
Future<String> unlockAchievement(UserAchievementsCompanion companion) async {
|
||||||
|
return await _db.achievementDao.unlockAchievement(companion);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Обновить прогресс достижения
|
||||||
|
Future<bool> updateAchievementProgress(
|
||||||
|
String userId,
|
||||||
|
String achievementId,
|
||||||
|
double progress,
|
||||||
|
) async {
|
||||||
|
return await _db.achievementDao.updateAchievementProgress(
|
||||||
|
userId,
|
||||||
|
achievementId,
|
||||||
|
progress,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить прогресс по всем достижениям пользователя
|
||||||
|
Future<Map<String, double>> getAchievementProgress(String userId) async {
|
||||||
|
return await _db.achievementDao.getAchievementProgress(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Удалить достижение пользователя (для сброса)
|
||||||
|
Future<void> removeAchievement(String userId, String achievementId) async {
|
||||||
|
await _db.achievementDao.removeAchievement(userId, achievementId);
|
||||||
|
}
|
||||||
|
}
|
||||||
138
mnemo_cards_backend/lib/repository/discount_repository.dart
Normal file
138
mnemo_cards_backend/lib/repository/discount_repository.dart
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
import 'package:injectable/injectable.dart';
|
||||||
|
import 'package:mnemo_cards_backend/database/database.dart';
|
||||||
|
|
||||||
|
/// Repository для работы со скидками
|
||||||
|
/// Работает с Drift моделями (DiscountCampaign, Discount), использует DAO для доступа к БД
|
||||||
|
@lazySingleton
|
||||||
|
class DiscountRepository {
|
||||||
|
final AppDatabase _db;
|
||||||
|
|
||||||
|
DiscountRepository(this._db);
|
||||||
|
|
||||||
|
// ==================== DiscountCampaigns ====================
|
||||||
|
|
||||||
|
/// Получить кампанию по ID
|
||||||
|
Future<DiscountCampaign?> getCampaignById(String id) async {
|
||||||
|
return await _db.discountDao.getCampaignById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить все активные кампании
|
||||||
|
Future<List<DiscountCampaign>> getActiveCampaigns() async {
|
||||||
|
return await _db.discountDao.getActiveCampaigns();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить все кампании
|
||||||
|
Future<List<DiscountCampaign>> getAllCampaigns() async {
|
||||||
|
return await _db.discountDao.getAllCampaigns();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить кампании по статусу
|
||||||
|
Future<List<DiscountCampaign>> getCampaignsByStatus(String status) async {
|
||||||
|
return await _db.discountDao.getCampaignsByStatus(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить кампании по статусам
|
||||||
|
Future<List<DiscountCampaign>> getCampaignsByStatuses(
|
||||||
|
List<String> statuses,
|
||||||
|
) async {
|
||||||
|
return await _db.discountDao.getCampaignsByStatuses(statuses);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить активные кампании для пользователя с учетом тегов и продуктов
|
||||||
|
Future<List<DiscountCampaign>> getActiveCampaignsForUser({
|
||||||
|
required List<String> userTags,
|
||||||
|
String? productType,
|
||||||
|
String? productId,
|
||||||
|
}) async {
|
||||||
|
return await _db.discountDao.getActiveCampaignsForUser(
|
||||||
|
userTags: userTags,
|
||||||
|
productType: productType,
|
||||||
|
productId: productId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Создать кампанию
|
||||||
|
Future<String> createCampaign(DiscountCampaignsCompanion campaign) async {
|
||||||
|
return await _db.discountDao.createCampaign(campaign);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Обновить кампанию
|
||||||
|
Future<bool> updateCampaign(DiscountCampaign campaign) async {
|
||||||
|
return await _db.discountDao.updateCampaign(campaign);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Обновить статус кампании
|
||||||
|
Future<void> updateCampaignStatus(String campaignId, String status) async {
|
||||||
|
await _db.discountDao.updateCampaignStatus(campaignId, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Удалить кампанию (soft delete)
|
||||||
|
Future<void> deleteCampaign(String campaignId) async {
|
||||||
|
await _db.discountDao.deleteCampaign(campaignId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Discounts ====================
|
||||||
|
|
||||||
|
/// Получить скидку по ID
|
||||||
|
Future<Discount?> getDiscountById(String id) async {
|
||||||
|
return await _db.discountDao.getDiscountById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить скидки кампании
|
||||||
|
Future<List<Discount>> getDiscountsByCampaignId(String campaignId) async {
|
||||||
|
return await _db.discountDao.getDiscountsByCampaignId(campaignId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Создать скидку
|
||||||
|
Future<String> createDiscount(DiscountsCompanion discount) async {
|
||||||
|
return await _db.discountDao.createDiscount(discount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Обновить скидку
|
||||||
|
Future<bool> updateDiscount(Discount discount) async {
|
||||||
|
return await _db.discountDao.updateDiscount(discount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Удалить скидку (soft delete)
|
||||||
|
Future<void> softDeleteDiscount(String discountId) async {
|
||||||
|
await _db.discountDao.softDeleteDiscount(discountId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== DiscountUserDatas ====================
|
||||||
|
|
||||||
|
/// Получить скидки пользователя
|
||||||
|
Future<List<Discount>> getUserDiscounts(String userId) async {
|
||||||
|
return await _db.discountDao.getUserDiscounts(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Дать пользователю доступ к скидке
|
||||||
|
Future<void> grantDiscountToUser(String userId, String discountId) async {
|
||||||
|
await _db.discountDao.grantDiscountToUser(userId, discountId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Дать пользователю доступ к нескольким скидкам
|
||||||
|
Future<void> grantDiscountsToUser(
|
||||||
|
String userId,
|
||||||
|
List<String> discountIds,
|
||||||
|
) async {
|
||||||
|
await _db.discountDao.grantDiscountsToUser(userId, discountIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Отозвать скидку у пользователя
|
||||||
|
Future<void> revokeDiscountFromUser(String userId, String discountId) async {
|
||||||
|
await _db.discountDao.revokeDiscountFromUser(userId, discountId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Отозвать несколько скидок у пользователя
|
||||||
|
Future<void> revokeDiscountsFromUser(
|
||||||
|
String userId,
|
||||||
|
List<String> discountIds,
|
||||||
|
) async {
|
||||||
|
await _db.discountDao.revokeDiscountsFromUser(userId, discountIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Проверить, есть ли у пользователя доступ к скидке
|
||||||
|
Future<bool> hasDiscountAccess(String userId, String discountId) async {
|
||||||
|
return await _db.discountDao.hasDiscountAccess(userId, discountId);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
mnemo_cards_backend/lib/repository/export.dart
Normal file
13
mnemo_cards_backend/lib/repository/export.dart
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
export 'user_repository.dart';
|
||||||
|
export 'pack_repository.dart';
|
||||||
|
export 'iap_repository.dart';
|
||||||
|
export 'payment_repository.dart';
|
||||||
|
export 'subscription_repository.dart';
|
||||||
|
export 'test_repository.dart';
|
||||||
|
export 'task_repository.dart';
|
||||||
|
export 'promo_code_repository.dart';
|
||||||
|
export 'discount_repository.dart';
|
||||||
|
export 'statistics_repository.dart';
|
||||||
|
export 'word_statistics_repository.dart';
|
||||||
|
export 'achievement_repository.dart';
|
||||||
|
export 'сonverters/export.dart';
|
||||||
219
mnemo_cards_backend/lib/repository/iap_repository.dart
Normal file
219
mnemo_cards_backend/lib/repository/iap_repository.dart
Normal file
|
|
@ -0,0 +1,219 @@
|
||||||
|
import 'package:googleapis/firestore/v1.dart';
|
||||||
|
|
||||||
|
import '../api/purchase/products.dart';
|
||||||
|
|
||||||
|
enum IAPSource { googleplay, appstore }
|
||||||
|
|
||||||
|
abstract class Purchase {
|
||||||
|
final IAPSource iapSource;
|
||||||
|
final String orderId;
|
||||||
|
final String productId;
|
||||||
|
final String? userId;
|
||||||
|
final DateTime purchaseDate;
|
||||||
|
final ProductType type;
|
||||||
|
|
||||||
|
const Purchase({
|
||||||
|
required this.iapSource,
|
||||||
|
required this.orderId,
|
||||||
|
required this.productId,
|
||||||
|
required this.userId,
|
||||||
|
required this.purchaseDate,
|
||||||
|
required this.type,
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, Value> toDocument() {
|
||||||
|
return {
|
||||||
|
'iapSource': Value(stringValue: iapSource.name),
|
||||||
|
'orderId': Value(stringValue: orderId),
|
||||||
|
'productId': Value(stringValue: productId),
|
||||||
|
'userId': Value(stringValue: userId),
|
||||||
|
'purchaseDate': Value(
|
||||||
|
timestampValue: purchaseDate.toUtc().toIso8601String(),
|
||||||
|
),
|
||||||
|
'type': Value(stringValue: type.name),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Value> updateDocument();
|
||||||
|
|
||||||
|
static Purchase fromDocument(Document e) {
|
||||||
|
final type = ProductType.values.firstWhere(
|
||||||
|
(element) => element.name == e.fields!['type']!.stringValue,
|
||||||
|
);
|
||||||
|
switch (type) {
|
||||||
|
case ProductType.subscription:
|
||||||
|
return SubscriptionPurchase(
|
||||||
|
iapSource: e.fields!['iapSource']!.stringValue == 'googleplay'
|
||||||
|
? IAPSource.googleplay
|
||||||
|
: IAPSource.appstore,
|
||||||
|
orderId: e.fields!['orderId']!.stringValue!,
|
||||||
|
productId: e.fields!['productId']!.stringValue!,
|
||||||
|
userId: e.fields!['userId']?.stringValue,
|
||||||
|
purchaseDate: DateTime.parse(
|
||||||
|
e.fields!['purchaseDate']!.timestampValue!,
|
||||||
|
),
|
||||||
|
status: SubscriptionStatus.values.firstWhere(
|
||||||
|
(element) => element.name == e.fields!['status']!.stringValue,
|
||||||
|
),
|
||||||
|
expiryDate:
|
||||||
|
DateTime.tryParse(
|
||||||
|
e.fields!['expiryDate']?.timestampValue ?? '',
|
||||||
|
) ??
|
||||||
|
DateTime.now(),
|
||||||
|
);
|
||||||
|
case ProductType.nonSubscription:
|
||||||
|
return NonSubscriptionPurchase(
|
||||||
|
iapSource: e.fields!['iapSource']!.stringValue == 'googleplay'
|
||||||
|
? IAPSource.googleplay
|
||||||
|
: IAPSource.appstore,
|
||||||
|
orderId: e.fields!['orderId']!.stringValue!,
|
||||||
|
productId: e.fields!['productId']!.stringValue!,
|
||||||
|
userId: e.fields!['userId']?.stringValue,
|
||||||
|
purchaseDate: DateTime.parse(
|
||||||
|
e.fields!['purchaseDate']!.timestampValue!,
|
||||||
|
),
|
||||||
|
status: NonSubscriptionStatus.values.firstWhere(
|
||||||
|
(element) => element.name == e.fields!['status']!.stringValue,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum NonSubscriptionStatus { pending, completed, cancelled }
|
||||||
|
|
||||||
|
enum SubscriptionStatus { pending, active, expired }
|
||||||
|
|
||||||
|
class NonSubscriptionPurchase extends Purchase {
|
||||||
|
final NonSubscriptionStatus status;
|
||||||
|
|
||||||
|
NonSubscriptionPurchase({
|
||||||
|
required super.iapSource,
|
||||||
|
required super.orderId,
|
||||||
|
required super.productId,
|
||||||
|
required super.userId,
|
||||||
|
required super.purchaseDate,
|
||||||
|
required this.status,
|
||||||
|
super.type = ProductType.nonSubscription,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, Value> toDocument() {
|
||||||
|
final doc = super.toDocument();
|
||||||
|
doc.addAll({'status': Value(stringValue: status.name)});
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, Value> updateDocument() {
|
||||||
|
return {'status': Value(stringValue: status.name)};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'NonSubscriptionPurchase { '
|
||||||
|
'iapSource: $iapSource, '
|
||||||
|
'orderId: $orderId, '
|
||||||
|
'productId: $productId, '
|
||||||
|
'userId: $userId, '
|
||||||
|
'purchaseDate: $purchaseDate, '
|
||||||
|
'status: $status, '
|
||||||
|
'type: $type '
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SubscriptionPurchase extends Purchase {
|
||||||
|
final SubscriptionStatus status;
|
||||||
|
final DateTime expiryDate;
|
||||||
|
|
||||||
|
SubscriptionPurchase({
|
||||||
|
required super.iapSource,
|
||||||
|
required super.orderId,
|
||||||
|
required super.productId,
|
||||||
|
required super.userId,
|
||||||
|
required super.purchaseDate,
|
||||||
|
required this.status,
|
||||||
|
required this.expiryDate,
|
||||||
|
super.type = ProductType.subscription,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, Value> toDocument() {
|
||||||
|
final doc = super.toDocument();
|
||||||
|
doc.addAll({
|
||||||
|
'expiryDate': Value(timestampValue: expiryDate.toUtc().toIso8601String()),
|
||||||
|
'status': Value(stringValue: status.name),
|
||||||
|
});
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, Value> updateDocument() {
|
||||||
|
return {'status': Value(stringValue: status.name)};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SubscriptionPurchase { '
|
||||||
|
'iapSource: $iapSource, '
|
||||||
|
'orderId: $orderId, '
|
||||||
|
'productId: $productId, '
|
||||||
|
'userId: $userId, '
|
||||||
|
'purchaseDate: $purchaseDate, '
|
||||||
|
'status: $status, '
|
||||||
|
'expiryDate: $expiryDate, '
|
||||||
|
'type: $type '
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class IapRepository {
|
||||||
|
final FirestoreApi api;
|
||||||
|
final String projectId;
|
||||||
|
|
||||||
|
IapRepository(this.api, this.projectId);
|
||||||
|
|
||||||
|
Future<void> createOrUpdatePurchase(Purchase purchaseData) async {
|
||||||
|
print('Updating $purchaseData');
|
||||||
|
final purchaseId = _purchaseId(purchaseData);
|
||||||
|
await api.projects.databases.documents.commit(
|
||||||
|
CommitRequest(
|
||||||
|
writes: [
|
||||||
|
Write(
|
||||||
|
update: Document(
|
||||||
|
fields: purchaseData.toDocument(),
|
||||||
|
name:
|
||||||
|
'projects/$projectId/databases/(default)/documents/purchases/$purchaseId',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
'projects/$projectId/databases/(default)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updatePurchase(Purchase purchaseData) async {
|
||||||
|
print('Updating $purchaseData');
|
||||||
|
final purchaseId = _purchaseId(purchaseData);
|
||||||
|
await api.projects.databases.documents.commit(
|
||||||
|
CommitRequest(
|
||||||
|
writes: [
|
||||||
|
Write(
|
||||||
|
update: Document(
|
||||||
|
fields: purchaseData.updateDocument(),
|
||||||
|
name:
|
||||||
|
'projects/$projectId/databases/(default)/documents/purchases/$purchaseId',
|
||||||
|
),
|
||||||
|
updateMask: DocumentMask(fieldPaths: ['status']),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
'projects/$projectId/databases/(default)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _purchaseId(Purchase purchaseData) {
|
||||||
|
return '${purchaseData.iapSource.name}_${purchaseData.orderId}';
|
||||||
|
}
|
||||||
|
}
|
||||||
224
mnemo_cards_backend/lib/repository/pack_repository.dart
Normal file
224
mnemo_cards_backend/lib/repository/pack_repository.dart
Normal file
|
|
@ -0,0 +1,224 @@
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:injectable/injectable.dart';
|
||||||
|
import 'package:mnemo_cards_backend/database/database.dart';
|
||||||
|
import 'package:mnemo_cards_backend/repository/сonverters/export.dart';
|
||||||
|
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart'
|
||||||
|
hide VoiceModel;
|
||||||
|
|
||||||
|
/// Repository для работы с паками
|
||||||
|
/// Работает с доменными моделями (CardPackModel), использует DAO для доступа к БД
|
||||||
|
@lazySingleton
|
||||||
|
class PackRepository {
|
||||||
|
final AppDatabase _db;
|
||||||
|
final CardPackConverter _cardPackConverter;
|
||||||
|
final GameCardConverter _gameCardConverter;
|
||||||
|
|
||||||
|
PackRepository(this._db, this._cardPackConverter, this._gameCardConverter);
|
||||||
|
|
||||||
|
/// Получить пак по 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 _cardPackConverter.toModel(pack);
|
||||||
|
|
||||||
|
if (includeCards) {
|
||||||
|
final cards = await _db.packDao.getPackCards(id);
|
||||||
|
packModel.cards.addAll(cards.map((c) => _gameCardConverter.toModel(c)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includePreviewCards) {
|
||||||
|
final previewCards = await _db.packDao.getPreviewCards(id);
|
||||||
|
packModel.previewCards.addAll(
|
||||||
|
previewCards.map((c) => _gameCardConverter.toModel(c)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 _cardPackConverter.toModel(pack);
|
||||||
|
|
||||||
|
if (includeCards) {
|
||||||
|
final cards = await _db.packDao.getPackCards(pack.id);
|
||||||
|
packModel.cards.addAll(
|
||||||
|
cards.map((c) => _gameCardConverter.toModel(c)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includePreviewCards) {
|
||||||
|
final previewCards = await _db.packDao.getPreviewCards(pack.id);
|
||||||
|
packModel.previewCards.addAll(
|
||||||
|
previewCards.map((c) => _gameCardConverter.toModel(c)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
packModels.add(packModel);
|
||||||
|
}
|
||||||
|
return packModels;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await Future.wait(packs.map((p) => _cardPackConverter.toModel(p)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Создать пак
|
||||||
|
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> updatePackPartial(CardPacksCompanion updates) async {
|
||||||
|
await _db.packDao.updatePackPartial(updates);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Удалить пак (soft delete)
|
||||||
|
Future<void> softDeletePack(String packId) async {
|
||||||
|
await _db.packDao.softDeletePack(packId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить карточки пака
|
||||||
|
Future<List<GameCardModel>> getPackCards(String packId) async {
|
||||||
|
final cards = await _db.packDao.getPackCards(packId);
|
||||||
|
return cards.map((c) => _gameCardConverter.toModel(c)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить превью карточки пака
|
||||||
|
Future<List<GameCardModel>> getPreviewCards(String packId) async {
|
||||||
|
final cards = await _db.packDao.getPreviewCards(packId);
|
||||||
|
return cards.map((c) => _gameCardConverter.toModel(c)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить карточку по ID
|
||||||
|
Future<GameCardModel?> getCardById(String id) async {
|
||||||
|
final card = await _db.packDao.getCardById(id);
|
||||||
|
return card != null ? _gameCardConverter.toModel(card) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Добавить карточку в пак
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Cards (дополнительные методы) ====================
|
||||||
|
|
||||||
|
/// Создать карточку
|
||||||
|
Future<String> createCard(GameCardsCompanion companion) async {
|
||||||
|
return await _db.packDao.createCard(companion);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Обновить карточку
|
||||||
|
Future<bool> updateCard(GameCard card) async {
|
||||||
|
return await _db.packDao.updateCard(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить все карточки
|
||||||
|
Future<List<GameCardModel>> getAllCards({int? limit, int? offset}) async {
|
||||||
|
final cards = await _db.packDao.getAllCards(limit: limit, offset: offset);
|
||||||
|
return cards.map((c) => _gameCardConverter.toModel(c)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить паки для карточки
|
||||||
|
Future<List<CardPackModel>> getPacksForCard(String cardId) async {
|
||||||
|
final packs = await _db.packDao.getPacksForCard(cardId);
|
||||||
|
return await Future.wait(packs.map((p) => _cardPackConverter.toModel(p)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Поиск карточек по оригинальному тексту
|
||||||
|
Future<List<GameCardModel>> searchCardsByOriginal(String original) async {
|
||||||
|
final cards = await _db.packDao.searchCardsByOriginal(original);
|
||||||
|
return cards.map((c) => _gameCardConverter.toModel(c)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Подсчитать карточки
|
||||||
|
Future<int> countCards() async {
|
||||||
|
return await _db.packDao.countCards();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Подсчитать паки
|
||||||
|
Future<int> countPacks({bool enabledOnly = false}) async {
|
||||||
|
return await _db.packDao.countPacks(enabledOnly: enabledOnly);
|
||||||
|
}
|
||||||
|
}
|
||||||
112
mnemo_cards_backend/lib/repository/payment_repository.dart
Normal file
112
mnemo_cards_backend/lib/repository/payment_repository.dart
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
import 'package:injectable/injectable.dart';
|
||||||
|
import 'package:mnemo_cards_backend/database/database.dart';
|
||||||
|
import 'package:mnemo_cards_backend/api/purchase/payment_drift_extension.dart';
|
||||||
|
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||||
|
|
||||||
|
/// Repository для работы с платежами
|
||||||
|
/// Работает с доменными моделями (PaymentDto), использует DAO для доступа к БД
|
||||||
|
@lazySingleton
|
||||||
|
class PaymentRepository {
|
||||||
|
final AppDatabase _db;
|
||||||
|
|
||||||
|
PaymentRepository(this._db);
|
||||||
|
|
||||||
|
/// Создать платеж
|
||||||
|
Future<PaymentDto> createPayment(PaymentDto paymentDto, String userId) async {
|
||||||
|
final companion = paymentDto.toCompanion(userId);
|
||||||
|
final paymentId = await _db.paymentDao.createPayment(companion);
|
||||||
|
final payment = await _db.paymentDao.getPaymentById(paymentId);
|
||||||
|
if (payment == null) {
|
||||||
|
throw Exception('Failed to create payment');
|
||||||
|
}
|
||||||
|
return payment.toDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить платеж по ID
|
||||||
|
Future<PaymentDto?> getPaymentById(String id) async {
|
||||||
|
final payment = await _db.paymentDao.getPaymentById(id);
|
||||||
|
return payment?.toDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить платеж по externalToken
|
||||||
|
Future<PaymentDto?> getPaymentByExternalToken(String token) async {
|
||||||
|
final payment = await _db.paymentDao.getPaymentByExternalToken(token);
|
||||||
|
return payment?.toDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить последний платеж для пользователя и пака
|
||||||
|
Future<PaymentDto?> getLatestPaymentForUserAndPack({
|
||||||
|
required String userId,
|
||||||
|
required String packId,
|
||||||
|
}) async {
|
||||||
|
final payment = await _db.paymentDao.getLatestPaymentForUserAndPack(
|
||||||
|
userId: userId,
|
||||||
|
packId: packId,
|
||||||
|
);
|
||||||
|
return payment?.toDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить платежи пользователя
|
||||||
|
Future<List<PaymentDto>> getPaymentsByUserId(
|
||||||
|
String userId, {
|
||||||
|
int? limit,
|
||||||
|
int? offset,
|
||||||
|
}) async {
|
||||||
|
final payments = await _db.paymentDao.getPaymentsByUserId(
|
||||||
|
userId,
|
||||||
|
limit: limit,
|
||||||
|
offset: offset,
|
||||||
|
);
|
||||||
|
return payments.map((p) => p.toDto()).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить платежи по продукту
|
||||||
|
Future<List<PaymentDto>> getPaymentsByProduct(String productId) async {
|
||||||
|
final payments = await _db.paymentDao.getPaymentsByProduct(productId);
|
||||||
|
return payments.map((p) => p.toDto()).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить платежи по статусу
|
||||||
|
Future<List<PaymentDto>> getPaymentsByStatus(String status) async {
|
||||||
|
final payments = await _db.paymentDao.getPaymentsByStatus(status);
|
||||||
|
return payments.map((p) => p.toDto()).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить все платежи (для админки)
|
||||||
|
Future<List<PaymentDto>> getAllPayments({int? limit, int? offset}) async {
|
||||||
|
final payments = await _db.paymentDao.getAllPayments(
|
||||||
|
limit: limit,
|
||||||
|
offset: offset,
|
||||||
|
);
|
||||||
|
return payments.map((p) => p.toDto()).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Обновить платеж
|
||||||
|
Future<void> updatePayment(String paymentId, PaymentDto paymentDto) async {
|
||||||
|
final companion = paymentDto.toUpdateCompanion();
|
||||||
|
await _db.paymentDao.updatePaymentCompanion(paymentId, companion);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Обновить статус платежа
|
||||||
|
Future<void> updatePaymentStatus(String externalToken, String status) async {
|
||||||
|
await _db.paymentDao.updatePaymentStatus(externalToken, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Обновить платеж частично
|
||||||
|
Future<void> updatePaymentCompanion(
|
||||||
|
String paymentId,
|
||||||
|
PaymentsCompanion companion,
|
||||||
|
) async {
|
||||||
|
await _db.paymentDao.updatePaymentCompanion(paymentId, companion);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Подсчитать все платежи
|
||||||
|
Future<int> countPayments() async {
|
||||||
|
return await _db.paymentDao.countAllPayments();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Подсчитать платежи пользователя
|
||||||
|
Future<int> countPaymentsByUserId(String userId) async {
|
||||||
|
return await _db.paymentDao.countPaymentsByUserId(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
import 'package:injectable/injectable.dart';
|
||||||
|
import 'package:mnemo_cards_backend/database/database.dart';
|
||||||
|
|
||||||
|
/// Repository для работы с промокодами
|
||||||
|
/// Работает с Drift моделями (PromoCodesCampaign, PromoCode), использует DAO для доступа к БД
|
||||||
|
@lazySingleton
|
||||||
|
class PromoCodeRepository {
|
||||||
|
final AppDatabase _db;
|
||||||
|
|
||||||
|
PromoCodeRepository(this._db);
|
||||||
|
|
||||||
|
// ==================== PromoCodesCampaigns ====================
|
||||||
|
|
||||||
|
/// Получить кампанию по ID
|
||||||
|
Future<PromoCodesCampaign?> getCampaignById(String id) async {
|
||||||
|
return await _db.promoCodeDao.getCampaignById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить все активные кампании
|
||||||
|
Future<List<PromoCodesCampaign>> getActiveCampaigns() async {
|
||||||
|
return await _db.promoCodeDao.getActiveCampaigns();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить кампании по статусам
|
||||||
|
Future<List<PromoCodesCampaign>> getCampaignsByStatuses(
|
||||||
|
List<String> statuses,
|
||||||
|
) async {
|
||||||
|
return await _db.promoCodeDao.getCampaignsByStatuses(statuses);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Создать кампанию
|
||||||
|
Future<String> createCampaign(PromoCodesCampaignsCompanion campaign) async {
|
||||||
|
return await _db.promoCodeDao.createCampaign(campaign);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Обновить кампанию
|
||||||
|
Future<bool> updateCampaign(PromoCodesCampaign campaign) async {
|
||||||
|
return await _db.promoCodeDao.updateCampaign(campaign);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Обновить статус кампании
|
||||||
|
Future<void> updateCampaignStatus(String campaignId, String status) async {
|
||||||
|
await _db.promoCodeDao.updateCampaignStatus(campaignId, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Удалить кампанию (soft delete)
|
||||||
|
Future<void> softDeleteCampaign(String campaignId) async {
|
||||||
|
await _db.promoCodeDao.softDeleteCampaign(campaignId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== PromoCodes ====================
|
||||||
|
|
||||||
|
/// Получить промокод по коду
|
||||||
|
Future<PromoCode?> getPromoCodeByCode(String code) async {
|
||||||
|
return await _db.promoCodeDao.getPromoCodeByCode(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить промокоды кампании
|
||||||
|
Future<List<PromoCode>> getPromoCodesByCampaignId(String campaignId) async {
|
||||||
|
return await _db.promoCodeDao.getPromoCodesByCampaignId(campaignId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить промокоды пользователя
|
||||||
|
Future<List<PromoCode>> getUserPromoCodes(String userId) async {
|
||||||
|
return await _db.promoCodeDao.getUserPromoCodes(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Создать промокод
|
||||||
|
Future<String> createPromoCode(PromoCodesCompanion promoCode) async {
|
||||||
|
return await _db.promoCodeDao.createPromoCode(promoCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Создать несколько промокодов
|
||||||
|
Future<List<String>> createPromoCodes(
|
||||||
|
List<PromoCodesCompanion> promoCodes,
|
||||||
|
) async {
|
||||||
|
return await _db.promoCodeDao.createPromoCodes(promoCodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Обновить промокод
|
||||||
|
Future<bool> updatePromoCode(PromoCode promoCode) async {
|
||||||
|
return await _db.promoCodeDao.updatePromoCode(promoCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Увеличить счетчик активаций
|
||||||
|
Future<void> incrementActivations(String promoCodeId) async {
|
||||||
|
await _db.promoCodeDao.incrementActivations(promoCodeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Удалить промокод (soft delete)
|
||||||
|
Future<void> softDeletePromoCode(String promoCodeId) async {
|
||||||
|
await _db.promoCodeDao.softDeletePromoCode(promoCodeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
import 'package:injectable/injectable.dart';
|
||||||
|
import 'package:mnemo_cards_backend/database/database.dart';
|
||||||
|
|
||||||
|
/// Repository для работы со статистикой сессий
|
||||||
|
/// Работает с Drift моделями (StudySession), использует DAO для доступа к БД
|
||||||
|
@lazySingleton
|
||||||
|
class StatisticsRepository {
|
||||||
|
final AppDatabase _db;
|
||||||
|
|
||||||
|
StatisticsRepository(this._db);
|
||||||
|
|
||||||
|
/// Получить сессию по ID
|
||||||
|
Future<StudySession?> getSessionById(String id) async {
|
||||||
|
return await _db.statisticsDao.getSessionById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить сессию по sessionId
|
||||||
|
Future<StudySession?> getSessionBySessionId(String sessionId) async {
|
||||||
|
return await _db.statisticsDao.getSessionBySessionId(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить активные сессии пользователя
|
||||||
|
Future<List<StudySession>> getActiveSessions(String userId) async {
|
||||||
|
return await _db.statisticsDao.getActiveSessions(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить сессии пользователя
|
||||||
|
Future<List<StudySession>> getSessionsByUserId(
|
||||||
|
String userId, {
|
||||||
|
int? limit,
|
||||||
|
int? offset,
|
||||||
|
DateTime? fromDate,
|
||||||
|
DateTime? toDate,
|
||||||
|
}) async {
|
||||||
|
return await _db.statisticsDao.getSessionsByUserId(
|
||||||
|
userId,
|
||||||
|
limit: limit,
|
||||||
|
offset: offset,
|
||||||
|
fromDate: fromDate,
|
||||||
|
toDate: toDate,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Создать сессию
|
||||||
|
Future<String> createSession(StudySessionsCompanion session) async {
|
||||||
|
return await _db.statisticsDao.createSession(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Обновить сессию
|
||||||
|
Future<bool> updateSession(StudySession session) async {
|
||||||
|
return await _db.statisticsDao.updateSession(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Завершить сессию
|
||||||
|
Future<void> endSession(
|
||||||
|
String sessionId, {
|
||||||
|
int? wordsLearned,
|
||||||
|
int? testsCompleted,
|
||||||
|
double? accuracy,
|
||||||
|
}) async {
|
||||||
|
await _db.statisticsDao.endSession(
|
||||||
|
sessionId,
|
||||||
|
wordsLearned: wordsLearned,
|
||||||
|
testsCompleted: testsCompleted,
|
||||||
|
accuracy: accuracy,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Подсчитать сессии пользователя
|
||||||
|
Future<int> countSessionsByUserId(String userId) async {
|
||||||
|
return await _db.statisticsDao.countSessionsByUserId(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
import 'package:injectable/injectable.dart';
|
||||||
|
import 'package:mnemo_cards_backend/database/database.dart';
|
||||||
|
import 'package:mnemo_cards_backend/repository/сonverters/subscription_converter.dart';
|
||||||
|
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
|
||||||
|
|
||||||
|
/// Repository для работы с подписками
|
||||||
|
/// Работает с доменными моделями (SubscriptionPlanModel, UserSubscriptionModel), использует DAO для доступа к БД
|
||||||
|
@lazySingleton
|
||||||
|
class SubscriptionRepository {
|
||||||
|
final AppDatabase _db;
|
||||||
|
final SubscriptionConverter _subscriptionConverter;
|
||||||
|
|
||||||
|
SubscriptionRepository(this._db, this._subscriptionConverter);
|
||||||
|
|
||||||
|
// ==================== SubscriptionPlans ====================
|
||||||
|
|
||||||
|
/// Получить план подписки по ID
|
||||||
|
Future<SubscriptionPlanModel?> getPlanById(String id) async {
|
||||||
|
final plan = await _db.subscriptionDao.getPlanById(id);
|
||||||
|
if (plan == null) return null;
|
||||||
|
return _subscriptionConverter.planToModel(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить все планы подписки
|
||||||
|
Future<List<SubscriptionPlanModel>> getAllPlans() async {
|
||||||
|
final plans = await _db.subscriptionDao.getAllPlans();
|
||||||
|
return plans.map((p) => _subscriptionConverter.planToModel(p)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Создать план подписки
|
||||||
|
Future<String> createPlan(SubscriptionPlansCompanion companion) async {
|
||||||
|
return await _db.subscriptionDao.createPlan(companion);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Обновить план подписки
|
||||||
|
Future<bool> updatePlan(SubscriptionPlan plan) async {
|
||||||
|
return await _db.subscriptionDao.updatePlan(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== UserSubscriptions ====================
|
||||||
|
|
||||||
|
/// Получить активную подписку пользователя
|
||||||
|
Future<UserSubscriptionModel?> getActiveSubscription(String userId) async {
|
||||||
|
final subscription = await _db.subscriptionDao.getActiveUserSubscription(
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
if (subscription == null) return null;
|
||||||
|
return _subscriptionConverter.toModel(subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить подписку пользователя (последнюю)
|
||||||
|
Future<UserSubscriptionModel?> getUserSubscription(String userId) async {
|
||||||
|
final subscription = await _db.subscriptionDao.getUserSubscription(userId);
|
||||||
|
if (subscription == null) return null;
|
||||||
|
return _subscriptionConverter.toModel(subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Проверить, есть ли у пользователя активная подписка
|
||||||
|
Future<bool> hasActiveSubscription(String userId) async {
|
||||||
|
return await _db.subscriptionDao.hasActiveSubscription(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Создать подписку пользователя
|
||||||
|
Future<String> createSubscription(
|
||||||
|
UserSubscriptionsCompanion companion,
|
||||||
|
) async {
|
||||||
|
return await _db.subscriptionDao.createUserSubscription(companion);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Обновить подписку пользователя
|
||||||
|
Future<bool> updateSubscription(UserSubscription subscription) async {
|
||||||
|
return await _db.subscriptionDao.updateUserSubscription(subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Отменить подписку пользователя
|
||||||
|
Future<void> cancelSubscription(String userId) async {
|
||||||
|
await _db.subscriptionDao.cancelUserSubscription(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить всех пользователей с активными подписками
|
||||||
|
Future<List<UserSubscriptionModel>> getActiveSubscriptions() async {
|
||||||
|
final subscriptions = await _db.subscriptionDao.getActiveSubscriptions();
|
||||||
|
return subscriptions.map((s) => _subscriptionConverter.toModel(s)).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
127
mnemo_cards_backend/lib/repository/task_repository.dart
Normal file
127
mnemo_cards_backend/lib/repository/task_repository.dart
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
import 'package:injectable/injectable.dart';
|
||||||
|
import 'package:mnemo_cards_backend/database/database.dart';
|
||||||
|
|
||||||
|
/// Repository для работы с задачами
|
||||||
|
/// Работает с Drift моделями (Task, UserTask), использует DAO для доступа к БД
|
||||||
|
@lazySingleton
|
||||||
|
class TaskRepository {
|
||||||
|
final AppDatabase _db;
|
||||||
|
|
||||||
|
TaskRepository(this._db);
|
||||||
|
|
||||||
|
// ==================== Tasks ====================
|
||||||
|
|
||||||
|
/// Получить задачу по ID
|
||||||
|
Future<Task?> getTaskById(String id) async {
|
||||||
|
return await _db.taskDao.getTaskById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить все задачи
|
||||||
|
Future<List<Task>> getAllTasks() async {
|
||||||
|
return await _db.taskDao.getAllTasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Создать задачу
|
||||||
|
Future<String> createTask(TasksCompanion task) async {
|
||||||
|
return await _db.taskDao.createTask(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Обновить задачу
|
||||||
|
Future<bool> updateTask(Task task) async {
|
||||||
|
return await _db.taskDao.updateTask(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Обновить время последнего выполнения
|
||||||
|
Future<void> updateLastExecution(String taskId) async {
|
||||||
|
await _db.taskDao.updateLastExecution(taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== UserTasks ====================
|
||||||
|
|
||||||
|
/// Получить задачу пользователя по ID
|
||||||
|
Future<UserTask?> getUserTaskById(String id) async {
|
||||||
|
return await _db.taskDao.getUserTaskById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить все задачи пользователей
|
||||||
|
Future<List<UserTask>> getAllUserTasks() async {
|
||||||
|
return await _db.taskDao.getAllUserTasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить задачи пользователя
|
||||||
|
Future<List<UserTask>> getUserTasks(
|
||||||
|
String userId, {
|
||||||
|
String? status,
|
||||||
|
bool activeOnly = false,
|
||||||
|
}) async {
|
||||||
|
return await _db.taskDao.getUserTasks(
|
||||||
|
userId,
|
||||||
|
status: status,
|
||||||
|
activeOnly: activeOnly,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Создать задачу пользователя
|
||||||
|
Future<String> createUserTask(UserTasksCompanion task) async {
|
||||||
|
return await _db.taskDao.createUserTask(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Создать задачу пользователя или игнорировать, если уже существует
|
||||||
|
Future<bool> createUserTaskOrIgnore(UserTasksCompanion task) async {
|
||||||
|
return await _db.taskDao.createUserTaskOrIgnore(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Обновить задачу пользователя
|
||||||
|
Future<bool> updateUserTask(UserTask task) async {
|
||||||
|
return await _db.taskDao.updateUserTask(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Завершить задачу пользователя
|
||||||
|
Future<void> completeUserTask(String taskId) async {
|
||||||
|
await _db.taskDao.completeUserTask(taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Подсчитать задачи пользователя
|
||||||
|
Future<int> countUserTasks(String userId, {String? status}) async {
|
||||||
|
return await _db.taskDao.countUserTasks(userId, status: status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Удалить задачу пользователя
|
||||||
|
Future<bool> deleteUserTask(String taskId) async {
|
||||||
|
return await _db.taskDao.deleteUserTask(taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== UserTaskProgresses ====================
|
||||||
|
|
||||||
|
/// Получить прогресс задачи пользователя
|
||||||
|
Future<UserTaskProgressesData?> getTaskProgress(
|
||||||
|
String userId,
|
||||||
|
String taskId,
|
||||||
|
) async {
|
||||||
|
return await _db.taskDao.getTaskProgress(userId, taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Создать прогресс задачи
|
||||||
|
Future<String> createTaskProgress(
|
||||||
|
UserTaskProgressesCompanion progress,
|
||||||
|
) async {
|
||||||
|
return await _db.taskDao.createTaskProgress(progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Обновить прогресс задачи
|
||||||
|
Future<bool> updateTaskProgress(UserTaskProgressesData progress) async {
|
||||||
|
return await _db.taskDao.updateTaskProgress(progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== UserTaskResults ====================
|
||||||
|
|
||||||
|
/// Получить результаты задач пользователя
|
||||||
|
Future<List<UserTaskResult>> getTaskResults(String userId) async {
|
||||||
|
return await _db.taskDao.getTaskResults(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Создать результат задачи
|
||||||
|
Future<String> createTaskResult(UserTaskResultsCompanion result) async {
|
||||||
|
return await _db.taskDao.createTaskResult(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
122
mnemo_cards_backend/lib/repository/test_repository.dart
Normal file
122
mnemo_cards_backend/lib/repository/test_repository.dart
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
import 'package:injectable/injectable.dart';
|
||||||
|
import 'package:mnemo_cards_backend/database/database.dart';
|
||||||
|
|
||||||
|
/// Repository для работы с тестами
|
||||||
|
/// Работает с Drift моделями (Test, TestQuestion, TestStatistic), использует DAO для доступа к БД
|
||||||
|
@lazySingleton
|
||||||
|
class TestRepository {
|
||||||
|
final AppDatabase _db;
|
||||||
|
|
||||||
|
TestRepository(this._db);
|
||||||
|
|
||||||
|
// ==================== Tests ====================
|
||||||
|
|
||||||
|
/// Получить тест по ID
|
||||||
|
Future<Test?> getTestById(String id) async {
|
||||||
|
return await _db.testDao.getTestById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить все тесты
|
||||||
|
Future<List<Test>> getAllTests() async {
|
||||||
|
return await _db.testDao.getAllTests();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить тесты пака
|
||||||
|
Future<List<Test>> getTestsByPackId(String packId) async {
|
||||||
|
return await _db.testDao.getTestsByPackId(packId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Создать тест
|
||||||
|
Future<String> createTest(TestsCompanion test) async {
|
||||||
|
return await _db.testDao.createTest(test);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Обновить тест
|
||||||
|
Future<bool> updateTest(Test test) async {
|
||||||
|
return await _db.testDao.updateTest(test);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Удалить тест (soft delete)
|
||||||
|
Future<void> softDeleteTest(String testId) async {
|
||||||
|
await _db.testDao.softDeleteTest(testId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hard delete старые сгенерированные тесты, которые были soft-deleted
|
||||||
|
Future<int> hardDeleteOldSoftDeletedGeneratedTests({
|
||||||
|
required Duration olderThan,
|
||||||
|
}) async {
|
||||||
|
return await _db.testDao.hardDeleteOldSoftDeletedGeneratedTests(
|
||||||
|
olderThan: olderThan,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hard delete сгенерированные тесты, которые не связаны ни с одним паком
|
||||||
|
Future<int> hardDeleteOrphanGeneratedTests({
|
||||||
|
required Duration olderThan,
|
||||||
|
}) async {
|
||||||
|
return await _db.testDao.hardDeleteOrphanGeneratedTests(
|
||||||
|
olderThan: olderThan,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Связать тест с паком
|
||||||
|
Future<void> linkTestToPack(String testId, String packId) async {
|
||||||
|
await _db.testDao.linkTestToPack(testId, packId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Удалить связь теста с паком
|
||||||
|
Future<void> unlinkTestFromPack(String testId, String packId) async {
|
||||||
|
await _db.testDao.unlinkTestFromPack(testId, packId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить packId для теста
|
||||||
|
Future<String?> getPackIdForTest(String testId) async {
|
||||||
|
return await _db.testDao.getPackIdForTest(testId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить все packId для теста
|
||||||
|
Future<List<String>> getPackIdsForTest(String testId) async {
|
||||||
|
return await _db.testDao.getPackIdsForTest(testId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== TestQuestions ====================
|
||||||
|
|
||||||
|
/// Получить вопросы теста
|
||||||
|
Future<List<TestQuestion>> getTestQuestions(String testId) async {
|
||||||
|
return await _db.testDao.getTestQuestions(testId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Создать вопрос теста
|
||||||
|
Future<String> createTestQuestion(TestQuestionsCompanion question) async {
|
||||||
|
return await _db.testDao.createTestQuestion(question);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Обновить вопрос теста
|
||||||
|
Future<bool> updateTestQuestion(TestQuestion question) async {
|
||||||
|
return await _db.testDao.updateTestQuestion(question);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Удалить вопрос теста (soft delete)
|
||||||
|
Future<void> softDeleteTestQuestion(String questionId) async {
|
||||||
|
await _db.testDao.softDeleteTestQuestion(questionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== TestStatistics ====================
|
||||||
|
|
||||||
|
/// Получить статистику теста пользователя
|
||||||
|
Future<TestStatistic?> getTestStatistics(String userId, String testId) async {
|
||||||
|
return await _db.testDao.getTestStatistics(userId, testId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Создать статистику теста
|
||||||
|
Future<String> createTestStatistics(
|
||||||
|
TestStatisticsCompanion statistics,
|
||||||
|
) async {
|
||||||
|
return await _db.testDao.createTestStatistics(statistics);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Обновить статистику теста
|
||||||
|
Future<bool> updateTestStatistics(TestStatistic statistics) async {
|
||||||
|
return await _db.testDao.updateTestStatistics(statistics);
|
||||||
|
}
|
||||||
|
}
|
||||||
217
mnemo_cards_backend/lib/repository/user_repository.dart
Normal file
217
mnemo_cards_backend/lib/repository/user_repository.dart
Normal file
|
|
@ -0,0 +1,217 @@
|
||||||
|
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/repository/сonverters/export.dart';
|
||||||
|
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
|
||||||
|
|
||||||
|
/// Repository для работы с пользователями
|
||||||
|
/// Работает с доменными моделями (UserModel), использует DAO для доступа к БД
|
||||||
|
@lazySingleton
|
||||||
|
class UserRepository {
|
||||||
|
final AppDatabase _db;
|
||||||
|
final UserConverter _userConverter;
|
||||||
|
final CardPackConverter _cardPackConverter;
|
||||||
|
final SubscriptionConverter _subscriptionConverter;
|
||||||
|
|
||||||
|
UserRepository(
|
||||||
|
this._db,
|
||||||
|
this._userConverter,
|
||||||
|
this._cardPackConverter,
|
||||||
|
this._subscriptionConverter,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Получить пользователя по ID
|
||||||
|
/// [includePacks] - загружать ли связанные паки пользователя
|
||||||
|
/// [withData] - загружать ли UserData пользователя
|
||||||
|
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 _userConverter.toModel(user, packs);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _userConverter.toModel(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<(UserModel, UserData?)?> getUserWithDataById(
|
||||||
|
String id, {
|
||||||
|
bool includePacks = false,
|
||||||
|
}) async {
|
||||||
|
final userWithData = await _db.userDao.getUserWithDataById(id);
|
||||||
|
if (userWithData == null) return null;
|
||||||
|
|
||||||
|
if (includePacks) {
|
||||||
|
final packs = await _db.userDao.getUserPacks(id);
|
||||||
|
final userModel = await _userConverter.toModel(userWithData.user, packs);
|
||||||
|
return (userModel, userWithData.userData);
|
||||||
|
}
|
||||||
|
|
||||||
|
final userModel = await _userConverter.toModel(userWithData.user);
|
||||||
|
return (userModel, userWithData.userData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить пользователя по externalUserId
|
||||||
|
Future<UserModel?> getUserByExternalId(String externalId) async {
|
||||||
|
final user = await _db.userDao.getUserByExternalId(externalId);
|
||||||
|
|
||||||
|
if (user == null) return null;
|
||||||
|
|
||||||
|
return await _userConverter.toModel(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Создать пользователя с UserData
|
||||||
|
Future<String> createUserWithData({
|
||||||
|
required UserModel userModel,
|
||||||
|
required UserDatasCompanion userData,
|
||||||
|
}) async {
|
||||||
|
final userCompanion = _userConverter.toCompanion(userModel);
|
||||||
|
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(
|
||||||
|
_userConverter
|
||||||
|
.toCompanion(userModel)
|
||||||
|
.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 _userConverter.toModel(user, packs);
|
||||||
|
userModels.add(userModel);
|
||||||
|
}
|
||||||
|
return userModels;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await Future.wait(users.map((u) => _userConverter.toModel(u)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Подсчитать пользователей
|
||||||
|
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) => _cardPackConverter.toModel(p)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить UserData пользователя
|
||||||
|
Future<UserData?> getUserData(String userId) async {
|
||||||
|
return await _db.userDao.getUserData(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить UserData пользователя по ID (алиас для getUserData)
|
||||||
|
Future<UserData?> getUserDataById(String userId) async {
|
||||||
|
return await getUserData(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Создать UserData
|
||||||
|
Future<void> createUserData(UserDatasCompanion userData) async {
|
||||||
|
await _db.userDao.createUserData(userData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить активную подписку пользователя
|
||||||
|
Future<UserSubscriptionModel?> getUserSubscription(String userId) async {
|
||||||
|
final subscription = await _db.subscriptionDao.getActiveSubscription(
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
if (subscription == null) return null;
|
||||||
|
return _subscriptionConverter.toModel(subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Обновить UserData частично
|
||||||
|
Future<void> updateUserDataPartial(UserDatasCompanion updates) async {
|
||||||
|
await _db.userDao.updateUserDataPartial(updates);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Tokens ====================
|
||||||
|
|
||||||
|
/// Получить токен по значению
|
||||||
|
Future<Token?> getTokenByValue(String tokenValue) async {
|
||||||
|
return await _db.userDao.getTokenByValue(tokenValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить токен пользователя
|
||||||
|
Future<Token?> getTokenByUserId(String userId) async {
|
||||||
|
return await _db.userDao.getTokenByUserId(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Создать токен
|
||||||
|
Future<String> createToken(TokensCompanion token) async {
|
||||||
|
return await _db.userDao.createToken(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Удалить токен (hard delete)
|
||||||
|
Future<void> deleteToken(String token) async {
|
||||||
|
await _db.userDao.deleteToken(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Удалить токен (soft delete)
|
||||||
|
Future<int> softDeleteToken(String tokenId) async {
|
||||||
|
return await _db.userDao.softDeleteToken(tokenId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== User Packs ====================
|
||||||
|
|
||||||
|
/// Проверить, есть ли у пользователя доступ к паку
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
import 'package:injectable/injectable.dart';
|
||||||
|
import 'package:mnemo_cards_backend/database/database.dart';
|
||||||
|
|
||||||
|
/// Repository для работы со статистикой слов
|
||||||
|
/// Работает с Drift моделями (WordStatistic), использует DAO для доступа к БД
|
||||||
|
@lazySingleton
|
||||||
|
class WordStatisticsRepository {
|
||||||
|
final AppDatabase _db;
|
||||||
|
|
||||||
|
WordStatisticsRepository(this._db);
|
||||||
|
|
||||||
|
/// Получить статистику для пользователя и карточки
|
||||||
|
Future<WordStatistic?> getByUserAndCard(String userId, String cardId) async {
|
||||||
|
return await _db.wordStatisticsDao.getByUserAndCard(userId, cardId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Создать новую запись статистики
|
||||||
|
Future<WordStatistic> create({
|
||||||
|
required String userId,
|
||||||
|
required String cardId,
|
||||||
|
required int correctAnswers,
|
||||||
|
required int incorrectAnswers,
|
||||||
|
}) async {
|
||||||
|
return await _db.wordStatisticsDao.create(
|
||||||
|
userId: userId,
|
||||||
|
cardId: cardId,
|
||||||
|
correctAnswers: correctAnswers,
|
||||||
|
incorrectAnswers: incorrectAnswers,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Обновить статистику
|
||||||
|
Future<void> updateStatistics({
|
||||||
|
required String id,
|
||||||
|
required int correctAnswers,
|
||||||
|
required int incorrectAnswers,
|
||||||
|
required DateTime lastReviewed,
|
||||||
|
}) async {
|
||||||
|
await _db.wordStatisticsDao.updateStatistics(
|
||||||
|
id: id,
|
||||||
|
correctAnswers: correctAnswers,
|
||||||
|
incorrectAnswers: incorrectAnswers,
|
||||||
|
lastReviewed: lastReviewed,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить статистику по всем карточкам пака для пользователя
|
||||||
|
Future<List<WordStatistic>> getPackStatistics(
|
||||||
|
String userId,
|
||||||
|
String packId,
|
||||||
|
) async {
|
||||||
|
return await _db.wordStatisticsDao.getPackStatistics(userId, packId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить все статистики пользователя
|
||||||
|
Future<List<WordStatistic>> getUserStatistics(String userId) async {
|
||||||
|
return await _db.wordStatisticsDao.getUserStatistics(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import 'package:injectable/injectable.dart';
|
||||||
|
import 'package:mnemo_cards_backend/database/database.dart';
|
||||||
|
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
|
||||||
|
|
||||||
|
/// Конвертер для преобразования CardPack (Drift) в CardPackModel
|
||||||
|
@lazySingleton
|
||||||
|
class CardPackConverter {
|
||||||
|
/// Конвертировать CardPack в CardPackModel
|
||||||
|
Future<CardPackModel> toModel(CardPack pack) async {
|
||||||
|
return CardPackModel(
|
||||||
|
id: pack.id,
|
||||||
|
title: pack.title,
|
||||||
|
subtitle: pack.subtitle,
|
||||||
|
size: pack.size,
|
||||||
|
color: pack.color,
|
||||||
|
version: pack.version,
|
||||||
|
cover: pack.cover,
|
||||||
|
description: pack.description,
|
||||||
|
googlePlayId: pack.googlePlayId,
|
||||||
|
rustoreId: pack.rustoreId,
|
||||||
|
appStoreId: pack.appStoreId,
|
||||||
|
price: pack.price,
|
||||||
|
currency: pack.currency,
|
||||||
|
enabled: pack.enabled,
|
||||||
|
order: pack.order,
|
||||||
|
cardsOrder: pack.cardsOrder,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
export 'card_pack_converter.dart';
|
||||||
|
export 'game_card_converter.dart';
|
||||||
|
export 'subscription_converter.dart';
|
||||||
|
export 'user_converter.dart';
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import 'package:injectable/injectable.dart';
|
||||||
|
import 'package:mnemo_cards_backend/database/database.dart';
|
||||||
|
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
|
||||||
|
|
||||||
|
/// Конвертер для преобразования GameCard (Drift) в GameCardModel
|
||||||
|
@lazySingleton
|
||||||
|
class GameCardConverter {
|
||||||
|
/// Конвертировать GameCard в GameCardModel
|
||||||
|
GameCardModel toModel(GameCard card) {
|
||||||
|
return GameCardModel(
|
||||||
|
id: card.id,
|
||||||
|
image: card.image,
|
||||||
|
mnemo: card.mnemo ?? '',
|
||||||
|
original: card.original,
|
||||||
|
translation: card.translation,
|
||||||
|
transcription: card.transcription,
|
||||||
|
transcriptionMnemo: card.transcriptionMnemo,
|
||||||
|
imageBack: card.imageBack,
|
||||||
|
back: card.back,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
import 'package:injectable/injectable.dart';
|
||||||
|
import 'package:mnemo_cards_backend/database/database.dart';
|
||||||
|
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||||
|
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
|
||||||
|
|
||||||
|
/// Конвертер для преобразования UserSubscription (Drift) в UserSubscriptionModel
|
||||||
|
@lazySingleton
|
||||||
|
class SubscriptionConverter {
|
||||||
|
/// Конвертировать UserSubscription в UserSubscriptionModel
|
||||||
|
UserSubscriptionModel toModel(UserSubscription subscription) {
|
||||||
|
return UserSubscriptionModel(
|
||||||
|
id: subscription.id,
|
||||||
|
start: subscription.start.toDateTime(),
|
||||||
|
finish: subscription.finish.toDateTime(),
|
||||||
|
features: subscription.features
|
||||||
|
.map(
|
||||||
|
(f) => SubscriptionFeatureEnum.values.firstWhere(
|
||||||
|
(e) => e.name == f,
|
||||||
|
orElse: () => SubscriptionFeatureEnum.unknown,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Конвертировать SubscriptionPlan в SubscriptionPlanModel
|
||||||
|
SubscriptionPlanModel planToModel(SubscriptionPlan plan) {
|
||||||
|
final uiMap = plan.ui;
|
||||||
|
final ui = uiMap != null ? SubscriptionPlanUI.fromJson(uiMap) : null;
|
||||||
|
|
||||||
|
return SubscriptionPlanModel(
|
||||||
|
id: plan.id,
|
||||||
|
ui: ui,
|
||||||
|
price: plan.price,
|
||||||
|
currency: plan.currency,
|
||||||
|
durationDays: plan.durationDays,
|
||||||
|
features: [],
|
||||||
|
paymentSystem: PaymentSystem.values.firstWhere(
|
||||||
|
(ps) => ps.name == plan.paymentSystem,
|
||||||
|
orElse: () => PaymentSystem.unknown,
|
||||||
|
),
|
||||||
|
paymentId: plan.paymentId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
import 'package:drift/drift.dart' as drift;
|
||||||
|
import 'package:injectable/injectable.dart';
|
||||||
|
import 'package:mnemo_cards_backend/database/daos/user_dao.dart';
|
||||||
|
import 'package:mnemo_cards_backend/database/database.dart';
|
||||||
|
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
|
||||||
|
|
||||||
|
import 'card_pack_converter.dart';
|
||||||
|
|
||||||
|
/// Конвертер для преобразования User (Drift) в UserModel
|
||||||
|
@lazySingleton
|
||||||
|
class UserConverter {
|
||||||
|
final CardPackConverter _cardPackConverter;
|
||||||
|
|
||||||
|
UserConverter(this._cardPackConverter);
|
||||||
|
|
||||||
|
/// Конвертировать User в UserModel
|
||||||
|
/// [packs] - опциональный список связанных паков пользователя
|
||||||
|
Future<UserModel> toModel(User user, [List<CardPack>? packs]) async {
|
||||||
|
final packModels = packs != null
|
||||||
|
? await Future.wait(packs.map((p) => _cardPackConverter.toModel(p)))
|
||||||
|
: <CardPackModel>[];
|
||||||
|
|
||||||
|
return UserModel(
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
telegram: user.telegram,
|
||||||
|
admin: user.admin,
|
||||||
|
purchases: user.purchases,
|
||||||
|
userSettings: user.userSettings,
|
||||||
|
packs: packModels,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Конвертировать UserModel в UsersCompanion
|
||||||
|
UsersCompanion toCompanion(UserModel userModel) {
|
||||||
|
return UsersCompanion(
|
||||||
|
id: userModel.id != null
|
||||||
|
? drift.Value(userModel.id!)
|
||||||
|
: const drift.Value.absent(),
|
||||||
|
externalUserId: const drift.Value.absent(),
|
||||||
|
name: drift.Value(userModel.name),
|
||||||
|
email: drift.Value(userModel.email),
|
||||||
|
telegram: drift.Value(userModel.telegram),
|
||||||
|
admin: drift.Value(userModel.admin),
|
||||||
|
purchases: drift.Value(userModel.purchases),
|
||||||
|
userSettings: drift.Value(userModel.userSettings),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Конвертер для преобразования UserWithData в UserDataModel
|
||||||
|
@lazySingleton
|
||||||
|
class UserWithDataConverter {
|
||||||
|
/// Конвертировать UserWithData в UserDataModel
|
||||||
|
Future<UserDataModel> toUserDataModel(UserWithData userWithData) async {
|
||||||
|
if (userWithData.userData == null) {
|
||||||
|
return UserDataModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
return UserDataModel(
|
||||||
|
id: userWithData.userData!.id,
|
||||||
|
lastTestSessionToken: userWithData.userData!.lastTestSessionToken,
|
||||||
|
lastTimeOnline: userWithData.userData!.lastTimeOnline?.toDateTime(),
|
||||||
|
tags: userWithData.userData!.tags,
|
||||||
|
totalStudyTimeMinutes: userWithData.userData!.totalStudyTimeMinutes,
|
||||||
|
currentStreak: userWithData.userData!.currentStreak,
|
||||||
|
longestStreak: userWithData.userData!.longestStreak,
|
||||||
|
// words, packProgress, studyDates, categoryMinutes, achievements
|
||||||
|
// are loaded separately from database when needed
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue