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

This commit is contained in:
Dmitry 2026-01-09 21:02:12 +03:00
parent 336bafc600
commit 99100deb20
20 changed files with 2219 additions and 0 deletions

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

View 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;
}

View file

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

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

View 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';

View 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}';
}
}

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

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

View file

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

View file

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

View file

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

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

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

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

View file

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

View file

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

View file

@ -0,0 +1,4 @@
export 'card_pack_converter.dart';
export 'game_card_converter.dart';
export 'subscription_converter.dart';
export 'user_converter.dart';

View file

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

View file

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

View file

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