+
Total Users
@@ -117,6 +123,19 @@ export default function DashboardPage() {
+
+
+
+ Active Tasks
+
+
+
+ {tasksStats?.active || 0}
+
+ of {tasksStats?.total || 0} total tasks
+
+
+
{/* Charts */}
diff --git a/mnemo_cards_admin/src/pages/TasksPage.tsx b/mnemo_cards_admin/src/pages/TasksPage.tsx
new file mode 100644
index 0000000..62bd8d9
--- /dev/null
+++ b/mnemo_cards_admin/src/pages/TasksPage.tsx
@@ -0,0 +1,519 @@
+import { useState } from 'react'
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
+import { toast } from 'sonner'
+import { tasksApi } from '@/api/tasks'
+import type { TaskDto, TaskFilters, PaginatedResponse } from '@/types/models'
+import type { AxiosError } from 'axios'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/table'
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog'
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+} from '@/components/ui/alert-dialog'
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select'
+import { TaskStatusBadge } from '@/components/tasks/TaskStatusBadge'
+import { TaskTypeBadge } from '@/components/tasks/TaskTypeBadge'
+import { TaskDifficultyBadge } from '@/components/tasks/TaskDifficultyBadge'
+import { TaskDetailsDialog } from '@/components/tasks/TaskDetailsDialog'
+import { TaskForm } from '@/components/tasks/TaskForm'
+import { Search, Plus, Edit, Trash2, Eye, ChevronLeft, ChevronRight } from 'lucide-react'
+
+export default function TasksPage() {
+ const queryClient = useQueryClient()
+ const [page, setPage] = useState(1)
+ const [search, setSearch] = useState('')
+ const [filters, setFilters] = useState
({})
+ const [selectedTask, setSelectedTask] = useState(null)
+ const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
+ const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
+ const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false)
+ const [isViewDialogOpen, setIsViewDialogOpen] = useState(false)
+ const [taskToDelete, setTaskToDelete] = useState(null)
+
+ const limit = 20
+
+ // Fetch tasks with filters
+ const { data, isLoading, error } = useQuery>({
+ queryKey: ['tasks', page, search, filters],
+ queryFn: () =>
+ tasksApi.getTasks({
+ page,
+ limit,
+ search: search || undefined,
+ ...filters,
+ }),
+ })
+
+ // Mutations
+ const createMutation = useMutation({
+ mutationFn: (taskData: Parameters[0]) =>
+ tasksApi.createTask(taskData),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['tasks'] })
+ toast.success('Задача успешно создана')
+ setIsCreateDialogOpen(false)
+ },
+ onError: (error: unknown) => {
+ const axiosError = error as AxiosError<{ message?: string }>
+ toast.error(
+ axiosError.response?.data?.message || 'Не удалось создать задачу'
+ )
+ },
+ })
+
+ const updateMutation = useMutation({
+ mutationFn: ({
+ taskId,
+ data,
+ }: {
+ taskId: string
+ data: Parameters[1]
+ }) => tasksApi.updateTask(taskId, data),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['tasks'] })
+ toast.success('Задача успешно обновлена')
+ setIsEditDialogOpen(false)
+ setSelectedTask(null)
+ },
+ onError: (error: unknown) => {
+ const axiosError = error as AxiosError<{ message?: string }>
+ toast.error(
+ axiosError.response?.data?.message || 'Не удалось обновить задачу'
+ )
+ },
+ })
+
+ const deleteMutation = useMutation({
+ mutationFn: (taskId: string) => tasksApi.deleteTask(taskId),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['tasks'] })
+ toast.success('Задача успешно удалена')
+ setIsDeleteDialogOpen(false)
+ setTaskToDelete(null)
+ },
+ onError: (error: unknown) => {
+ const axiosError = error as AxiosError<{ message?: string }>
+ toast.error(
+ axiosError.response?.data?.message || 'Не удалось удалить задачу'
+ )
+ },
+ })
+
+ const handleCreate = async (
+ taskData: Parameters[0]
+ ) => {
+ await createMutation.mutateAsync(taskData)
+ }
+
+ const handleEdit = (task: TaskDto) => {
+ setSelectedTask(task)
+ setIsEditDialogOpen(true)
+ }
+
+ const handleUpdate = async (
+ taskData: Parameters[1]
+ ) => {
+ if (!selectedTask?.id) {
+ toast.error('ID задачи не найден')
+ return
+ }
+ await updateMutation.mutateAsync({ taskId: selectedTask.id, data: taskData })
+ }
+
+ const handleDelete = (task: TaskDto) => {
+ setTaskToDelete(task)
+ setIsDeleteDialogOpen(true)
+ }
+
+ const confirmDelete = () => {
+ if (taskToDelete?.id) {
+ deleteMutation.mutate(taskToDelete.id)
+ }
+ }
+
+ const handleView = (task: TaskDto) => {
+ setSelectedTask(task)
+ setIsViewDialogOpen(true)
+ }
+
+ const handleFilterChange = (
+ key: K,
+ value: TaskFilters[K] | undefined
+ ) => {
+ setFilters((prev) => {
+ const newFilters = { ...prev }
+ if (value === undefined || value === '') {
+ delete newFilters[key]
+ } else {
+ newFilters[key] = value
+ }
+ // Reset to first page when filters change
+ setPage(1)
+ return newFilters
+ })
+ }
+
+ const handleSearch = (value: string) => {
+ setSearch(value)
+ setPage(1) // Reset to first page when search changes
+ }
+
+ if (error) {
+ return (
+
+
+
Управление задачами
+
Ошибка загрузки задач
+
+
+
+
+ Не удалось загрузить задачи. Попробуйте обновить страницу.
+
+
+
+
+ )
+ }
+
+ return (
+
+
+
+
Управление задачами
+
+ Просмотр и управление задачами пользователей
+
+
+
+
+
+ {/* Search and Filters */}
+
+
+ Поиск и фильтры
+
+
+ {/* Search */}
+
+
+ handleSearch(e.target.value)}
+ className="max-w-sm"
+ />
+
+
+ {/* Filters */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Clear filters button */}
+ {(filters.type || filters.difficulty || filters.status) && (
+
+ )}
+
+
+
+ {/* Tasks Table */}
+
+
+ Все задачи ({data?.total || 0})
+ Управление задачами пользователей
+
+
+ {isLoading ? (
+ Загрузка задач...
+ ) : (
+ <>
+
+
+
+ ID
+ Название
+ Тип
+ Сложность
+ Статус
+ Срок действия
+ Действия
+
+
+
+ {data?.items && data.items.length > 0 ? (
+ data.items.map((task) => (
+
+
+ {task.id.slice(0, 8)}...
+
+ {task.title}
+
+
+
+
+
+
+
+
+
+
+ {new Date(task.expiresAt).toLocaleDateString('ru-RU', {
+ day: '2-digit',
+ month: '2-digit',
+ year: 'numeric',
+ })}
+
+
+
+
+
+
+
+
+
+ ))
+ ) : (
+
+
+ Задачи не найдены
+
+
+ )}
+
+
+
+ {/* Pagination */}
+ {data && data.totalPages > 1 && (
+
+
+ Показано {((page - 1) * limit) + 1} до {Math.min(page * limit, data.total)} из{' '}
+ {data.total} задач
+
+
+
+
+ Страница {page} из {data.totalPages}
+
+
+
+
+ )}
+ >
+ )}
+
+
+
+ {/* Create Task Dialog */}
+
+
+ {/* Edit Task Dialog */}
+
+
+ {/* View Task Dialog */}
+ {selectedTask && (
+
{
+ setIsViewDialogOpen(false)
+ setSelectedTask(null)
+ }}
+ />
+ )}
+
+ {/* Delete Confirmation Dialog */}
+
+
+
+ Удалить задачу
+
+ Вы уверены, что хотите удалить задачу "{taskToDelete?.title || taskToDelete?.id}"?
+ Это действие нельзя отменить.
+
+
+
+ Отмена
+
+ {deleteMutation.isPending ? 'Удаление...' : 'Удалить'}
+
+
+
+
+
+ )
+}
+
diff --git a/mnemo_cards_admin/src/types/models.ts b/mnemo_cards_admin/src/types/models.ts
index 6d6cc7b..b0249f4 100644
--- a/mnemo_cards_admin/src/types/models.ts
+++ b/mnemo_cards_admin/src/types/models.ts
@@ -196,3 +196,55 @@ export interface TestDto {
statistics?: unknown
packs?: TestPackInfo[]
}
+
+// Task types
+export interface TaskRewardDto {
+ type: 'xp' | 'coins' | 'achievement'
+ amount: number
+ achievementId?: string
+}
+
+export interface TaskDto {
+ id: string
+ title: string
+ description: string
+ type: 'app_internal' | 'external' | 'social'
+ difficulty: 'easy' | 'medium' | 'hard'
+ status: 'available' | 'in_progress' | 'completed' | 'expired' | 'failed'
+ rewards: TaskRewardDto[]
+ createdAt: string
+ expiresAt: string
+ completedAt?: string
+ proofUrl?: string
+ instructions?: string
+ tags: string[]
+ imageUrl?: string
+ userId?: string
+}
+
+export interface CreateTaskDto {
+ title: string
+ description: string
+ type: 'app_internal' | 'external' | 'social'
+ difficulty: 'easy' | 'medium' | 'hard'
+ rewards: TaskRewardDto[]
+ expiresAt: string
+ instructions?: string
+ tags?: string[]
+ imageUrl?: string
+ userId?: string
+}
+
+export interface UpdateTaskDto extends Partial {
+ status?: TaskDto['status']
+ completedAt?: string
+ proofUrl?: string
+}
+
+export interface TaskFilters {
+ type?: TaskDto['type']
+ difficulty?: TaskDto['difficulty']
+ status?: TaskDto['status']
+ userId?: string
+ search?: string
+}
diff --git a/mnemo_cards_backend/lib/api/di/injector.config.dart b/mnemo_cards_backend/lib/api/di/injector.config.dart
index 960b05b..0957ba0 100644
--- a/mnemo_cards_backend/lib/api/di/injector.config.dart
+++ b/mnemo_cards_backend/lib/api/di/injector.config.dart
@@ -19,10 +19,26 @@ import '../../discounts/discounts_manager.dart' as _i891;
import '../../packs/free_packs_distributor.dart' as _i1062;
import '../../packs/pack_dto_converter.dart' as _i433;
import '../../packs/pack_manager.dart' as _i833;
-import '../../packs/pack_repository.dart' as _i258;
import '../../packs/product_availability_manager.dart' as _i797;
import '../../packs/products_price_resolver.dart' as _i908;
import '../../promo_codes/promo_codes_manager.dart' as _i151;
+import '../../repository/%D1%81onverters/card_pack_converter.dart' as _i103;
+import '../../repository/%D1%81onverters/export.dart' as _i381;
+import '../../repository/%D1%81onverters/game_card_converter.dart' as _i650;
+import '../../repository/%D1%81onverters/subscription_converter.dart' as _i768;
+import '../../repository/%D1%81onverters/user_converter.dart' as _i491;
+import '../../repository/achievement_repository.dart' as _i403;
+import '../../repository/discount_repository.dart' as _i864;
+import '../../repository/export.dart' as _i69;
+import '../../repository/pack_repository.dart' as _i851;
+import '../../repository/payment_repository.dart' as _i32;
+import '../../repository/promo_code_repository.dart' as _i1060;
+import '../../repository/statistics_repository.dart' as _i499;
+import '../../repository/subscription_repository.dart' as _i1018;
+import '../../repository/task_repository.dart' as _i499;
+import '../../repository/test_repository.dart' as _i223;
+import '../../repository/user_repository.dart' as _i384;
+import '../../repository/word_statistics_repository.dart' as _i226;
import '../../statistics/achievement_manager.dart' as _i802;
import '../../statistics/session_tracker.dart' as _i71;
import '../../statistics/statistics_calculator.dart' as _i1029;
@@ -33,7 +49,6 @@ import '../../tasks/task_manager.dart' as _i586;
import '../../tests/test_manager.dart' as _i259;
import '../../user/user_manager.dart' as _i280;
import '../../user/user_manager_drift.dart' as _i560;
-import '../../user/user_repository.dart' as _i950;
import '../ads/ads_manager.dart' as _i846;
import '../mnemo_shelf.dart' as _i561;
import '../purchase/payment_manager.dart' as _i1009;
@@ -45,6 +60,7 @@ import '../v2/admin_analytics_api_v2.dart' as _i368;
import '../v2/admin_auth_api_v2.dart' as _i483;
import '../v2/admin_cards_api_v2.dart' as _i922;
import '../v2/admin_packs_api_v2.dart' as _i1015;
+import '../v2/admin_tasks_api_v2.dart' as _i1071;
import '../v2/admin_tests_api_v2.dart' as _i116;
import '../v2/admin_users_api_v2.dart' as _i895;
import '../v2/auth_api_v2.dart' as _i52;
@@ -80,6 +96,14 @@ extension GetItInjectableX on _i174.GetIt {
gh.lazySingleton<_i240.TelegramAuthCodeService>(
() => _i240.TelegramAuthCodeService(),
);
+ gh.lazySingleton<_i103.CardPackConverter>(() => _i103.CardPackConverter());
+ gh.lazySingleton<_i650.GameCardConverter>(() => _i650.GameCardConverter());
+ gh.lazySingleton<_i768.SubscriptionConverter>(
+ () => _i768.SubscriptionConverter(),
+ );
+ gh.lazySingleton<_i491.UserWithDataConverter>(
+ () => _i491.UserWithDataConverter(),
+ );
gh.lazySingleton<_i747.MinioService>(
() => _i747.MinioService(gh<_i533.MinioConfig>()),
);
@@ -95,132 +119,213 @@ extension GetItInjectableX on _i174.GetIt {
gh<_i747.MinioService>(),
),
);
- gh.lazySingleton<_i259.TestManager>(
- () =>
- _i259.TestManager(gh<_i1072.AppDatabase>(), gh<_i747.MinioService>()),
- );
- gh.lazySingleton<_i377.SubscriptionManager>(
- () => _i377.SubscriptionManager(gh<_i1072.AppDatabase>()),
- );
gh.lazySingleton<_i108.JwtService>(
() => _i108.JwtService(gh<_i1072.AppDatabase>()),
);
- gh.lazySingleton<_i258.PackRepository>(
- () => _i258.PackRepository(gh<_i1072.AppDatabase>()),
+ gh.lazySingleton<_i403.AchievementRepository>(
+ () => _i403.AchievementRepository(gh<_i1072.AppDatabase>()),
);
- gh.lazySingleton<_i71.SessionTracker>(
- () => _i71.SessionTracker(gh<_i1072.AppDatabase>()),
+ gh.lazySingleton<_i864.DiscountRepository>(
+ () => _i864.DiscountRepository(gh<_i1072.AppDatabase>()),
);
- gh.lazySingleton<_i909.WordStatisticsManager>(
- () => _i909.WordStatisticsManager(gh<_i1072.AppDatabase>()),
+ gh.lazySingleton<_i32.PaymentRepository>(
+ () => _i32.PaymentRepository(gh<_i1072.AppDatabase>()),
+ );
+ gh.lazySingleton<_i1060.PromoCodeRepository>(
+ () => _i1060.PromoCodeRepository(gh<_i1072.AppDatabase>()),
+ );
+ gh.lazySingleton<_i499.StatisticsRepository>(
+ () => _i499.StatisticsRepository(gh<_i1072.AppDatabase>()),
+ );
+ gh.lazySingleton<_i499.TaskRepository>(
+ () => _i499.TaskRepository(gh<_i1072.AppDatabase>()),
+ );
+ gh.lazySingleton<_i223.TestRepository>(
+ () => _i223.TestRepository(gh<_i1072.AppDatabase>()),
+ );
+ gh.lazySingleton<_i226.WordStatisticsRepository>(
+ () => _i226.WordStatisticsRepository(gh<_i1072.AppDatabase>()),
);
gh.lazySingleton<_i586.TaskManager>(
- () => _i586.TaskManager(gh<_i1072.AppDatabase>()),
- );
- gh.lazySingleton<_i950.UserRepository>(
- () => _i950.UserRepository(gh<_i1072.AppDatabase>()),
- );
- gh.lazySingleton<_i964.SubscriptionsApiV2>(
- () => _i964.SubscriptionsApiV2(gh<_i377.SubscriptionManager>()),
- );
- gh.lazySingleton<_i296.TelegramBotApiV2>(
- () => _i296.TelegramBotApiV2(
+ () => _i586.TaskManager(
+ gh<_i69.TaskRepository>(),
gh<_i1072.AppDatabase>(),
- gh<_i950.UserRepository>(),
),
);
- gh.lazySingleton<_i891.DiscountsManager>(
- () => _i891.DiscountsManager(
+ gh.lazySingleton<_i71.SessionTracker>(
+ () => _i71.SessionTracker(gh<_i69.StatisticsRepository>()),
+ );
+ gh.lazySingleton<_i851.PackRepository>(
+ () => _i851.PackRepository(
gh<_i1072.AppDatabase>(),
- gh<_i950.UserRepository>(),
+ gh<_i381.CardPackConverter>(),
+ gh<_i381.GameCardConverter>(),
),
);
- gh.lazySingleton<_i802.AchievementManager>(
- () => _i802.AchievementManager(
+ gh.lazySingleton<_i1018.SubscriptionRepository>(
+ () => _i1018.SubscriptionRepository(
gh<_i1072.AppDatabase>(),
- gh<_i950.UserRepository>(),
- ),
- );
- gh.lazySingleton<_i1029.StatisticsCalculator>(
- () => _i1029.StatisticsCalculator(
- gh<_i1072.AppDatabase>(),
- gh<_i950.UserRepository>(),
- ),
- );
- gh.factory<_i895.AdminUsersApiV2>(
- () => _i895.AdminUsersApiV2(
- gh<_i1072.AppDatabase>(),
- gh<_i950.UserRepository>(),
+ gh<_i768.SubscriptionConverter>(),
),
);
gh.lazySingleton<_i365.MediaApiV2>(
() => _i365.MediaApiV2(gh<_i747.MinioService>()),
);
+ gh.factory<_i1071.AdminTasksApiV2>(
+ () => _i1071.AdminTasksApiV2(
+ gh<_i1072.AppDatabase>(),
+ gh<_i69.TaskRepository>(),
+ ),
+ );
gh.lazySingleton<_i985.TasksApiV2>(
() => _i985.TasksApiV2(gh<_i586.TaskManager>()),
);
+ gh.lazySingleton<_i491.UserConverter>(
+ () => _i491.UserConverter(gh<_i103.CardPackConverter>()),
+ );
+ gh.lazySingleton<_i909.WordStatisticsManager>(
+ () => _i909.WordStatisticsManager(gh<_i69.WordStatisticsRepository>()),
+ );
gh.factory<_i1015.AdminPacksApiV2>(
() => _i1015.AdminPacksApiV2(
gh<_i1072.AppDatabase>(),
- gh<_i258.PackRepository>(),
+ gh<_i69.PackRepository>(),
+ gh<_i69.TestRepository>(),
),
);
- gh.lazySingleton<_i368.AdminAnalyticsApiV2>(
- () => _i368.AdminAnalyticsApiV2(
- gh<_i950.UserRepository>(),
+ gh.lazySingleton<_i377.SubscriptionManager>(
+ () => _i377.SubscriptionManager(gh<_i69.SubscriptionRepository>()),
+ );
+ gh.lazySingleton<_i259.TestManager>(
+ () => _i259.TestManager(
gh<_i1072.AppDatabase>(),
+ gh<_i69.PackRepository>(),
+ gh<_i69.TestRepository>(),
+ gh<_i747.MinioService>(),
+ ),
+ );
+ gh.lazySingleton<_i384.UserRepository>(
+ () => _i384.UserRepository(
+ gh<_i1072.AppDatabase>(),
+ gh<_i381.UserConverter>(),
+ gh<_i381.CardPackConverter>(),
+ gh<_i381.SubscriptionConverter>(),
+ ),
+ );
+ gh.lazySingleton<_i891.DiscountsManager>(
+ () => _i891.DiscountsManager(
+ gh<_i69.DiscountRepository>(),
+ gh<_i69.UserRepository>(),
),
);
gh.lazySingleton<_i908.ProductsPriceResolver>(
() => _i908.ProductsPriceResolver(
gh<_i891.DiscountsManager>(),
- gh<_i950.UserRepository>(),
+ gh<_i69.UserRepository>(),
),
);
+ gh.lazySingleton<_i802.AchievementManager>(
+ () => _i802.AchievementManager(
+ gh<_i69.AchievementRepository>(),
+ gh<_i69.UserRepository>(),
+ ),
+ );
+ gh.lazySingleton<_i368.AdminAnalyticsApiV2>(
+ () => _i368.AdminAnalyticsApiV2(
+ gh<_i69.UserRepository>(),
+ gh<_i1072.AppDatabase>(),
+ ),
+ );
+ gh.lazySingleton<_i296.TelegramBotApiV2>(
+ () => _i296.TelegramBotApiV2(
+ gh<_i1072.AppDatabase>(),
+ gh<_i69.UserRepository>(),
+ ),
+ );
+ gh.factory<_i895.AdminUsersApiV2>(
+ () => _i895.AdminUsersApiV2(
+ gh<_i1072.AppDatabase>(),
+ gh<_i69.UserRepository>(),
+ ),
+ );
+ gh.lazySingleton<_i964.SubscriptionsApiV2>(
+ () => _i964.SubscriptionsApiV2(gh<_i377.SubscriptionManager>()),
+ );
gh.lazySingleton<_i858.DiscountsApiV2>(
() => _i858.DiscountsApiV2(gh<_i891.DiscountsManager>()),
);
+ gh.lazySingleton<_i1029.StatisticsCalculator>(
+ () => _i1029.StatisticsCalculator(
+ gh<_i1072.AppDatabase>(),
+ gh<_i69.UserRepository>(),
+ gh<_i69.PackRepository>(),
+ gh<_i69.WordStatisticsRepository>(),
+ gh<_i69.StatisticsRepository>(),
+ ),
+ );
gh.lazySingleton<_i433.PackDtoConverter>(
() => _i433.PackDtoConverter(
gh<_i908.ProductsPriceResolver>(),
gh<_i846.AdsManager>(),
+ gh<_i69.UserRepository>(),
),
);
gh.lazySingleton<_i833.PackManager>(
() => _i833.PackManager(
- gh<_i1072.AppDatabase>(),
- gh<_i258.PackRepository>(),
+ gh<_i69.PackRepository>(),
gh<_i433.PackDtoConverter>(),
+ gh<_i1072.AppDatabase>(),
),
);
gh.lazySingleton<_i797.ProductAvailabilityManager>(
() => _i797.ProductAvailabilityManager(
gh<_i833.PackManager>(),
- gh<_i1072.AppDatabase>(),
+ gh<_i69.UserRepository>(),
),
);
gh.lazySingleton<_i1062.FreePacksDistributor>(
() => _i1062.FreePacksDistributor(
gh<_i797.ProductAvailabilityManager>(),
- gh<_i258.PackRepository>(),
+ gh<_i69.PackRepository>(),
),
);
- gh.lazySingleton<_i280.UserManager>(
- () => _i280.UserManager(
+ gh.lazySingleton<_i1009.PaymentManager>(
+ () => _i1009.PaymentManager(
+ gh<_i69.PaymentRepository>(),
+ gh<_i69.SubscriptionRepository>(),
+ gh<_i69.UserRepository>(),
+ gh<_i797.ProductAvailabilityManager>(),
+ gh<_i988.YooMoneyHandler>(),
+ gh<_i222.RustorePurchaseHandler>(),
gh<_i1072.AppDatabase>(),
- gh<_i950.UserRepository>(),
- gh<_i1062.FreePacksDistributor>(),
- gh<_i71.SessionTracker>(),
- gh<_i1029.StatisticsCalculator>(),
- gh<_i802.AchievementManager>(),
- gh<_i909.WordStatisticsManager>(),
),
);
- gh.lazySingleton<_i483.AdminAuthApiV2>(
- () => _i483.AdminAuthApiV2(
- gh<_i240.TelegramAuthCodeService>(),
- gh<_i280.UserManager>(),
- gh<_i108.JwtService>(),
+ gh.lazySingleton<_i800.PacksApiV2>(
+ () => _i800.PacksApiV2(
+ gh<_i833.PackManager>(),
+ gh<_i259.TestManager>(),
+ gh<_i69.PackRepository>(),
+ gh<_i797.ProductAvailabilityManager>(),
+ gh<_i1072.AppDatabase>(),
+ gh<_i747.MinioService>(),
+ ),
+ );
+ gh.lazySingleton<_i69.PurchasesApiV2>(
+ () => _i69.PurchasesApiV2(
+ gh<_i1009.PaymentManager>(),
+ gh<_i833.PackManager>(),
+ gh<_i69.UserRepository>(),
+ gh<_i69.SubscriptionRepository>(),
+ gh<_i69.PaymentRepository>(),
+ gh<_i797.ProductAvailabilityManager>(),
+ gh<_i1072.AppDatabase>(),
+ ),
+ );
+ gh.lazySingleton<_i151.PromoCodesManager>(
+ () => _i151.PromoCodesManager(
+ gh<_i69.PromoCodeRepository>(),
+ gh<_i1009.PaymentManager>(),
+ gh<_i1072.AppDatabase>(),
),
);
gh.lazySingleton<_i560.UserManager>(
@@ -232,35 +337,20 @@ extension GetItInjectableX on _i174.GetIt {
gh<_i802.AchievementManager>(),
),
);
- gh.lazySingleton<_i561.MnemoShelf>(
- () => _i561.MnemoShelf(gh<_i280.UserManager>()),
- );
- gh.lazySingleton<_i52.AuthApiV2>(
- () => _i52.AuthApiV2(
- gh<_i280.UserManager>(),
- gh<_i972.GoogleApi>(),
- gh<_i108.JwtService>(),
- gh<_i240.TelegramAuthCodeService>(),
- ),
- );
- gh.lazySingleton<_i1009.PaymentManager>(
- () => _i1009.PaymentManager(
+ gh.lazySingleton<_i280.UserManager>(
+ () => _i280.UserManager(
gh<_i1072.AppDatabase>(),
- gh<_i950.UserRepository>(),
- gh<_i797.ProductAvailabilityManager>(),
- gh<_i988.YooMoneyHandler>(),
- gh<_i222.RustorePurchaseHandler>(),
+ gh<_i69.UserRepository>(),
+ gh<_i69.PackRepository>(),
+ gh<_i1062.FreePacksDistributor>(),
+ gh<_i71.SessionTracker>(),
+ gh<_i1029.StatisticsCalculator>(),
+ gh<_i802.AchievementManager>(),
+ gh<_i909.WordStatisticsManager>(),
),
);
- gh.lazySingleton<_i800.PacksApiV2>(
- () => _i800.PacksApiV2(
- gh<_i833.PackManager>(),
- gh<_i259.TestManager>(),
- gh<_i258.PackRepository>(),
- gh<_i797.ProductAvailabilityManager>(),
- gh<_i1072.AppDatabase>(),
- gh<_i747.MinioService>(),
- ),
+ gh.lazySingleton<_i273.PromocodesApiV2>(
+ () => _i273.PromocodesApiV2(gh<_i151.PromoCodesManager>()),
);
gh.lazySingleton<_i735.CheckPaymentTask>(
() => _i735.CheckPaymentTask(
@@ -275,13 +365,11 @@ extension GetItInjectableX on _i174.GetIt {
gh<_i1072.AppDatabase>(),
),
);
- gh.lazySingleton<_i69.PurchasesApiV2>(
- () => _i69.PurchasesApiV2(
- gh<_i1009.PaymentManager>(),
- gh<_i833.PackManager>(),
- gh<_i950.UserRepository>(),
- gh<_i797.ProductAvailabilityManager>(),
- gh<_i1072.AppDatabase>(),
+ gh.lazySingleton<_i483.AdminAuthApiV2>(
+ () => _i483.AdminAuthApiV2(
+ gh<_i240.TelegramAuthCodeService>(),
+ gh<_i280.UserManager>(),
+ gh<_i108.JwtService>(),
),
);
gh.lazySingleton<_i247.UsersApiV2>(
@@ -289,18 +377,20 @@ extension GetItInjectableX on _i174.GetIt {
gh<_i280.UserManager>(),
gh<_i1009.PaymentManager>(),
gh<_i1029.StatisticsCalculator>(),
- gh<_i950.UserRepository>(),
+ gh<_i69.UserRepository>(),
gh<_i1072.AppDatabase>(),
),
);
- gh.lazySingleton<_i151.PromoCodesManager>(
- () => _i151.PromoCodesManager(
- gh<_i1072.AppDatabase>(),
- gh<_i1009.PaymentManager>(),
- ),
+ gh.lazySingleton<_i561.MnemoShelf>(
+ () => _i561.MnemoShelf(gh<_i280.UserManager>()),
);
- gh.lazySingleton<_i273.PromocodesApiV2>(
- () => _i273.PromocodesApiV2(gh<_i151.PromoCodesManager>()),
+ gh.lazySingleton<_i52.AuthApiV2>(
+ () => _i52.AuthApiV2(
+ gh<_i280.UserManager>(),
+ gh<_i972.GoogleApi>(),
+ gh<_i108.JwtService>(),
+ gh<_i240.TelegramAuthCodeService>(),
+ ),
);
return this;
}
diff --git a/mnemo_cards_backend/lib/api/mnemo_shelf.dart b/mnemo_cards_backend/lib/api/mnemo_shelf.dart
index 6f02b9e..324d450 100644
--- a/mnemo_cards_backend/lib/api/mnemo_shelf.dart
+++ b/mnemo_cards_backend/lib/api/mnemo_shelf.dart
@@ -10,6 +10,7 @@ import 'v2/admin_analytics_api_v2.dart';
import 'v2/admin_auth_api_v2.dart';
import 'v2/admin_cards_api_v2.dart';
import 'v2/admin_packs_api_v2.dart';
+import 'v2/admin_tasks_api_v2.dart';
import 'v2/admin_tests_api_v2.dart';
import 'v2/admin_users_api_v2.dart';
import 'v2/auth_api_v2.dart';
@@ -56,6 +57,7 @@ class MnemoShelf {
v2Router.mount('/', getIt.get().router);
v2Router.mount('/', getIt.get().router);
v2Router.mount('/', getIt.get().router);
+ v2Router.mount('/', getIt.get().router);
v2Router.mount('/', getIt.get().router);
v2Router.mount('/', getIt.get().router);
v2Router.mount('/', getIt.get().router);
diff --git a/mnemo_cards_backend/lib/api/purchase/google_play_purchase_handler.dart b/mnemo_cards_backend/lib/api/purchase/google_play_purchase_handler.dart
index 05ffa2d..8473245 100644
--- a/mnemo_cards_backend/lib/api/purchase/google_play_purchase_handler.dart
+++ b/mnemo_cards_backend/lib/api/purchase/google_play_purchase_handler.dart
@@ -5,7 +5,7 @@ import 'package:googleapis/androidpublisher/v3.dart' as ap;
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'constaints.dart';
-import 'iap_repository.dart';
+import '../../repository/export.dart';
import 'products.dart';
class GooglePlayPurchaseHandler {
diff --git a/mnemo_cards_backend/lib/api/purchase/iap_repository.dart b/mnemo_cards_backend/lib/api/purchase/iap_repository.dart
deleted file mode 100644
index c44034e..0000000
--- a/mnemo_cards_backend/lib/api/purchase/iap_repository.dart
+++ /dev/null
@@ -1,227 +0,0 @@
-import 'package:googleapis/firestore/v1.dart';
-
-import '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 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 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 toDocument() {
- final doc = super.toDocument();
- doc.addAll({'status': Value(stringValue: status.name)});
- return doc;
- }
-
- @override
- Map 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 toDocument() {
- final doc = super.toDocument();
- doc.addAll({
- 'expiryDate': Value(timestampValue: expiryDate.toUtc().toIso8601String()),
- 'status': Value(stringValue: status.name),
- });
- return doc;
- }
-
- @override
- Map 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 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 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}';
- }
-
- Future> getPurchases() async {
- final list = await api.projects.databases.documents.list(
- 'projects/$projectId/databases/(default)/documents',
- 'purchases',
- );
- return list.documents!.map((e) => Purchase.fromDocument(e)).toList();
- }
-}
diff --git a/mnemo_cards_backend/lib/api/purchase/payment_manager.dart b/mnemo_cards_backend/lib/api/purchase/payment_manager.dart
index 0f94815..fc1bf15 100644
--- a/mnemo_cards_backend/lib/api/purchase/payment_manager.dart
+++ b/mnemo_cards_backend/lib/api/purchase/payment_manager.dart
@@ -7,11 +7,10 @@ import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'package:drift/drift.dart' as drift;
-import 'payment_drift_extension.dart';
import 'rustore/rustore_purchase_handler.dart';
import 'yoo_money.dart';
import '../../packs/product_availability_manager.dart';
-import '../../user/user_repository.dart';
+import '../../repository/export.dart';
/// Result of creating YooKassa payment URL
class YookassaPaymentResult {
@@ -26,19 +25,23 @@ class YookassaPaymentResult {
@lazySingleton
class PaymentManager {
- final AppDatabase _db;
+ final PaymentRepository _paymentRepository;
+ final SubscriptionRepository _subscriptionRepository;
final UserRepository _userRepository;
final ProductAvailabilityManager _productAvailabilityManager;
late final GooglePlayPurchaseHandler googlePurchaseHandler;
final YooMoneyHandler _yooMoneyHandler;
final RustorePurchaseHandler _rustorePurchaseHandler;
+ final AppDatabase _db;
PaymentManager(
- this._db,
+ this._paymentRepository,
+ this._subscriptionRepository,
this._userRepository,
this._productAvailabilityManager,
this._yooMoneyHandler,
this._rustorePurchaseHandler,
+ this._db,
);
/// Создать платеж в базе данных
@@ -47,32 +50,22 @@ class PaymentManager {
print(
'🔍 PaymentManager.createPayment: userId=$userId, externalToken=${paymentDto.externalToken}',
);
- final companion = paymentDto.toCompanion(userId);
- print('🔍 PaymentManager.createPayment: Calling paymentDao.createPayment');
- final paymentId = await _db.paymentDao.createPayment(companion);
- print('✅ PaymentManager.createPayment: Payment created, id=$paymentId');
- print('🔍 PaymentManager.createPayment: Getting payment by id');
- final payment = await _db.paymentDao.getPaymentById(paymentId);
- if (payment == null) {
- print(
- '❌ PaymentManager.createPayment: Payment not found after creation, id=$paymentId',
- );
- throw Exception('Failed to create payment');
- }
- print('✅ PaymentManager.createPayment: Payment retrieved successfully');
- return payment.toDto();
+ print(
+ '🔍 PaymentManager.createPayment: Calling paymentRepository.createPayment',
+ );
+ final payment = await _paymentRepository.createPayment(paymentDto, userId);
+ print('✅ PaymentManager.createPayment: Payment created successfully');
+ return payment;
}
/// Обновить платеж в базе данных
Future updatePayment(String paymentId, PaymentDto paymentDto) async {
- final companion = paymentDto.toUpdateCompanion();
- await _db.paymentDao.updatePaymentCompanion(paymentId, companion);
+ await _paymentRepository.updatePayment(paymentId, paymentDto);
}
/// Получить платеж по ID
Future getPaymentById(String id) async {
- final payment = await _db.paymentDao.getPaymentById(id);
- return payment?.toDto();
+ return await _paymentRepository.getPaymentById(id);
}
/// Получить последний платеж для пользователя и пака
@@ -81,11 +74,10 @@ class PaymentManager {
required String userId,
required String packId,
}) async {
- final payment = await _db.paymentDao.getLatestPaymentForUserAndPack(
+ return await _paymentRepository.getLatestPaymentForUserAndPack(
userId: userId,
packId: packId,
);
- return payment?.toDto();
}
/// Выдать продукт пользователю (для промокодов и других бесплатных активаций)
@@ -104,17 +96,17 @@ class PaymentManager {
} else if (product.type == MnemoCardsProductType.subscription &&
product.id != null) {
final planId = product.id!;
- final plan = await _db.subscriptionDao.getPlanById(planId);
+ final plan = await _subscriptionRepository.getPlanById(planId);
if (plan != null) {
final now = DateTime.now();
final endDate = now.add(Duration(days: plan.durationDays));
- await _db.subscriptionDao.createUserSubscription(
+ await _subscriptionRepository.createSubscription(
UserSubscriptionsCompanion.insert(
userId: userId,
start: PgDateTime(now),
finish: PgDateTime(endDate),
- features: drift.Value(plan.features),
+ features: drift.Value(plan.features.map((f) => f.name).toList()),
),
);
log('Granted subscription $planId to user $userId via promo code');
@@ -135,10 +127,7 @@ class PaymentManager {
}
// Получить пользователя
- final userModel = await _userRepository.getUserById(
- payment.userId,
- withData: true,
- );
+ final userModel = await _userRepository.getUserById(payment.userId);
if (userModel == null) {
log('User not found: ${payment.userId}');
return;
@@ -188,17 +177,17 @@ class PaymentManager {
// Создать подписки
for (final subscriptionId in subscriptionIds) {
- final plan = await _db.subscriptionDao.getPlanById(subscriptionId);
+ final plan = await _subscriptionRepository.getPlanById(subscriptionId);
if (plan != null) {
final now = DateTime.now();
final endDate = now.add(Duration(days: plan.durationDays));
- await _db.subscriptionDao.createUserSubscription(
+ await _subscriptionRepository.createSubscription(
UserSubscriptionsCompanion.insert(
userId: payment.userId,
start: PgDateTime(now),
finish: PgDateTime(endDate),
- features: drift.Value(plan.features),
+ features: drift.Value(plan.features.map((f) => f.name).toList()),
),
);
log(
@@ -215,7 +204,7 @@ class PaymentManager {
try {
// Try to update by externalToken (if it's used as identifier)
// TODO: Once database is regenerated, use payment.id directly
- await _db.paymentDao.updatePaymentStatus(
+ await _paymentRepository.updatePaymentStatus(
payment.externalToken!,
PaymentStatus.processed.name,
);
@@ -294,24 +283,30 @@ class PaymentManager {
.checkPayment(subscriptionToken);
// Найти платеж по продукту
- final payments = await _db.paymentDao.getPaymentsByProduct(productId);
- final payment = payments.isNotEmpty ? payments.first : null;
+ final paymentsDto = await _paymentRepository.getPaymentsByProduct(
+ productId,
+ );
+ final paymentDto = paymentsDto.isNotEmpty ? paymentsDto.first : null;
- if (payment == null) {
+ if (paymentDto == null) {
log('Payment not found for product: $productId');
return false;
}
if (rustorePurchaseResponse?.invoiceStatus.name == 'paid') {
// Find payment id by productId or use subscriptionToken
- final token = payment.externalToken ?? subscriptionToken;
+ final token = paymentDto.externalToken ?? subscriptionToken;
if (token.isNotEmpty) {
- await _db.paymentDao.updatePaymentStatus(
+ await _paymentRepository.updatePaymentStatus(
token,
PaymentStatus.succeeded.name,
);
}
- await processPayment(payment);
+ // Обработать платеж - нужно получить Payment из БД для processPayment
+ final payment = await _db.paymentDao.getPaymentByExternalToken(token);
+ if (payment != null) {
+ await processPayment(payment);
+ }
return true;
}
@@ -325,8 +320,10 @@ class PaymentManager {
/// Проверить платеж YooKassa
Future checkYookassaPayment(String token) async {
try {
- final payment = await _db.paymentDao.getPaymentByExternalToken(token);
- if (payment == null) {
+ final paymentDto = await _paymentRepository.getPaymentByExternalToken(
+ token,
+ );
+ if (paymentDto == null) {
print('Payment not found for token: $token');
return false;
}
@@ -336,14 +333,18 @@ class PaymentManager {
if (yookassaPayment.status == 'succeeded') {
// Use token as payment identifier
- await _db.paymentDao.updatePaymentStatus(
+ await _paymentRepository.updatePaymentStatus(
token,
PaymentStatus.succeeded.name,
);
- await processPayment(payment);
+ // Обработать платеж - нужно получить Payment из БД для processPayment
+ final payment = await _db.paymentDao.getPaymentByExternalToken(token);
+ if (payment != null) {
+ await processPayment(payment);
+ }
return true;
} else if (yookassaPayment.status == 'canceled') {
- await _db.paymentDao.updatePaymentStatus(
+ await _paymentRepository.updatePaymentStatus(
token,
PaymentStatus.canceled.name,
);
@@ -415,8 +416,7 @@ class PaymentManager {
/// Получить платежи пользователя
Future> getUserPayments(String userIdString) async {
- final payments = await _db.paymentDao.getPaymentsByUserId(userIdString);
- return payments.map((p) => p.toDto()).toList();
+ return await _paymentRepository.getPaymentsByUserId(userIdString);
}
/// Проверить и обработать платеж (для cron задач)
diff --git a/mnemo_cards_backend/lib/api/purchase/yoo_money.dart b/mnemo_cards_backend/lib/api/purchase/yoo_money.dart
index 5b7baaa..85b447f 100644
--- a/mnemo_cards_backend/lib/api/purchase/yoo_money.dart
+++ b/mnemo_cards_backend/lib/api/purchase/yoo_money.dart
@@ -259,11 +259,7 @@ class YooMoneyHandler {
/// Build return URL for payment confirmation
/// Uses YOOKASSA_RETURN_URL env var if set, otherwise defaults to web app URL
- String _buildReturnUrl(
- String userId, {
- String? packId,
- String? paymentId,
- }) {
+ String _buildReturnUrl(String userId, {String? packId, String? paymentId}) {
// Construct query parameters
final params = {
'userId': userId,
diff --git a/mnemo_cards_backend/lib/api/subscription/subscription_manager.dart b/mnemo_cards_backend/lib/api/subscription/subscription_manager.dart
index 4f13b08..5189b7b 100644
--- a/mnemo_cards_backend/lib/api/subscription/subscription_manager.dart
+++ b/mnemo_cards_backend/lib/api/subscription/subscription_manager.dart
@@ -1,15 +1,16 @@
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/export.dart';
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'package:drift/drift.dart';
@lazySingleton
class SubscriptionManager {
- final AppDatabase _db;
+ final SubscriptionRepository _subscriptionRepository;
- SubscriptionManager(this._db);
+ SubscriptionManager(this._subscriptionRepository);
Future getSubscriptionDto(UserModel user) async {
if (user.id == null) {
@@ -21,15 +22,15 @@ class SubscriptionManager {
);
}
- final subscription = await _db.subscriptionDao.getActiveUserSubscription(
+ final subscription = await _subscriptionRepository.getActiveSubscription(
user.id!,
);
return SubscriptionDto(
page: null,
isActive: subscription != null,
- start: subscription?.start.dateTime,
- finish: subscription?.finish.dateTime,
+ start: subscription?.start,
+ finish: subscription?.finish,
);
}
@@ -44,7 +45,7 @@ class SubscriptionManager {
final now = DateTime.now();
final endDate = now.add(Duration(days: plan.durationDays));
- await _db.subscriptionDao.createUserSubscription(
+ await _subscriptionRepository.createSubscription(
UserSubscriptionsCompanion.insert(
userId: user.id!,
start: PgDateTime(now),
@@ -55,25 +56,7 @@ class SubscriptionManager {
}
Future> getAllPlans() async {
- final plans = await _db.subscriptionDao.getAllPlans();
- return plans.map((plan) {
- final uiMap = plan.ui as Map?;
- 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,
- );
- }).toList();
+ return await _subscriptionRepository.getAllPlans();
}
Future> getAllSubscriptionPlans() async {
@@ -81,7 +64,7 @@ class SubscriptionManager {
}
Future purchaseSubscription(String userId, String planId) async {
- final plan = await _db.subscriptionDao.getPlanById(planId);
+ final plan = await _subscriptionRepository.getPlanById(planId);
if (plan == null) {
throw StateError('Subscription plan not found');
}
@@ -89,17 +72,17 @@ class SubscriptionManager {
final now = DateTime.now();
final endDate = now.add(Duration(days: plan.durationDays));
- await _db.subscriptionDao.createUserSubscription(
+ await _subscriptionRepository.createSubscription(
UserSubscriptionsCompanion.insert(
userId: userId,
start: PgDateTime(now),
finish: PgDateTime(endDate),
- features: Value(plan.features as List),
+ features: Value(plan.features.map((f) => f.name).toList()),
),
);
}
Future cancelSubscription(String userId) async {
- await _db.subscriptionDao.cancelUserSubscription(userId);
+ await _subscriptionRepository.cancelSubscription(userId);
}
}
diff --git a/mnemo_cards_backend/lib/api/v2/admin_analytics_api_v2.dart b/mnemo_cards_backend/lib/api/v2/admin_analytics_api_v2.dart
index 97ea9f5..1b25913 100644
--- a/mnemo_cards_backend/lib/api/v2/admin_analytics_api_v2.dart
+++ b/mnemo_cards_backend/lib/api/v2/admin_analytics_api_v2.dart
@@ -5,7 +5,7 @@ import 'package:mnemo_cards_backend/api/authorize/acl_types.dart';
import 'package:mnemo_cards_backend/api/authorize/access_service.dart';
import 'package:mnemo_cards_backend/api/authorize/helpers.dart';
import 'package:mnemo_cards_backend/database/database.dart';
-import 'package:mnemo_cards_backend/user/user_repository.dart';
+import 'package:mnemo_cards_backend/repository/export.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
diff --git a/mnemo_cards_backend/lib/api/v2/admin_packs_api_v2.dart b/mnemo_cards_backend/lib/api/v2/admin_packs_api_v2.dart
index 96a9f61..4bcfa38 100644
--- a/mnemo_cards_backend/lib/api/v2/admin_packs_api_v2.dart
+++ b/mnemo_cards_backend/lib/api/v2/admin_packs_api_v2.dart
@@ -9,7 +9,7 @@ import 'package:mnemo_cards_backend/api/authorize/access_service.dart';
import 'package:mnemo_cards_backend/api/authorize/helpers.dart';
import 'package:mnemo_cards_backend/packs/card_pack_drift_extension.dart';
import 'package:mnemo_cards_backend/packs/card_image_storage.dart';
-import 'package:mnemo_cards_backend/packs/pack_repository.dart';
+import 'package:mnemo_cards_backend/repository/export.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
import 'package:shelf/shelf.dart';
@@ -22,8 +22,9 @@ part 'admin_packs_api_v2.g.dart';
class AdminPacksApiV2 {
final AppDatabase _db;
final PackRepository _packRepository;
+ final TestRepository _testRepository;
- AdminPacksApiV2(this._db, this._packRepository);
+ AdminPacksApiV2(this._db, this._packRepository, this._testRepository);
static final _uuidRegex = RegExp(
r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$',
@@ -70,8 +71,7 @@ class AdminPacksApiV2 {
mnemo: const drift.Value('test_image'),
);
- // Note: createCard is not in PackRepository, keeping direct DAO call for now
- final cardId = await _db.packDao.createCard(companion);
+ final cardId = await _packRepository.createCard(companion);
await _tryLinkCardToPack(packId: packId, cardId: cardId);
final stored = await CardImageStorage.persistFromBase64(
@@ -85,12 +85,11 @@ class AdminPacksApiV2 {
final created = await _packRepository.getCardById(cardId);
if (created == null) return null;
- // Note: updateCard is not in PackRepository, keeping direct DAO call for now
- // We need to get the Drift GameCard to update it
+ // TODO: Refactor to use PackRepository.updateCard with GameCardModel
final driftCard = await _db.packDao.getCardById(cardId);
if (driftCard == null) return null;
- await _db.packDao.updateCard(
+ await _packRepository.updateCard(
driftCard.copyWith(
image: stored.fileName,
updatedAt: PgDateTime(DateTime.now()),
@@ -143,6 +142,7 @@ class AdminPacksApiV2 {
packId: packId,
);
if ((test.cover ?? '').trim() != (normalizedCover ?? '').trim()) {
+ // TODO: Refactor to use TestRepository.updateTest
await _db.testDao.updateTest(
test.copyWith(
cover: drift.Value(normalizedCover),
@@ -155,7 +155,7 @@ class AdminPacksApiV2 {
}
// Ensure all question/button images are normalized and cards linked to pack.
- final questions = await _db.testDao.getTestQuestions(testId);
+ final questions = await _testRepository.getTestQuestions(testId);
for (final q in questions) {
// Parse options/buttons
List buttons;
@@ -220,6 +220,7 @@ class AdminPacksApiV2 {
}
if (mutated) {
+ // TODO: Refactor to use TestRepository.updateTestQuestion
await _db.testDao.updateTestQuestion(
q.copyWith(
options: jsonEncode(normalizedButtons),
@@ -402,7 +403,7 @@ class AdminPacksApiV2 {
final previewCardIds = previewCards.map((c) => c.id).toList();
// Get tests for this pack
- final packTests = await _db.testDao.getTestsByPackId(packId);
+ final packTests = await _testRepository.getTestsByPackId(packId);
final testIds = packTests.map((t) => t.id).toList();
// Create EditCardPackDto
@@ -596,7 +597,7 @@ class AdminPacksApiV2 {
try {
for (final testId in editDto.addTestIds!) {
// Verify test exists before adding
- final test = await _db.testDao.getTestById(testId);
+ final test = await _testRepository.getTestById(testId);
if (test == null) {
return _json({
'error': 'Test not found',
@@ -607,7 +608,7 @@ class AdminPacksApiV2 {
'Test with ID "$testId" was not found. Please verify all test IDs before adding them to the pack.',
}, statusCode: 404);
}
- await _db.testDao.linkTestToPack(testId, packId);
+ await _testRepository.linkTestToPack(testId, packId);
// Important: tests may reference cardIds (or even base64) in
// question/button images. Ensure those cards are linked to this pack
// so `/api/v2/packs//cards//image` works.
@@ -631,7 +632,7 @@ class AdminPacksApiV2 {
if (editDto.removeTestIds != null && editDto.removeTestIds!.isNotEmpty) {
for (final testId in editDto.removeTestIds!) {
- await _db.testDao.unlinkTestFromPack(testId, packId);
+ await _testRepository.unlinkTestFromPack(testId, packId);
}
}
@@ -661,7 +662,7 @@ class AdminPacksApiV2 {
final previewCardIds = previewCards.map((c) => c.id).toList();
// Get updated tests for this pack
- final updatedPackTests = await _db.testDao.getTestsByPackId(packId);
+ final updatedPackTests = await _testRepository.getTestsByPackId(packId);
final updatedTestIds = updatedPackTests.map((t) => t.id).toList();
final updatedDto = EditCardPackDto(
diff --git a/mnemo_cards_backend/lib/api/v2/admin_users_api_v2.dart b/mnemo_cards_backend/lib/api/v2/admin_users_api_v2.dart
index f42b6e5..c1a215f 100644
--- a/mnemo_cards_backend/lib/api/v2/admin_users_api_v2.dart
+++ b/mnemo_cards_backend/lib/api/v2/admin_users_api_v2.dart
@@ -9,7 +9,8 @@ import 'package:mnemo_cards_backend/api/authorize/helpers.dart';
import 'package:mnemo_cards_backend/database/database.dart';
import 'package:mnemo_cards_backend/user/user_model.dart'
show UserModelExtension;
-import 'package:mnemo_cards_backend/user/user_repository.dart';
+import 'package:mnemo_cards_backend/user/user_data_model.dart';
+import 'package:mnemo_cards_backend/repository/export.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
import 'package:shelf/shelf.dart';
@@ -25,6 +26,30 @@ class AdminUsersApiV2 {
AdminUsersApiV2(this._db, this._userRepository);
+ /// Вспомогательный метод для загрузки userData и subscription
+ Future<(UserDataModel?, UserSubscriptionModel?)> _loadUserDataAndSubscription(
+ String? userId,
+ ) async {
+ if (userId == null) return (null, null);
+
+ final userData = await _userRepository.getUserDataById(userId);
+ final subscription = await _userRepository.getUserSubscription(userId);
+
+ final userDataModel = userData != null
+ ? UserDataModel(
+ id: userData.id,
+ lastTestSessionToken: userData.lastTestSessionToken,
+ lastTimeOnline: userData.lastTimeOnline?.toDateTime(),
+ tags: userData.tags,
+ totalStudyTimeMinutes: userData.totalStudyTimeMinutes,
+ currentStreak: userData.currentStreak,
+ longestStreak: userData.longestStreak,
+ )
+ : null;
+
+ return (userDataModel, subscription);
+ }
+
Response _json(
Object? data, {
int statusCode = 200,
@@ -96,7 +121,13 @@ class AdminUsersApiV2 {
// Convert to DTOs
final userDtos =