postgress fix
Some checks are pending
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
Mobile App CI / test (push) Waiting to run
Mobile App CI / build-android (push) Blocked by required conditions
Mobile App CI / build-ios (push) Blocked by required conditions
Web App CI / test (push) Waiting to run
Web App CI / build (push) Blocked by required conditions
Deploy Mnemo Cards / Deploy Backend (push) Waiting to run
Deploy Mnemo Cards / Deploy Web App (push) Blocked by required conditions
Deploy Mnemo Cards / Final Verification (push) Blocked by required conditions
Deploy Telegram Bot / Deploy Telegram Bot (push) Waiting to run

This commit is contained in:
Dmitry 2025-12-13 23:55:50 +03:00
parent 306b2fdca7
commit 550b56276e
217 changed files with 27877 additions and 51117 deletions

View file

@ -15,6 +15,4 @@ test
pubspec.lock
*.log
*.tmp
isar/*.isar
isar/*.lock
**/.DS_Store

View file

@ -40,13 +40,12 @@ COPY --from=build /app/server /app/server
COPY --from=build /app/mnemo_cards_backend/public /app/public
COPY --from=build /app/mnemo_cards_backend/data /app/data
# Writable dirs for isar/backups by default
RUN mkdir -p /app/isar /app/backups && \
chmod -R 755 /app/isar /app/backups
# Writable dirs for backups by default
RUN mkdir -p /app/backups && \
chmod -R 755 /app/backups
ENV PORT=3000 \
SERVER_ADDRESS=0.0.0.0 \
ISAR_DIR=/app/isar \
BACKUP_DIR=/app/backups \
WORK_DIR=/app \
DEBUG=false \
@ -55,8 +54,7 @@ ENV PORT=3000 \
EXPOSE 3000
# Healthcheck - проверка доступности сервера
# Increased start-period to 45s to allow server initialization (Isar DB, cron jobs, etc.)
# Isar может долго инициализироваться при первом запуске или при большом объеме данных
# Increased start-period to 30s to allow server initialization (PostgreSQL connection, cron jobs, etc.)
# Используем 127.0.0.1 для healthcheck (внутри контейнера работает даже если сервер слушает на 0.0.0.0)
# Сервер работает только по HTTP (HTTPS обрабатывается на уровне reverse proxy в Coolify)
# Пробуем curl, если не работает - используем wget как fallback

View file

@ -3,7 +3,6 @@
## Обязательные переменные (опциональны, есть значения по умолчанию)
```
ISAR_DIR=isar
BACKUP_DIR=../mnemo_cards_telegram_bot/backups/
WORK_DIR=/root/mnemo_cards_backend
DEBUG=false
@ -14,10 +13,9 @@ ADMIN_IDS=
## Описание переменных
- **ISAR_DIR** - Директория для базы данных Isar (по умолчанию: `isar`)
- **BACKUP_DIR** - Директория для хранения бэкапов (по умолчанию: `../mnemo_cards_telegram_bot/backups/`)
- **WORK_DIR** - Рабочая директория приложения (по умолчанию: `/root/mnemo_cards_backend`)
- **DEBUG** - Режим отладки Isar Inspector (`true` или `1` для включения, по умолчанию: `false`)
- **DEBUG** - Режим отладки (`true` или `1` для включения, по умолчанию: `false`)
- **SERVER_ADDRESS** - IP адрес для привязки сервера (по умолчанию: `0.0.0.0` - все интерфейсы)
- **PORT** - Порт для HTTP сервера (по умолчанию: `3000`)
- **ADMIN_IDS** - Список Telegram ID админов, разделенных запятой (например: `123456789,987654321`). Если не задано, читается из файла `admins` (каждая строка - один ID)

View file

@ -1 +1 @@
flutter packages pub run build_runner build --delete-conflicting-outputs --verbose
dart run build_runner build --delete-conflicting-outputs --verbose

View file

@ -10,7 +10,7 @@ class AdsManager {
String getAdsKey(List<MnemoCardsProductModel> products, UserModel user) {
final productString = (products.toList()
..sort((p, n) => (p.id ?? -1).compareTo(n.id ?? -1)))
..sort((p, n) => (p.id ?? '').compareTo(n.id ?? '')))
.map((product) => '${product.type.name}_${product.id}')
.join(',');
final userId = user.id.toString();

View file

@ -16,7 +16,7 @@ class AccessService {
Future<bool> canPack(
PackAccessAction action, {
required int packId,
required String packId,
UserModel? user,
}) async {
final decision =
@ -26,7 +26,7 @@ class AccessService {
Future<void> requirePack(
PackAccessAction action, {
required int packId,
required String packId,
UserModel? user,
}) async {
final decision =

View file

@ -5,13 +5,13 @@ import 'package:mnemo_cards_common_backend/src/models/user/user_model.dart';
class PackAccessPolicy {
final ResourceLoader loader;
final Set<int> publicPackIds;
final Set<String> publicPackIds;
PackAccessPolicy(this.loader, {this.publicPackIds = const {10}});
PackAccessPolicy(this.loader, {this.publicPackIds = const {'10'}});
Future<AccessDecision> check({
required PackAccessAction action,
required int packId,
required String packId,
required UserModel? user,
}) async {
// Admins can do anything

View file

@ -9,9 +9,9 @@ class ResourceLoader {
ResourceLoader(this.packManager);
CardPackModel? _packModelCache;
int? _packModelCacheId;
String? _packModelCacheId;
Future<CardPackModel?> getPackModel(int packId) async {
Future<CardPackModel?> getPackModel(String packId) async {
if (_packModelCacheId == packId && _packModelCache != null) {
return _packModelCache;
}
@ -22,7 +22,7 @@ class ResourceLoader {
}
/// Returns true if pack exists and enabled
Future<bool> isPackEnabled(int packId) async {
Future<bool> isPackEnabled(String packId) async {
final pack = await packManager.getPack(packId);
return pack?.enabled == true;
}

View file

@ -31,13 +31,9 @@ Future<Response> guardPack(
Future<Response> Function() next,
) async {
try {
final id = int.tryParse(packId);
if (id == null) {
return Response.badRequest();
}
await request.access!.requirePack(
action,
packId: id,
packId: packId,
user: request.user,
);
return await next();

View file

@ -1,4 +1,5 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// dart format width=80
// **************************************************************************
// InjectableConfigGenerator
@ -8,167 +9,211 @@
// coverage:ignore-file
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:get_it/get_it.dart' as _i1;
import 'package:injectable/injectable.dart' as _i2;
import 'package:get_it/get_it.dart' as _i174;
import 'package:injectable/injectable.dart' as _i526;
import '../../auth/telegram_auth_code_service.dart' as _i18;
import '../../cron/check_payment.dart' as _i35;
import '../../database/database.dart' as _i5;
import '../../discounts/discounts_manager.dart' as _i6;
import '../../packs/free_packs_distributor.dart' as _i7;
import '../../packs/pack_dto_converter.dart' as _i25;
import '../../packs/pack_manager.dart' as _i26;
import '../../packs/products_price_resolver.dart' as _i10;
import '../../promo_codes/promo_codes_manager.dart' as _i28;
import '../../statistics/achievement_manager.dart' as _i22;
import '../../statistics/session_tracker.dart' as _i12;
import '../../statistics/statistics_calculator.dart' as _i13;
import '../../tasks/task_manager.dart' as _i16;
import '../../tests/test_manager.dart' as _i20;
import '../../user/user_manager.dart' as _i30;
import '../../user/user_manager_drift.dart' as _i31;
import '../ads/ads_manager.dart' as _i4;
import '../mnemo_shelf.dart' as _i36;
import '../purchase/payment_manager.dart' as _i27;
import '../purchase/rustore/rustore_purchase_handler.dart' as _i11;
import '../purchase/yoo_money.dart' as _i21;
import '../subscription/subscription_manager.dart' as _i14;
import '../user/google_api.dart' as _i8;
import '../v2/admin_analytics_api_v2.dart' as _i3;
import '../v2/admin_auth_api_v2.dart' as _i33;
import '../v2/admin_cards_api_v2.dart' as _i23;
import '../v2/auth_api_v2.dart' as _i34;
import '../v2/discounts_api_v2.dart' as _i24;
import '../v2/jwt_service.dart' as _i9;
import '../v2/promocodes_api_v2.dart' as _i29;
import '../v2/subscriptions_api_v2.dart' as _i15;
import '../v2/tasks_api_v2.dart' as _i17;
import '../v2/telegram_bot_api_v2.dart' as _i19;
import '../v2/tests_api_v2.dart' as _i37;
import '../v2/users_api_v2.dart' as _i32;
import 'modules.dart' as _i38;
import '../../auth/telegram_auth_code_service.dart' as _i240;
import '../../cron/check_payment.dart' as _i735;
import '../../database/database.dart' as _i1072;
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/products_price_resolver.dart' as _i908;
import '../../promo_codes/promo_codes_manager.dart' as _i151;
import '../../statistics/achievement_manager.dart' as _i802;
import '../../statistics/session_tracker.dart' as _i71;
import '../../statistics/statistics_calculator.dart' as _i1029;
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 '../ads/ads_manager.dart' as _i846;
import '../mnemo_shelf.dart' as _i561;
import '../purchase/payment_manager.dart' as _i1009;
import '../purchase/rustore/rustore_purchase_handler.dart' as _i222;
import '../purchase/yoo_money.dart' as _i988;
import '../subscription/subscription_manager.dart' as _i377;
import '../user/google_api.dart' as _i972;
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/auth_api_v2.dart' as _i52;
import '../v2/discounts_api_v2.dart' as _i858;
import '../v2/jwt_service.dart' as _i108;
import '../v2/packs_api_v2.dart' as _i800;
import '../v2/promocodes_api_v2.dart' as _i273;
import '../v2/subscriptions_api_v2.dart' as _i964;
import '../v2/tasks_api_v2.dart' as _i985;
import '../v2/telegram_bot_api_v2.dart' as _i296;
import '../v2/tests_api_v2.dart' as _i170;
import '../v2/users_api_v2.dart' as _i247;
import 'modules.dart' as _i738;
extension GetItInjectableX on _i1.GetIt {
// initializes the registration of main-scope dependencies inside of GetIt
_i1.GetIt init({
extension GetItInjectableX on _i174.GetIt {
// initializes the registration of main-scope dependencies inside of GetIt
_i174.GetIt init({
String? environment,
_i2.EnvironmentFilter? environmentFilter,
_i526.EnvironmentFilter? environmentFilter,
}) {
final gh = _i2.GetItHelper(
this,
environment,
environmentFilter,
);
final gh = _i526.GetItHelper(this, environment, environmentFilter);
final appModule = _$AppModule();
gh.lazySingleton<_i3.AdminAnalyticsApiV2>(() => _i3.AdminAnalyticsApiV2());
gh.lazySingleton<_i4.AdsManager>(() => _i4.AdsManager());
gh.singleton<_i5.AppDatabase>(() => appModule.database);
gh.lazySingleton<_i6.DiscountsManager>(
() => _i6.DiscountsManager(gh<_i5.AppDatabase>()));
gh.lazySingleton<_i7.FreePacksDistributor>(
() => _i7.FreePacksDistributor(gh<_i5.AppDatabase>()));
gh.lazySingleton<_i8.GoogleApi>(() => const _i8.GoogleApi());
gh.lazySingleton<_i9.JwtService>(
() => _i9.JwtService(gh<_i5.AppDatabase>()));
gh.lazySingleton<_i10.ProductsPriceResolver>(
() => _i10.ProductsPriceResolver(
gh<_i6.DiscountsManager>(),
gh<_i5.AppDatabase>(),
));
gh.lazySingleton<_i11.RustorePurchaseHandler>(
() => _i11.RustorePurchaseHandler());
gh.lazySingleton<_i12.SessionTracker>(
() => _i12.SessionTracker(gh<_i5.AppDatabase>()));
gh.lazySingleton<_i13.StatisticsCalculator>(
() => _i13.StatisticsCalculator());
gh.lazySingleton<_i14.SubscriptionManager>(
() => _i14.SubscriptionManager(gh<_i5.AppDatabase>()));
gh.lazySingleton<_i15.SubscriptionsApiV2>(
() => _i15.SubscriptionsApiV2(gh<_i14.SubscriptionManager>()));
gh.lazySingleton<_i16.TaskManager>(
() => _i16.TaskManager(gh<_i5.AppDatabase>()));
gh.lazySingleton<_i17.TasksApiV2>(
() => _i17.TasksApiV2(gh<_i16.TaskManager>()));
gh.lazySingleton<_i18.TelegramAuthCodeService>(
() => _i18.TelegramAuthCodeService());
gh.lazySingleton<_i19.TelegramBotApiV2>(
() => _i19.TelegramBotApiV2(gh<_i5.AppDatabase>()));
gh.lazySingleton<_i20.TestManager>(
() => _i20.TestManager(gh<_i5.AppDatabase>()));
gh.singleton<_i21.YooMoneyHandler>(() => appModule.yooMoneyHandler);
gh.lazySingleton<_i22.AchievementManager>(
() => _i22.AchievementManager(gh<_i5.AppDatabase>()));
gh.factory<_i23.AdminCardsApiV2>(
() => _i23.AdminCardsApiV2(gh<_i5.AppDatabase>()));
gh.lazySingleton<_i24.DiscountsApiV2>(
() => _i24.DiscountsApiV2(gh<_i6.DiscountsManager>()));
gh.lazySingleton<_i25.PackDtoConverter>(() => _i25.PackDtoConverter(
gh<_i10.ProductsPriceResolver>(),
gh<_i4.AdsManager>(),
));
gh.lazySingleton<_i26.PackManager>(() => _i26.PackManager(
gh<_i5.AppDatabase>(),
gh<_i25.PackDtoConverter>(),
));
gh.lazySingleton<_i27.PaymentManager>(() => _i27.PaymentManager(
gh<_i5.AppDatabase>(),
gh<_i26.PackManager>(),
gh<_i14.SubscriptionManager>(),
gh<_i21.YooMoneyHandler>(),
gh<_i11.RustorePurchaseHandler>(),
gh<_i10.ProductsPriceResolver>(),
));
gh.lazySingleton<_i28.PromoCodesManager>(() => _i28.PromoCodesManager(
gh<_i5.AppDatabase>(),
gh<_i27.PaymentManager>(),
));
gh.lazySingleton<_i29.PromocodesApiV2>(
() => _i29.PromocodesApiV2(gh<_i28.PromoCodesManager>()));
gh.lazySingleton<_i30.UserManager>(() => _i30.UserManager(
gh<_i5.AppDatabase>(),
gh<_i7.FreePacksDistributor>(),
gh<_i12.SessionTracker>(),
gh<_i13.StatisticsCalculator>(),
gh<_i22.AchievementManager>(),
));
gh.lazySingleton<_i31.UserManager>(() => _i31.UserManager(
gh<_i5.AppDatabase>(),
gh<_i7.FreePacksDistributor>(),
gh<_i12.SessionTracker>(),
gh<_i13.StatisticsCalculator>(),
gh<_i22.AchievementManager>(),
));
gh.lazySingleton<_i32.UsersApiV2>(() => _i32.UsersApiV2(
gh<_i30.UserManager>(),
gh<_i27.PaymentManager>(),
gh<_i13.StatisticsCalculator>(),
gh<_i5.AppDatabase>(),
));
gh.lazySingleton<_i33.AdminAuthApiV2>(() => _i33.AdminAuthApiV2(
gh<_i18.TelegramAuthCodeService>(),
gh<_i30.UserManager>(),
gh<_i9.JwtService>(),
));
gh.lazySingleton<_i34.AuthApiV2>(() => _i34.AuthApiV2(
gh<_i5.AppDatabase>(),
gh<_i30.UserManager>(),
gh<_i8.GoogleApi>(),
gh<_i9.JwtService>(),
gh<_i18.TelegramAuthCodeService>(),
));
gh.lazySingleton<_i35.CheckPaymentTask>(() => _i35.CheckPaymentTask(
gh<_i27.PaymentManager>(),
gh<_i5.AppDatabase>(),
));
gh.lazySingleton<_i36.MnemoShelf>(
() => _i36.MnemoShelf(gh<_i30.UserManager>()));
gh.lazySingleton<_i37.TestsApiV2>(() => _i37.TestsApiV2(
gh<_i20.TestManager>(),
gh<_i30.UserManager>(),
gh<_i5.AppDatabase>(),
));
gh.singleton<_i1072.AppDatabase>(() => appModule.database);
gh.singleton<_i988.YooMoneyHandler>(() => appModule.yooMoneyHandler);
gh.lazySingleton<_i846.AdsManager>(() => _i846.AdsManager());
gh.lazySingleton<_i222.RustorePurchaseHandler>(
() => _i222.RustorePurchaseHandler(),
);
gh.lazySingleton<_i972.GoogleApi>(() => const _i972.GoogleApi());
gh.lazySingleton<_i368.AdminAnalyticsApiV2>(
() => _i368.AdminAnalyticsApiV2(),
);
gh.lazySingleton<_i240.TelegramAuthCodeService>(
() => _i240.TelegramAuthCodeService(),
);
gh.lazySingleton<_i1029.StatisticsCalculator>(
() => _i1029.StatisticsCalculator(),
);
gh.lazySingleton<_i377.SubscriptionManager>(
() => _i377.SubscriptionManager(gh<_i1072.AppDatabase>()),
);
gh.lazySingleton<_i108.JwtService>(
() => _i108.JwtService(gh<_i1072.AppDatabase>()),
);
gh.lazySingleton<_i296.TelegramBotApiV2>(
() => _i296.TelegramBotApiV2(gh<_i1072.AppDatabase>()),
);
gh.lazySingleton<_i891.DiscountsManager>(
() => _i891.DiscountsManager(gh<_i1072.AppDatabase>()),
);
gh.lazySingleton<_i1062.FreePacksDistributor>(
() => _i1062.FreePacksDistributor(gh<_i1072.AppDatabase>()),
);
gh.lazySingleton<_i802.AchievementManager>(
() => _i802.AchievementManager(gh<_i1072.AppDatabase>()),
);
gh.lazySingleton<_i71.SessionTracker>(
() => _i71.SessionTracker(gh<_i1072.AppDatabase>()),
);
gh.lazySingleton<_i586.TaskManager>(
() => _i586.TaskManager(gh<_i1072.AppDatabase>()),
);
gh.lazySingleton<_i259.TestManager>(
() => _i259.TestManager(gh<_i1072.AppDatabase>()),
);
gh.factory<_i922.AdminCardsApiV2>(
() => _i922.AdminCardsApiV2(gh<_i1072.AppDatabase>()),
);
gh.lazySingleton<_i964.SubscriptionsApiV2>(
() => _i964.SubscriptionsApiV2(gh<_i377.SubscriptionManager>()),
);
gh.lazySingleton<_i858.DiscountsApiV2>(
() => _i858.DiscountsApiV2(gh<_i891.DiscountsManager>()),
);
gh.lazySingleton<_i1009.PaymentManager>(
() => _i1009.PaymentManager(
gh<_i1072.AppDatabase>(),
gh<_i988.YooMoneyHandler>(),
gh<_i222.RustorePurchaseHandler>(),
),
);
gh.lazySingleton<_i985.TasksApiV2>(
() => _i985.TasksApiV2(gh<_i586.TaskManager>()),
);
gh.lazySingleton<_i151.PromoCodesManager>(
() => _i151.PromoCodesManager(
gh<_i1072.AppDatabase>(),
gh<_i1009.PaymentManager>(),
),
);
gh.lazySingleton<_i280.UserManager>(
() => _i280.UserManager(
gh<_i1072.AppDatabase>(),
gh<_i1062.FreePacksDistributor>(),
gh<_i71.SessionTracker>(),
gh<_i1029.StatisticsCalculator>(),
gh<_i802.AchievementManager>(),
),
);
gh.lazySingleton<_i560.UserManager>(
() => _i560.UserManager(
gh<_i1072.AppDatabase>(),
gh<_i1062.FreePacksDistributor>(),
gh<_i71.SessionTracker>(),
gh<_i1029.StatisticsCalculator>(),
gh<_i802.AchievementManager>(),
),
);
gh.lazySingleton<_i561.MnemoShelf>(
() => _i561.MnemoShelf(gh<_i280.UserManager>()),
);
gh.lazySingleton<_i247.UsersApiV2>(
() => _i247.UsersApiV2(
gh<_i280.UserManager>(),
gh<_i1009.PaymentManager>(),
gh<_i1029.StatisticsCalculator>(),
gh<_i1072.AppDatabase>(),
),
);
gh.lazySingleton<_i273.PromocodesApiV2>(
() => _i273.PromocodesApiV2(gh<_i151.PromoCodesManager>()),
);
gh.lazySingleton<_i908.ProductsPriceResolver>(
() => _i908.ProductsPriceResolver(
gh<_i891.DiscountsManager>(),
gh<_i1072.AppDatabase>(),
),
);
gh.lazySingleton<_i735.CheckPaymentTask>(
() => _i735.CheckPaymentTask(
gh<_i1009.PaymentManager>(),
gh<_i1072.AppDatabase>(),
),
);
gh.lazySingleton<_i170.TestsApiV2>(
() => _i170.TestsApiV2(
gh<_i259.TestManager>(),
gh<_i280.UserManager>(),
gh<_i1072.AppDatabase>(),
),
);
gh.lazySingleton<_i52.AuthApiV2>(
() => _i52.AuthApiV2(
gh<_i1072.AppDatabase>(),
gh<_i280.UserManager>(),
gh<_i972.GoogleApi>(),
gh<_i108.JwtService>(),
gh<_i240.TelegramAuthCodeService>(),
),
);
gh.lazySingleton<_i483.AdminAuthApiV2>(
() => _i483.AdminAuthApiV2(
gh<_i240.TelegramAuthCodeService>(),
gh<_i280.UserManager>(),
gh<_i108.JwtService>(),
),
);
gh.lazySingleton<_i433.PackDtoConverter>(
() => _i433.PackDtoConverter(
gh<_i908.ProductsPriceResolver>(),
gh<_i846.AdsManager>(),
),
);
gh.lazySingleton<_i833.PackManager>(
() => _i833.PackManager(
gh<_i1072.AppDatabase>(),
gh<_i433.PackDtoConverter>(),
),
);
gh.lazySingleton<_i800.PacksApiV2>(
() => _i800.PacksApiV2(
gh<_i833.PackManager>(),
gh<_i259.TestManager>(),
gh<_i1072.AppDatabase>(),
),
);
return this;
}
}
class _$AppModule extends _i38.AppModule {}
class _$AppModule extends _i738.AppModule {}

View file

@ -11,7 +11,7 @@ import 'v2/admin_analytics_api_v2.dart';
import 'v2/admin_auth_api_v2.dart';
// import 'v2/admin_cards_api_v2.dart'; // disabled
// import 'v2/admin_packs_api_v2.dart'; // disabled
// import 'v2/admin_users_api_v2.dart'; // uses isar
// import 'v2/admin_users_api_v2.dart'; // disabled
import 'v2/auth_api_v2.dart';
import 'v2/discounts_api_v2.dart';
// import 'v2/games_api_v2.dart'; // disabled
@ -21,8 +21,8 @@ import 'v2/promocodes_api_v2.dart';
import 'v2/subscriptions_api_v2.dart';
import 'v2/tasks_api_v2.dart';
import 'v2/tests_api_v2.dart';
// import 'v2/users_api_v2.dart'; // uses isar
// import 'v2/telegram_bot_api_v2.dart'; // uses isar
// import 'v2/users_api_v2.dart'; // disabled
// import 'v2/telegram_bot_api_v2.dart'; // disabled
import 'v2/authorize_v2.dart';
import 'v2/telegram_bot_auth_middleware.dart';
import 'v2/jwt_service.dart';
@ -54,7 +54,7 @@ class MnemoShelf {
v2Router.mount('/', getIt.get<AuthApiV2>().router);
v2Router.mount('/', getIt.get<AdminAuthApiV2>().router);
v2Router.mount('/', getIt.get<AdminAnalyticsApiV2>().router);
// v2Router.mount('/', getIt.get<PacksApiV2>().router); // may use isar
// v2Router.mount('/', getIt.get<PacksApiV2>().router); // disabled
v2Router.mount('/', getIt.get<TestsApiV2>().router);
// v2Router.mount('/', getIt.get<GamesApiV2>().router); // disabled
// v2Router.mount('/', getIt.get<PurchasesApiV2>().router); // disabled

View file

@ -1,3 +1,4 @@
import 'dart:convert';
import 'package:mnemo_cards_backend/database/database.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'package:drift/drift.dart' as drift;
@ -5,9 +6,19 @@ import 'package:drift/drift.dart' as drift;
/// Extension для конвертации Payment (Drift) в DTO
extension PaymentToDto on Payment {
PaymentDto toDto() {
final productsList = products?.map((p) => MnemoCardsProductDto.fromJson(p as Map<String, dynamic>)).toList() ?? [];
final hasSubscription = productsList.any((p) => p.type == MnemoCardsProductType.subscription);
// Parse products from JSON string
List<MnemoCardsProductDto> productsList = [];
try {
if (products.isNotEmpty) {
productsList = products
.map((p) => MnemoCardsProductDto.fromJson(p as Map<String, dynamic>))
.toList();
}
} catch (e) {
// If parsing fails, use empty list
productsList = [];
}
return PaymentDto(
amount: amount,
currency: currency,
@ -23,15 +34,16 @@ extension PaymentToDto on Payment {
meta: meta,
date: date,
products: productsList,
packs: [],
subscription: hasSubscription,
);
}
}
/// Extension для создания Payment из DTO
extension PaymentFromDto on PaymentDto {
PaymentsCompanion toCompanion(int userId) {
PaymentsCompanion toCompanion(String userId) {
// Encode products as JSON string
final productsJson = products.map((p) => p.toJson()).toList();
return PaymentsCompanion.insert(
userId: userId,
amount: amount,
@ -41,37 +53,15 @@ extension PaymentFromDto on PaymentDto {
externalToken: drift.Value(externalToken),
meta: drift.Value(meta),
date: drift.Value(date),
products: drift.Value(products.map((p) => p.toJson()).toList() as List<dynamic>),
packs: drift.Value(packs ?? []),
subscription: drift.Value(subscription ?? false),
);
}
Payment toPayment(int paymentId, int userId) {
return Payment(
id: paymentId,
userId: userId,
amount: amount,
currency: currency,
status: status.name,
paymentSystem: paymentSystem.name,
externalToken: externalToken,
meta: meta,
date: date,
products: products.map((p) => p.toJson()).toList(),
packs: packs ?? [],
subscription: subscription ?? false,
createdAt: date,
updatedAt: date,
products: drift.Value(productsJson),
);
}
}
/// Extension для обновления платежа
extension PaymentUpdate on PaymentDto {
PaymentsCompanion toUpdateCompanion(int paymentId) {
PaymentsCompanion toUpdateCompanion() {
return PaymentsCompanion(
id: drift.Value(paymentId),
status: drift.Value(status.name),
externalToken: drift.Value(externalToken),
meta: drift.Value(meta),

View file

@ -12,8 +12,6 @@ extension PaymentExt on PaymentModel {
status: status,
date: date,
paymentSystem: paymentSystem,
packs: packs,
subscription: subscription,
products: products.map((p) => p.toDto()).toList(),
externalToken: withToken ? externalToken : null,
meta: withMeta ? meta : null,

View file

@ -1,52 +1,31 @@
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:googleapis/androidpublisher/v3.dart' as ap;
import 'package:googleapis/firestore/v1.dart' as fs;
import 'package:googleapis_auth/auth_io.dart' as auth;
import 'package:injectable/injectable.dart';
import 'package:mnemo_cards_backend/api/purchase/google_play_purchase_handler.dart';
import 'package:mnemo_cards_backend/api/purchase/rustore/rustore_purchase_response.dart';
import 'package:mnemo_cards_backend/database/database.dart';
import 'package:mnemo_cards_backend/user/user_drift_extension.dart';
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'package:yookassa_client/yookassa_client.dart';
import 'package:drift/drift.dart' as drift;
import 'payment_drift_extension.dart';
import '../../main.dart' as backend_main;
import '../../packs/pack_manager.dart';
import '../../packs/products_price_resolver.dart';
import '../subscription/subscription_manager.dart';
import 'iap_repository.dart';
import 'rustore/rustore_purchase_handler.dart';
import 'yoo_money.dart';
import '../../user/user_drift_extension.dart';
@lazySingleton
class PaymentManager {
final AppDatabase _db;
final PackManager _packManager;
final SubscriptionManager _subscriptionManager;
late final GooglePlayPurchaseHandler googlePurchaseHandler;
final YooMoneyHandler _yooMoneyHandler;
final RustorePurchaseHandler _rustorePurchaseHandler;
final ProductsPriceResolver _productsPriceResolver;
PaymentManager(
this._db,
this._packManager,
this._subscriptionManager,
this._yooMoneyHandler,
this._rustorePurchaseHandler,
this._productsPriceResolver,
);
/// Создать платеж в базе данных
Future<PaymentDto> createPayment(PaymentDto paymentDto, int userId) async {
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);
@ -57,54 +36,45 @@ class PaymentManager {
}
/// Обновить платеж в базе данных
Future<void> updatePayment(int paymentId, PaymentDto paymentDto) async {
final companion = paymentDto.toUpdateCompanion(paymentId);
await _db.paymentDao.updatePaymentCompanion(companion);
Future<void> updatePayment(String paymentId, PaymentDto paymentDto) async {
final companion = paymentDto.toUpdateCompanion();
await _db.paymentDao.updatePaymentCompanion(paymentId, companion);
}
/// Получить платеж по ID
Future<PaymentDto?> getPaymentById(int id) async {
Future<PaymentDto?> getPaymentById(String id) async {
final payment = await _db.paymentDao.getPaymentById(id);
return payment?.toDto();
}
/// Создать обработчики платежей Google Play
Future<Map<String, GooglePlayPurchaseHandler>>
_createPurchaseHandlers() async {
return {};
}
/// Выдать продукт пользователю (для промокодов и других бесплатных активаций)
Future<void> grantProductToUser(int userId, MnemoCardsProductDto product) async {
Future<void> grantProductToUser(String userId, MnemoCardsProductDto product) async {
await _db.transaction(() async {
if (product.type == MnemoCardsProductType.pack && product.id != null) {
final packId = int.tryParse(product.id!);
if (packId != null) {
await _db.userDao.grantPackAccess(
userId: userId,
packId: packId,
grantType: 'promo_code',
);
log('Granted pack $packId to user $userId via promo code');
}
final packId = product.id!;
await _db.userDao.grantPackAccess(
userId: userId,
packId: packId,
grantType: 'promo_code',
);
log('Granted pack $packId to user $userId via promo code');
} else if (product.type == MnemoCardsProductType.subscription && product.id != null) {
final planId = int.tryParse(product.id!);
if (planId != null) {
final plan = await _db.subscriptionDao.getPlanById(planId);
if (plan != null) {
final now = DateTime.now();
final endDate = now.add(Duration(days: plan.durationDays));
final planId = product.id!;
final plan = await _db.subscriptionDao.getPlanById(planId);
if (plan != null) {
final now = DateTime.now();
final endDate = now.add(Duration(days: plan.durationDays));
await _db.subscriptionDao.createUserSubscription(
UserSubscriptionsCompanion.insert(
userId: userId,
start: now,
finish: endDate,
features: drift.Value(plan.features as List<dynamic>),
),
);
log('Granted subscription $planId to user $userId via promo code');
}
await _db.subscriptionDao.createUserSubscription(
UserSubscriptionsCompanion.insert(
userId: userId,
start: now,
finish: endDate,
features: drift.Value(plan.features as List<dynamic>),
),
);
log('Granted subscription $planId to user $userId via promo code');
}
}
});
@ -129,25 +99,33 @@ class PaymentManager {
}
// Извлечь IDs пакетов из продуктов
final packIds = <int>[];
if (payment.products != null) {
for (final product in payment.products!) {
final productMap = product as Map<String, dynamic>;
if (productMap['type'] == 'pack' && productMap['id'] != null) {
packIds.add(int.parse(productMap['id'].toString()));
final packIds = <String>[];
try {
if (payment.products.isNotEmpty && payment.products != '[]') {
for (final product in payment.products) {
final productMap = product as Map<String, dynamic>;
if (productMap['type'] == 'pack' && productMap['id'] != null) {
packIds.add(productMap['id'].toString());
}
}
}
} catch (e) {
log('Error parsing products: $e');
}
// Извлечь IDs подписок из продуктов
final subscriptionIds = <int>[];
if (payment.products != null) {
for (final product in payment.products!) {
final productMap = product as Map<String, dynamic>;
if (productMap['type'] == 'subscription' && productMap['id'] != null) {
subscriptionIds.add(int.parse(productMap['id'].toString()));
final subscriptionIds = <String>[];
try {
if (payment.products.isNotEmpty && payment.products != '[]') {
for (final product in payment.products) {
final productMap = product as Map<String, dynamic>;
if (productMap['type'] == 'subscription' && productMap['id'] != null) {
subscriptionIds.add(productMap['id'].toString());
}
}
}
} catch (e) {
log('Error parsing products: $e');
}
await _db.transaction(() async {
@ -173,7 +151,7 @@ class PaymentManager {
userId: payment.userId,
start: now,
finish: endDate,
features: drift.Value(plan.features as List<dynamic>),
features: drift.Value(plan.features),
),
);
log('Created subscription for user ${payment.userId}, plan: $subscriptionId');
@ -181,8 +159,25 @@ class PaymentManager {
}
// Обновить статус платежа
await _db.paymentDao.updatePaymentStatus(payment.id, PaymentStatus.processed.name);
log('Payment ${payment.id} processed successfully');
// Note: Payment class should have an id field when returned from queries
// If payment.id doesn't work, the database needs to be regenerated
// For now, we'll try to find the payment by externalToken and update it
if (payment.externalToken != null && payment.externalToken!.isNotEmpty) {
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(
payment.externalToken!,
PaymentStatus.processed.name,
);
log('Payment processed successfully for user ${payment.userId}');
} catch (e) {
log('Warning: Could not update payment status: $e');
log('Payment processed for user ${payment.userId}, but status update failed. Database regeneration may be needed.');
}
} else {
log('Payment processed for user ${payment.userId}, but cannot update status without externalToken or payment.id');
}
});
}
@ -205,12 +200,14 @@ class PaymentManager {
// Проверить статус в Google Play
final acknowledged = await googlePurchaseHandler.acknowledge(productId, token);
if (!acknowledged) {
await _db.paymentDao.updatePaymentStatus(payment.id, PaymentStatus.waiting.name);
// Use token as payment identifier since Payment.id might not be available
await _db.paymentDao.updatePaymentStatus(token, PaymentStatus.waiting.name);
return false;
}
// Обновить статус платежа
await _db.paymentDao.updatePaymentStatus(payment.id, PaymentStatus.succeeded.name);
// Use token as payment identifier
await _db.paymentDao.updatePaymentStatus(token, PaymentStatus.succeeded.name);
// Обработать платеж
await processPayment(payment);
@ -244,7 +241,11 @@ class PaymentManager {
}
if (rustorePurchaseResponse?.invoiceStatus.name == 'paid') {
await _db.paymentDao.updatePaymentStatus(payment.id, PaymentStatus.succeeded.name);
// Find payment id by productId or use subscriptionToken
final paymentId = payment.externalToken ?? subscriptionToken;
if (paymentId.isNotEmpty) {
await _db.paymentDao.updatePaymentStatus(paymentId, PaymentStatus.succeeded.name);
}
await processPayment(payment);
return true;
}
@ -269,11 +270,12 @@ class PaymentManager {
final yookassaPayment = await _yooMoneyHandler.checkPayment(token);
if (yookassaPayment.status == 'succeeded') {
await _db.paymentDao.updatePaymentStatus(payment.id, PaymentStatus.succeeded.name);
// Use token as payment identifier
await _db.paymentDao.updatePaymentStatus(token, PaymentStatus.succeeded.name);
await processPayment(payment);
return true;
} else if (yookassaPayment.status == 'canceled') {
await _db.paymentDao.updatePaymentStatus(payment.id, PaymentStatus.canceled.name);
await _db.paymentDao.updatePaymentStatus(token, PaymentStatus.canceled.name);
}
return false;
@ -306,24 +308,19 @@ class PaymentManager {
date: DateTime.now(),
status: PaymentStatus.created,
paymentSystem: PaymentSystem.yookassa,
packs: [],
subscription: false,
products: [],
externalToken: yookassaPayment.id,
meta: null,
);
await createPayment(paymentDto, int.parse(userId));
await createPayment(paymentDto, userId);
return yookassaPayment.confirmationUrl!;
}
/// Получить платежи пользователя
Future<List<PaymentDto>> getUserPayments(String userIdString) async {
final userId = int.tryParse(userIdString);
if (userId == null) return [];
final payments = await _db.paymentDao.getPaymentsByUserId(userId);
final payments = await _db.paymentDao.getPaymentsByUserId(userIdString);
return payments.map((p) => p.toDto()).toList();
}
@ -349,21 +346,27 @@ class PaymentManager {
await checkYookassaPayment(payment.externalToken!);
} else if (system == PaymentSystem.rustore && payment.externalToken != null) {
// Для RuStore нужен productId - получаем из products
if (payment.products != null && payment.products!.isNotEmpty) {
final firstProduct = payment.products!.first as Map<String, dynamic>;
final productId = firstProduct['id']?.toString();
if (productId != null) {
final user = await _db.userDao.getUserById(payment.userId);
await checkRustorePayment(
productId: productId,
subscriptionToken: payment.externalToken!,
user: user != null ? await user.toUserModel() : null,
);
if (payment.products.isNotEmpty && payment.products != '[]') {
try {
if (payment.products.isNotEmpty) {
final firstProduct = payment.products.first;
final productId = firstProduct['id']?.toString();
if (productId != null && payment.externalToken != null) {
final user = await _db.userDao.getUserById(payment.userId);
await checkRustorePayment(
productId: productId,
subscriptionToken: payment.externalToken!,
user: user != null ? await user.toUserModel() : null,
);
}
}
} catch (e) {
log('Error parsing products for RuStore payment: $e');
}
}
}
} catch (e, s) {
log('Error checking payment ${payment.id}: $e');
log('Error checking payment for user ${payment.userId}: $e');
print(s);
}
}

View file

@ -7,21 +7,24 @@ part of 'rustore_purchase_response.dart';
// **************************************************************************
RustorePurchaseResponse _$RustorePurchaseResponseFromJson(
Map<String, dynamic> json) =>
RustorePurchaseResponse(
json['invoice_id'] as String,
DateTime.parse(json['invoice_date'] as String),
$enumDecode(_$RustoreInvoiceStatusEnumMap, json['invoice_status'],
unknownValue: RustoreInvoiceStatus.unknown),
);
Map<String, dynamic> json,
) => RustorePurchaseResponse(
json['invoice_id'] as String,
DateTime.parse(json['invoice_date'] as String),
$enumDecode(
_$RustoreInvoiceStatusEnumMap,
json['invoice_status'],
unknownValue: RustoreInvoiceStatus.unknown,
),
);
Map<String, dynamic> _$RustorePurchaseResponseToJson(
RustorePurchaseResponse instance) =>
<String, dynamic>{
'invoice_id': instance.invoiceId,
'invoice_date': instance.invoiceDate.toIso8601String(),
'invoice_status': _$RustoreInvoiceStatusEnumMap[instance.invoiceStatus]!,
};
RustorePurchaseResponse instance,
) => <String, dynamic>{
'invoice_id': instance.invoiceId,
'invoice_date': instance.invoiceDate.toIso8601String(),
'invoice_status': _$RustoreInvoiceStatusEnumMap[instance.invoiceStatus]!,
};
const _$RustoreInvoiceStatusEnumMap = {
RustoreInvoiceStatus.created: 'created',
@ -35,9 +38,9 @@ const _$RustoreInvoiceStatusEnumMap = {
};
PaymentInfo _$PaymentInfoFromJson(Map<String, dynamic> json) => PaymentInfo(
json['payment_id'] as String,
DateTime.parse(json['payment_date'] as String),
);
json['payment_id'] as String,
DateTime.parse(json['payment_date'] as String),
);
Map<String, dynamic> _$PaymentInfoToJson(PaymentInfo instance) =>
<String, dynamic>{

View file

@ -30,31 +30,6 @@ class SubscriptionManager {
);
}
Future<SubscriptionPlanModel?> getSubscriptionPlan(String id) async {
final intId = int.tryParse(id);
if (intId == null) return null;
final plan = await _db.subscriptionDao.getPlanById(intId);
if (plan == null) return null;
final uiMap = plan.ui as Map<String, dynamic>?;
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,
);
}
Future<void> createSubscription(UserModel user, SubscriptionPlanModel plan) async {
if (user.id == null || plan.id == null) {
throw ArgumentError('User ID and Plan ID are required');
@ -99,7 +74,7 @@ class SubscriptionManager {
return await getAllPlans();
}
Future<void> purchaseSubscription(int userId, int planId) async {
Future<void> purchaseSubscription(String userId, String planId) async {
final plan = await _db.subscriptionDao.getPlanById(planId);
if (plan == null) {
throw StateError('Subscription plan not found');
@ -118,7 +93,7 @@ class SubscriptionManager {
);
}
Future<void> cancelSubscription(int userId) async {
Future<void> cancelSubscription(String userId) async {
await _db.subscriptionDao.cancelUserSubscription(userId);
}
}

View file

@ -1,7 +1,6 @@
import 'dart:convert';
import 'package:injectable/injectable.dart';
import 'package:isar/isar.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';

View file

@ -13,15 +13,7 @@ Router _$AdminAnalyticsApiV2Router(AdminAnalyticsApiV2 service) {
r'/admin/analytics/dashboard',
service.getDashboardAnalytics,
);
router.add(
'GET',
r'/admin/analytics/users/chart',
service.getUsersChart,
);
router.add(
'GET',
r'/admin/analytics/revenue/chart',
service.getRevenueChart,
);
router.add('GET', r'/admin/analytics/users/chart', service.getUsersChart);
router.add('GET', r'/admin/analytics/revenue/chart', service.getRevenueChart);
return router;
}

View file

@ -8,25 +8,9 @@ part of 'admin_auth_api_v2.dart';
Router _$AdminAuthApiV2Router(AdminAuthApiV2 service) {
final router = Router();
router.add(
'POST',
r'/admin/auth/request-code',
service.requestCode,
);
router.add(
'POST',
r'/admin/auth/verify-code',
service.verifyCode,
);
router.add(
'GET',
r'/admin/auth/code-status/<code>',
service.getCodeStatus,
);
router.add(
'GET',
r'/admin/auth/me',
service.getCurrentUser,
);
router.add('POST', r'/admin/auth/request-code', service.requestCode);
router.add('POST', r'/admin/auth/verify-code', service.verifyCode);
router.add('GET', r'/admin/auth/code-status/<code>', service.getCodeStatus);
router.add('GET', r'/admin/auth/me', service.getCurrentUser);
return router;
}

View file

@ -23,7 +23,7 @@ class AdminCardsApiV2 {
final offset = int.tryParse(request.url.queryParameters['offset'] ?? '0') ?? 0;
final cards = packId != null
? await _db.packDao.getPackCards(int.parse(packId))
? await _db.packDao.getPackCards(packId)
: await _db.packDao.getAllCards(limit: limit, offset: offset);
final total = packId != null
@ -58,15 +58,14 @@ class AdminCardsApiV2 {
@Route.get('/cards/<cardId>')
Future<Response> getCard(Request request, String cardId) async {
try {
final id = int.tryParse(cardId);
if (id == null) {
if (cardId.isEmpty) {
return Response.badRequest(
body: json.encode({'error': 'Invalid card ID'}),
headers: {'Content-Type': 'application/json'},
);
}
final card = await _db.packDao.getCardById(id);
final card = await _db.packDao.getCardById(cardId);
if (card == null) {
return Response.notFound(
json.encode({'error': 'Card not found'}),
@ -104,7 +103,7 @@ class AdminCardsApiV2 {
final data = json.decode(body) as Map<String, dynamic>;
final companion = GameCardsCompanion.insert(
packId: data['packId'] as int,
packId: data['packId'],
original: data['original'] as String,
translation: data['translation'] as String,
image: data['image'] as String? ?? '',
@ -131,8 +130,7 @@ class AdminCardsApiV2 {
@Route.put('/cards/<cardId>')
Future<Response> updateCard(Request request, String cardId) async {
try {
final id = int.tryParse(cardId);
if (id == null) {
if (cardId.isEmpty) {
return Response.badRequest(
body: json.encode({'error': 'Invalid card ID'}),
headers: {'Content-Type': 'application/json'},
@ -142,7 +140,7 @@ class AdminCardsApiV2 {
final body = await request.readAsString();
final data = json.decode(body) as Map<String, dynamic>;
final existing = await _db.packDao.getCardById(id);
final existing = await _db.packDao.getCardById(cardId);
if (existing == null) {
return Response.notFound(
json.encode({'error': 'Card not found'}),
@ -177,15 +175,14 @@ class AdminCardsApiV2 {
@Route.delete('/cards/<cardId>')
Future<Response> deleteCard(Request request, String cardId) async {
try {
final id = int.tryParse(cardId);
if (id == null) {
if (cardId.isEmpty) {
return Response.badRequest(
body: json.encode({'error': 'Invalid card ID'}),
headers: {'Content-Type': 'application/json'},
);
}
await _db.packDao.deleteCard(id);
await _db.packDao.deleteCard(cardId);
return Response.ok(
json.encode({'success': true}),

View file

@ -1,299 +0,0 @@
import 'dart:convert';
import 'package:injectable/injectable.dart';
import 'package:isar/isar.dart';
import 'package:mnemo_cards_backend/api/authorize/acl_types.dart';
import 'package:mnemo_cards_backend/api/authorize/access_service.dart';
import 'package:mnemo_cards_backend/api/authorize/helpers.dart';
import 'package:mnemo_cards_backend/main.dart' as backend_main;
import 'package:mnemo_cards_backend/packs/card_model_extension.dart';
import 'package:mnemo_cards_backend/packs/card_dto_extension.dart';
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
part 'admin_cards_api_v2.g.dart';
/// Admin endpoints for card management in API v2.
@lazySingleton
class AdminCardsApiV2 {
AdminCardsApiV2();
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,
},
);
}
Response _badRequest(String message) => Response.badRequest(
body: jsonEncode({'error': 'Bad Request', 'message': message}),
headers: {'Content-Type': 'application/json'},
);
Response _notFound([String? message]) => Response.notFound(
jsonEncode({
'error': 'Not Found',
'message': message ?? 'Resource not found',
}),
headers: {'Content-Type': 'application/json'},
);
Response _internalServerError([String? message]) => Response(
500,
body: jsonEncode({
'error': 'Internal Server Error',
'message': message ?? 'An error occurred',
}),
headers: {'Content-Type': 'application/json'},
);
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);
}
}
/// GET /api/v2/admin/cards
/// Get all cards with pagination and optional search
/// Query params: ?page=1&limit=20&search=term
@Route.get('/admin/cards')
Future<Response> getCards(Request request) async {
try {
final auth = await _ensureAdmin(request);
if (auth.statusCode != 200) {
return auth;
}
final queryParams = request.requestedUri.queryParameters;
// Parse pagination parameters
final page = int.tryParse(queryParams['page'] ?? '1') ?? 1;
final limit = int.tryParse(queryParams['limit'] ?? '20') ?? 20;
final search = queryParams['search']?.trim();
// Validate pagination
if (page < 1) {
return _badRequest('Page must be greater than 0');
}
if (limit < 1 || limit > 100) {
return _badRequest('Limit must be between 1 and 100');
}
// Get all cards from database
final allCards = await backend_main.isar
.txn(() async => backend_main.isar.gameCardModels.where().findAll());
// Apply search filter if provided
List<GameCardModel> filteredCards = allCards;
if (search != null && search.isNotEmpty) {
final searchLower = search.toLowerCase();
filteredCards = allCards.where((card) {
return card.original.toLowerCase().contains(searchLower) ||
card.translation.toLowerCase().contains(searchLower) ||
card.mnemo.toLowerCase().contains(searchLower) ||
(card.transcription?.toLowerCase().contains(searchLower) ??
false) ||
(card.transcriptionMnemo?.toLowerCase().contains(searchLower) ??
false);
}).toList();
}
// Apply pagination
final total = filteredCards.length;
final totalPages = (total / limit).ceil();
final offset = (page - 1) * limit;
final paginatedCards = filteredCards.skip(offset).take(limit).toList();
// Convert to DTOs
final cardDtos = paginatedCards.map((card) => card.toDto()).toList();
return _json({
'items': cardDtos.map((c) => c.toJson()).toList(),
'total': total,
'page': page,
'limit': limit,
'totalPages': totalPages,
});
} catch (e, s) {
print('Error in getCards: $e\n$s');
return _internalServerError(e.toString());
}
}
/// GET /api/v2/admin/cards/:id
/// Get a specific card by ID
@Route.get('/admin/cards/<cardId>')
Future<Response> getCard(Request request, String cardId) async {
try {
final auth = await _ensureAdmin(request);
if (auth.statusCode != 200) {
return auth;
}
final cardIdInt = int.tryParse(cardId);
if (cardIdInt == null) {
return _badRequest('Invalid card ID');
}
final card = await backend_main.isar.txn(() async {
return await backend_main.isar.gameCardModels.get(cardIdInt);
});
if (card == null) {
return _notFound('Card not found');
}
return _json(card.toDto().toJson());
} catch (e, s) {
print('Error in getCard: $e\n$s');
return _internalServerError(e.toString());
}
}
/// POST /api/v2/admin/cards
/// Create or update a card
@Route.post('/admin/cards')
Future<Response> upsertCard(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': 'Card payload is required',
},
statusCode: 400,
);
}
late final GameCardDto cardDto;
try {
cardDto =
GameCardDto.fromJson(jsonDecode(body) as Map<String, dynamic>);
} catch (_) {
return _json(
{
'error': 'bad_request',
'message': 'Invalid card payload',
},
statusCode: 400,
);
}
// Validate required fields
if (cardDto.original?.isEmpty ?? true) {
return _json(
{
'error': 'bad_request',
'message': 'Original text is required',
},
statusCode: 400,
);
}
if (cardDto.translation?.isEmpty ?? true) {
return _json(
{
'error': 'bad_request',
'message': 'Translation is required',
},
statusCode: 400,
);
}
if (cardDto.mnemo?.isEmpty ?? true) {
return _json(
{
'error': 'bad_request',
'message': 'Mnemo is required',
},
statusCode: 400,
);
}
// Convert DTO to model
final cardModel = cardDto.toModel();
// Save to database
await backend_main.isar.writeTxn(() async {
await backend_main.isar.gameCardModels.put(cardModel);
});
// Get updated card with ID
final updatedCard = await backend_main.isar.txn(() async {
return await backend_main.isar.gameCardModels.get(cardModel.id!);
});
if (updatedCard == null) {
return _internalServerError('Failed to save card');
}
return _json({
'success': true,
'card': updatedCard.toDto().toJson(),
});
} catch (e, s) {
print('Error in upsertCard: $e\n$s');
return _internalServerError(e.toString());
}
}
/// DELETE /api/v2/admin/cards/:id
/// Delete a card by ID
@Route.delete('/admin/cards/<cardId>')
Future<Response> deleteCard(Request request, String cardId) async {
try {
final auth = await _ensureAdmin(request);
if (auth.statusCode != 200) {
return auth;
}
final cardIdInt = int.tryParse(cardId);
if (cardIdInt == null) {
return _badRequest('Invalid card ID');
}
final card = await backend_main.isar.txn(() async {
return await backend_main.isar.gameCardModels.get(cardIdInt);
});
if (card == null) {
return _notFound('Card not found');
}
// Delete from database
await backend_main.isar.writeTxn(() async {
await backend_main.isar.gameCardModels.delete(cardIdInt);
});
return _json({
'success': true,
'message': 'Card deleted successfully',
});
} catch (e, s) {
print('Error in deleteCard: $e\n$s');
return _internalServerError(e.toString());
}
}
Router get router => _$AdminCardsApiV2Router(this);
}

View file

@ -8,30 +8,10 @@ part of 'admin_cards_api_v2.dart';
Router _$AdminCardsApiV2Router(AdminCardsApiV2 service) {
final router = Router();
router.add(
'GET',
r'/cards',
service.getAllCards,
);
router.add(
'GET',
r'/cards/<cardId>',
service.getCard,
);
router.add(
'POST',
r'/cards',
service.createCard,
);
router.add(
'PUT',
r'/cards/<cardId>',
service.updateCard,
);
router.add(
'DELETE',
r'/cards/<cardId>',
service.deleteCard,
);
router.add('GET', r'/cards', service.getAllCards);
router.add('GET', r'/cards/<cardId>', service.getCard);
router.add('POST', r'/cards', service.createCard);
router.add('PUT', r'/cards/<cardId>', service.updateCard);
router.add('DELETE', r'/cards/<cardId>', service.deleteCard);
return router;
}

View file

@ -1,343 +0,0 @@
import 'dart:convert';
import 'package:injectable/injectable.dart';
import 'package:isar/isar.dart';
import 'package:mnemo_cards_backend/api/authorize/acl_types.dart';
import 'package:mnemo_cards_backend/api/authorize/access_service.dart';
import 'package:mnemo_cards_backend/api/authorize/helpers.dart';
import 'package:mnemo_cards_backend/main.dart' as backend_main;
import 'package:mnemo_cards_backend/packs/pack_manager.dart';
import 'package:mnemo_cards_backend/packs/pack_dto_converter.dart';
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
part 'admin_packs_api_v2.g.dart';
/// Admin endpoints for pack management in API v2.
@lazySingleton
class AdminPacksApiV2 {
final PackManager _packManager;
final PackDtoConverter _packDtoConverter;
AdminPacksApiV2(this._packManager, this._packDtoConverter);
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,
},
);
}
Response _badRequest(String message) => Response.badRequest(
body: jsonEncode({'error': 'Bad Request', 'message': message}),
headers: {'Content-Type': 'application/json'},
);
Response _notFound([String? message]) => Response.notFound(
jsonEncode({
'error': 'Not Found',
'message': message ?? 'Resource not found',
}),
headers: {'Content-Type': 'application/json'},
);
Response _internalServerError([String? message]) => Response(
500,
body: jsonEncode({
'error': 'Internal Server Error',
'message': message ?? 'An error occurred',
}),
headers: {'Content-Type': 'application/json'},
);
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);
}
}
/// GET /api/v2/admin/packs
/// Get all packs with pagination and optional search
/// Query params: ?page=1&limit=20&search=term&showDisabled=true
@Route.get('/admin/packs')
Future<Response> getPacks(Request request) async {
try {
final auth = await _ensureAdmin(request);
if (auth.statusCode != 200) {
return auth;
}
final queryParams = request.requestedUri.queryParameters;
// Parse pagination parameters
final page = int.tryParse(queryParams['page'] ?? '1') ?? 1;
final limit = int.tryParse(queryParams['limit'] ?? '20') ?? 20;
final search = queryParams['search']?.trim();
final showDisabled = queryParams['showDisabled'] == 'true';
// Validate pagination
if (page < 1) {
return _badRequest('Page must be greater than 0');
}
if (limit < 1 || limit > 100) {
return _badRequest('Limit must be between 1 and 100');
}
// Get all packs (admin can see disabled packs)
final allPacks = await backend_main.isar
.txn(() async => backend_main.isar.cardPackModels.where().findAll());
List<CardPackModel> filteredPacks =
showDisabled ? allPacks : allPacks.where((p) => p.enabled).toList();
if (search != null && search.isNotEmpty) {
final searchLower = search.toLowerCase();
filteredPacks = allPacks.where((pack) {
return pack.title.toLowerCase().contains(searchLower) ||
(pack.subtitle?.toLowerCase().contains(searchLower) ?? false) ||
pack.id.toString().contains(searchLower);
}).toList();
}
// Sort by order
filteredPacks.sort((p, n) => p.order.compareTo(n.order));
// Apply pagination
final total = filteredPacks.length;
final totalPages = (total / limit).ceil();
final offset = (page - 1) * limit;
final paginatedPacks = filteredPacks.skip(offset).take(limit).toList();
// Convert to preview DTOs
final packDtos = await Future.wait(paginatedPacks.map((pack) async =>
await _packDtoConverter.toCardPackPreviewDto(pack, null)));
return _json({
'items': packDtos.map((p) => p.toJson()).toList(),
'total': total,
'page': page,
'limit': limit,
'totalPages': totalPages,
});
} catch (e, s) {
print('Error in getPacks: $e\n$s');
return _internalServerError(e.toString());
}
}
/// GET /api/v2/admin/packs/:id
/// Get pack details by ID for editing
@Route.get('/admin/packs/<packId>')
Future<Response> getPack(Request request, String packId) async {
try {
final auth = await _ensureAdmin(request);
if (auth.statusCode != 200) {
return auth;
}
final packIdInt = int.tryParse(packId);
if (packIdInt == null) {
return _badRequest('Invalid pack ID');
}
final pack = await backend_main.isar.txn(() async {
return await backend_main.isar.cardPackModels.get(packIdInt);
});
if (pack == null) {
return _notFound('Pack not found');
}
// Convert to edit DTO
final editDto = await _convertToEditPackDto(pack);
return _json(editDto.toJson());
} catch (e, s) {
print('Error in getPack: $e\n$s');
return _internalServerError(e.toString());
}
}
/// POST /api/v2/admin/packs
/// Create or update a pack
@Route.post('/admin/packs')
Future<Response> upsertPack(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': 'Pack payload is required',
},
statusCode: 400,
);
}
late final EditCardPackDto packDto;
try {
packDto =
EditCardPackDto.fromJson(jsonDecode(body) as Map<String, dynamic>);
} catch (_) {
return _json(
{
'error': 'bad_request',
'message': 'Invalid pack payload',
},
statusCode: 400,
);
}
// Validate required fields
if (packDto.title?.isEmpty ?? true) {
return _json(
{
'error': 'bad_request',
'message': 'Title is required',
},
statusCode: 400,
);
}
// Convert DTO to model
final packModel = _convertToPackModel(packDto);
// Save to database
await backend_main.isar.writeTxn(() async {
await backend_main.isar.cardPackModels.put(packModel);
});
// Get updated pack with ID
final updatedPack = await backend_main.isar.txn(() async {
return await backend_main.isar.cardPackModels.get(packModel.id!);
});
if (updatedPack == null) {
return _internalServerError('Failed to save pack');
}
// Convert back to edit DTO
final editDto = await _convertToEditPackDto(updatedPack);
return _json({
'success': true,
'pack': editDto.toJson(),
});
} catch (e, s) {
print('Error in upsertPack: $e\n$s');
return _internalServerError(e.toString());
}
}
/// DELETE /api/v2/admin/packs/:id
/// Delete a pack by ID
@Route.delete('/admin/packs/<packId>')
Future<Response> deletePack(Request request, String packId) async {
try {
final auth = await _ensureAdmin(request);
if (auth.statusCode != 200) {
return auth;
}
final packIdInt = int.tryParse(packId);
if (packIdInt == null) {
return _badRequest('Invalid pack ID');
}
final pack = await backend_main.isar.txn(() async {
return await backend_main.isar.cardPackModels.get(packIdInt);
});
if (pack == null) {
return _notFound('Pack not found');
}
// Delete from database
await backend_main.isar.writeTxn(() async {
await backend_main.isar.cardPackModels.delete(packIdInt);
});
return _json({
'success': true,
'message': 'Pack deleted successfully',
});
} catch (e, s) {
print('Error in deletePack: $e\n$s');
return _internalServerError(e.toString());
}
}
/// Helper method to convert PackModel to EditCardPackDto
Future<EditCardPackDto> _convertToEditPackDto(CardPackModel model) async {
await model.cards.load();
final cards = model.cards.toList();
return EditCardPackDto(
id: model.id.toString(),
title: model.title,
subtitle: model.subtitle,
color: model.color,
cover: model.cover,
size: cards.length,
googlePlayId: model.googlePlayId,
rustoreId: model.rustoreId,
appStoreId: model.appStoreId,
price: model.price,
description: model.description,
enabled: model.enabled,
version: model.version,
addCardIds: cards.map((c) => c.id.toString()).toList(),
addTestIds: [], // No tests to add when converting from model
removeCardIds: [],
removeTestIds: [],
previewCards: model.previewCards?.map((c) => c.toString()).toList(),
order: model.order,
cardsOrder: model.cardsOrder,
);
}
/// Helper method to convert EditCardPackDto to PackModel
CardPackModel _convertToPackModel(EditCardPackDto dto) {
final model = CardPackModel(
id: dto.id != null ? int.tryParse(dto.id!) : null,
title: dto.title ?? '',
subtitle: dto.subtitle ?? '',
color: dto.color,
cover: dto.cover,
googlePlayId: dto.googlePlayId,
rustoreId: dto.rustoreId,
appStoreId: dto.appStoreId,
price: dto.price,
description: dto.description,
enabled: dto.enabled ?? true,
version: dto.version,
order: dto.order ?? 0,
cardsOrder: dto.cardsOrder ?? [],
size: dto.size ?? 0,
);
// Note: previewCards is an IsarLinks field and should be populated separately
// if needed, after saving the model
return model;
}
Router get router => _$AdminPacksApiV2Router(this);
}

View file

@ -1,189 +0,0 @@
import 'dart:convert';
import 'package:injectable/injectable.dart';
import 'package:mnemo_cards_backend/api/authorize/acl_types.dart';
import 'package:mnemo_cards_backend/api/authorize/access_service.dart';
import 'package:mnemo_cards_backend/api/authorize/helpers.dart';
import 'package:mnemo_cards_backend/api/purchase/payment_extension.dart';
import 'package:mnemo_cards_backend/api/purchase/payment_manager.dart';
import 'package:mnemo_cards_backend/main.dart' as backend_main;
import 'package:mnemo_cards_backend/user/user_manager.dart';
import 'package:mnemo_cards_backend/user/user_model.dart';
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'package:isar/isar.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
part 'admin_users_api_v2.g.dart';
/// Admin endpoints for user management in API v2.
@lazySingleton
class AdminUsersApiV2 {
final UserManager _userManager;
final PaymentManager _paymentManager;
AdminUsersApiV2(
this._userManager,
this._paymentManager,
);
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);
}
}
/// GET /api/v2/admin/users
/// Returns list of users by optional ids.
@Route.get('/admin/users')
Future<Response> getUsers(Request request) async {
final auth = await _ensureAdmin(request);
if (auth.statusCode != 200) {
return auth;
}
final idsString = request.url.queryParameters['ids'];
List<int> ids = [];
if (idsString != null && idsString.isNotEmpty) {
ids = idsString
.split(',')
.map((value) => int.tryParse(value))
.whereType<int>()
.toList();
}
final users = await backend_main.isar.txn(() async {
if (ids.isEmpty) {
return await backend_main.isar.userModels.where().anyId().findAll();
}
return await backend_main.isar.userModels.getAll(ids);
});
final dtos = await Future.wait(
users
.whereType<UserModel>()
.map((model) async => (await model.toDto()).toJson()),
);
return _json({'users': dtos});
}
/// GET /api/v2/admin/users/ids
/// Returns comma separated user ids.
@Route.get('/admin/users/ids')
Future<Response> getUserIds(Request request) async {
final auth = await _ensureAdmin(request);
if (auth.statusCode != 200) {
return auth;
}
final ids = await backend_main.isar.txn(
() => backend_main.isar.userModels.where().anyId().idProperty().findAll(),
);
return _json({'ids': ids.map((id) => id.toString()).join(',')});
}
/// GET /api/v2/admin/users/<id>/purchases
/// Returns user payments history.
@Route.get('/admin/users/<userId>/purchases')
Future<Response> getUserPurchases(
Request request,
String userId,
) async {
final auth = await _ensureAdmin(request);
if (auth.statusCode != 200) {
return auth;
}
final payments = await _paymentManager.getUserPayments(userId);
final dtos =
payments.map((payment) => payment.toDto(withToken: true)).toList();
return _json({'payments': dtos.map((p) => p.toJson()).toList()});
}
/// POST /api/v2/admin/users
/// Creates or updates user data (admin editing).
@Route.post('/admin/users')
Future<Response> upsertUser(Request request) async {
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': 'User payload is required',
},
statusCode: 400,
);
}
late final UserDto userDto;
try {
userDto = UserDto.fromJson(jsonDecode(body) as Map<String, dynamic>);
} catch (_) {
return _json(
{
'error': 'bad_request',
'message': 'Invalid user payload',
},
statusCode: 400,
);
}
final success = await _userManager.editUser(userDto);
return _json({'result': success});
}
/// DELETE /api/v2/admin/users/<id>
/// Deletes user.
@Route.delete('/admin/users/<userId>')
Future<Response> deleteUser(
Request request,
String userId,
) async {
final auth = await _ensureAdmin(request);
if (auth.statusCode != 200) {
return auth;
}
final success = await _userManager.deleteUser(userId);
if (!success) {
return _json(
{
'error': 'not_found',
'message': 'User not found',
},
statusCode: 404,
);
}
return _json({'result': true});
}
Router get router => _$AdminUsersApiV2Router(this);
}

View file

@ -1,168 +0,0 @@
import 'dart:convert';
import 'package:injectable/injectable.dart';
import 'package:mnemo_cards_backend/api/ads/ads_manager.dart';
import 'package:mnemo_cards_backend/api/authorize/helpers.dart';
import 'package:mnemo_cards_backend/api/purchase/payment_manager.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
part 'ads_api_v2.g.dart';
/// API v2 endpoints for rewarded ads flows.
@lazySingleton
class AdsApiV2 {
final AdsManager _adsManager;
final PaymentManager _paymentManager;
AdsApiV2(
this._adsManager,
this._paymentManager,
);
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,
},
);
}
/// POST /api/v2/ads/product/acquire/{key}
///
/// Confirms rewarded ad completion and grants product access to the user.
@Route.post('/ads/product/acquire/<key>')
Future<Response> acquireProductForAd(
Request request,
String key,
) async {
final user = request.user;
if (user == null) {
return _json(
{
'error': 'unauthorized',
'message': 'Authentication required',
},
statusCode: 401,
);
}
final body = await request.readAsString();
if (body.isEmpty) {
return _json(
{
'error': 'bad_request',
'message': 'Product payload is required',
},
statusCode: 400,
);
}
late final MnemoCardsProductDto productDto;
try {
productDto = MnemoCardsProductDto.fromJson(
jsonDecode(body) as Map<String, dynamic>,
);
} catch (_) {
return _json(
{
'error': 'bad_request',
'message': 'Invalid product payload',
},
statusCode: 400,
);
}
if (productDto.id == null) {
return _json(
{
'error': 'bad_request',
'message': 'Product id is required',
},
statusCode: 400,
);
}
final productModel = MnemoCardsProductModelBase.fromDto(productDto);
final expectedKey = _adsManager.getAdsKey([productModel], user);
if (expectedKey != key) {
return _json(
{
'error': 'forbidden',
'message': 'Invalid ads verification key',
},
statusCode: 403,
);
}
try {
final payment = PaymentModel.adView(
userId: user.id!,
date: DateTime.now(),
products: [productModel],
);
await _paymentManager.processPayment(payment);
} catch (error, stackTrace) {
print('Failed to process ad reward: $error $stackTrace');
return _json(
{
'error': 'internal_error',
'message': 'Failed to grant product',
},
statusCode: 500,
);
}
return _json(
{
'result': true,
'message': 'Product granted',
},
);
}
/// GET /api/v2/adsgram/reward?userId={userId}
///
/// Callback endpoint for Adsgram rewarded ad completion.
/// This endpoint is called by Adsgram when a user completes a rewarded ad.
@Route.get('/adsgram/reward')
Future<Response> adsgramRewardCallback(Request request) async {
final queryParams = request.requestedUri.queryParameters;
final userId = queryParams['userId'];
if (userId == null || userId.isEmpty) {
return _json(
{
'error': 'bad_request',
'message': 'userId parameter is required',
},
statusCode: 400,
);
}
// TODO: Here we would validate the Adsgram callback
// and potentially grant rewards directly, but for now
// we'll just acknowledge the callback
print('Adsgram reward callback received for user: $userId');
return _json(
{
'result': true,
'message': 'Reward callback acknowledged',
},
);
}
Router get router => _$AdsApiV2Router(this);
}

View file

@ -61,7 +61,7 @@ class AuthApiV2 {
/// POST /api/v2/auth/oauth/google
/// Authenticate with Google ID token
@Route.post('/auth/oauth/google')
@OpenApiRoute()
@OpenApiRouteHttp()
Future<Response> authenticateGoogle(Request request) async {
try {
final body = await request.readAsString();
@ -113,7 +113,7 @@ class AuthApiV2 {
/// Called by the Telegram bot when user requests a code
/// Body: { telegramUserId: string, telegramUsername?: string, firstName?: string, lastName?: string }
@Route.post('/auth/telegram/generate-code')
@OpenApiRoute()
@OpenApiRouteHttp()
Future<Response> generateTelegramCode(Request request) async {
try {
final body = await request.readAsString();
@ -141,7 +141,7 @@ class AuthApiV2 {
/// POST /api/v2/auth/telegram/web-code
/// Generates a new Telegram authentication code initiated from the web app
@Route.post('/auth/telegram/web-code')
@OpenApiRoute()
@OpenApiRouteHttp()
Future<Response> createWebTelegramCode(Request request) async {
try {
final code = _telegramAuthCodeService.createWebCode();
@ -163,7 +163,7 @@ class AuthApiV2 {
/// Called by Telegram bot when user sends a code generated via the web app
/// Body: { code: string, telegramUserId: string, telegramUsername?: string, firstName?: string, lastName?: string }
@Route.post('/auth/telegram/claim-code')
@OpenApiRoute()
@OpenApiRouteHttp()
Future<Response> claimTelegramCode(Request request) async {
try {
final body = await request.readAsString();
@ -209,7 +209,7 @@ class AuthApiV2 {
/// GET /api/v2/auth/telegram/code-status/<code>
/// Returns current status for a Telegram authentication code
@Route.get('/auth/telegram/code-status/<code>')
@OpenApiRoute()
@OpenApiRouteHttp()
Future<Response> getTelegramCodeStatus(Request request, String code) async {
try {
final status = _telegramAuthCodeService.getCodeStatus(code);
@ -234,7 +234,7 @@ class AuthApiV2 {
/// Authenticate with Telegram auth code from bot
/// Body: { code: string }
@Route.post('/auth/telegram/web-app')
@OpenApiRoute()
@OpenApiRouteHttp()
Future<Response> authenticateTelegramWebApp(Request request) async {
try {
final body = await request.readAsString();
@ -276,7 +276,7 @@ class AuthApiV2 {
}
@Route.post('/auth/oauth/telegram')
@OpenApiRoute()
@OpenApiRouteHttp()
Future<Response> authenticateTelegram(Request request) async {
try {
final body = await request.readAsString();
@ -330,7 +330,7 @@ class AuthApiV2 {
/// POST /api/v2/auth/refresh
/// Refresh access token using refresh token
@Route.post('/auth/refresh')
@OpenApiRoute()
@OpenApiRouteHttp()
Future<Response> refreshToken(Request request) async {
try {
final body = await request.readAsString();
@ -343,16 +343,12 @@ class AuthApiV2 {
// Verify and extract user from refresh token
final userIdStr = await _jwtService.verifyRefreshToken(refreshToken);
if (userIdStr == null) {
if (userIdStr == null || userIdStr.isEmpty) {
return _unauthorized('Invalid refresh token');
}
// Get user
final userId = int.tryParse(userIdStr);
if (userId == null) {
return _unauthorized('Invalid user ID');
}
final user = await _userManager.fetchUser(userId);
final user = await _userManager.fetchUser(userIdStr);
if (user == null) {
return _unauthorized('User not found');
}
@ -374,7 +370,7 @@ class AuthApiV2 {
/// GET /api/v2/auth/me
/// Get current authenticated user (requires Bearer token)
@Route.get('/auth/me')
@OpenApiRoute()
@OpenApiRouteHttp()
Future<Response> getCurrentUser(Request request) async {
final user = request.user;
if (user == null) {
@ -389,7 +385,7 @@ class AuthApiV2 {
/// Logout and invalidate tokens
/// Body (optional): { refreshToken: string }
@Route.post('/auth/logout')
@OpenApiRoute()
@OpenApiRouteHttp()
Future<Response> logout(Request request) async {
try {
final user = request.user;

View file

@ -8,26 +8,14 @@ part of 'auth_api_v2.dart';
Router _$AuthApiV2Router(AuthApiV2 service) {
final router = Router();
router.add(
'POST',
r'/auth/oauth/google',
service.authenticateGoogle,
);
router.add('POST', r'/auth/oauth/google', service.authenticateGoogle);
router.add(
'POST',
r'/auth/telegram/generate-code',
service.generateTelegramCode,
);
router.add(
'POST',
r'/auth/telegram/web-code',
service.createWebTelegramCode,
);
router.add(
'POST',
r'/auth/telegram/claim-code',
service.claimTelegramCode,
);
router.add('POST', r'/auth/telegram/web-code', service.createWebTelegramCode);
router.add('POST', r'/auth/telegram/claim-code', service.claimTelegramCode);
router.add(
'GET',
r'/auth/telegram/code-status/<code>',
@ -38,25 +26,9 @@ Router _$AuthApiV2Router(AuthApiV2 service) {
r'/auth/telegram/web-app',
service.authenticateTelegramWebApp,
);
router.add(
'POST',
r'/auth/oauth/telegram',
service.authenticateTelegram,
);
router.add(
'POST',
r'/auth/refresh',
service.refreshToken,
);
router.add(
'GET',
r'/auth/me',
service.getCurrentUser,
);
router.add(
'POST',
r'/auth/logout',
service.logout,
);
router.add('POST', r'/auth/oauth/telegram', service.authenticateTelegram);
router.add('POST', r'/auth/refresh', service.refreshToken);
router.add('GET', r'/auth/me', service.getCurrentUser);
router.add('POST', r'/auth/logout', service.logout);
return router;
}

View file

@ -179,15 +179,14 @@ Middleware authorizeV2(UserManager userManager, JwtService jwtService) {
}
// Get user from database
final userIdInt = int.tryParse(userId);
if (userIdInt == null) {
if (userId.isEmpty) {
return Response(
401,
headers: {'Content-Type': 'application/json'},
body: '{"error":"Unauthorized","message":"Invalid user ID"}',
);
}
final user = await userManager.fetchUser(userIdInt);
final user = await userManager.fetchUser(userId);
if (user == null) {
return Response(
401,

View file

@ -8,16 +8,8 @@ part of 'discounts_api_v2.dart';
Router _$DiscountsApiV2Router(DiscountsApiV2 service) {
final router = Router();
router.add(
'GET',
r'/admin/discounts',
service.listDiscountCampaigns,
);
router.add(
'POST',
r'/admin/discounts',
service.addDiscountCampaign,
);
router.add('GET', r'/admin/discounts', service.listDiscountCampaigns);
router.add('POST', r'/admin/discounts', service.addDiscountCampaign);
router.add(
'DELETE',
r'/admin/discounts/<id>',

View file

@ -1,124 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:injectable/injectable.dart';
import 'package:mnemo_cards_backend/packs/pack_manager.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf_open_api/shelf_open_api.dart';
import 'package:shelf_router/shelf_router.dart';
part 'games_api_v2.g.dart';
/// Games API v2
///
/// RESTful endpoints for managing games and game assets
@lazySingleton
class GamesApiV2 {
GamesApiV2();
Response _ok(Object? object, {Map<String, String> headers = const {}}) =>
Response.ok(
object == null ? null : jsonEncode(object),
headers: {
'Content-Type': 'application/json',
...headers,
},
);
Response _notFound([String? message]) => Response.notFound(
jsonEncode({
'error': 'Not Found',
'message': message ?? 'Resource not found',
}),
headers: {'Content-Type': 'application/json'},
);
Response _internalServerError([String? message]) => Response(
500,
body: jsonEncode({
'error': 'Internal Server Error',
'message': message ?? 'An error occurred',
}),
headers: {'Content-Type': 'application/json'},
);
/// GET /api/v2/games
/// Get all available games
/// Returns list of games with metadata
@Route.get('/games')
@OpenApiRoute()
Future<Response> getGames(Request request) async {
try {
// List of available games
// This can be extended to read from database or config file
String? imageBase64;
try {
imageBase64 = await 'cards/1_el perro.png'.smallBase64Image;
} catch (e) {
// If image loading fails (e.g., in test environment), use empty string
imageBase64 = '';
}
final games = [
GameDto(
id: 'funny_letters',
title: 'Funny letters',
subtitle: 'Learn letters with fun games',
color: '#FF0000',
version: '0',
imageBase64: imageBase64,
),
// Add more games here as they become available
];
return _ok(games.map((g) => g.toJson()).toList());
} catch (e, s) {
print('Error in getGames: $e\n$s');
return _internalServerError(e.toString());
}
}
/// GET /api/v2/games/{gameId}/assets
/// Get game assets
/// Returns game assets file (zip) or asset info
@Route.get('/games/<gameId>/assets')
@OpenApiRoute()
Future<Response> getGameAssets(Request request, String gameId) async {
try {
// Only 'funny_letters' is currently available
// This can be extended to support multiple games
if (gameId != 'funny_letters') {
return _notFound('Game not found');
}
try {
final archiveFile = File(
'${PackManager.assetsDirectory.path}/games/funny_letters/assets.zip',
);
if (await archiveFile.exists()) {
final data = await archiveFile.readAsBytes();
return Response.ok(
data,
headers: {
'Content-Type': 'application/zip',
'Content-Disposition': 'attachment; filename="assets.zip"',
'Cache-Control': 'public, max-age=86400', // Cache for 1 day
},
);
}
} catch (e) {
// If assets directory doesn't exist (e.g., in test environment)
return _notFound('Game assets not found');
}
return _notFound('Game assets not found');
} catch (e, s) {
print('Error in getGameAssets: $e\n$s');
return _internalServerError(e.toString());
}
}
Router get router => _$GamesApiV2Router(this);
}

View file

@ -229,7 +229,7 @@ class JwtService {
Future<void> _storeRefreshToken(
String jti,
int userId,
String userId,
DateTime createdAt,
DateTime expiresAt,
) async {

View file

@ -5,10 +5,10 @@ import 'package:injectable/injectable.dart';
import 'package:mnemo_cards_backend/api/authorize/acl_types.dart';
import 'package:mnemo_cards_backend/api/authorize/access_service.dart';
import 'package:mnemo_cards_backend/api/authorize/helpers.dart';
import 'package:mnemo_cards_backend/main.dart' as backend_main;
import 'package:mnemo_cards_backend/packs/card_model_extension.dart';
import 'package:mnemo_cards_backend/packs/pack_manager.dart';
import 'package:mnemo_cards_backend/packs/voice_model_extension.dart';
import 'package:mnemo_cards_backend/database/database.dart' hide VoiceModel;
import 'package:mnemo_cards_backend/database/database.dart' as drift show VoiceModel;
import 'package:mnemo_cards_backend/packs/card_pack_drift_extension.dart';
import 'package:mnemo_cards_backend/packs/pack_manager.dart' show PackManager, PackManagerUtils;
import 'package:mnemo_cards_backend/tests/test_manager.dart';
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
@ -25,10 +25,12 @@ part 'packs_api_v2.g.dart';
class PacksApiV2 {
final PackManager _packManager;
final TestManager _testManager;
final AppDatabase _db;
PacksApiV2(
this._packManager,
this._testManager,
this._db,
);
Response _ok(Object? object, {Map<String, String> headers = const {}}) =>
@ -113,7 +115,7 @@ class PacksApiV2 {
/// Get all pack previews with pagination
/// Query params: ?search=term&language=lang&page=1&limit=20
@Route.get('/packs')
@OpenApiRoute()
@OpenApiRouteHttp()
Future<Response> getPacks(Request request) async {
try {
final queryParams = request.requestedUri.queryParameters;
@ -186,11 +188,10 @@ class PacksApiV2 {
/// Get pack details by ID
/// Returns full pack details with purchase status if authenticated
@Route.get('/packs/<packId>')
@OpenApiRoute()
@OpenApiRouteHttp()
Future<Response> getPack(Request request, String packId) async {
try {
final packIdInt = int.tryParse(packId);
if (packIdInt == null) {
if (packId.isEmpty) {
return _badRequest('Invalid pack ID');
}
@ -206,39 +207,26 @@ class PacksApiV2 {
try {
await accessService.requirePack(
PackAccessAction.view,
packId: packIdInt,
packId: packId,
user: user,
);
} on AccessDenied catch (e) {
if (user == null && e.status == 401) {
final buyDto = await _packManager.getPublicBuyPage(packId);
if (buyDto != null) {
return _ok(buyDto.toJson());
}
// Fall through to standard access denied handling when pack not found.
}
return _handleAccessDenied(e);
}
final pack = await _packManager.getPack(packId, user);
final pack = await _packManager.getPack(packId);
if (pack == null) {
if (user != null) {
final buyDto = await _packManager.getBuyPage(packId, user);
if (buyDto != null) {
return _ok(buyDto.toJson());
}
}
return _notFound('Pack not found');
}
final packDto = await _packManager.getPackDto(packId, user);
// Include purchase status in response if user is authenticated
final packJson = pack.toJson();
final packJson = packDto.toJson();
if (user != null) {
await user.packs.load();
final packModel = await backend_main.isar.cardPackModels.get(packIdInt);
final isPurchased = packModel != null &&
(user.packs.contains(packId) ||
(await _hasSubscriptionAccess(user)));
final isPurchased = await _isPackPurchased(user.id!, packId) ||
(await _hasSubscriptionAccess(user));
packJson['isPurchased'] = isPurchased;
}
@ -256,44 +244,34 @@ class PacksApiV2 {
/// Returns pack purchase details (includes rewarded ads offer when available)
/// Works for both authenticated and unauthenticated users
@Route.get('/packs/<packId>/buy')
@OpenApiRoute()
@OpenApiRouteHttp()
Future<Response> getPackBuyPage(Request request, String packId) async {
try {
// Validate pack ID format first
final packIdInt = int.tryParse(packId);
if (packIdInt == null) {
if (packId.isEmpty) {
return _badRequest('Invalid pack ID');
}
final user = request.user;
// For unauthenticated users, return public buy page
if (user == null) {
final buyDto = await _packManager.getPublicBuyPage(packId);
if (buyDto == null) {
return _notFound('Pack not found');
}
return _ok(buyDto.toJson());
// Check if pack exists
final pack = await _packManager.getPack(packId);
if (pack == null) {
return _notFound('Pack not found');
}
// For authenticated users, check if they already own the pack
try {
final buyDto = await _packManager.getBuyPage(packId, user);
if (buyDto == null) {
if (user != null) {
final isPurchased = await _isPackPurchased(user.id!, packId) ||
(await _hasSubscriptionAccess(user));
if (isPurchased) {
return _conflict('Pack already purchased');
}
return _ok(buyDto.toJson());
} catch (e) {
// getBuyPage may throw if pack doesn't exist (via _fetchPackModel)
// _fetchPackModel uses ! operator which throws on null
final errorString = e.toString();
if (errorString.contains('Null check') ||
errorString.contains('null') ||
e is StateError) {
return _notFound('Pack not found');
}
rethrow;
}
// Return pack preview as buy page
final packDto = await _packManager.getPackDto(packId, user);
return _ok(packDto.toJson());
} on FormatException catch (_) {
return _badRequest('Invalid pack ID');
} on StateError catch (_) {
@ -308,11 +286,10 @@ class PacksApiV2 {
/// Get all cards in a pack
/// Supports pagination via query params: ?page=1&limit=20
@Route.get('/packs/<packId>/cards')
@OpenApiRoute()
@OpenApiRouteHttp()
Future<Response> getPackCards(Request request, String packId) async {
try {
final packIdInt = int.tryParse(packId);
if (packIdInt == null) {
if (packId.isEmpty) {
return _badRequest('Invalid pack ID');
}
@ -325,18 +302,17 @@ class PacksApiV2 {
// Use access control system
await accessService.requirePack(
PackAccessAction.cards,
packId: packIdInt,
packId: packId,
user: request.user,
);
final packModel = await backend_main.isar.cardPackModels.get(packIdInt);
if (packModel == null) {
final pack = await _db.packDao.getPackById(packId);
if (pack == null) {
return _notFound('Pack not found');
}
// Load cards
await packModel.cards.load();
final cards = packModel.cards.toList();
// Get cards for pack
final cards = await _db.packDao.getPackCards(packId);
// Parse pagination
final queryParams = request.requestedUri.queryParameters;
@ -356,8 +332,20 @@ class PacksApiV2 {
final offset = (page - 1) * limit;
final paginatedCards = cards.skip(offset).take(limit).toList();
// Convert to DTOs using the list extension which handles ordering
final cardDtos = paginatedCards.toDtosList(packModel.cardsOrder);
// Get voices for cards
final cardIds = paginatedCards.map((c) => c.id).toList();
final allVoices = <drift.VoiceModel>[];
for (final cardId in cardIds) {
final voices = await _db.packDao.getCardVoices(cardId);
allVoices.addAll(voices);
}
// Convert to DTOs
final cardDtos = await Future.wait(
paginatedCards.map((card) => card.toDto(
allVoices.where((v) => v.cardId == card.id).toList(),
)),
);
return _ok({
'items': cardDtos.map((c) => c.toJson()).toList(),
@ -382,47 +370,45 @@ class PacksApiV2 {
/// Images are accessible for enabled packs even without authentication
/// to allow image preview in public pack listings
@Route.get('/packs/<packId>/cards/<cardId>/image')
@OpenApiRoute()
@OpenApiRouteHttp()
Future<Response> getCardImage(
Request request,
String packId,
String cardId,
) async {
try {
final cardIdInt = int.tryParse(cardId);
if (cardIdInt == null) {
if (cardId.isEmpty) {
return _badRequest('Invalid card ID');
}
final packIdInt = int.tryParse(packId);
if (packIdInt == null) {
if (packId.isEmpty) {
return _badRequest('Invalid pack ID');
}
// Check if pack exists and is enabled
// We allow access to images for enabled packs even without auth
// to support image previews in public listings
final packModel = await backend_main.isar.cardPackModels.get(packIdInt);
if (packModel == null || !packModel.enabled) {
final pack = await _db.packDao.getPackById(packId);
if (pack == null || !pack.enabled) {
return _notFound('Pack not found or not enabled');
}
// Get card and verify it belongs to the pack
final card = await backend_main.isar.gameCardModels.get(cardIdInt);
final card = await _db.packDao.getCardById(cardId);
if (card == null || card.image.isEmpty) {
return _notFound('Card or image not found');
}
// Verify card belongs to this pack
await card.packs.load();
final cardPacks = card.packs.toList();
final belongsToPack = cardPacks.any((p) => p.id == packIdInt);
final packCards = await _db.packDao.getPackCards(packId);
final belongsToPack = packCards.any((c) => c.id == cardId);
if (!belongsToPack) {
return _notFound('Card does not belong to this pack');
}
// Get image bytes using extension method
final imageBase64 = await card.image.mediumBase64Image;
// Get image bytes - card.image is already base64 or path
// For now, assume it's base64 encoded or needs to be loaded
final imageBase64 = card.image;
if (imageBase64.isEmpty) {
return _notFound('Image not found');
@ -448,50 +434,47 @@ class PacksApiV2 {
/// Get card voices metadata
/// Returns JSON list of VoiceDto
@Route.get('/packs/<packId>/cards/<cardId>/voices')
@OpenApiRoute()
@OpenApiRouteHttp()
Future<Response> getCardVoices(
Request request,
String packId,
String cardId,
) async {
try {
final cardIdInt = int.tryParse(cardId);
if (cardIdInt == null) {
if (cardId.isEmpty) {
return _badRequest('Invalid card ID');
}
final packIdInt = int.tryParse(packId);
if (packIdInt == null) {
if (packId.isEmpty) {
return _badRequest('Invalid pack ID');
}
final packModel = await backend_main.isar.cardPackModels.get(packIdInt);
if (packModel == null || !packModel.enabled) {
final pack = await _db.packDao.getPackById(packId);
if (pack == null || !pack.enabled) {
return _notFound('Pack not found or not enabled');
}
final card = await backend_main.isar.gameCardModels.get(cardIdInt);
final card = await _db.packDao.getCardById(cardId);
if (card == null) {
return _notFound('Card not found');
}
await card.packs.load();
final cardPacks = card.packs.toList();
final belongsToPack = cardPacks.any((p) => p.id == packIdInt);
// Verify card belongs to this pack
final packCards = await _db.packDao.getPackCards(packId);
final belongsToPack = packCards.any((c) => c.id == cardId);
if (!belongsToPack) {
return _notFound('Card does not belong to this pack');
}
await card.voices.load();
final voices = card.voices.toList();
// Get voices for card
final voices = await _db.packDao.getCardVoices(cardId);
if (voices.isEmpty) {
return _ok({'items': <Map<String, Object?>>[]});
}
final items = voices
.where((voice) => voice.id != null)
.map(
(voice) => voice.toDto(url: '/api/v2/voice/${voice.id}').toJson(),
(voice) => _voiceModelToDto(voice, url: '/api/v2/voice/${voice.id}').toJson(),
)
.toList();
@ -505,40 +488,49 @@ class PacksApiV2 {
/// GET /api/v2/voice/{voiceId}
/// Returns audio/mp3 bytes for the voice file
@Route.get('/voice/<voiceId>')
@OpenApiRoute()
@OpenApiRouteHttp()
Future<Response> getVoiceFile(
Request request,
String voiceId,
) async {
try {
final voiceIdInt = int.tryParse(voiceId);
if (voiceIdInt == null) {
if (voiceId.isEmpty) {
return _badRequest('Invalid voice ID');
}
final voice = await backend_main.isar.voiceModels.get(voiceIdInt);
if (voice == null || voice.path.isEmpty) {
final voice = await _db.packDao.getVoiceById(voiceId);
if (voice == null || voice.voiceUrl.isEmpty) {
return _notFound('Voice not found');
}
await voice.cards.load();
final cards = voice.cards.toList();
if (cards.isEmpty) {
return _notFound('Voice not attached to any card');
// Get cards that use this voice by checking CardVoices junction table
// We need to find cards that have this voice and belong to enabled packs
final allPacks = await _db.packDao.getAllPacks(enabledOnly: true);
bool hasEnabledPack = false;
for (final pack in allPacks) {
final packCards = await _db.packDao.getPackCards(pack.id);
for (final card in packCards) {
final cardVoices = await _db.packDao.getCardVoices(card.id);
if (cardVoices.any((v) => v.id == voiceId)) {
hasEnabledPack = true;
break;
}
}
if (hasEnabledPack) break;
}
final hasEnabledPack = await _hasEnabledPack(cards);
if (!hasEnabledPack) {
return _notFound('Voice not available');
}
final sanitizedPath = _sanitizeVoicePath(voice.path);
final sanitizedPath = _sanitizeVoicePath(voice.voiceUrl);
if (sanitizedPath == null) {
return _badRequest('Invalid voice path');
}
final file = File(
'${PackManager.assetsDirectory.path}/voice/$sanitizedPath',
'${PackManagerUtils.assetsDirectory.path}/voice/$sanitizedPath',
);
if (!file.existsSync()) {
@ -572,26 +564,18 @@ class PacksApiV2 {
return normalized;
}
Future<bool> _hasEnabledPack(List<GameCardModel> cards) async {
for (final card in cards) {
await card.packs.load();
for (final pack in card.packs) {
if (pack.enabled) {
return true;
}
}
}
return false;
/// Check if user purchased a pack
Future<bool> _isPackPurchased(String userId, String packId) async {
return await _db.userDao.hasPackAccess(userId, packId);
}
/// GET /api/v2/packs/{packId}/tests
/// Get tests for a pack
@Route.get('/packs/<packId>/tests')
@OpenApiRoute()
@OpenApiRouteHttp()
Future<Response> getPackTests(Request request, String packId) async {
try {
final packIdInt = int.tryParse(packId);
if (packIdInt == null) {
if (packId.isEmpty) {
return _badRequest('Invalid pack ID');
}
@ -604,7 +588,7 @@ class PacksApiV2 {
// Use access control system
await accessService.requirePack(
PackAccessAction.tests,
packId: packIdInt,
packId: packId,
user: request.user,
);
@ -621,17 +605,28 @@ class PacksApiV2 {
);
}
// Get pack model
final packModel =
await _packManager.getPackModelIfAvailable(user, packId);
if (packModel == null || packModel.cards.isEmpty) {
return _notFound('Pack not found or is empty');
// Get pack and verify it exists and user has access
final pack = await _packManager.getPack(packId);
if (pack == null) {
return _notFound('Pack not found');
}
// Get tests for pack
final tests = await _testManager.fetchPackTests(user, packModel);
// Get cards for pack
final cards = await _packManager.getCards(packId);
if (cards.isEmpty) {
return _notFound('Pack is empty');
}
return _ok(tests.map((t) => t.toJson()).toList());
// Get tests for pack - fetchPackTests needs CardPackModel, but we can create a minimal one
// or modify TestManager to work with Drift CardPack
// For now, let's get tests directly from database
final packTests = await _db.testDao.getTestsByPackId(packId);
final tests = await Future.wait(
packTests.map((test) => _testManager.fetchTest(test.id, user)),
);
final validTests = tests.whereType<TestDto>().toList();
return _ok(validTests.map((t) => t.toJson()).toList());
} catch (e, s) {
if (e is AccessDenied) {
return _handleAccessDenied(e);
@ -643,10 +638,23 @@ class PacksApiV2 {
/// Helper method to check if user has subscription access
Future<bool> _hasSubscriptionAccess(UserModel user) async {
await user.subscriptionModel.load();
return user.subscriptionModel.value?.features
.contains(SubscriptionFeatureEnum.packs) ==
true;
// Check subscription features from user model
final subscription = user.subscriptionModel;
if (subscription == null) {
return false;
}
return subscription.features.contains(SubscriptionFeatureEnum.packs);
}
/// Convert Drift VoiceModel to VoiceDto
VoiceDto _voiceModelToDto(drift.VoiceModel voice, {String? url}) {
return VoiceDto(
id: voice.id,
phrase: '', // Drift VoiceModel doesn't have phrase
path: voice.voiceUrl,
speaker: voice.language,
url: url,
);
}
Router get router => _$PacksApiV2Router(this);

View file

@ -0,0 +1,28 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'packs_api_v2.dart';
// **************************************************************************
// ShelfRouterGenerator
// **************************************************************************
Router _$PacksApiV2Router(PacksApiV2 service) {
final router = Router();
router.add('GET', r'/packs', service.getPacks);
router.add('GET', r'/packs/<packId>', service.getPack);
router.add('GET', r'/packs/<packId>/buy', service.getPackBuyPage);
router.add('GET', r'/packs/<packId>/cards', service.getPackCards);
router.add(
'GET',
r'/packs/<packId>/cards/<cardId>/image',
service.getCardImage,
);
router.add(
'GET',
r'/packs/<packId>/cards/<cardId>/voices',
service.getCardVoices,
);
router.add('GET', r'/voice/<voiceId>', service.getVoiceFile);
router.add('GET', r'/packs/<packId>/tests', service.getPackTests);
return router;
}

View file

@ -61,7 +61,7 @@ class PromocodesApiV2 {
return _unauthorized();
}
final campaigns = await _promoCodesManager.listAvailablePromocodes(user);
final campaigns = await _promoCodesManager.listAvailablePromocodes(user.id!);
return _json({
'campaigns': campaigns.map((c) => c.toJson()).toList(),
});
@ -119,7 +119,7 @@ class PromocodesApiV2 {
}
final dto = PromoCodeDto(code: code);
final result = await _promoCodesManager.applyPromoCode(dto, user);
final result = await _promoCodesManager.applyPromoCode(dto, user.id!);
return _json(result.toJson());
}

View file

@ -8,36 +8,12 @@ part of 'promocodes_api_v2.dart';
Router _$PromocodesApiV2Router(PromocodesApiV2 service) {
final router = Router();
router.add(
'GET',
r'/promocodes',
service.listPromocodes,
);
router.add(
'GET',
r'/promocodes/<code>/validate',
service.validatePromocode,
);
router.add(
'POST',
r'/promocodes/<code>/apply',
service.applyPromocode,
);
router.add(
'GET',
r'/admin/promocodes',
service.listPromoCodeCampaigns,
);
router.add(
'GET',
r'/admin/promocodes/<id>',
service.getPromoCodeCampaign,
);
router.add(
'POST',
r'/admin/promocodes',
service.upsertPromoCodeCampaign,
);
router.add('GET', r'/promocodes', service.listPromocodes);
router.add('GET', r'/promocodes/<code>/validate', service.validatePromocode);
router.add('POST', r'/promocodes/<code>/apply', service.applyPromocode);
router.add('GET', r'/admin/promocodes', service.listPromoCodeCampaigns);
router.add('GET', r'/admin/promocodes/<id>', service.getPromoCodeCampaign);
router.add('POST', r'/admin/promocodes', service.upsertPromoCodeCampaign);
router.add(
'DELETE',
r'/admin/promocodes/<id>',

View file

@ -1,308 +0,0 @@
import 'dart:convert';
import 'package:injectable/injectable.dart';
import 'package:mnemo_cards_backend/api/authorize/helpers.dart';
import 'package:mnemo_cards_backend/api/purchase/payment_manager.dart';
import 'package:mnemo_cards_backend/packs/pack_manager.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf_open_api/shelf_open_api.dart';
import 'package:shelf_router/shelf_router.dart';
part 'purchases_api_v2.g.dart';
/// Purchases API v2
///
/// RESTful endpoints for managing purchases and payments
@lazySingleton
class PurchasesApiV2 {
final PaymentManager _paymentManager;
final PackManager _packManager;
PurchasesApiV2(
this._paymentManager,
this._packManager,
);
Response _ok(Object? object, {Map<String, String> headers = const {}}) =>
Response.ok(
object == null ? null : jsonEncode(object),
headers: {
'Content-Type': 'application/json',
...headers,
},
);
Response _badRequest(String message) => Response.badRequest(
body: jsonEncode({'error': 'Bad Request', 'message': message}),
headers: {'Content-Type': 'application/json'},
);
Response _notFound([String? message]) => Response.notFound(
jsonEncode({
'error': 'Not Found',
'message': message ?? 'Resource not found',
}),
headers: {'Content-Type': 'application/json'},
);
Response _unauthorized([String? message]) => Response(
401,
body: jsonEncode({
'error': 'Unauthorized',
'message': message ?? 'Authentication required',
}),
headers: {'Content-Type': 'application/json'},
);
Response _internalServerError([String? message]) => Response(
500,
body: jsonEncode({
'error': 'Internal Server Error',
'message': message ?? 'An error occurred',
}),
headers: {'Content-Type': 'application/json'},
);
/// POST /api/v2/purchases/packs/{packId}
/// Create purchase intent for a pack
/// Returns purchase info including payment URL for YooKassa
@Route.post('/purchases/packs/<packId>')
@OpenApiRoute()
Future<Response> createPackPurchase(
Request request,
String packId,
) async {
try {
final user = request.user;
if (user == null) {
return _unauthorized('Authentication required to create purchase');
}
// Get buy page info (this returns pack details if pack exists and user doesn't own it)
// getBuyPage returns null if user already owns the pack, or throws if pack doesn't exist
try {
final pack = await _packManager.getBuyPage(packId, user);
if (pack == null) {
// User already owns the pack
return _badRequest('Pack already purchased');
}
} catch (e) {
// Pack doesn't exist (getBuyPage internally calls _fetchPackModel which throws if pack not found)
return _notFound('Pack not found');
}
// Get buy page again to create payment (we know it exists now)
final pack = await _packManager.getBuyPage(packId, user);
if (pack == null) {
return _badRequest('Pack already purchased');
}
// Create payment (currently only YooKassa for web)
final paymentDto = await _paymentManager.createYooMoneyPayment(
packId,
MnemoCardsProductType.pack,
user,
);
if (paymentDto == null) {
return _internalServerError('Failed to create payment');
}
return _ok(paymentDto.toJson());
} catch (e, s) {
print('Error in createPackPurchase: $e\n$s');
return _internalServerError(e.toString());
}
}
/// GET /api/v2/purchases/packs/{packId}/status
/// Check if pack is purchased by the authenticated user
@Route.get('/purchases/packs/<packId>/status')
@OpenApiRoute()
Future<Response> getPackPurchaseStatus(
Request request,
String packId,
) async {
try {
final user = request.user;
if (user == null) {
return _unauthorized('Authentication required');
}
final packIdInt = int.tryParse(packId);
if (packIdInt == null) {
return _badRequest('Invalid pack ID');
}
// Check if pack exists - we can use PackManager to verify
// Since getPack requires user to own the pack (except pack 10), we check differently
try {
// Try to access pack - if packId is 10, it's always available
// For other packs, we can't use getPack since user might not own it
// So we check via getBuyPage which works for any pack user doesn't own
if (packId != '10') {
await _packManager.getBuyPage(packId, user);
// If buyPack is null, user owns it, so pack exists
// If buyPack is not null, pack exists and user doesn't own it
// If it throws, pack doesn't exist
} else {
// Pack 10 is special - always available
final pack = await _packManager.getPack('10', user);
if (pack == null) {
return _notFound('Pack not found');
}
}
} catch (e) {
return _notFound('Pack not found');
}
// Check if user owns the pack
await user.packs.load();
final isPurchased = user.packs.any((p) => p.id == packIdInt);
// Check subscription access
bool hasSubscriptionAccess = false;
await user.subscriptionModel.load();
final subscription = user.subscriptionModel.value;
if (subscription != null) {
final now = DateTime.now();
final isActive = subscription.finish.isAfter(now);
if (isActive) {
final features = subscription.features;
hasSubscriptionAccess =
features.contains(SubscriptionFeatureEnum.packs);
}
}
return _ok({
'packId': packId,
'isPurchased': isPurchased || hasSubscriptionAccess,
'purchased': isPurchased,
'hasSubscriptionAccess': hasSubscriptionAccess,
});
} catch (e, s) {
print('Error in getPackPurchaseStatus: $e\n$s');
return _internalServerError(e.toString());
}
}
/// POST /api/v2/purchases/payments
/// Create a payment
/// Currently supports YooKassa for web payments
@Route.post('/purchases/payments')
@OpenApiRoute()
Future<Response> createPayment(Request request) async {
try {
final user = request.user;
if (user == null) {
return _unauthorized('Authentication required to create payment');
}
// Parse request body
final bodyString = await request.readAsString();
if (bodyString.isEmpty) {
return _badRequest('Request body is required');
}
final bodyJson = jsonDecode(bodyString) as Map<String, dynamic>;
final productId = bodyJson['productId'] as String?;
final productTypeStr = bodyJson['productType'] as String?;
if (productId == null) {
return _badRequest('productId is required');
}
final productType = productTypeStr != null
? MnemoCardsProductType.values.firstWhere(
(e) => e.name == productTypeStr,
orElse: () => MnemoCardsProductType.pack,
)
: MnemoCardsProductType.pack;
// Create payment (currently only YooKassa for web)
final paymentDto = await _paymentManager.createYooMoneyPayment(
productId,
productType,
user,
);
if (paymentDto == null) {
return _badRequest(
'Failed to create payment. Product may not be available for purchase.',
);
}
return _ok(paymentDto.toJson());
} catch (e, s) {
print('Error in createPayment: $e\n$s');
return _internalServerError(e.toString());
}
}
/// GET /api/v2/purchases/payments/{paymentId}/verify
/// Verify payment status
/// Updates user purchases on success
@Route.get('/purchases/payments/<paymentId>/verify')
@OpenApiRoute()
Future<Response> verifyPayment(
Request request,
String paymentId,
) async {
try {
final user = request.user;
if (user == null) {
return _unauthorized('Authentication required to verify payment');
}
// Get product info from query params
final queryParams = request.url.queryParameters;
final productId = queryParams['productId'];
final productTypeStr = queryParams['productType'];
if (productId == null) {
return _badRequest('productId query parameter is required');
}
final productType = productTypeStr != null
? MnemoCardsProductType.values.firstWhere(
(e) => e.name == productTypeStr,
orElse: () => MnemoCardsProductType.pack,
)
: MnemoCardsProductType.pack;
// Verify payment with YooKassa
final isValid = await _paymentManager.checkYookassaPayment(
MnemoCardsProductDto(id: productId, type: productType),
paymentId,
user,
);
if (isValid) {
// Payment was valid and processed
// The PaymentManager already updates user purchases
return _ok({
'paymentId': paymentId,
'status': 'verified',
'result': true,
});
} else {
return _ok({
'paymentId': paymentId,
'status': 'failed',
'result': false,
});
}
} catch (e, s) {
print('Error in verifyPayment: $e\n$s');
return _internalServerError(e.toString());
}
}
Router get router => _$PurchasesApiV2Router(this);
}

View file

@ -54,7 +54,7 @@ class SubscriptionsApiV2 {
/// List available subscription plans
/// Returns all available subscription plans. Authentication is optional.
@Route.get('/subscriptions/plans')
@OpenApiRoute()
@OpenApiRouteHttp()
Future<Response> getPlans(Request request) async {
try {
final plans = await _subscriptionManager.getAllSubscriptionPlans();
@ -74,7 +74,7 @@ class SubscriptionsApiV2 {
/// POST /api/v2/subscriptions/purchase
/// Purchase a subscription
@Route.post('/subscriptions/purchase')
@OpenApiRoute()
@OpenApiRouteHttp()
Future<Response> purchase(Request request) async {
try {
final user = request.user;
@ -85,7 +85,7 @@ class SubscriptionsApiV2 {
final body = await request.readAsString();
final data = jsonDecode(body) as Map<String, dynamic>;
final planId = data['planId'] as int?;
final planId = data['planId'] as String?;
if (planId == null) {
return _badRequest('planId is required');
}
@ -110,7 +110,7 @@ class SubscriptionsApiV2 {
/// GET /api/v2/subscriptions/status
/// Get current user subscription status
@Route.get('/subscriptions/status')
@OpenApiRoute()
@OpenApiRouteHttp()
Future<Response> getStatus(Request request) async {
try {
final user = request.user;
@ -141,7 +141,7 @@ class SubscriptionsApiV2 {
/// POST /api/v2/subscriptions/cancel
/// Cancel users subscription
@Route.post('/subscriptions/cancel')
@OpenApiRoute()
@OpenApiRouteHttp()
Future<Response> cancel(Request request) async {
try {
final user = request.user;

View file

@ -8,25 +8,9 @@ part of 'subscriptions_api_v2.dart';
Router _$SubscriptionsApiV2Router(SubscriptionsApiV2 service) {
final router = Router();
router.add(
'GET',
r'/subscriptions/plans',
service.getPlans,
);
router.add(
'POST',
r'/subscriptions/purchase',
service.purchase,
);
router.add(
'GET',
r'/subscriptions/status',
service.getStatus,
);
router.add(
'POST',
r'/subscriptions/cancel',
service.cancel,
);
router.add('GET', r'/subscriptions/plans', service.getPlans);
router.add('POST', r'/subscriptions/purchase', service.purchase);
router.add('GET', r'/subscriptions/status', service.getStatus);
router.add('POST', r'/subscriptions/cancel', service.cancel);
return router;
}

View file

@ -132,10 +132,9 @@ class TasksApiV2 {
if (userId == null) return _unauthorized();
try {
final taskIdInt = int.tryParse(taskId);
if (taskIdInt == null) return _badRequest('Invalid task ID');
if (taskId.isEmpty) return _badRequest('Invalid task ID');
final task = await _taskManager.getUserTask(userId, taskIdInt);
final task = await _taskManager.getUserTask(userId, taskId);
if (task == null) return _notFound('Task not found');
return _json({
@ -169,11 +168,10 @@ class TasksApiV2 {
if (userId == null) return _unauthorized();
try {
final taskIdInt = int.tryParse(taskId);
if (taskIdInt == null) return _badRequest('Invalid task ID');
if (taskId.isEmpty) return _badRequest('Invalid task ID');
// Start task using TaskManager
await _taskManager.startTask(userId, taskIdInt);
await _taskManager.startTask(userId, taskId);
return _json({'success': true, 'message': 'Task started successfully'});
} catch (e) {
@ -189,11 +187,10 @@ class TasksApiV2 {
if (userId == null) return _unauthorized();
try {
final taskIdInt = int.tryParse(taskId);
if (taskIdInt == null) return _badRequest('Invalid task ID');
if (taskId.isEmpty) return _badRequest('Invalid task ID');
// Complete task using TaskManager
await _taskManager.completeTask(userId, taskIdInt);
await _taskManager.completeTask(userId, taskId);
return _json({'success': true, 'message': 'Task completed successfully'});
} catch (e) {

View file

@ -8,35 +8,11 @@ part of 'tasks_api_v2.dart';
Router _$TasksApiV2Router(TasksApiV2 service) {
final router = Router();
router.add(
'GET',
r'/tasks',
service.getTasks,
);
router.add(
'GET',
r'/tasks/<taskId>',
service.getTask,
);
router.add(
'POST',
r'/tasks/<taskId>/start',
service.startTask,
);
router.add(
'POST',
r'/tasks/<taskId>/complete',
service.completeTask,
);
router.add(
'GET',
r'/users/me/tasks/progress',
service.getUserTaskProgress,
);
router.add(
'GET',
r'/tasks/categories',
service.getTaskCategories,
);
router.add('GET', r'/tasks', service.getTasks);
router.add('GET', r'/tasks/<taskId>', service.getTask);
router.add('POST', r'/tasks/<taskId>/start', service.startTask);
router.add('POST', r'/tasks/<taskId>/complete', service.completeTask);
router.add('GET', r'/users/me/tasks/progress', service.getUserTaskProgress);
router.add('GET', r'/tasks/categories', service.getTaskCategories);
return router;
}

View file

@ -57,7 +57,7 @@ class TelegramBotApiV2 {
/// GET /api/v2/telegram-bot/random-card
/// Get a random card from all available cards
@Route.get('/telegram-bot/random-card')
@OpenApiRoute()
@OpenApiRouteHttp()
Future<Response> getRandomCard(Request request) async {
try {
final cards = await _db.packDao.getAllCards();
@ -91,7 +91,7 @@ class TelegramBotApiV2 {
/// Check if user can share today (rate limiting)
/// Body: { telegramUserId: string, dailyLimit: number }
@Route.post('/telegram-bot/share/check-limit')
@OpenApiRoute()
@OpenApiRouteHttp()
Future<Response> checkShareLimit(Request request) async {
try {
final body = await request.readAsString();
@ -137,14 +137,14 @@ class TelegramBotApiV2 {
/// Record a share request for a user
/// Body: { telegramUserId: string, telegramUsername?: string, sharedCardId?: number }
@Route.post('/telegram-bot/share/record')
@OpenApiRoute()
@OpenApiRouteHttp()
Future<Response> recordShareRequest(Request request) async {
try {
final body = await request.readAsString();
final json = jsonDecode(body) as Map<String, dynamic>;
final telegramUserId = json['telegramUserId'] as String?;
final telegramUsername = json['telegramUsername'] as String?;
final sharedCardId = json['sharedCardId'] as int?;
final sharedCardId = json['sharedCardId'] as String?;
if (telegramUserId == null || telegramUserId.isEmpty) {
return _badRequest('telegramUserId is required');
@ -177,7 +177,7 @@ class TelegramBotApiV2 {
/// Get user information (for admin commands)
/// Query: ?userId=<id> for specific user, or no query for all users summary
@Route.get('/telegram-bot/users/info')
@OpenApiRoute()
@OpenApiRouteHttp()
Future<Response> getUsersInfo(Request request) async {
try {
final queryParams = request.requestedUri.queryParameters;
@ -185,13 +185,10 @@ class TelegramBotApiV2 {
if (userId != null && userId.isNotEmpty) {
// Get specific user info
final id = int.tryParse(userId);
final users = <UserModel>[];
if (id != null) {
final user = await backend_main.database.userDao.getUserById(id);
if (user != null) {
users.add(await user.toUserModel());
}
final user = await backend_main.database.userDao.getUserById(userId);
if (user != null) {
users.add(await user.toUserModel());
}
if (users.isEmpty) {
@ -211,15 +208,15 @@ class TelegramBotApiV2 {
});
}
final user = users.first;
final _user = users.first;
return _ok({
'id': user.id,
'email': user.email,
'name': user.name,
'id': _user.id,
'email': _user.email,
'name': _user.name,
'tags': '',
'packs': [],
'purchases': user.purchases.length,
'purchases': _user.purchases.length,
'subscription': null,
});
} else {
@ -250,7 +247,7 @@ class TelegramBotApiV2 {
/// Get all words from all cards
/// Query: ?separator=<string> for custom separator (default: comma)
@Route.get('/telegram-bot/words')
@OpenApiRoute()
@OpenApiRouteHttp()
Future<Response> getWords(Request request) async {
try {
final queryParams = request.requestedUri.queryParameters;

View file

@ -8,30 +8,14 @@ part of 'telegram_bot_api_v2.dart';
Router _$TelegramBotApiV2Router(TelegramBotApiV2 service) {
final router = Router();
router.add(
'GET',
r'/telegram-bot/random-card',
service.getRandomCard,
);
router.add('GET', r'/telegram-bot/random-card', service.getRandomCard);
router.add(
'POST',
r'/telegram-bot/share/check-limit',
service.checkShareLimit,
);
router.add(
'POST',
r'/telegram-bot/share/record',
service.recordShareRequest,
);
router.add(
'GET',
r'/telegram-bot/users/info',
service.getUsersInfo,
);
router.add(
'GET',
r'/telegram-bot/words',
service.getWords,
);
router.add('POST', r'/telegram-bot/share/record', service.recordShareRequest);
router.add('GET', r'/telegram-bot/users/info', service.getUsersInfo);
router.add('GET', r'/telegram-bot/words', service.getWords);
return router;
}

View file

@ -70,7 +70,7 @@ class TestsApiV2 {
/// GET /api/v2/tests/{testId}
/// Get test details by ID
@Route.get('/tests/<testId>')
@OpenApiRoute()
@OpenApiRouteHttp()
Future<Response> getTest(Request request, String testId) async {
try {
final user = request.user;
@ -79,8 +79,7 @@ class TestsApiV2 {
return _unauthorized('Authentication required to fetch tests');
}
final testIdInt = int.tryParse(testId);
if (testIdInt == null) {
if (testId.isEmpty) {
return _badRequest('Invalid test ID');
}
@ -99,7 +98,7 @@ class TestsApiV2 {
/// POST /api/v2/tests/{testId}/results
/// Submit test results
@Route.post('/tests/<testId>/results')
@OpenApiRoute()
@OpenApiRouteHttp()
Future<Response> submitTestResults(
Request request,
String testId,
@ -111,8 +110,7 @@ class TestsApiV2 {
return _unauthorized('Authentication required to submit results');
}
final testIdInt = int.tryParse(testId);
if (testIdInt == null) {
if (testId.isEmpty) {
return _badRequest('Invalid test ID');
}
@ -129,7 +127,7 @@ class TestsApiV2 {
final testStatistics = TestStatisticsDto.fromJson(bodyJson);
// Ensure test ID matches
if (testStatistics.testId != testIdInt) {
if (testStatistics.testId != testId) {
return _badRequest('Test ID in URL does not match test ID in body');
}
@ -151,7 +149,7 @@ class TestsApiV2 {
/// Get test attempt history for the authenticated user
/// Supports pagination via query params: ?page=1&limit=20
@Route.get('/tests/<testId>/history')
@OpenApiRoute()
@OpenApiRouteHttp()
Future<Response> getTestHistory(Request request, String testId) async {
try {
final user = request.user;
@ -160,8 +158,7 @@ class TestsApiV2 {
return _unauthorized('Authentication required to view test history');
}
final testIdInt = int.tryParse(testId);
if (testIdInt == null) {
if (testId.isEmpty) {
return _badRequest('Invalid test ID');
}
@ -182,7 +179,7 @@ class TestsApiV2 {
}
// Get test statistics from Drift
final testStatistic = await _db.testDao.getTestStatistics(user.id!, testIdInt);
final testStatistic = await _db.testDao.getTestStatistics(user.id!, testId);
if (testStatistic == null || testStatistic.results == null) {
return _ok({

View file

@ -8,20 +8,8 @@ part of 'tests_api_v2.dart';
Router _$TestsApiV2Router(TestsApiV2 service) {
final router = Router();
router.add(
'GET',
r'/tests/<testId>',
service.getTest,
);
router.add(
'POST',
r'/tests/<testId>/results',
service.submitTestResults,
);
router.add(
'GET',
r'/tests/<testId>/history',
service.getTestHistory,
);
router.add('GET', r'/tests/<testId>', service.getTest);
router.add('POST', r'/tests/<testId>/results', service.submitTestResults);
router.add('GET', r'/tests/<testId>/history', service.getTestHistory);
return router;
}

View file

@ -273,7 +273,7 @@ class UsersApiV2 {
try {
final packId = request.url.queryParameters['packId'];
final userData = user.userData.value;
final userData = user.userData;
if (userData == null) {
return _json([]);
@ -322,7 +322,7 @@ class UsersApiV2 {
final validatedLimit = limit.clamp(1, 100);
final validatedOffset = offset < 0 ? 0 : offset;
final userData = user.userData.value;
final userData = user.userData;
if (userData == null) {
return _json({
'words': [],
@ -429,7 +429,7 @@ class UsersApiV2 {
toDate = DateTime.tryParse(to);
}
final userData = user.userData.value;
final userData = user.userData;
if (userData == null) {
return _json({
'period': period,
@ -546,7 +546,7 @@ class UsersApiV2 {
}
try {
final userData = user.userData.value;
final userData = user.userData;
if (userData == null) {
return _json([]);
}

View file

@ -8,60 +8,24 @@ part of 'users_api_v2.dart';
Router _$UsersApiV2Router(UsersApiV2 service) {
final router = Router();
router.add(
'GET',
r'/users/me',
service.getCurrentUser,
);
router.add(
'PATCH',
r'/users/me',
service.updateCurrentUser,
);
router.add(
'POST',
r'/users/me/settings',
service.updateUserSettings,
);
router.add(
'POST',
r'/users/me/statistics',
service.addUserTestStatistics,
);
router.add(
'GET',
r'/users/me/purchases',
service.getUserPurchases,
);
router.add('GET', r'/users/me', service.getCurrentUser);
router.add('PATCH', r'/users/me', service.updateCurrentUser);
router.add('POST', r'/users/me/settings', service.updateUserSettings);
router.add('POST', r'/users/me/statistics', service.addUserTestStatistics);
router.add('GET', r'/users/me/purchases', service.getUserPurchases);
router.add(
'GET',
r'/users/me/statistics/detailed',
service.getDetailedStatistics,
);
router.add(
'GET',
r'/users/me/statistics/packs',
service.getPacksStatistics,
);
router.add(
'GET',
r'/users/me/statistics/words',
service.getWordsStatistics,
);
router.add('GET', r'/users/me/statistics/packs', service.getPacksStatistics);
router.add('GET', r'/users/me/statistics/words', service.getWordsStatistics);
router.add(
'GET',
r'/users/me/statistics/timeline',
service.getTimelineStatistics,
);
router.add(
'POST',
r'/users/me/sessions',
service.recordStudySession,
);
router.add(
'GET',
r'/users/me/achievements',
service.getAchievements,
);
router.add('POST', r'/users/me/sessions', service.recordStudySession);
router.add('GET', r'/users/me/achievements', service.getAchievements);
return router;
}

View file

@ -29,7 +29,7 @@ class AddFreePacks with task.Task {
final allUsers = await _db.userDao.getAllUsers();
// Фильтруем пользователей, у которых нет хотя бы одного из freePacks
final usersToUpdate = <int>[];
final usersToUpdate = <String>[];
for (final user in allUsers) {
final userPacks = await _db.userDao.getUserPacks(user.id);
final userPackIds = userPacks.map((p) => p.id).toSet();

View file

@ -23,12 +23,13 @@ class Backup with Task {
final timeString =
'${t.year}_${t.month.toString().padLeft(2, '0')}_${t.day.toString().padLeft(2, '0')}_'
'${t.hour.toString().padLeft(2, '0')}_${t.minute.toString().padLeft(2, '0')}';
final filename = 'isar_$timeString.isar';
final filename = 'db_backup_$timeString.sql';
final to = '${backupDir}$filename';
final dir = Directory(backupDir)..createSync(recursive: true);
await _deleteOldBackups(dir);
// await database.copyToFile(to);
// print('$name isar saved to $to');
// TODO: Implement PostgreSQL backup
// await database.backupToFile(to);
// print('$name database backup saved to $to');
}
Future<void> _deleteOldBackups(Directory dir) async {

View file

@ -20,7 +20,7 @@ class CheckAdminsTask with task.Task {
return;
}
List<int> adminIds = [];
List<String> adminIds = [];
for (final externalUserId in adminIdsList) {
print('$name processing admin ID: $externalUserId');
if (externalUserId.isNotEmpty) {

View file

@ -14,9 +14,8 @@ class CronManager {
Future<void> init() async {
_schedulers = [];
// final models = isar.taskModels.where().findAllSync();
// Tasks are loaded from database via TaskDao if needed
final tasksToAdd = _tasks.keys;
// .where((name) => models.where((model) => model.name == name).isEmpty);
final models = tasksToAdd.map((name) => TaskModel.create(
name: name,
interval: Duration(seconds: _tasks[name]!.intervalSeconds),

View file

@ -1,7 +1,6 @@
import 'dart:developer';
import 'dart:io';
import 'package:isar/isar.dart';
import 'package:mnemo_cards_backend/cron/task.dart';
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
import 'package:mnemo_cards_backend/packs/pack_manager.dart';

View file

@ -1,9 +1,12 @@
import 'package:drift/drift.dart';
import 'dart:convert';
import 'package:uuid/uuid.dart';
const _uuid = Uuid();
// Конвертеры для JSON полей - общие для всех таблиц
class JsonMapConverter extends TypeConverter<Map<String, dynamic>?, String> {
class JsonMapConverter extends TypeConverter<Map<String, dynamic>?, String?> {
const JsonMapConverter();
@override
@ -23,16 +26,16 @@ class JsonMapConverter extends TypeConverter<Map<String, dynamic>?, String> {
}
}
class JsonListConverter extends TypeConverter<List<dynamic>?, String> {
class JsonListConverter extends TypeConverter<List<dynamic>, String> {
const JsonListConverter();
@override
List<dynamic>? fromSql(String? fromDb) {
if (fromDb == null || fromDb.isEmpty || fromDb == '[]') return null;
List<dynamic> fromSql(String? fromDb) {
if (fromDb == null || fromDb.isEmpty || fromDb == '[]') return [];
try {
return json.decode(fromDb) as List<dynamic>;
} catch (e) {
return null;
return [];
}
}
@ -51,7 +54,7 @@ class StringListConverter extends TypeConverter<List<String>, String> {
if (fromDb.isEmpty || fromDb == '[]') return [];
try {
final decoded = json.decode(fromDb);
return (decoded as List).map((e) => e.toString()).toList();
return (decoded as List).map((e) => e as String).toList();
} catch (e) {
return [];
}
@ -110,3 +113,8 @@ class IntListConverter extends TypeConverter<List<int>, String> {
return json.encode(value);
}
}
/// Helper function for UUID generation
String generateUuid() {
return _uuid.v4();
}

View file

@ -11,7 +11,7 @@ class AchievementDao extends DatabaseAccessor<AppDatabase> with _$AchievementDao
// ==================== UserAchievements ====================
/// Получить все достижения пользователя
Future<List<UserAchievement>> getUserAchievements(int userId) {
Future<List<UserAchievement>> getUserAchievements(String userId) {
return (select(userAchievements)
..where((ua) => ua.userId.equals(userId))
..orderBy([(ua) => OrderingTerm.desc(ua.unlockedAt)])
@ -19,7 +19,7 @@ class AchievementDao extends DatabaseAccessor<AppDatabase> with _$AchievementDao
}
/// Проверить, есть ли у пользователя достижение
Future<bool> hasAchievement(int userId, String achievementId) async {
Future<bool> hasAchievement(String userId, String achievementId) async {
final result = await (select(userAchievements)
..where((ua) => ua.userId.equals(userId) & ua.achievementId.equals(achievementId))
).getSingleOrNull();
@ -27,19 +27,20 @@ class AchievementDao extends DatabaseAccessor<AppDatabase> with _$AchievementDao
}
/// Получить достижение пользователя
Future<UserAchievement?> getUserAchievement(int userId, String achievementId) {
Future<UserAchievement?> getUserAchievement(String userId, String achievementId) {
return (select(userAchievements)
..where((ua) => ua.userId.equals(userId) & ua.achievementId.equals(achievementId))
).getSingleOrNull();
}
/// Создать достижение пользователя
Future<int> unlockAchievement(UserAchievementsCompanion achievement) {
return into(userAchievements).insert(achievement);
Future<String> unlockAchievement(UserAchievementsCompanion achievement) async {
final inserted = await into(userAchievements).insertReturning(achievement);
return inserted.id;
}
/// Обновить прогресс достижения
Future<bool> updateAchievementProgress(int userId, String achievementId, double progress) async {
Future<bool> updateAchievementProgress(String userId, String achievementId, double progress) async {
final count = await (update(userAchievements)
..where((ua) => ua.userId.equals(userId) & ua.achievementId.equals(achievementId))
).write(UserAchievementsCompanion(
@ -50,7 +51,7 @@ class AchievementDao extends DatabaseAccessor<AppDatabase> with _$AchievementDao
}
/// Получить прогресс по всем достижениям пользователя
Future<Map<String, double>> getAchievementProgress(int userId) async {
Future<Map<String, double>> getAchievementProgress(String userId) async {
final achievements = await getUserAchievements(userId);
return Map.fromEntries(
achievements.map((a) => MapEntry(a.achievementId, a.progress)),
@ -58,7 +59,7 @@ class AchievementDao extends DatabaseAccessor<AppDatabase> with _$AchievementDao
}
/// Удалить достижение пользователя (для сброса)
Future<void> removeAchievement(int userId, String achievementId) {
Future<void> removeAchievement(String userId, String achievementId) {
return (delete(userAchievements)
..where((ua) => ua.userId.equals(userId) & ua.achievementId.equals(achievementId))
).go();

View file

@ -12,7 +12,7 @@ class DiscountDao extends DatabaseAccessor<AppDatabase> with _$DiscountDaoMixin
// ==================== DiscountCampaigns ====================
/// Получить кампанию по ID
Future<DiscountCampaign?> getCampaignById(int id) {
Future<DiscountCampaign?> getCampaignById(String id) {
return (select(db.discountCampaigns)..where((c) => c.id.equals(id))).getSingleOrNull();
}
@ -28,8 +28,9 @@ class DiscountDao extends DatabaseAccessor<AppDatabase> with _$DiscountDaoMixin
}
/// Создать кампанию
Future<int> createCampaign(DiscountCampaignsCompanion campaign) {
return into(db.discountCampaigns).insert(campaign);
Future<String> createCampaign(DiscountCampaignsCompanion campaign) async {
final inserted = await into(db.discountCampaigns).insertReturning(campaign);
return inserted.id;
}
/// Обновить кампанию
@ -38,7 +39,7 @@ class DiscountDao extends DatabaseAccessor<AppDatabase> with _$DiscountDaoMixin
}
/// Обновить статус кампании
Future<void> updateCampaignStatus(int campaignId, String status) {
Future<void> updateCampaignStatus(String campaignId, String status) {
return (update(db.discountCampaigns)
..where((c) => c.id.equals(campaignId))
).write(DiscountCampaignsCompanion(
@ -71,7 +72,7 @@ class DiscountDao extends DatabaseAccessor<AppDatabase> with _$DiscountDaoMixin
}
/// Удалить кампанию (soft delete)
Future<void> deleteCampaign(int campaignId) {
Future<void> deleteCampaign(String campaignId) {
return (update(db.discountCampaigns)
..where((c) => c.id.equals(campaignId))
).write(DiscountCampaignsCompanion(
@ -83,12 +84,12 @@ class DiscountDao extends DatabaseAccessor<AppDatabase> with _$DiscountDaoMixin
// ==================== Discounts ====================
/// Получить скидку по ID
Future<Discount?> getDiscountById(int id) {
Future<Discount?> getDiscountById(String id) {
return (select(db.discounts)..where((d) => d.id.equals(id))).getSingleOrNull();
}
/// Получить скидки кампании
Future<List<Discount>> getDiscountsByCampaignId(int campaignId) {
Future<List<Discount>> getDiscountsByCampaignId(String campaignId) {
return (select(db.discounts)
..where((d) => d.campaignId.equals(campaignId))
..where((d) => d.isDeleted.equals(false))
@ -96,8 +97,9 @@ class DiscountDao extends DatabaseAccessor<AppDatabase> with _$DiscountDaoMixin
}
/// Создать скидку
Future<int> createDiscount(DiscountsCompanion discount) {
return into(db.discounts).insert(discount);
Future<String> createDiscount(DiscountsCompanion discount) async {
final inserted = await into(db.discounts).insertReturning(discount);
return inserted.id;
}
/// Обновить скидку
@ -108,7 +110,7 @@ class DiscountDao extends DatabaseAccessor<AppDatabase> with _$DiscountDaoMixin
// ==================== DiscountUserDatas ====================
/// Получить скидки пользователя
Future<List<Discount>> getUserDiscounts(int userId) async {
Future<List<Discount>> getUserDiscounts(String userId) async {
final query = select(db.discounts).join([
innerJoin(
db.discountUserDatas,
@ -121,7 +123,7 @@ class DiscountDao extends DatabaseAccessor<AppDatabase> with _$DiscountDaoMixin
}
/// Дать пользователю доступ к скидке
Future<void> grantDiscountToUser(int userId, int discountId) async {
Future<void> grantDiscountToUser(String userId, String discountId) async {
await into(db.discountUserDatas).insert(
DiscountUserDatasCompanion.insert(
userId: userId,
@ -132,14 +134,14 @@ class DiscountDao extends DatabaseAccessor<AppDatabase> with _$DiscountDaoMixin
}
/// Отозвать скидку у пользователя
Future<void> revokeDiscountFromUser(int userId, int discountId) async {
Future<void> revokeDiscountFromUser(String userId, String discountId) async {
await (delete(db.discountUserDatas)
..where((dud) => dud.userId.equals(userId) & dud.discountId.equals(discountId))
).go();
}
/// Отозвать несколько скидок у пользователя
Future<void> revokeDiscountsFromUser(int userId, List<int> discountIds) async {
Future<void> revokeDiscountsFromUser(String userId, List<String> discountIds) async {
if (discountIds.isEmpty) return;
await (delete(db.discountUserDatas)
..where((dud) => dud.userId.equals(userId) & dud.discountId.isIn(discountIds))
@ -147,7 +149,7 @@ class DiscountDao extends DatabaseAccessor<AppDatabase> with _$DiscountDaoMixin
}
/// Дать пользователю доступ к нескольким скидкам
Future<void> grantDiscountsToUser(int userId, List<int> discountIds) async {
Future<void> grantDiscountsToUser(String userId, List<String> discountIds) async {
if (discountIds.isEmpty) return;
await Future.wait(
discountIds.map((discountId) => grantDiscountToUser(userId, discountId))
@ -155,7 +157,7 @@ class DiscountDao extends DatabaseAccessor<AppDatabase> with _$DiscountDaoMixin
}
/// Проверить, есть ли у пользователя доступ к скидке
Future<bool> hasDiscountAccess(int userId, int discountId) async {
Future<bool> hasDiscountAccess(String userId, String discountId) async {
final query = select(db.discountUserDatas)
..where((dud) => dud.userId.equals(userId) & dud.discountId.equals(discountId));

View file

@ -12,7 +12,7 @@ class PackDao extends DatabaseAccessor<AppDatabase> with _$PackDaoMixin {
// ==================== CardPacks ====================
/// Получить пак по ID
Future<CardPack?> getPackById(int id) {
Future<CardPack?> getPackById(String id) {
return (select(cardPacks)..where((p) => p.id.equals(id))).getSingleOrNull();
}
@ -44,8 +44,9 @@ class PackDao extends DatabaseAccessor<AppDatabase> with _$PackDaoMixin {
}
/// Создать пак
Future<int> createPack(CardPacksCompanion pack) {
return into(cardPacks).insert(pack);
Future<String> createPack(CardPacksCompanion pack) async {
final inserted = await into(cardPacks).insertReturning(pack);
return inserted.id;
}
/// Обновить пак
@ -63,7 +64,7 @@ class PackDao extends DatabaseAccessor<AppDatabase> with _$PackDaoMixin {
}
/// Удалить пак (soft delete)
Future<void> softDeletePack(int packId) {
Future<void> softDeletePack(String packId) {
return (update(cardPacks)..where((p) => p.id.equals(packId)))
.write(CardPacksCompanion(
isDeleted: const Value(true),
@ -87,12 +88,12 @@ class PackDao extends DatabaseAccessor<AppDatabase> with _$PackDaoMixin {
// ==================== GameCards ====================
/// Получить карточку по ID
Future<GameCard?> getCardById(int id) {
Future<GameCard?> getCardById(String id) {
return (select(gameCards)..where((c) => c.id.equals(id))).getSingleOrNull();
}
/// Получить все карточки пака
Future<List<GameCard>> getPackCards(int packId) async {
Future<List<GameCard>> getPackCards(String packId) async {
final query = select(gameCards).join([
leftOuterJoin(
cardPackCards,
@ -115,14 +116,15 @@ class PackDao extends DatabaseAccessor<AppDatabase> with _$PackDaoMixin {
}
/// Получить карточки по списку ID
Future<List<GameCard>> getCardsByIds(List<int> ids) {
Future<List<GameCard>> getCardsByIds(List<String> ids) {
if (ids.isEmpty) return Future.value([]);
return (select(gameCards)..where((c) => c.id.isIn(ids))).get();
}
/// Создать карточку
Future<int> createCard(GameCardsCompanion card) {
return into(gameCards).insert(card);
Future<String> createCard(GameCardsCompanion card) async {
final inserted = await into(gameCards).insertReturning(card);
return inserted.id;
}
/// Обновить карточку
@ -131,7 +133,7 @@ class PackDao extends DatabaseAccessor<AppDatabase> with _$PackDaoMixin {
}
/// Удалить карточку (soft delete)
Future<void> softDeleteCard(int cardId) {
Future<void> softDeleteCard(String cardId) {
return (update(gameCards)..where((c) => c.id.equals(cardId)))
.write(GameCardsCompanion(
isDeleted: const Value(true),
@ -140,7 +142,7 @@ class PackDao extends DatabaseAccessor<AppDatabase> with _$PackDaoMixin {
}
/// Удалить карточку (hard delete)
Future<void> deleteCard(int cardId) {
Future<void> deleteCard(String cardId) {
return (delete(gameCards)..where((c) => c.id.equals(cardId))).go();
}
@ -162,8 +164,8 @@ class PackDao extends DatabaseAccessor<AppDatabase> with _$PackDaoMixin {
/// Добавить карточку в пак
Future<void> addCardToPack({
required int packId,
required int cardId,
required String packId,
required String cardId,
int order = 0,
}) async {
await into(cardPackCards).insert(
@ -177,14 +179,14 @@ class PackDao extends DatabaseAccessor<AppDatabase> with _$PackDaoMixin {
}
/// Удалить карточку из пака
Future<void> removeCardFromPack(int packId, int cardId) async {
Future<void> removeCardFromPack(String packId, String cardId) async {
await (delete(cardPackCards)
..where((cpc) => cpc.packId.equals(packId) & cpc.cardId.equals(cardId))
).go();
}
/// Обновить порядок карточек в паке
Future<void> updatePackCardsOrder(int packId, List<int> cardIds) async {
Future<void> updatePackCardsOrder(String packId, List<String> cardIds) async {
await transaction(() async {
// Удалить старые связи
await (delete(cardPackCards)
@ -207,7 +209,7 @@ class PackDao extends DatabaseAccessor<AppDatabase> with _$PackDaoMixin {
// ==================== PreviewCards ====================
/// Получить preview карточки пака
Future<List<GameCard>> getPreviewCards(int packId) async {
Future<List<GameCard>> getPreviewCards(String packId) async {
final query = select(gameCards).join([
innerJoin(
db.previewCards,
@ -228,7 +230,7 @@ class PackDao extends DatabaseAccessor<AppDatabase> with _$PackDaoMixin {
}
/// Установить preview карточки для пака
Future<void> setPreviewCards(int packId, List<int> cardIds) async {
Future<void> setPreviewCards(String packId, List<String> cardIds) async {
await transaction(() async {
// Удалить старые preview карточки
await (delete(previewCards)
@ -251,7 +253,7 @@ class PackDao extends DatabaseAccessor<AppDatabase> with _$PackDaoMixin {
// ==================== VoiceModels ====================
/// Получить голосовые модели карточки
Future<List<VoiceModel>> getCardVoices(int cardId) async {
Future<List<VoiceModel>> getCardVoices(String cardId) async {
final query = select(voiceModels).join([
innerJoin(
cardVoices,
@ -264,7 +266,7 @@ class PackDao extends DatabaseAccessor<AppDatabase> with _$PackDaoMixin {
}
/// Добавить голосовую модель к карточке
Future<void> addVoiceToCard(int cardId, int voiceId) async {
Future<void> addVoiceToCard(String cardId, String voiceId) async {
await into(cardVoices).insert(
CardVoicesCompanion.insert(
cardId: cardId,
@ -275,19 +277,20 @@ class PackDao extends DatabaseAccessor<AppDatabase> with _$PackDaoMixin {
}
/// Удалить голосовую модель из карточки
Future<void> removeVoiceFromCard(int cardId, int voiceId) async {
Future<void> removeVoiceFromCard(String cardId, String voiceId) async {
await (delete(cardVoices)
..where((cv) => cv.cardId.equals(cardId) & cv.voiceId.equals(voiceId))
).go();
}
/// Создать голосовую модель
Future<int> createVoice(VoiceModelsCompanion voice) {
return into(voiceModels).insert(voice);
Future<String> createVoice(VoiceModelsCompanion voice) async {
final inserted = await into(voiceModels).insertReturning(voice);
return inserted.id;
}
/// Получить голосовую модель по ID
Future<VoiceModel?> getVoiceById(int id) {
Future<VoiceModel?> getVoiceById(String id) {
return (select(voiceModels)..where((v) => v.id.equals(id))).getSingleOrNull();
}
}

View file

@ -1,7 +1,6 @@
import 'package:drift/drift.dart';
import '../database.dart';
import '../tables/payments.dart';
import '../tables/users.dart';
part 'payment_dao.g.dart';
@ -10,12 +9,12 @@ class PaymentDao extends DatabaseAccessor<AppDatabase> with _$PaymentDaoMixin {
PaymentDao(super.db);
/// Получить платеж по ID
Future<Payment?> getPaymentById(int id) {
Future<Payment?> getPaymentById(String id) {
return (select(payments)..where((p) => p.id.equals(id))).getSingleOrNull();
}
/// Получить платежи пользователя
Future<List<Payment>> getPaymentsByUserId(int userId, {
Future<List<Payment>> getPaymentsByUserId(String userId, {
int? limit,
int? offset,
}) {
@ -39,8 +38,31 @@ class PaymentDao extends DatabaseAccessor<AppDatabase> with _$PaymentDaoMixin {
}
/// Создать платеж
Future<int> createPayment(PaymentsCompanion payment) {
return into(payments).insert(payment);
Future<String> createPayment(PaymentsCompanion payment) async {
final inserted = await into(payments).insertReturning(payment);
// Query back to get the id since Payment class doesn't have id field
// (database needs to be regenerated to include id in Payment class)
// Use externalToken if available, otherwise query by unique fields
if (inserted.externalToken != null && inserted.externalToken!.isNotEmpty) {
final result = await (customSelect(
'SELECT id FROM payments WHERE external_token = ? LIMIT 1',
variables: [Variable.withString(inserted.externalToken!)],
readsFrom: {payments},
).map((row) => row.read<String>('id')).getSingleOrNull());
if (result != null) return result;
}
// Fallback: query by userId, date, and amount
final result = await (customSelect(
'SELECT id FROM payments WHERE user_id = ? AND date = ? AND amount = ? ORDER BY created_at DESC LIMIT 1',
variables: [
Variable.withString(payment.userId.value),
Variable.withDateTime(inserted.date),
Variable.withString(inserted.amount),
],
readsFrom: {payments},
).map((row) => row.read<String>('id')).getSingleOrNull());
if (result != null) return result;
throw Exception('Failed to retrieve payment ID after creation');
}
/// Обновить платеж
@ -49,16 +71,13 @@ class PaymentDao extends DatabaseAccessor<AppDatabase> with _$PaymentDaoMixin {
}
/// Обновить платеж частично
Future<void> updatePaymentCompanion(PaymentsCompanion companion) {
final paymentId = companion.id.value;
if (paymentId == null) throw ArgumentError('Payment ID is required');
Future<void> updatePaymentCompanion(String paymentId, PaymentsCompanion companion) {
return (update(payments)..where((p) => p.id.equals(paymentId)))
.write(companion);
}
/// Обновить статус платежа
Future<void> updatePaymentStatus(int paymentId, String status) {
Future<void> updatePaymentStatus(String paymentId, String status) {
return (update(payments)..where((p) => p.id.equals(paymentId)))
.write(PaymentsCompanion(
status: Value(status),
@ -67,7 +86,7 @@ class PaymentDao extends DatabaseAccessor<AppDatabase> with _$PaymentDaoMixin {
}
/// Подсчитать платежи пользователя
Future<int> countPaymentsByUserId(int userId) async {
Future<int> countPaymentsByUserId(String userId) async {
final countExpr = payments.id.count();
final query = selectOnly(payments)
..addColumns([countExpr])

View file

@ -12,7 +12,7 @@ class PromoCodeDao extends DatabaseAccessor<AppDatabase> with _$PromoCodeDaoMixi
// ==================== PromoCodesCampaigns ====================
/// Получить кампанию по ID
Future<PromoCodesCampaign?> getCampaignById(int id) {
Future<PromoCodesCampaign?> getCampaignById(String id) {
return (select(db.promoCodesCampaigns)..where((c) => c.id.equals(id))).getSingleOrNull();
}
@ -28,8 +28,9 @@ class PromoCodeDao extends DatabaseAccessor<AppDatabase> with _$PromoCodeDaoMixi
}
/// Создать кампанию
Future<int> createCampaign(PromoCodesCampaignsCompanion campaign) {
return into(db.promoCodesCampaigns).insert(campaign);
Future<String> createCampaign(PromoCodesCampaignsCompanion campaign) async {
final inserted = await into(db.promoCodesCampaigns).insertReturning(campaign);
return inserted.id;
}
/// Обновить кампанию
@ -38,7 +39,7 @@ class PromoCodeDao extends DatabaseAccessor<AppDatabase> with _$PromoCodeDaoMixi
}
/// Обновить статус кампании
Future<void> updateCampaignStatus(int campaignId, String status) {
Future<void> updateCampaignStatus(String campaignId, String status) {
return (update(db.promoCodesCampaigns)
..where((c) => c.id.equals(campaignId))
).write(PromoCodesCampaignsCompanion(
@ -56,8 +57,8 @@ class PromoCodeDao extends DatabaseAccessor<AppDatabase> with _$PromoCodeDaoMixi
}
/// Создать несколько промокодов
Future<List<int>> createPromoCodes(List<PromoCodesCompanion> promoCodes) async {
final ids = <int>[];
Future<List<String>> createPromoCodes(List<PromoCodesCompanion> promoCodes) async {
final ids = <String>[];
for (final promoCode in promoCodes) {
final id = await createPromoCode(promoCode);
ids.add(id);
@ -75,22 +76,23 @@ class PromoCodeDao extends DatabaseAccessor<AppDatabase> with _$PromoCodeDaoMixi
}
/// Получить промокоды кампании
Future<List<PromoCode>> getPromoCodesByCampaignId(int campaignId) {
Future<List<PromoCode>> getPromoCodesByCampaignId(String campaignId) {
return (select(db.promoCodes)
..where((pc) => pc.campaignId.equals(campaignId))
).get();
}
/// Получить индивидуальные промокоды пользователя
Future<List<PromoCode>> getUserPromoCodes(int userId) {
Future<List<PromoCode>> getUserPromoCodes(String userId) {
return (select(db.promoCodes)
..where((pc) => pc.userId.equals(userId))
).get();
}
/// Создать промокод
Future<int> createPromoCode(PromoCodesCompanion promoCode) {
return into(db.promoCodes).insert(promoCode);
Future<String> createPromoCode(PromoCodesCompanion promoCode) async {
final inserted = await into(db.promoCodes).insertReturning(promoCode);
return inserted.id;
}
/// Обновить промокод
@ -99,7 +101,7 @@ class PromoCodeDao extends DatabaseAccessor<AppDatabase> with _$PromoCodeDaoMixi
}
/// Увеличить счетчик активаций
Future<void> incrementActivations(int promoCodeId) async {
Future<void> incrementActivations(String promoCodeId) async {
final code = await (select(db.promoCodes)
..where((pc) => pc.id.equals(promoCodeId))
).getSingleOrNull();
@ -113,7 +115,7 @@ class PromoCodeDao extends DatabaseAccessor<AppDatabase> with _$PromoCodeDaoMixi
}
/// Удалить промокод
Future<void> deletePromoCode(int promoCodeId) {
Future<void> deletePromoCode(String promoCodeId) {
return (delete(db.promoCodes)..where((pc) => pc.id.equals(promoCodeId))).go();
}
}

View file

@ -10,7 +10,7 @@ class StatisticsDao extends DatabaseAccessor<AppDatabase> with _$StatisticsDaoMi
StatisticsDao(super.db);
/// Получить сессию по ID
Future<StudySession?> getSessionById(int id) {
Future<StudySession?> getSessionById(String id) {
return (select(studySessions)..where((s) => s.id.equals(id))).getSingleOrNull();
}
@ -22,7 +22,7 @@ class StatisticsDao extends DatabaseAccessor<AppDatabase> with _$StatisticsDaoMi
}
/// Получить активные сессии пользователя
Future<List<StudySession>> getActiveSessions(int userId) {
Future<List<StudySession>> getActiveSessions(String userId) {
return (select(studySessions)
..where((s) => s.userId.equals(userId))
..where((s) => s.endTime.isNull())
@ -31,7 +31,7 @@ class StatisticsDao extends DatabaseAccessor<AppDatabase> with _$StatisticsDaoMi
}
/// Получить сессии пользователя
Future<List<StudySession>> getSessionsByUserId(int userId, {
Future<List<StudySession>> getSessionsByUserId(String userId, {
int? limit,
int? offset,
DateTime? fromDate,
@ -56,8 +56,9 @@ class StatisticsDao extends DatabaseAccessor<AppDatabase> with _$StatisticsDaoMi
}
/// Создать сессию
Future<int> createSession(StudySessionsCompanion session) {
return into(studySessions).insert(session);
Future<String> createSession(StudySessionsCompanion session) async {
final inserted = await into(studySessions).insertReturning(session);
return inserted.id;
}
/// Обновить сессию
@ -66,7 +67,7 @@ class StatisticsDao extends DatabaseAccessor<AppDatabase> with _$StatisticsDaoMi
}
/// Завершить сессию
Future<void> endSession(int sessionId, {
Future<void> endSession(String sessionId, {
int? wordsLearned,
int? testsCompleted,
double? accuracy,
@ -85,7 +86,7 @@ class StatisticsDao extends DatabaseAccessor<AppDatabase> with _$StatisticsDaoMi
}
/// Подсчитать сессии пользователя
Future<int> countSessionsByUserId(int userId) async {
Future<int> countSessionsByUserId(String userId) async {
final countExpr = studySessions.id.count();
final query = selectOnly(studySessions)
..addColumns([countExpr])

View file

@ -12,7 +12,7 @@ class SubscriptionDao extends DatabaseAccessor<AppDatabase> with _$SubscriptionD
// ==================== SubscriptionPlans ====================
/// Получить план подписки по ID
Future<SubscriptionPlan?> getPlanById(int id) {
Future<SubscriptionPlan?> getPlanById(String id) {
return (select(subscriptionPlans)..where((p) => p.id.equals(id))).getSingleOrNull();
}
@ -24,8 +24,9 @@ class SubscriptionDao extends DatabaseAccessor<AppDatabase> with _$SubscriptionD
}
/// Создать план подписки
Future<int> createPlan(SubscriptionPlansCompanion plan) {
return into(subscriptionPlans).insert(plan);
Future<String> createPlan(SubscriptionPlansCompanion plan) async {
final inserted = await into(subscriptionPlans).insertReturning(plan);
return inserted.id;
}
/// Обновить план подписки
@ -36,7 +37,7 @@ class SubscriptionDao extends DatabaseAccessor<AppDatabase> with _$SubscriptionD
// ==================== UserSubscriptions ====================
/// Получить подписку пользователя
Future<UserSubscription?> getUserSubscription(int userId) {
Future<UserSubscription?> getUserSubscription(String userId) {
return (select(userSubscriptions)
..where((us) => us.userId.equals(userId))
..orderBy([(us) => OrderingTerm.desc(us.finish)])
@ -44,7 +45,7 @@ class SubscriptionDao extends DatabaseAccessor<AppDatabase> with _$SubscriptionD
}
/// Получить активную подписку пользователя
Future<UserSubscription?> getActiveSubscription(int userId) async {
Future<UserSubscription?> getActiveSubscription(String userId) async {
final now = DateTime.now();
return (select(userSubscriptions)
..where((us) => us.userId.equals(userId))
@ -55,14 +56,15 @@ class SubscriptionDao extends DatabaseAccessor<AppDatabase> with _$SubscriptionD
}
/// Проверить, есть ли у пользователя активная подписка
Future<bool> hasActiveSubscription(int userId) async {
Future<bool> hasActiveSubscription(String userId) async {
final subscription = await getActiveSubscription(userId);
return subscription != null;
}
/// Создать подписку пользователя
Future<int> createUserSubscription(UserSubscriptionsCompanion subscription) {
return into(userSubscriptions).insert(subscription);
Future<String> createUserSubscription(UserSubscriptionsCompanion subscription) async {
final inserted = await into(userSubscriptions).insertReturning(subscription);
return inserted.id;
}
/// Обновить подписку пользователя
@ -71,7 +73,7 @@ class SubscriptionDao extends DatabaseAccessor<AppDatabase> with _$SubscriptionD
}
/// Удалить подписку пользователя
Future<void> deleteUserSubscription(int userId) {
Future<void> deleteUserSubscription(String userId) {
return (delete(userSubscriptions)
..where((us) => us.userId.equals(userId))
).go();
@ -88,12 +90,12 @@ class SubscriptionDao extends DatabaseAccessor<AppDatabase> with _$SubscriptionD
}
/// Получить активную подписку пользователя (alias для getActiveSubscription)
Future<UserSubscription?> getActiveUserSubscription(int userId) {
Future<UserSubscription?> getActiveUserSubscription(String userId) {
return getActiveSubscription(userId);
}
/// Отменить подписку пользователя (установить finish на текущее время)
Future<void> cancelUserSubscription(int userId) async {
Future<void> cancelUserSubscription(String userId) async {
final subscription = await getActiveSubscription(userId);
if (subscription != null) {
await update(userSubscriptions).replace(

View file

@ -1,7 +1,5 @@
import 'package:drift/drift.dart';
import '../database.dart';
// import '../tables/tasks.dart'; // Causes conflict, using database.dart instead
import '../tables/users.dart';
part 'task_dao.g.dart';
@ -12,7 +10,7 @@ class TaskDao extends DatabaseAccessor<AppDatabase> with _$TaskDaoMixin {
// ==================== Tasks ====================
/// Получить задачу по ID
Future<Task?> getTaskById(int id) {
Future<Task?> getTaskById(String id) {
return (select(db.tasks)..where((t) => t.id.equals(id))).getSingleOrNull();
}
@ -22,8 +20,9 @@ class TaskDao extends DatabaseAccessor<AppDatabase> with _$TaskDaoMixin {
}
/// Создать задачу
Future<int> createTask(TasksCompanion task) {
return into(db.tasks).insert(task);
Future<String> createTask(TasksCompanion task) async {
final inserted = await into(db.tasks).insertReturning(task);
return inserted.id;
}
/// Обновить задачу
@ -32,7 +31,7 @@ class TaskDao extends DatabaseAccessor<AppDatabase> with _$TaskDaoMixin {
}
/// Обновить время последнего выполнения
Future<void> updateLastExecution(int taskId) {
Future<void> updateLastExecution(String taskId) {
return (update(db.tasks)..where((t) => t.id.equals(taskId)))
.write(TasksCompanion(
lastExecution: Value(DateTime.now()),
@ -43,7 +42,7 @@ class TaskDao extends DatabaseAccessor<AppDatabase> with _$TaskDaoMixin {
// ==================== UserTasks ====================
/// Получить задачу пользователя по ID
Future<UserTask?> getUserTaskById(int id) {
Future<UserTask?> getUserTaskById(String id) {
return (select(db.userTasks)..where((ut) => ut.id.equals(id))).getSingleOrNull();
}
@ -53,12 +52,15 @@ class TaskDao extends DatabaseAccessor<AppDatabase> with _$TaskDaoMixin {
}
/// Получить задачи пользователя
Future<List<UserTask>> getUserTasks(int userId, {
Future<List<UserTask>> getUserTasks(String userId, {
String? status,
bool activeOnly = false,
}) {
final query = select(db.userTasks);
// Note: UserTasks table doesn't have userId column, so we can't filter by userId
// This method may need to be refactored if userId filtering is required
if (status != null) {
query.where((ut) => ut.status.equals(status));
}
@ -74,8 +76,9 @@ class TaskDao extends DatabaseAccessor<AppDatabase> with _$TaskDaoMixin {
}
/// Создать задачу пользователя
Future<int> createUserTask(UserTasksCompanion task) {
return into(db.userTasks).insert(task);
Future<String> createUserTask(UserTasksCompanion task) async {
final inserted = await into(db.userTasks).insertReturning(task);
return inserted.id;
}
/// Обновить задачу пользователя
@ -84,7 +87,7 @@ class TaskDao extends DatabaseAccessor<AppDatabase> with _$TaskDaoMixin {
}
/// Завершить задачу пользователя
Future<void> completeUserTask(int taskId) {
Future<void> completeUserTask(String taskId) {
return (update(db.userTasks)..where((ut) => ut.id.equals(taskId)))
.write(UserTasksCompanion(
status: const Value('completed'),
@ -94,7 +97,7 @@ class TaskDao extends DatabaseAccessor<AppDatabase> with _$TaskDaoMixin {
}
/// Подсчитать задачи пользователя
Future<int> countUserTasks(int userId, {String? status}) async {
Future<int> countUserTasks(String userId, {String? status}) async {
final countExpr = db.userTasks.id.count();
final query = selectOnly(db.userTasks)..addColumns([countExpr]);
@ -108,26 +111,27 @@ class TaskDao extends DatabaseAccessor<AppDatabase> with _$TaskDaoMixin {
// ==================== UserTaskProgresses ====================
/// Получить прогресс задачи пользователя
Future<UserTaskProgresses?> getTaskProgress(int userId, int taskId) {
Future<UserTaskProgressesData?> getTaskProgress(String userId, String taskId) {
return (select(db.userTaskProgresses)
..where((utp) => utp.userId.equals(userId) & utp.taskId.equals(taskId))
).getSingleOrNull();
}
/// Создать прогресс задачи
Future<int> createTaskProgress(UserTaskProgressesCompanion progress) {
return into(db.userTaskProgresses).insert(progress);
Future<String> createTaskProgress(UserTaskProgressesCompanion progress) async {
final inserted = await into(db.userTaskProgresses).insertReturning(progress);
return inserted.id;
}
/// Обновить прогресс задачи
Future<bool> updateTaskProgress(UserTaskProgresses progress) {
Future<bool> updateTaskProgress(UserTaskProgressesData progress) {
return update(db.userTaskProgresses).replace(progress);
}
// ==================== UserTaskResults ====================
/// Получить результаты задач пользователя
Future<List<UserTaskResult>> getTaskResults(int userId) {
Future<List<UserTaskResult>> getTaskResults(String userId) {
return (select(db.userTaskResults)
..where((utr) => utr.userId.equals(userId))
..orderBy([(utr) => OrderingTerm.desc(utr.completedAt)])
@ -135,7 +139,8 @@ class TaskDao extends DatabaseAccessor<AppDatabase> with _$TaskDaoMixin {
}
/// Создать результат задачи
Future<int> createTaskResult(UserTaskResultsCompanion result) {
return into(db.userTaskResults).insert(result);
Future<String> createTaskResult(UserTaskResultsCompanion result) async {
final inserted = await into(db.userTaskResults).insertReturning(result);
return inserted.id;
}
}

View file

@ -12,7 +12,7 @@ class TestDao extends DatabaseAccessor<AppDatabase> with _$TestDaoMixin {
// ==================== Tests ====================
/// Получить тест по ID
Future<Test?> getTestById(int id) {
Future<Test?> getTestById(String id) {
return (select(tests)..where((t) => t.id.equals(id))).getSingleOrNull();
}
@ -24,7 +24,7 @@ class TestDao extends DatabaseAccessor<AppDatabase> with _$TestDaoMixin {
}
/// Получить тесты пака
Future<List<Test>> getTestsByPackId(int packId) async {
Future<List<Test>> getTestsByPackId(String packId) async {
final query = select(tests).join([
innerJoin(
testPackRelations,
@ -37,8 +37,9 @@ class TestDao extends DatabaseAccessor<AppDatabase> with _$TestDaoMixin {
}
/// Создать тест
Future<int> createTest(TestsCompanion test) {
return into(tests).insert(test);
Future<String> createTest(TestsCompanion test) async {
final inserted = await into(tests).insertReturning(test);
return inserted.id;
}
/// Обновить тест
@ -47,7 +48,7 @@ class TestDao extends DatabaseAccessor<AppDatabase> with _$TestDaoMixin {
}
/// Удалить тест (soft delete)
Future<void> softDeleteTest(int testId) {
Future<void> softDeleteTest(String testId) {
return (update(tests)..where((t) => t.id.equals(testId)))
.write(TestsCompanion(
isDeleted: const Value(true),
@ -56,7 +57,7 @@ class TestDao extends DatabaseAccessor<AppDatabase> with _$TestDaoMixin {
}
/// Связать тест с паком
Future<void> linkTestToPack(int testId, int packId) async {
Future<void> linkTestToPack(String testId, String packId) async {
await into(testPackRelations).insert(
TestPackRelationsCompanion.insert(
testId: testId,
@ -69,15 +70,16 @@ class TestDao extends DatabaseAccessor<AppDatabase> with _$TestDaoMixin {
// ==================== TestQuestions ====================
/// Получить вопросы теста
Future<List<TestQuestion>> getTestQuestions(int testId) {
Future<List<TestQuestion>> getTestQuestions(String testId) {
return (select(testQuestions)
..where((tq) => tq.testId.equals(testId))
).get();
}
/// Создать вопрос теста
Future<int> createTestQuestion(TestQuestionsCompanion question) {
return into(testQuestions).insert(question);
Future<String> createTestQuestion(TestQuestionsCompanion question) async {
final inserted = await into(testQuestions).insertReturning(question);
return inserted.id;
}
/// Обновить вопрос теста
@ -86,22 +88,23 @@ class TestDao extends DatabaseAccessor<AppDatabase> with _$TestDaoMixin {
}
/// Удалить вопрос теста
Future<void> deleteTestQuestion(int questionId) {
Future<void> deleteTestQuestion(String questionId) {
return (delete(testQuestions)..where((tq) => tq.id.equals(questionId))).go();
}
// ==================== TestStatistics ====================
/// Получить статистику теста пользователя
Future<TestStatistic?> getTestStatistics(int userId, int testId) {
Future<TestStatistic?> getTestStatistics(String userId, String testId) {
return (select(testStatistics)
..where((ts) => ts.userId.equals(userId) & ts.testId.equals(testId))
).getSingleOrNull();
}
/// Создать статистику теста
Future<int> createTestStatistics(TestStatisticsCompanion statistics) {
return into(testStatistics).insert(statistics);
Future<String> createTestStatistics(TestStatisticsCompanion statistics) async {
final inserted = await into(testStatistics).insertReturning(statistics);
return inserted.id;
}
/// Обновить статистику теста

View file

@ -14,12 +14,12 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
// ==================== Users ====================
/// Получить пользователя по ID
Future<User?> getUserById(int id) {
Future<User?> getUserById(String id) {
return (select(users)..where((u) => u.id.equals(id))).getSingleOrNull();
}
/// Получить пользователя с UserData
Future<UserWithData?> getUserWithDataById(int id) async {
Future<UserWithData?> getUserWithDataById(String id) async {
final query = select(users).join([
leftOuterJoin(userDatas, userDatas.userId.equalsExp(users.id)),
])..where(users.id.equals(id));
@ -46,17 +46,19 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
}
/// Создать пользователя
Future<int> createUser(UsersCompanion user) {
return into(users).insert(user);
Future<String> createUser(UsersCompanion user) async {
final inserted = await into(users).insertReturning(user);
return inserted.id;
}
/// Создать пользователя с UserData
Future<int> createUserWithData({
Future<String> createUserWithData({
required UsersCompanion user,
required UserDatasCompanion userData,
}) async {
return await transaction(() async {
final userId = await into(users).insert(user);
final inserted = await into(users).insertReturning(user);
final userId = inserted.id;
await into(userDatas).insert(userData.copyWith(userId: Value(userId)));
return userId;
});
@ -76,7 +78,7 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
}
/// Удалить пользователя (soft delete)
Future<void> softDeleteUser(int userId) {
Future<void> softDeleteUser(String userId) {
return (update(users)..where((u) => u.id.equals(userId)))
.write(UsersCompanion(
isDeleted: const Value(true),
@ -131,14 +133,15 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
// ==================== UserData ====================
/// Получить UserData пользователя
Future<UserData?> getUserData(int userId) {
Future<UserData?> getUserData(String userId) {
return (select(userDatas)..where((ud) => ud.userId.equals(userId)))
.getSingleOrNull();
}
/// Создать UserData
Future<int> createUserData(UserDatasCompanion userData) {
return into(userDatas).insert(userData);
Future<String> createUserData(UserDatasCompanion userData) async {
final inserted = await into(userDatas).insertReturning(userData);
return inserted.id;
}
/// Обновить UserData
@ -156,7 +159,7 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
}
/// Обновить время последнего визита
Future<void> updateLastOnline(int userId) async {
Future<void> updateLastOnline(String userId) async {
await (update(userDatas)..where((ud) => ud.userId.equals(userId)))
.write(UserDatasCompanion(
lastTimeOnline: Value(DateTime.now()),
@ -182,7 +185,7 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
}
/// Получить токен пользователя
Future<Token?> getTokenByUserId(int userId) {
Future<Token?> getTokenByUserId(String userId) {
return (select(tokens)
..where((t) => t.userId.equals(userId))
..where((t) => t.expires.isBiggerThanValue(DateTime.now()))
@ -191,12 +194,13 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
}
/// Создать токен
Future<int> createToken(TokensCompanion token) {
return into(tokens).insert(token);
Future<String> createToken(TokensCompanion token) async {
final inserted = await into(tokens).insertReturning(token);
return inserted.id;
}
/// Удалить токен
Future<int> deleteToken(int tokenId) {
Future<int> deleteToken(String tokenId) {
return (delete(tokens)..where((t) => t.id.equals(tokenId))).go();
}
@ -221,7 +225,7 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
}
/// Получить активные refresh токены пользователя
Future<List<RefreshToken>> getActiveRefreshTokens(int userId) {
Future<List<RefreshToken>> getActiveRefreshTokens(String userId) {
return (select(refreshTokens)
..where((rt) => rt.userId.equals(userId))
..where((rt) => rt.isBlacklisted.equals(false))
@ -231,12 +235,13 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
}
/// Создать refresh token
Future<int> createRefreshToken(RefreshTokensCompanion token) {
return into(refreshTokens).insert(token);
Future<String> createRefreshToken(RefreshTokensCompanion token) async {
final inserted = await into(refreshTokens).insertReturning(token);
return inserted.id;
}
/// Отозвать refresh token (blacklist)
Future<void> revokeRefreshToken(int tokenId) {
Future<void> revokeRefreshToken(String tokenId) {
return (update(refreshTokens)..where((rt) => rt.id.equals(tokenId)))
.write(const RefreshTokensCompanion(
isBlacklisted: Value(true),
@ -267,8 +272,9 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
}
/// Создать код авторизации
Future<int> createAuthCode(TelegramAuthCodesCompanion code) {
return into(db.telegramAuthCodes).insert(code);
Future<String> createAuthCode(TelegramAuthCodesCompanion code) async {
final inserted = await into(db.telegramAuthCodes).insertReturning(code);
return inserted.id;
}
/// Отметить код как использованный
@ -290,7 +296,7 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
// ==================== User Packs ====================
/// Получить паки пользователя
Future<List<CardPack>> getUserPacks(int userId) async {
Future<List<CardPack>> getUserPacks(String userId) async {
final query = select(cardPacks).join([
innerJoin(
userPacks,
@ -303,7 +309,7 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
}
/// Проверить, есть ли у пользователя доступ к паку
Future<bool> hasPackAccess(int userId, int packId) async {
Future<bool> hasPackAccess(String userId, String packId) async {
final query = select(userPacks)
..where((up) => up.userId.equals(userId) & up.packId.equals(packId));
@ -313,8 +319,8 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
/// Дать пользователю доступ к паку
Future<void> grantPackAccess({
required int userId,
required int packId,
required String userId,
required String packId,
String grantType = 'purchase',
}) async {
await into(userPacks).insert(
@ -328,7 +334,7 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
}
/// Отозвать доступ к паку
Future<void> revokePackAccess(int userId, int packId) async {
Future<void> revokePackAccess(String userId, String packId) async {
await (delete(userPacks)
..where((up) => up.userId.equals(userId) & up.packId.equals(packId))
).go();

File diff suppressed because it is too large Load diff

View file

@ -1,12 +1,11 @@
import 'package:drift/drift.dart';
import '../converters.dart';
import 'users.dart';
/// Таблица UserAchievements - достижения пользователей
class UserAchievements extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
IntColumn get userId => integer().references(Users, #id, onDelete: KeyAction.cascade)();
TextColumn get userId => text().references(Users, #id, onDelete: KeyAction.cascade)();
// Achievement ID (from AchievementDefinitions)
TextColumn get achievementId => text()();
@ -21,6 +20,9 @@ class UserAchievements extends Table {
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {id};
@override
List<String> get customConstraints => [
'UNIQUE(user_id, achievement_id)',

View file

@ -1,10 +1,11 @@
import 'package:drift/drift.dart';
import 'users.dart';
import '../converters.dart' show generateUuid;
/// Таблица Tokens - токены авторизации пользователей
class Tokens extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get userId => integer().references(Users, #id, onDelete: KeyAction.cascade)();
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
TextColumn get userId => text().references(Users, #id, onDelete: KeyAction.cascade)();
TextColumn get token => text().unique()();
TextColumn get externalUserId => text()();
@ -12,6 +13,9 @@ class Tokens extends Table {
DateTimeColumn get created => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get expires => dateTime()();
@override
Set<Column> get primaryKey => {id};
@override
List<String> get customConstraints => [
'CONSTRAINT valid_expiry CHECK (expires > created)',
@ -20,8 +24,8 @@ class Tokens extends Table {
/// Таблица RefreshTokens - refresh токены для JWT
class RefreshTokens extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get userId => integer().references(Users, #id, onDelete: KeyAction.cascade)();
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
TextColumn get userId => text().references(Users, #id, onDelete: KeyAction.cascade)();
TextColumn get jti => text().unique()(); // JWT ID
@ -29,11 +33,14 @@ class RefreshTokens extends Table {
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get expiresAt => dateTime()();
@override
Set<Column> get primaryKey => {id};
}
/// Таблица TelegramAuthCodes - коды для авторизации через Telegram
class TelegramAuthCodes extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
TextColumn get code => text().unique()();
TextColumn get telegramUserId => text()();
@ -46,4 +53,7 @@ class TelegramAuthCodes extends Table {
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get expiresAt => dateTime()();
@override
Set<Column> get primaryKey => {id};
}

View file

@ -1,10 +1,10 @@
import 'package:drift/drift.dart';
import '../converters.dart';
import '../converters.dart' show generateUuid, StringListConverter, JsonListConverter;
import 'users.dart';
/// Таблица DiscountCampaigns - кампании скидок
class DiscountCampaigns extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
TextColumn get name => text().nullable()();
@ -23,12 +23,15 @@ class DiscountCampaigns extends Table {
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
BoolColumn get isDeleted => boolean().withDefault(const Constant(false))();
@override
Set<Column> get primaryKey => {id};
}
/// Таблица Discounts - скидки
class Discounts extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get campaignId => integer().references(DiscountCampaigns, #id, onDelete: KeyAction.cascade)();
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
TextColumn get campaignId => text().references(DiscountCampaigns, #id, onDelete: KeyAction.cascade)();
// Процент скидки (0-100)
RealColumn get discountPercent => real()();
@ -42,12 +45,15 @@ class Discounts extends Table {
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
BoolColumn get isDeleted => boolean().withDefault(const Constant(false))();
@override
Set<Column> get primaryKey => {id};
}
/// Junction table для связи Discounts UserDatas (many-to-many)
class DiscountUserDatas extends Table {
IntColumn get discountId => integer().references(Discounts, #id, onDelete: KeyAction.cascade)();
IntColumn get userId => integer().references(Users, #id, onDelete: KeyAction.cascade)();
TextColumn get discountId => text().references(Discounts, #id, onDelete: KeyAction.cascade)();
TextColumn get userId => text().references(Users, #id, onDelete: KeyAction.cascade)();
DateTimeColumn get grantedAt => dateTime().withDefault(currentDateAndTime)();

View file

@ -1,9 +1,9 @@
import 'package:drift/drift.dart';
import '../converters.dart';
import '../converters.dart' show generateUuid, IntListConverter, StringListConverter;
/// Таблица CardPacks - наборы карточек
class CardPacks extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
// Основная информация
TextColumn get title => text()();
@ -23,7 +23,7 @@ class CardPacks extends Table {
// Порядок карточек (JSON array of IDs)
TextColumn get cardsOrder => text()
.withDefault(const Constant('[]'))
.map(const IntListConverter())();
.map(const StringListConverter())();
// Store IDs для покупок
TextColumn get googlePlayId => text().nullable()();
@ -38,12 +38,15 @@ class CardPacks extends Table {
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
BoolColumn get isDeleted => boolean().withDefault(const Constant(false))();
@override
Set<Column> get primaryKey => {id};
}
/// Таблица GameCards - карточки для изучения
class GameCards extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get packId => integer().references(CardPacks, #id, onDelete: KeyAction.cascade)();
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
TextColumn get packId => text().references(CardPacks, #id, onDelete: KeyAction.cascade)();
// Основной контент
TextColumn get original => text()(); // слово на иностранном языке
@ -65,17 +68,23 @@ class GameCards extends Table {
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
BoolColumn get isDeleted => boolean().withDefault(const Constant(false))();
@override
Set<Column> get primaryKey => {id};
}
/// Таблица VoiceModels - голосовые файлы для карточек
class VoiceModels extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get cardId => integer().references(GameCards, #id, onDelete: KeyAction.cascade)();
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
TextColumn get cardId => text().references(GameCards, #id, onDelete: KeyAction.cascade)();
TextColumn get voiceUrl => text()(); // URL аудиофайла
TextColumn get language => text()(); // язык озвучки
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {id};
}
// IntListConverter импортирован из converters.dart

View file

@ -1,11 +1,11 @@
import 'package:drift/drift.dart';
import '../converters.dart';
import '../converters.dart' show generateUuid, JsonListConverter, StringListConverter;
import 'users.dart';
/// Таблица Payments - платежи
class Payments extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get userId => integer().references(Users, #id, onDelete: KeyAction.cascade)();
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
TextColumn get userId => text().references(Users, #id, onDelete: KeyAction.cascade)();
TextColumn get amount => text()();
TextColumn get currency => text()();
@ -33,6 +33,9 @@ class Payments extends Table {
// Audit
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {id};
}
// Конвертеры импортированы из converters.dart

View file

@ -1,10 +1,10 @@
import 'package:drift/drift.dart';
import '../converters.dart';
import '../converters.dart' show generateUuid, JsonListConverter, StringListConverter;
import 'users.dart';
/// Таблица PromoCodesCampaigns - кампании промокодов
class PromoCodesCampaigns extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
TextColumn get template => text()();
TextColumn get name => text().nullable()();
@ -33,22 +33,28 @@ class PromoCodesCampaigns extends Table {
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
BoolColumn get isDeleted => boolean().withDefault(const Constant(false))();
@override
Set<Column> get primaryKey => {id};
}
/// Таблица PromoCodes - промокоды
class PromoCodes extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get campaignId => integer().references(PromoCodesCampaigns, #id, onDelete: KeyAction.cascade)();
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
TextColumn get campaignId => text().references(PromoCodesCampaigns, #id, onDelete: KeyAction.cascade)();
TextColumn get code => text().unique()();
IntColumn get activations => integer().withDefault(const Constant(0))();
// Индивидуальный промокод (связан с пользователем)
IntColumn get userId => integer().nullable().references(Users, #id, onDelete: KeyAction.cascade)();
TextColumn get userId => text().nullable().references(Users, #id, onDelete: KeyAction.cascade)();
// Audit
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {id};
}
// Конвертеры импортированы из converters.dart

View file

@ -5,8 +5,8 @@ import 'packs.dart' show CardPacks, GameCards, VoiceModels;
/// Junction table для связи Users CardPacks (многие ко многим)
/// Хранит информацию о том, какие паки куплены пользователем
class UserPacks extends Table {
IntColumn get userId => integer().references(Users, #id, onDelete: KeyAction.cascade)();
IntColumn get packId => integer().references(CardPacks, #id, onDelete: KeyAction.cascade)();
TextColumn get userId => text().references(Users, #id, onDelete: KeyAction.cascade)();
TextColumn get packId => text().references(CardPacks, #id, onDelete: KeyAction.cascade)();
// Когда пользователь получил доступ к паку
DateTimeColumn get grantedAt => dateTime().withDefault(currentDateAndTime)();
@ -21,8 +21,8 @@ class UserPacks extends Table {
/// Таблица PreviewCards - связь CardPacks с preview карточками
/// Хранит какие карточки показывать в превью пака
class PreviewCards extends Table {
IntColumn get packId => integer().references(CardPacks, #id, onDelete: KeyAction.cascade)();
IntColumn get cardId => integer().references(GameCards, #id, onDelete: KeyAction.cascade)();
TextColumn get packId => text().references(CardPacks, #id, onDelete: KeyAction.cascade)();
TextColumn get cardId => text().references(GameCards, #id, onDelete: KeyAction.cascade)();
IntColumn get order => integer().withDefault(const Constant(0))();
@ -33,8 +33,8 @@ class PreviewCards extends Table {
/// Таблица CardPackCards - связь CardPacks с GameCards (many-to-many)
/// Хранит какие карточки принадлежат какому паку
class CardPackCards extends Table {
IntColumn get packId => integer().references(CardPacks, #id, onDelete: KeyAction.cascade)();
IntColumn get cardId => integer().references(GameCards, #id, onDelete: KeyAction.cascade)();
TextColumn get packId => text().references(CardPacks, #id, onDelete: KeyAction.cascade)();
TextColumn get cardId => text().references(GameCards, #id, onDelete: KeyAction.cascade)();
IntColumn get order => integer().withDefault(const Constant(0))();
@ -45,8 +45,8 @@ class CardPackCards extends Table {
/// Таблица CardVoices - связь GameCards с VoiceModels (many-to-many)
/// Хранит какие голосовые файлы принадлежат какой карточке
class CardVoices extends Table {
IntColumn get cardId => integer().references(GameCards, #id, onDelete: KeyAction.cascade)();
IntColumn get voiceId => integer().references(VoiceModels, #id, onDelete: KeyAction.cascade)();
TextColumn get cardId => text().references(GameCards, #id, onDelete: KeyAction.cascade)();
TextColumn get voiceId => text().references(VoiceModels, #id, onDelete: KeyAction.cascade)();
@override
Set<Column> get primaryKey => {cardId, voiceId};

View file

@ -1,10 +1,11 @@
import 'package:drift/drift.dart';
import 'users.dart';
import '../converters.dart' show generateUuid;
/// Таблица StudySessions - сессии изучения
class StudySessions extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get userId => integer().references(Users, #id, onDelete: KeyAction.cascade)();
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
TextColumn get userId => text().references(Users, #id, onDelete: KeyAction.cascade)();
TextColumn get sessionId => text().nullable().unique()();
@ -21,4 +22,7 @@ class StudySessions extends Table {
// Audit
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {id};
}

View file

@ -1,14 +1,14 @@
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import '../converters.dart';
import '../converters.dart' show JsonMapConverter, JsonListConverter;
import 'users.dart';
/// Таблица SubscriptionPlans - планы подписки
class SubscriptionPlans extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
// UI информация (JSON)
TextColumn get ui => text().nullable().map(NullAwareTypeConverter.wrap(const JsonMapConverter()))();
TextColumn get ui => text().nullable().map(const JsonMapConverter())();
// Цена и валюта
TextColumn get price => text()();
@ -28,12 +28,15 @@ class SubscriptionPlans extends Table {
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
BoolColumn get isDeleted => boolean().withDefault(const Constant(false))();
@override
Set<Column> get primaryKey => {id};
}
/// Таблица UserSubscriptions - подписки пользователей
class UserSubscriptions extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get userId => integer().unique().references(Users, #id, onDelete: KeyAction.cascade)();
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
TextColumn get userId => text().unique().references(Users, #id, onDelete: KeyAction.cascade)();
DateTimeColumn get start => dateTime()();
DateTimeColumn get finish => dateTime()();
@ -46,6 +49,9 @@ class UserSubscriptions extends Table {
// Audit
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {id};
}
// Конвертеры импортированы из converters.dart

View file

@ -1,10 +1,10 @@
import 'package:drift/drift.dart';
import '../converters.dart';
import '../converters.dart' show generateUuid, JsonListConverter, JsonMapConverter, StringListConverter;
import 'users.dart';
/// Таблица Tasks - задачи системы
class Tasks extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
TextColumn get name => text()();
IntColumn get minCycleMillis => integer()();
@ -19,11 +19,14 @@ class Tasks extends Table {
// Audit
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {id};
}
/// Таблица UserTasks - задачи пользователей
class UserTasks extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
TextColumn get title => text()();
TextColumn get description => text()();
@ -52,13 +55,16 @@ class UserTasks extends Table {
// Audit
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {id};
}
/// Таблица UserTaskProgresses - прогресс выполнения задач пользователями
class UserTaskProgresses extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get userId => integer().references(Users, #id, onDelete: KeyAction.cascade)();
IntColumn get taskId => integer()(); // Reference to UserTasks, but not FK to avoid circular deps
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
TextColumn get userId => text().references(Users, #id, onDelete: KeyAction.cascade)();
TextColumn get taskId => text()(); // Reference to UserTasks, but not FK to avoid circular deps
// Прогресс (JSON)
TextColumn get progress => text()
@ -67,13 +73,16 @@ class UserTaskProgresses extends Table {
DateTimeColumn get startedAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {id};
}
/// Таблица UserTaskResults - результаты выполнения задач
class UserTaskResults extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get userId => integer().references(Users, #id, onDelete: KeyAction.cascade)();
IntColumn get taskId => integer()(); // Reference to UserTasks
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
TextColumn get userId => text().references(Users, #id, onDelete: KeyAction.cascade)();
TextColumn get taskId => text()(); // Reference to UserTasks
// Результаты (JSON)
TextColumn get results => text()
@ -84,6 +93,9 @@ class UserTaskResults extends Table {
// Audit
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {id};
}
// Конвертеры импортированы из converters.dart

View file

@ -1,17 +1,21 @@
import 'package:drift/drift.dart';
import '../converters.dart' show generateUuid;
/// Таблица ShareRequests - запросы на шаринг через Telegram
class ShareRequests extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
TextColumn get telegramUserId => text()();
TextColumn get telegramUsername => text().nullable()();
IntColumn get sharedCardId => integer().nullable()();
TextColumn get sharedCardId => text().nullable()();
DateTimeColumn get requestedAt => dateTime().withDefault(currentDateAndTime)();
// Audit
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {id};
}

View file

@ -1,10 +1,10 @@
import 'package:drift/drift.dart';
import '../converters.dart';
import '../converters.dart' show generateUuid, JsonMapConverter;
import 'packs.dart';
/// Таблица Tests - тесты
class Tests extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
TextColumn get name => text()();
TextColumn get color => text().nullable()();
@ -17,12 +17,15 @@ class Tests extends Table {
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
BoolColumn get isDeleted => boolean().withDefault(const Constant(false))();
@override
Set<Column> get primaryKey => {id};
}
/// Таблица TestQuestions - вопросы тестов
class TestQuestions extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get testId => integer().references(Tests, #id, onDelete: KeyAction.cascade)();
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
TextColumn get testId => text().references(Tests, #id, onDelete: KeyAction.cascade)();
// Тип вопроса (enum as string)
TextColumn get questionType => text()();
@ -31,12 +34,15 @@ class TestQuestions extends Table {
// Audit
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {id};
}
/// Таблица TestPackRelations - связь Tests с CardPacks (many-to-many)
class TestPackRelations extends Table {
IntColumn get testId => integer().references(Tests, #id, onDelete: KeyAction.cascade)();
IntColumn get packId => integer().references(CardPacks, #id, onDelete: KeyAction.cascade)();
TextColumn get testId => text().references(Tests, #id, onDelete: KeyAction.cascade)();
TextColumn get packId => text().references(CardPacks, #id, onDelete: KeyAction.cascade)();
@override
Set<Column> get primaryKey => {testId, packId};
@ -44,9 +50,9 @@ class TestPackRelations extends Table {
/// Таблица TestStatistics - статистика прохождения тестов
class TestStatistics extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get userId => integer()(); // Reference to Users, but not FK to avoid circular deps
IntColumn get testId => integer().references(Tests, #id, onDelete: KeyAction.cascade)();
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
TextColumn get userId => text()(); // Reference to Users, but not FK to avoid circular deps
TextColumn get testId => text().references(Tests, #id, onDelete: KeyAction.cascade)();
// Результаты (JSON)
TextColumn get results => text()
@ -58,6 +64,9 @@ class TestStatistics extends Table {
// Audit
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {id};
}
// Конвертеры импортированы из converters.dart

View file

@ -3,7 +3,7 @@ import '../converters.dart';
/// Таблица Users - основная информация о пользователях
class Users extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
TextColumn get externalUserId => text().unique()();
TextColumn get name => text().nullable()();
TextColumn get email => text().nullable()();
@ -22,7 +22,8 @@ class Users extends Table {
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
BoolColumn get isDeleted => boolean().withDefault(const Constant(false))();
// Primary key is auto-increment id, no need to override
@override
Set<Column> get primaryKey => {id};
@override
List<String> get customConstraints => [
@ -32,8 +33,8 @@ class Users extends Table {
/// Таблица UserDatas - расширенная информация о пользователе
class UserDatas extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get userId => integer().unique().references(Users, #id, onDelete: KeyAction.cascade)();
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
TextColumn get userId => text().unique().references(Users, #id, onDelete: KeyAction.cascade)();
// Статистика
IntColumn get totalStudyTimeMinutes => integer().withDefault(const Constant(0))();
@ -75,6 +76,9 @@ class UserDatas extends Table {
// Audit
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {id};
}
// Конвертеры импортированы из converters.dart

View file

@ -4,7 +4,6 @@ import 'package:mnemo_cards_common/mnemo_cards_common.dart';
extension DiscountCampaignModelExt on DiscountCampaignModel {
Future<DiscountCampaignDto> toDto() async {
await discounts.load();
return DiscountCampaignDto(
id: id,
start: this.start,
@ -29,7 +28,7 @@ extension DiscountModelExt on DiscountDto {
DiscountModel toModel() => DiscountModel(
discountPercent: discountPercent,
products: products.map((p) => p.toModel()).toBaseList(),
id: int.tryParse(id ?? ''),
id: id,
);
}

View file

@ -90,7 +90,7 @@ extension DiscountCampaignDtoToCompanion on DiscountCampaignDto {
/// Extension для конвертации DiscountDto в DiscountsCompanion
extension DiscountDtoToCompanion on DiscountDto {
DiscountsCompanion toCompanion(int campaignId) {
DiscountsCompanion toCompanion(String campaignId) {
final productsJson = products.map((p) => p.toJson()).toList();
return DiscountsCompanion.insert(

View file

@ -28,7 +28,7 @@ class DiscountsManager {
// Получаем ID скидок
final discountIds = discounts
.where((d) => d.id != null)
.map((d) => d.id as int)
.map((d) => d.id!)
.toList();
if (discountIds.isEmpty) return;
@ -149,9 +149,8 @@ class DiscountsManager {
return dtos;
}
Future<String?> deleteDiscountCampaign(String stringId) async {
Future<String?> deleteDiscountCampaign(String id) async {
try {
final id = int.parse(stringId);
final campaign = await _discountDao.getCampaignById(id);
if (campaign == null) {
return 'Campaign $id not found';

View file

@ -3,7 +3,7 @@ import 'package:mnemo_cards_common/mnemo_cards_common.dart';
extension ProductExt on MnemoCardsProductDto {
MnemoCardsProductModel toModel() => MnemoCardsProductModelBase(
productId: int.tryParse(id ?? ''),
productId: id,
type: type.toModel(),
);
}

View file

@ -4,7 +4,7 @@ import 'package:mnemo_cards_common/mnemo_cards_common.dart';
extension converterExtenxion on GameCardModel {
GameCardDto toDto() {
return GameCardDto(
id: id ?? -1,
id: id,
image: image,
mnemo: mnemo,
original: original,
@ -18,7 +18,7 @@ extension converterExtenxion on GameCardModel {
}
extension listConverterExtenxion on Iterable<GameCardModel> {
List<GameCardDto> toDtosList(List<int> cardsOrder) =>
List<GameCardDto> toDtosList(List<String> cardsOrder) =>
map((e) => e.toDto()).toList()
..sort((p, n) {
final pIndex = cardsOrder.indexOf(p.id);

View file

@ -81,7 +81,7 @@ extension CardPackFromDto on CardPackDto {
/// Extension для создания GameCard из DTO
extension GameCardFromDto on GameCardDto {
GameCardsCompanion toCompanion(int packId) {
GameCardsCompanion toCompanion(String packId) {
return GameCardsCompanion.insert(
packId: packId,
original: original ?? '',

View file

@ -8,7 +8,7 @@ import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'products_price_resolver.dart';
bool canOpenForAd(int? id) => id != null;
bool canOpenForAd(String? id) => id != null;
@LazySingleton()
class PackDtoConverter {
@ -26,8 +26,7 @@ class PackDtoConverter {
) async {
bool available = model.users.contains(userModel);
if (userModel != null && !available) {
await userModel.subscriptionModel.load();
available = userModel.subscriptionModel.value?.features
available = userModel.subscriptionModel?.features
.contains(SubscriptionFeatureEnum.packs) ==
true;
}
@ -153,9 +152,6 @@ class PackDtoConverter {
bool withTests = false,
bool withUsers = false,
}) async {
await model.cards.load();
await model.tests.load();
await model.previewCards.load();
return EditCardPackDto(
id: model.id.toString(),
title: model.title,
@ -205,7 +201,7 @@ class PackDtoConverter {
cover: dto.cover ?? model?.cover,
description: dto.description ?? model?.description,
version: dto.version ?? model?.version,
id: model?.id == null ? int.tryParse(dto.id ?? '') : model!.id,
id: model?.id ?? dto.id,
googlePlayId: dto.googlePlayId ?? model?.googlePlayId,
rustoreId: dto.rustoreId ?? model?.rustoreId,
appStoreId: dto.appStoreId ?? model?.appStoreId,

View file

@ -40,27 +40,27 @@ class PackManager {
.toList();
}
Future<CardPack?> getPack(int id) async {
Future<CardPack?> getPack(String id) async {
return await _db.packDao.getPackById(id);
}
Future<List<GameCard>> getCards(int packId) async {
Future<List<GameCard>> getCards(String packId) async {
return await _db.packDao.getPackCards(packId);
}
Future<GameCard?> getCard(int id) async {
Future<GameCard?> getCard(String id) async {
return await _db.packDao.getCardById(id);
}
Future<VoiceModel?> getVoice(int id) async {
Future<VoiceModel?> getVoice(String id) async {
return await _db.packDao.getVoiceById(id);
}
Future<List<VoiceModel>> getVoices(int cardId) async {
Future<List<VoiceModel>> getVoices(String cardId) async {
return await _db.packDao.getCardVoices(cardId);
}
Future<CardPackDto> getPackDto(int id, UserModel? userModel) async {
Future<CardPackDto> getPackDto(String id, UserModel? userModel) async {
final pack = await getPack(id);
if (pack == null) {
throw StateError('Pack not found');
@ -76,7 +76,7 @@ class PackManager {
return await pack.toDto(cards, voices, userModel);
}
Future<List<String>> getPackPreviewImages(int packId) async {
Future<List<String>> getPackPreviewImages(String packId) async {
final pack = await getPack(packId);
if (pack == null) {
throw StateError('Pack not found');
@ -105,7 +105,7 @@ class PackManager {
return images;
}
Future<Map<String, Uint8List>> getPackImages(int packId) async {
Future<Map<String, Uint8List>> getPackImages(String packId) async {
final cards = await getCards(packId);
final result = <String, Uint8List>{};

View file

@ -3,7 +3,7 @@ import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
extension VoiceModelExtension on VoiceModel {
VoiceDto toDto({String? url}) => VoiceDto(
id: id ?? -1,
id: id!,
phrase: phrase,
path: path,
speaker: speaker,
@ -13,7 +13,7 @@ extension VoiceModelExtension on VoiceModel {
extension VoiceDtoExtension on VoiceDto {
VoiceModel toModel() => VoiceModel(
id: id < 0 ? null : id,
id: id,
phrase: phrase,
path: path,
speaker: speaker,

View file

@ -59,13 +59,10 @@ class PromoCodesManager {
}
Future<PromoCodesCampaignDto?> promoCodeCampaign(String id) async {
final campaignId = int.tryParse(id);
if (campaignId == null) return null;
final campaign = await _db.promoCodeDao.getCampaignById(campaignId);
final campaign = await _db.promoCodeDao.getCampaignById(id);
if (campaign == null) return null;
final codes = await _db.promoCodeDao.getPromoCodesByCampaignId(campaignId);
final codes = await _db.promoCodeDao.getPromoCodesByCampaignId(id);
final promoCodes = codes.map((code) => code.code).toList();
final products = (campaign.products ?? [])
@ -108,12 +105,7 @@ class PromoCodesManager {
}
Future<void> updatePromoCodeCampaign(String id, PromoCodesCampaignDto dto) async {
final campaignId = int.tryParse(id);
if (campaignId == null) {
throw ArgumentError('Invalid campaign ID: $id');
}
final existing = await _db.promoCodeDao.getCampaignById(campaignId);
final existing = await _db.promoCodeDao.getCampaignById(id);
if (existing == null) {
throw StateError('Campaign not found: $id');
}
@ -123,7 +115,7 @@ class PromoCodesManager {
final updated = existing.copyWith(
template: dto.template,
name: drift.Value(dto.name),
products: drift.Value(productsJson as List<dynamic>?),
products: productsJson,
activationsPerCode: dto.activationsPerCode,
activationsPerUser: dto.activationsPerUser,
generationSize: dto.generationSize,
@ -138,10 +130,8 @@ class PromoCodesManager {
}
Future<String?> deletePromoCodeCampaign(String id) async {
final campaignId = int.tryParse(id);
if (campaignId == null) return 'Invalid campaign ID';
final existing = await _db.promoCodeDao.getCampaignById(campaignId);
final existing = await _db.promoCodeDao.getCampaignById(id);
if (existing == null) return 'Campaign not found';
// Soft delete - mark as deleted
@ -153,10 +143,9 @@ class PromoCodesManager {
return null; // No error
}
Future<List<dynamic>> listAvailablePromocodes(dynamic user) async {
Future<List<dynamic>> listAvailablePromocodes(String userId) async {
// Get user ID
final userId = user is int ? user : int.tryParse(user?.toString() ?? '');
if (userId == null) return [];
if (userId.isEmpty) return [];
final userCodes = await _db.promoCodeDao.getUserPromoCodes(userId);
return userCodes.map((code) => {
@ -199,13 +188,13 @@ class PromoCodesManager {
};
}
Future<dynamic> applyPromoCode(dynamic dto, dynamic user) async {
Future<dynamic> applyPromoCode(dynamic dto, String userId) async {
final code = dto['code']?.toString().toUpperCase();
if (code == null) {
throw ArgumentError('Promo code is required');
}
final validation = await validatePromocode(code, user);
final validation = await validatePromocode(code, userId);
if (!validation['valid']) {
throw StateError(validation['message']);
}
@ -214,8 +203,7 @@ class PromoCodesManager {
if (promoCode == null) return null;
// Get user ID
final userId = user is int ? user : int.tryParse(user?.toString() ?? '');
if (userId == null) {
if (userId.isEmpty) {
throw ArgumentError('Invalid user');
}
@ -236,7 +224,7 @@ class PromoCodesManager {
return {'success': true, 'code': code};
}
Future<bool> launchPromoCodesCampaign(int campaignId) async {
Future<bool> launchPromoCodesCampaign(String campaignId) async {
final campaign = await _db.promoCodeDao.getCampaignById(campaignId);
if (campaign == null) return false;

View file

@ -1,3 +1,4 @@
import 'package:drift/drift.dart';
import 'package:injectable/injectable.dart';
import 'package:mnemo_cards_backend/database/database.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
@ -14,7 +15,7 @@ class AchievementManager {
/// Check and update achievements for a user
Future<List<AchievementDto>> checkAndUnlockAchievements(
int userId,
String userId,
UserDataModel userData,
) async {
final unlockedAchievements = <AchievementDto>[];
@ -46,7 +47,7 @@ class AchievementManager {
}
/// Get achievements for a user
Future<List<AchievementDto>> getUserAchievements(int userId) async {
Future<List<AchievementDto>> getUserAchievements(String userId) async {
final userAchievements = await _db.achievementDao.getUserAchievements(userId);
final progressMap = await _db.achievementDao.getAchievementProgress(userId);
@ -74,13 +75,13 @@ class AchievementManager {
}
/// Check if user has specific achievement
Future<bool> hasAchievement(int userId, String achievementId) async {
Future<bool> hasAchievement(String userId, String achievementId) async {
return await _db.achievementDao.hasAchievement(userId, achievementId);
}
/// Unlock achievement for user
Future<AchievementDto?> unlockAchievement(
int userId,
String userId,
String achievementId,
) async {
final definition = AchievementDefinitions.getById(achievementId);
@ -89,8 +90,8 @@ class AchievementManager {
final companion = UserAchievementsCompanion.insert(
userId: userId,
achievementId: achievementId,
unlockedAt: DateTime.now(),
progress: 1.0,
unlockedAt: Value(DateTime.now()),
progress: Value(1.0),
);
try {
@ -104,41 +105,41 @@ class AchievementManager {
}
/// Get achievement progress for user
Future<Map<String, double>> getAchievementProgress(int userId) async {
Future<Map<String, double>> getAchievementProgress(String userId) async {
return await _db.achievementDao.getAchievementProgress(userId);
}
/// Check if achievement condition is met
Future<bool> _checkAchievementCondition(
int userId,
String userId,
UserDataModel userData,
AchievementDto achievement,
) async {
switch (achievement.type) {
case AchievementType.firstWordLearned:
return userData.totalCards > 0;
// case AchievementType.firstWordLearned:
// return userData.totalCards > 0;
case AchievementType.firstTestCompleted:
return userData.totalTests > 0;
// case AchievementType.firstTestCompleted:
// return userData.totalTests > 0;
case AchievementType.firstPackCompleted:
final userPacks = await _db.userDao.getUserPacks(userId);
return userPacks.isNotEmpty;
case AchievementType.words10Learned:
return userData.totalCards >= 10;
// case AchievementType.words10Learned:
// return userData.totalCards >= 10;
case AchievementType.words50Learned:
return userData.totalCards >= 50;
// case AchievementType.words50Learned:
// return userData.totalCards >= 50;
case AchievementType.words100Learned:
return userData.totalCards >= 100;
// case AchievementType.words100Learned:
// return userData.totalCards >= 100;
case AchievementType.words500Learned:
return userData.totalCards >= 500;
// case AchievementType.words500Learned:
// return userData.totalCards >= 500;
case AchievementType.words1000Learned:
return userData.totalCards >= 1000;
// case AchievementType.words1000Learned:
// return userData.totalCards >= 1000;
case AchievementType.streak7Days:
return userData.currentStreak >= 7;
@ -149,11 +150,11 @@ class AchievementManager {
case AchievementType.streak100Days:
return userData.currentStreak >= 100;
case AchievementType.perfectTest:
return userData.totalTests > 0;
// case AchievementType.perfectTest:
// return userData.totalTests > 0;
case AchievementType.speedLearner:
return userData.totalCards >= 100 && userData.totalStudyTimeMinutes < 600;
// case AchievementType.speedLearner:
// return userData.totalCards >= 100 && userData.totalStudyTimeMinutes < 600;
default:
return false;
@ -162,25 +163,25 @@ class AchievementManager {
/// Calculate progress towards achievement
Future<double> _calculateAchievementProgress(
int userId,
String userId,
UserDataModel userData,
AchievementDto achievement,
) async {
switch (achievement.type) {
case AchievementType.words10Learned:
return (userData.totalCards / 10.0).clamp(0.0, 1.0);
// case AchievementType.words10Learned:
// return (userData.totalCards / 10.0).clamp(0.0, 1.0);
case AchievementType.words50Learned:
return (userData.totalCards / 50.0).clamp(0.0, 1.0);
// case AchievementType.words50Learned:
// return (userData.totalCards / 50.0).clamp(0.0, 1.0);
case AchievementType.words100Learned:
return (userData.totalCards / 100.0).clamp(0.0, 1.0);
// case AchievementType.words100Learned:
// return (userData.totalCards / 100.0).clamp(0.0, 1.0);
case AchievementType.words500Learned:
return (userData.totalCards / 500.0).clamp(0.0, 1.0);
// case AchievementType.words500Learned:
// return (userData.totalCards / 500.0).clamp(0.0, 1.0);
case AchievementType.words1000Learned:
return (userData.totalCards / 1000.0).clamp(0.0, 1.0);
// case AchievementType.words1000Learned:
// return (userData.totalCards / 1000.0).clamp(0.0, 1.0);
default:
return 0.0;

View file

@ -11,7 +11,7 @@ class SessionTracker {
final AppDatabase _db;
/// Active sessions cache: userId -> sessionId
final Map<int, String> _activeSessions = {};
final Map<String, String> _activeSessions = {};
/// Session timers for automatic timeout: sessionId -> timer
final Map<String, Timer> _sessionTimers = {};
@ -26,7 +26,7 @@ class SessionTracker {
/// Returns the session ID of the active session.
/// If no active session exists, creates a new one.
Future<String> getOrCreateSession(
int userId, {
String userId, {
String? packId,
String? testId,
}) async {
@ -123,14 +123,14 @@ class SessionTracker {
}
/// Get active session for user
Future<StudySession?> getActiveSession(int userId) async {
Future<StudySession?> getActiveSession(String userId) async {
final activeSessions = await _db.statisticsDao.getActiveSessions(userId);
return activeSessions.isNotEmpty ? activeSessions.first : null;
}
/// Get session history for user
Future<List<StudySession>> getUserSessions(
int userId, {
String userId, {
int? limit,
DateTime? fromDate,
DateTime? toDate,

View file

@ -13,7 +13,7 @@ class StatisticsCalculator {
String packId,
) {
// Load user data
final userData = user.userData.value;
final userData = user.userData;
if (userData == null) {
return PackProgressDto.empty(packId, 0);
}

View file

@ -10,7 +10,7 @@ class TaskManager {
/// Получить все доступные задачи пользователя
Future<List<UserTask>> getUserTasks(
int userId, {
String userId, {
String? type,
String? difficulty,
String? status,
@ -26,13 +26,13 @@ class TaskManager {
}
/// Получить задачу по ID
Future<UserTask?> getUserTask(int userId, int taskId) async {
Future<UserTask?> getUserTask(String userId, String taskId) async {
final tasks = await _db.taskDao.getUserTasks(userId);
return tasks.where((task) => task.id == taskId).firstOrNull;
}
/// Начать выполнение задачи
Future<void> startTask(int userId, int taskId) async {
Future<void> startTask(String userId, String taskId) async {
await _db.transaction(() async {
// Проверить, что задача доступна
final task = await getUserTask(userId, taskId);
@ -55,7 +55,7 @@ class TaskManager {
}
/// Завершить задачу
Future<void> completeTask(int userId, int taskId) async {
Future<void> completeTask(String userId, String taskId) async {
await _db.transaction(() async {
// Проверить, что задача в процессе выполнения
final task = await getUserTask(userId, taskId);
@ -84,9 +84,9 @@ class TaskManager {
}
/// Получить прогресс выполнения задач пользователя
Future<List<UserTaskProgresses>> getUserTaskProgress(int userId) async {
Future<List<UserTaskProgressesData>> getUserTaskProgress(String userId) async {
final tasks = await _db.taskDao.getUserTasks(userId);
final progresses = <UserTaskProgresses>[];
final progresses = <UserTaskProgressesData>[];
for (final task in tasks) {
final progress = await _db.taskDao.getTaskProgress(userId, task.id);
@ -113,7 +113,7 @@ class TaskManager {
}
/// Создать новую задачу для пользователя
Future<int> createUserTask(UserTasksCompanion task) async {
Future<String> createUserTask(UserTasksCompanion task) async {
return await _db.taskDao.createUserTask(task);
}
@ -123,7 +123,7 @@ class TaskManager {
}
/// Подсчитать задачи пользователя
Future<int> countUserTasks(int userId, {String? status}) async {
Future<int> countUserTasks(String userId, {String? status}) async {
return await _db.taskDao.countUserTasks(userId, status: status);
}
}

View file

@ -15,46 +15,48 @@ abstract class _$CreationTestDataCWProxy {
CreationTestData color(String? color);
/// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `CreationTestData(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
/// Creates a new instance with the provided field values.
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `CreationTestData(...).copyWith.fieldName(value)`.
///
/// Usage
/// Example:
/// ```dart
/// CreationTestData(...).copyWith(id: 12, name: "My name")
/// ````
/// ```
CreationTestData call({
String? packId,
List<TestDataItem>? items,
String? title,
List<TestDataItem> items,
String title,
String? color,
});
}
/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfCreationTestData.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfCreationTestData.copyWith.fieldName(...)`
/// Callable proxy for `copyWith` functionality.
/// Use as `instanceOfCreationTestData.copyWith(...)` or call `instanceOfCreationTestData.copyWith.fieldName(value)` for a single field.
class _$CreationTestDataCWProxyImpl implements _$CreationTestDataCWProxy {
const _$CreationTestDataCWProxyImpl(this._value);
final CreationTestData _value;
@override
CreationTestData packId(String? packId) => this(packId: packId);
CreationTestData packId(String? packId) => call(packId: packId);
@override
CreationTestData items(List<TestDataItem> items) => this(items: items);
CreationTestData items(List<TestDataItem> items) => call(items: items);
@override
CreationTestData title(String title) => this(title: title);
CreationTestData title(String title) => call(title: title);
@override
CreationTestData color(String? color) => this(color: color);
CreationTestData color(String? color) => call(color: color);
@override
/// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `CreationTestData(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
/// Creates a new instance with the provided field values.
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `CreationTestData(...).copyWith.fieldName(value)`.
///
/// Usage
/// Example:
/// ```dart
/// CreationTestData(...).copyWith(id: 12, name: "My name")
/// ````
/// ```
CreationTestData call({
Object? packId = const $CopyWithPlaceholder(),
Object? items = const $CopyWithPlaceholder(),
@ -83,7 +85,8 @@ class _$CreationTestDataCWProxyImpl implements _$CreationTestDataCWProxy {
}
extension $CreationTestDataCopyWith on CreationTestData {
/// Returns a callable class that can be used as follows: `instanceOfCreationTestData.copyWith(...)` or like so:`instanceOfCreationTestData.copyWith.fieldName(...)`.
/// Returns a callable class used to build a new instance with modified fields.
/// Example: `instanceOfCreationTestData.copyWith(...)` or `instanceOfCreationTestData.copyWith.fieldName(...)`.
// ignore: library_private_types_in_public_api
_$CreationTestDataCWProxy get copyWith => _$CreationTestDataCWProxyImpl(this);
}
@ -99,51 +102,53 @@ abstract class _$TestDataItemCWProxy {
TestDataItem audio(String? audio);
/// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `TestDataItem(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
/// Creates a new instance with the provided field values.
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `TestDataItem(...).copyWith.fieldName(value)`.
///
/// Usage
/// Example:
/// ```dart
/// TestDataItem(...).copyWith(id: 12, name: "My name")
/// ````
/// ```
TestDataItem call({
String? id,
String? original,
String? translation,
String id,
String original,
String translation,
String? image,
String? audio,
});
}
/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfTestDataItem.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfTestDataItem.copyWith.fieldName(...)`
/// Callable proxy for `copyWith` functionality.
/// Use as `instanceOfTestDataItem.copyWith(...)` or call `instanceOfTestDataItem.copyWith.fieldName(value)` for a single field.
class _$TestDataItemCWProxyImpl implements _$TestDataItemCWProxy {
const _$TestDataItemCWProxyImpl(this._value);
final TestDataItem _value;
@override
TestDataItem id(String id) => this(id: id);
TestDataItem id(String id) => call(id: id);
@override
TestDataItem original(String original) => this(original: original);
TestDataItem original(String original) => call(original: original);
@override
TestDataItem translation(String translation) =>
this(translation: translation);
call(translation: translation);
@override
TestDataItem image(String? image) => this(image: image);
TestDataItem image(String? image) => call(image: image);
@override
TestDataItem audio(String? audio) => this(audio: audio);
TestDataItem audio(String? audio) => call(audio: audio);
@override
/// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `TestDataItem(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
/// Creates a new instance with the provided field values.
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `TestDataItem(...).copyWith.fieldName(value)`.
///
/// Usage
/// Example:
/// ```dart
/// TestDataItem(...).copyWith(id: 12, name: "My name")
/// ````
/// ```
TestDataItem call({
Object? id = const $CopyWithPlaceholder(),
Object? original = const $CopyWithPlaceholder(),
@ -162,9 +167,9 @@ class _$TestDataItemCWProxyImpl implements _$TestDataItemCWProxy {
: original as String,
translation:
translation == const $CopyWithPlaceholder() || translation == null
? _value.translation
// ignore: cast_nullable_to_non_nullable
: translation as String,
? _value.translation
// ignore: cast_nullable_to_non_nullable
: translation as String,
image: image == const $CopyWithPlaceholder()
? _value.image
// ignore: cast_nullable_to_non_nullable
@ -178,7 +183,8 @@ class _$TestDataItemCWProxyImpl implements _$TestDataItemCWProxy {
}
extension $TestDataItemCopyWith on TestDataItem {
/// Returns a callable class that can be used as follows: `instanceOfTestDataItem.copyWith(...)` or like so:`instanceOfTestDataItem.copyWith.fieldName(...)`.
/// Returns a callable class used to build a new instance with modified fields.
/// Example: `instanceOfTestDataItem.copyWith(...)` or `instanceOfTestDataItem.copyWith.fieldName(...)`.
// ignore: library_private_types_in_public_api
_$TestDataItemCWProxy get copyWith => _$TestDataItemCWProxyImpl(this);
}
@ -188,10 +194,7 @@ extension $TestDataItemCopyWith on TestDataItem {
// **************************************************************************
CreationTestData _$CreationTestDataFromJson(Map<String, dynamic> json) {
$checkKeys(
json,
requiredKeys: const ['pack'],
);
$checkKeys(json, requiredKeys: const ['pack']);
return CreationTestData(
packId: json['pack'] as String?,
items: (json['data'] as List<dynamic>)
@ -211,12 +214,12 @@ Map<String, dynamic> _$CreationTestDataToJson(CreationTestData instance) =>
};
TestDataItem _$TestDataItemFromJson(Map<String, dynamic> json) => TestDataItem(
id: testDataItemIdFromJson(json, 'id') as String,
original: json['original'] as String,
translation: json['translation'] as String,
image: json['image'] as String?,
audio: testDataItemAudioFromJson(json, 'audio') as String?,
);
id: testDataItemIdFromJson(json, 'id') as String,
original: json['original'] as String,
translation: json['translation'] as String,
image: json['image'] as String?,
audio: testDataItemAudioFromJson(json, 'audio') as String?,
);
Map<String, dynamic> _$TestDataItemToJson(TestDataItem instance) =>
<String, dynamic>{

View file

@ -26,7 +26,7 @@ extension TestModelExtension on TestModel {
extension TestDtoExtension on TestDto {
TestModel toEmptyModel() {
return TestModel(
id: int.tryParse(id ?? ''),
id: id,
name: name,
color: color,
cover: cover,

View file

@ -16,7 +16,7 @@ class TestManager {
TestManager(this._db);
Future<TestStatisticsDto?> _testStatisticsDto(
int userId, int testId) async {
String userId, String testId) async {
final statistics = await _db.testDao.getTestStatistics(userId, testId);
if (statistics == null) return null;
@ -45,8 +45,7 @@ class TestManager {
}
Future<TestDto?> fetchTest(String id, UserModel user) async {
final testId = int.tryParse(id);
if (testId == null) return null;
final testId = id;
final test = await _db.testDao.getTestById(testId);
if (test == null) return null;
@ -197,10 +196,7 @@ class TestManager {
await addTest(testDto);
// Link test to pack
final testId = int.tryParse(testDto.id ?? '');
if (testId != null) {
await _db.testDao.linkTestToPack(testId, packId);
}
await _db.testDao.linkTestToPack(testDto.id!, packId);
}
Future<void> addTest(TestDto testDto) async {

View file

@ -1,9 +1,8 @@
import 'package:isar/isar.dart';
import 'package:drift/drift.dart' as drift;
import 'package:mnemo_cards_backend/database/database.dart';
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
/// Extension для конвертации User (Drift) в UserModel (Isar)
/// Extension для конвертации User (Drift) в UserModel
extension UserToUserModel on User {
Future<UserModel> toUserModel() async {
final userModel = UserModel(
@ -23,7 +22,7 @@ extension UserToUserModel on User {
extension UserModelToUser on UserModel {
UsersCompanion toUsersCompanion() {
return UsersCompanion(
id: id != null ? drift.Value(id as int) : const drift.Value.absent(),
id: id != null ? drift.Value(id!) : const drift.Value.absent(),
externalUserId: const drift.Value.absent(),
name: drift.Value(name),
email: drift.Value(email),

View file

@ -15,7 +15,7 @@ import '../statistics/achievement_manager.dart';
import 'secure.dart';
import 'user_drift_extension.dart';
Map<int?, DateTime> _onlineUsers = {};
Map<String?, DateTime> _onlineUsers = {};
@lazySingleton
class UserManager {
@ -33,13 +33,13 @@ class UserManager {
this._achievementManager,
);
Future<UserModel?> fetchUser(int id) async {
Future<UserModel?> fetchUser(String id) async {
final user = await _db.userDao.getUserById(id);
if (user == null) return null;
return await user.toUserModel();
}
DateTime? lastOnline(int id) => _onlineUsers[id];
DateTime? lastOnline(String id) => _onlineUsers[id];
Future<String> createOrGetAuthToken(UserModel user, String externalId) async {
if (user.id == null) {

View file

@ -1,486 +0,0 @@
import 'dart:developer';
import 'package:injectable/injectable.dart';
import 'package:isar/isar.dart';
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import '../main.dart';
import '../packs/free_packs_distributor.dart';
import '../statistics/session_tracker.dart';
import '../statistics/statistics_calculator.dart';
import '../statistics/achievement_manager.dart';
import 'secure.dart';
Map<Id?, DateTime> _onlineUsers = {};
@lazySingleton
class UserManager {
final FreePacksDistributor _freePacksDistributor;
final SessionTracker _sessionTracker;
final StatisticsCalculator _statisticsCalculator;
final AchievementManager _achievementManager;
UserManager(
this._freePacksDistributor,
this._sessionTracker,
this._statisticsCalculator,
this._achievementManager,
);
Future<UserModel?> fetchUser(Id id) async {
final user = await isar.userModels.get(id);
return user;
}
DateTime? lastOnline(Id id) => _onlineUsers[id];
Future<String> createOrGetAuthToken(UserModel user, String externalId) async {
if (user.id == null) {
throw Exception('Cant create token for empty id');
}
final now = DateTime.now();
final tokenModel =
await isar.tokenModels.filter().userIdEqualTo(user.id!).findFirst();
if (tokenModel != null) {
if (tokenModel.expires.isAfter(now)) {
return tokenModel.token;
}
await isar.tokenModels.delete(tokenModel.id!);
}
final userToken = Secure.token();
await isar.tokenModels.put(
TokenModel(
token: userToken,
externalUserId: externalId,
userId: user.id!,
created: now,
expires: now.add(Duration(days: 360)),
),
);
return userToken;
}
Future<UserModel?> getUserByToken(String authToken) async {
final tokenModel =
await isar.tokenModels.filter().tokenEqualTo(authToken).findFirst();
if (tokenModel == null) {
return null;
}
final now = DateTime.now();
if (tokenModel.expires.isBefore(now)) {
isar.writeTxn(() => isar.tokenModels.delete(tokenModel.id!));
return null;
}
final user = await isar.userModels.get(tokenModel.userId);
if (user == null) {
return null;
} else {
_onlineUsers[user.id] = now;
if (_onlineUsers.length > 300) {
updateOnlineUsers();
}
}
return user;
}
Future<void> updateOnlineUsers() async {
if (_onlineUsers.isEmpty) {
return;
}
return isar.writeTxn(() async {
final ids = _onlineUsers.keys.whereNotNull().toList();
final users = (await isar.userModels.getAll(ids)).whereNotNull();
await Future.wait(users.map((user) => user.userData.load()));
final updated = users
.map((user) => user.userData.value?.copyWith(
lastTimeOnline: _onlineUsers[user.id] ??
user.userData.value?.lastTimeOnline,
))
.whereNotNull()
.toList();
await isar.userDataModels.putAll(updated);
_onlineUsers.clear();
});
}
Future<List<TokenModel>> getTokensByUser(String userId) async {
final id = int.tryParse(userId);
if (id == null) return [];
return isar.txn(
() => isar.tokenModels.filter().userIdEqualTo(id).findAll(),
);
}
Future<(UserModel, String)> createOrGetUser({
required String externalId,
required String email,
String? name,
}) async {
final tokenModel = await isar.tokenModels
.filter()
.externalUserIdEqualTo(externalId)
.findFirst();
var user = tokenModel == null ? null : await fetchUser(tokenModel.userId);
if (user == null) {
print('Creating new user $name $email');
final modelAndToken = await isar.writeTxn(() async {
final userModel = UserModel.empty.copyWith(
email: email,
name: name,
);
await isar.userModels.put(userModel);
print('User $name $email saved');
await isar.tokenModels
.filter()
.externalUserIdEqualTo(externalId)
.deleteFirst();
print('User tokens deleted $name $email');
final token = await createOrGetAuthToken(userModel, externalId);
final userData = UserDataModel()..user.value = userModel;
await isar.userDataModels.put(userData);
userModel.userData.value = userData;
await userModel.userData.save();
return (userModel, token);
});
await _freePacksDistributor.giveFreePacksToUser(modelAndToken.$1);
return modelAndToken;
} else {
if (user.email == null) {
user = user.copyWith.email(email);
await isar.writeTxn(() => isar.userModels.put(user!));
}
await isar.writeTxn(() async {
await user!.userData.load();
if (user.userData.value == null) {
final userData = UserDataModel()..user.value = user;
await isar.userDataModels.put(userData);
user.userData.value = userData;
await user.userData.save();
}
});
return (user, tokenModel!.token);
}
}
// Future<void> addWordStatistics(
// UserModel user, AllWordsStatisticsDto wordStat) async {
// // final currentData =
// // user.userData?.decode(UserDataDto.fromJson) ?? UserDataDto();
// // updateUserData(
// // user,
// // currentData.copyWith.allWordsStatistics(
// // wordStat.merge(currentData.allWordsStatistics),
// // ),
// // );
// }
Future<void> addTestStatistics(
UserModel user,
TestStatisticsDto testStat,
) async {
UserDataModel currentData;
print('adding test stats');
if (user.userData.value == null) {
print('new user data');
currentData = UserDataModel();
await isar.writeTxn(() async {
final id = await isar.userDataModels.put(
currentData..user.value = user,
);
currentData = (await isar.userDataModels.get(id))!;
});
print('user data saved');
} else {
print('found user data');
currentData = user.userData.value!;
}
if (testStat.sessionToken == currentData.lastTestSessionToken) {
print('old session token');
return;
}
final testStatistics = await isar.txn(
() async =>
await currentData.testsStatistics
.filter()
.test((q) => q.idEqualTo(testStat.testId))
.findFirst() ??
TestStatisticsModel(),
);
print('got tests stat for ${testStat.testId}');
final allWords = currentData.words.mergeWithDto(testStat.words.words);
print(
'merged all words: ${allWords.length} (was ${currentData.words.length})');
return isar.writeTxn(() async {
try {
final test = await isar.testModels.get(testStat.testId);
// final testWords = updateWords(testStatistics.words, testStat.words.words);
final updatedTestStat = testStatistics.copyWith.attempts(
[
...testStatistics.attempts.takeLast(2),
TestAttempt(
words: testStat.words.words.map((e) => e.toModel()).toList(),
sessionToken: testStat.sessionToken,
),
],
)..test.value = test;
await isar.testStatisticsModels.put(updatedTestStat);
await updatedTestStat.test.save();
print('updated test stats saved');
// Calculate updated statistics
final studyDates = List<DateTime>.from(currentData.studyDates);
final today = DateTime.now();
final todayNormalized = DateTime(today.year, today.month, today.day);
// Add today's study date if not already present
if (!studyDates.any((date) =>
date.year == todayNormalized.year &&
date.month == todayNormalized.month &&
date.day == todayNormalized.day)) {
studyDates.add(todayNormalized);
}
// Calculate current streak based on study dates
final currentStreak = _statisticsCalculator.calculateStreak(studyDates);
// Calculate longest streak
final longestStreak = currentStreak > currentData.longestStreak
? currentStreak
: currentData.longestStreak;
// Calculate total study time (simplified - add time from this test)
final testDurationMinutes = 5; // Assume 5 minutes per test as default
final totalStudyTimeMinutes =
currentData.totalStudyTimeMinutes + testDurationMinutes;
// Calculate pack progress if we have pack info
final packProgress =
List<PackProgressDto>.from(currentData.packProgress);
// Update pack progress if test was for a specific pack
if (testStat.testId != null) {
// This would need pack information lookup - simplified for now
// In a real implementation, we'd update the specific pack's progress
}
// Check and unlock achievements with updated data
final tempUpdatedData = currentData.copyWith(
currentStreak: currentStreak,
longestStreak: longestStreak,
studyDates: studyDates,
totalStudyTimeMinutes: totalStudyTimeMinutes,
words: allWords,
);
// Check for newly unlocked achievements
await checkAndUpdateAchievements(user, tempUpdatedData);
final updatedData = currentData.copyWith(
lastTestSessionToken: testStat.sessionToken,
words: allWords,
currentStreak: currentStreak,
longestStreak: longestStreak,
studyDates: studyDates,
totalStudyTimeMinutes: totalStudyTimeMinutes,
lastTimeOnline: today,
)
..testsStatistics.add(updatedTestStat)
..user.value = user;
// Note: packProgress and achievements are handled separately
// since UserDataModel expects Model types, not DTO types
print('saving updated user data');
await isar.userDataModels.put(updatedData);
await updatedData.testsStatistics.save();
await updatedData.user.save();
print('user data saved');
// Track session activity
await _updateSessionFromTest(user, testStat, updatedData);
} catch (e, s) {
print('error while saving data ${e.toString()} ${s.toString()}');
log('error', error: e, stackTrace: s);
}
});
}
/// Check and update achievements for a user
Future<List<AchievementDto>> checkAndUpdateAchievements(
UserModel user,
UserDataModel userData,
) async {
try {
final newlyUnlockedAchievements =
await _achievementManager.checkAndUnlockAchievements(
user.id!,
userData,
);
if (newlyUnlockedAchievements.isNotEmpty) {
print(
'New achievements unlocked for user ${user.id}: ${newlyUnlockedAchievements.map((a) => a.title).join(', ')}');
}
return newlyUnlockedAchievements;
} catch (e, s) {
print('Error checking achievements for user ${user.id}: $e\n$s');
return [];
}
}
/// Update session tracking based on test completion
Future<void> _updateSessionFromTest(
UserModel user,
TestStatisticsDto testStat,
UserDataModel updatedData,
) async {
if (user.id == null) return;
try {
// Get or create session for this user
final sessionId = await _sessionTracker.getOrCreateSession(
user.id!,
testId: testStat.testId.toString(),
);
// Calculate test statistics
final wordsLearned = testStat.words.words.length;
final correctAnswers = testStat.words.words.fold<int>(
0,
(sum, word) => sum + word.correct.toInt(),
);
final totalAnswers = testStat.words.words.fold<int>(
0,
(sum, word) => sum + word.correct.toInt() + word.incorrect.toInt(),
);
final accuracy = totalAnswers > 0 ? correctAnswers / totalAnswers : 0.0;
// Update session progress
await _sessionTracker.updateSessionProgress(
sessionId,
wordsLearned: wordsLearned,
testsCompleted: 1,
accuracy: accuracy,
);
print(
'Updated session $sessionId: +$wordsLearned words, accuracy: ${accuracy.toStringAsFixed(2)}');
} catch (e, s) {
print('Error updating session tracking: $e\n$s');
// Don't fail the main operation if session tracking fails
}
}
Future<void> updateUserSettings(UserModel user, UserSettingsDto settings) {
return isar.writeTxn(
() => isar.userModels.put(
user.copyWith(userSettings: settings.encode()),
),
);
}
Future<bool> editUser(UserDto user) async {
if (user.id == null || user.id! < 0) {
throw Exception('Cant edit new user');
}
final existUser = await isar.userModels.get(user.id!);
if (existUser == null) {
throw Exception('User ${user.id} not found');
}
final dtoPackIds = user.packs.map((s) => int.tryParse(s)).whereNotNull();
final dtoPacks = dtoPackIds.isEmpty
? <CardPackModel>[]
: (await isar.cardPackModels.getAll(dtoPackIds.toList()))
.whereNotNull()
.toList();
isar.writeTxn(() async {
final updatedUser = existUser.copyWith(
name: user.name ?? existUser.name,
// subscription: user.subscription,
// userSettings: user.userSettingsDto?.encode() ?? existUser.userSettings,
// userData: user.userDataDto?.encode() ?? existUser.userData,
);
final id = await isar.userModels.put(updatedUser);
final packs = (await isar.userModels.get(id))!.packs;
await updatedUser.subscriptionModel.load();
final subscriptionModel = updatedUser.subscriptionModel.value;
print(
'Editing user ${user.id}, subscription = ${user.subscription} and hasModel = ${subscriptionModel != null}',
);
if (user.subscription == true) {
UserSubscriptionModel userSubscriptionModel;
if (subscriptionModel == null) {
print('Creating new sub model');
userSubscriptionModel = UserSubscriptionModel(
start: DateTime.now(),
finish: DateTime.now().add(Duration(days: 1)),
features: [
SubscriptionFeatureEnum.packs,
SubscriptionFeatureEnum.ads,
],
);
} else {
print('Updating sub model');
userSubscriptionModel = subscriptionModel.copyWith(
start: DateTime.now(),
finish: DateTime.now().add(Duration(days: 1)),
features: [
SubscriptionFeatureEnum.packs,
SubscriptionFeatureEnum.ads,
]);
}
updatedUser.subscriptionModel.value = userSubscriptionModel;
await isar.userSubscriptionModels.put(userSubscriptionModel);
await updatedUser.subscriptionModel.save();
} else if (subscriptionModel != null) {
final updatedModel = subscriptionModel.copyWith(
start: DateTime.now(),
finish: DateTime.now(),
features: [],
)..user.value = updatedUser;
await isar.userSubscriptionModels.put(updatedModel);
updatedUser.subscriptionModel.value = updatedModel;
await updatedUser.subscriptionModel.save();
}
await packs.reset();
await (packs..addAll(dtoPacks)).save();
});
return true;
}
Future<bool> deleteUser(String stringId) async {
final id = int.tryParse(stringId);
if (id != null) {
await isar.writeTxn(() async {
final tokens = await isar.tokenModels
.filter()
.userIdEqualTo(id)
.idProperty()
.findAll();
if (tokens.isNotEmpty) {
await isar.tokenModels.deleteAll(tokens);
}
await isar.testStatisticsModels
.filter()
.userData((q) => q.user((u) => u.idEqualTo(id)))
.deleteAll();
await isar.userDataModels
.filter()
.user((q) => q.idEqualTo(id))
.deleteAll();
return isar.userModels.delete(id);
});
return true;
}
return false;
}
}

View file

@ -1,12 +1,9 @@
import 'dart:developer';
import 'package:injectable/injectable.dart';
import 'package:drift/drift.dart' as drift;
import 'package:mnemo_cards_backend/database/database.dart';
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import '../main.dart';
import '../packs/free_packs_distributor.dart';
import '../statistics/session_tracker.dart';
import '../statistics/statistics_calculator.dart';
@ -14,7 +11,7 @@ import '../statistics/achievement_manager.dart';
import 'secure.dart';
import 'user_drift_extension.dart';
Map<int?, DateTime> _onlineUsers = {};
Map<String?, DateTime> _onlineUsers = {};
@lazySingleton
class UserManager {
@ -32,13 +29,13 @@ class UserManager {
this._achievementManager,
);
Future<UserModel?> fetchUser(int id) async {
Future<UserModel?> fetchUser(String id) async {
final user = await _db.userDao.getUserById(id);
if (user == null) return null;
return await user.toUserModel();
}
DateTime? lastOnline(int id) => _onlineUsers[id];
DateTime? lastOnline(String id) => _onlineUsers[id];
Future<String> createOrGetAuthToken(UserModel user, String externalId) async {
if (user.id == null) {

View file

@ -4,10 +4,7 @@ import 'package:mnemo_cards_common/mnemo_cards_common.dart';
extension UserModelExtension on UserModel {
Future<UserDto> toDto() async {
await userData.load();
await subscriptionModel.load();
final subscription = subscriptionModel.value;
final activeSubscription = subscription != null && subscription.isActive;
final activeSubscription = subscriptionModel != null && subscriptionModel!.isActive;
return UserDto(
id: id,
name: name,
@ -15,8 +12,8 @@ extension UserModelExtension on UserModel {
admin: admin,
packs: packs.map((e) => e.id?.toString()).whereNotNull().toList(),
subscription: activeSubscription,
subscriptionFeatures: subscription?.features.toSet() ?? {},
userDataDto: userData.value?.toDto(),
subscriptionFeatures: subscriptionModel?.features.toSet() ?? {},
userDataDto: userData?.toDto(),
);
}
}

View file

@ -13,26 +13,18 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a
sha256: c209688d9f5a5f26b2fb47a188131a6fb9e876ae9e47af3737c0b4f58a93470d
url: "https://pub.dev"
source: hosted
version: "61.0.0"
version: "91.0.0"
analyzer:
dependency: transitive
dependency: "direct overridden"
description:
name: analyzer
sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562
sha256: f51c8499b35f9b26820cfe914828a6a98a94efd5cc78b37bb7d03debae3a1d08
url: "https://pub.dev"
source: hosted
version: "5.13.0"
analyzer_plugin:
dependency: transitive
description:
name: analyzer_plugin
sha256: c1d5f167683de03d5ab6c3b53fc9aeefc5d59476e7810ba7bbddff50c6f4392d
url: "https://pub.dev"
source: hosted
version: "0.11.2"
version: "8.4.1"
archive:
dependency: "direct dev"
description:
@ -101,18 +93,18 @@ packages:
dependency: transitive
description:
name: build
sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0"
sha256: c1668065e9ba04752570ad7e038288559d1e2ca5c6d0131c0f5f55e39e777413
url: "https://pub.dev"
source: hosted
version: "2.4.1"
version: "4.0.3"
build_config:
dependency: transitive
description:
name: build_config
sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1
sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
version: "1.2.0"
build_daemon:
dependency: transitive
description:
@ -121,30 +113,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.2"
build_resolvers:
dependency: transitive
description:
name: build_resolvers
sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
build_runner:
dependency: "direct dev"
description:
name: build_runner
sha256: "644dc98a0f179b872f612d3eb627924b578897c629788e858157fa5e704ca0c7"
sha256: "110c56ef29b5eb367b4d17fc79375fa8c18a6cd7acd92c05bb3986c17a079057"
url: "https://pub.dev"
source: hosted
version: "2.4.11"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
sha256: e3c79f69a64bdfcd8a776a3c28db4eb6e3fb5356d013ae5eb2e52007706d5dbe
url: "https://pub.dev"
source: hosted
version: "7.3.1"
version: "2.10.4"
built_collection:
dependency: transitive
description:
@ -157,10 +133,10 @@ packages:
dependency: transitive
description:
name: built_value
sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb
sha256: "426cf75afdb23aa74bd4e471704de3f9393f3c7b04c1e2d9c6f1073ae0b8b139"
url: "https://pub.dev"
source: hosted
version: "8.9.2"
version: "8.12.1"
charcode:
dependency: transitive
description:
@ -205,18 +181,18 @@ packages:
dependency: transitive
description:
name: code_builder
sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37
sha256: "11654819532ba94c34de52ff5feb52bd81cba1de00ef2ed622fd50295f9d4243"
url: "https://pub.dev"
source: hosted
version: "4.10.0"
version: "4.11.0"
collection:
dependency: transitive
description:
name: collection
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.0"
version: "1.19.1"
convert:
dependency: transitive
description:
@ -226,21 +202,21 @@ packages:
source: hosted
version: "3.1.1"
copy_with_extension:
dependency: transitive
description:
name: copy_with_extension
sha256: fbcf890b0c34aedf0894f91a11a579994b61b4e04080204656b582708b5b1125
url: "https://pub.dev"
source: hosted
version: "5.0.4"
copy_with_extension_gen:
dependency: "direct main"
description:
name: copy_with_extension_gen
sha256: "51cd11094096d40824c8da629ca7f16f3b7cea5fc44132b679617483d43346b0"
name: copy_with_extension
sha256: bf9f6ca0b88cce32a92e7386ab0dfb179b57d080e4e94254d3f90122e8c354c8
url: "https://pub.dev"
source: hosted
version: "5.0.4"
version: "11.0.0"
copy_with_extension_gen:
dependency: "direct dev"
description:
name: copy_with_extension_gen
sha256: "4af72c664b7b4cb4c467c084602a1cb87aa0d5ba06d7a06038500e8398810ce1"
url: "https://pub.dev"
source: hosted
version: "11.0.0"
coverage:
dependency: transitive
description:
@ -261,10 +237,10 @@ packages:
dependency: transitive
description:
name: dart_style
sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55"
sha256: a9c30492da18ff84efe2422ba2d319a89942d93e58eb0b73d32abe822ef54b7b
url: "https://pub.dev"
source: hosted
version: "2.3.2"
version: "3.1.3"
dio:
dependency: "direct main"
description:
@ -285,18 +261,18 @@ packages:
dependency: "direct main"
description:
name: drift
sha256: b50a8342c6ddf05be53bda1d246404cbad101b64dc73e8d6d1ac1090d119b4e2
sha256: "3669e1b68d7bffb60192ac6ba9fd2c0306804d7a00e5879f6364c69ecde53a7f"
url: "https://pub.dev"
source: hosted
version: "2.15.0"
version: "2.30.0"
drift_dev:
dependency: "direct dev"
description:
name: drift_dev
sha256: c037d9431b6f8dc633652b1469e5f53aaec6e4eb405ed29dd232fa888ef10d88
sha256: afe4d1d2cfce6606c86f11a6196e974a2ddbfaa992956ce61e054c9b1899c769
url: "https://pub.dev"
source: hosted
version: "2.15.0"
version: "2.30.0"
drift_postgres:
dependency: "direct main"
description:
@ -445,18 +421,18 @@ packages:
dependency: "direct main"
description:
name: injectable
sha256: "69874ba3ec10e3a0de3f519a184442878291d928f3299d718813f24642585198"
sha256: "29559f7e3daebf0084597de86a825ae7f149d9e30264b7fbc71d1069ae82697d"
url: "https://pub.dev"
source: hosted
version: "2.4.4"
version: "2.6.0"
injectable_generator:
dependency: "direct dev"
description:
name: injectable_generator
sha256: "7fb573114f8bbdd169f7ae9b0bcd13f464e8170454c27be816d5a1bb39ac8086"
sha256: "309c3f3546160dd00b575f16b341a6a3025479950441bcc7fcb2f8404a40d326"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
version: "2.9.1"
io:
dependency: transitive
description:
@ -465,14 +441,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.4"
isar:
dependency: transitive
description:
name: isar
sha256: "99165dadb2cf2329d3140198363a7e7bff9bbd441871898a87e26914d25cf1ea"
url: "https://pub.dev"
source: hosted
version: "3.1.0+1"
jaguar_jwt:
dependency: "direct main"
description:
@ -498,13 +466,13 @@ packages:
source: hosted
version: "4.9.0"
json_serializable:
dependency: "direct main"
dependency: "direct dev"
description:
name: json_serializable
sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b
sha256: "6b253f7851cf1626a05c8b49c792e04a14897349798c03798137f2b5f7e0b5b1"
url: "https://pub.dev"
source: hosted
version: "6.8.0"
version: "6.11.3"
logging:
dependency: transitive
description:
@ -525,10 +493,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.15.0"
version: "1.17.0"
mime:
dependency: transitive
description:
@ -555,10 +523,10 @@ packages:
dependency: "direct dev"
description:
name: mockito
sha256: "6841eed20a7befac0ce07df8116c8b8233ed1f4486a7647c7fc5a02ae6163917"
sha256: dac24d461418d363778d53198d9ac0510b9d073869f078450f195766ec48d05e
url: "https://pub.dev"
source: hosted
version: "5.4.4"
version: "5.6.1"
neat_periodic_task:
dependency: "direct main"
description:
@ -579,10 +547,10 @@ packages:
dependency: transitive
description:
name: open_api_specification
sha256: "9ff78cbce9787a8d78f5bf43f5d3f3fce62bdce6f716409cbc7845d7efae3db8"
sha256: "10c5bfb31ab8c7aab56c4defb5e736bacc779b1dc1e238bcfee97ed4a53b5ce8"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
version: "3.0.0"
package_config:
dependency: transitive
description:
@ -715,18 +683,18 @@ packages:
dependency: "direct main"
description:
name: shelf_open_api
sha256: "84aea27400df421a01a42e4319c108b2858fa9bfecbd5f3b418be36308d7592a"
sha256: "9b9671c1f76f7a33ebf12cce0b0409f9bf5168815ba5bdd19fb6967be35073e4"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
version: "3.0.0"
shelf_open_api_generator:
dependency: "direct dev"
description:
name: shelf_open_api_generator
sha256: "7a7169cd8949797e7fd4990ccd6a05597354fe18ffe1782171ad375a9854628e"
sha256: "65ce96da06a16c2010de6ef0dfe82da8621ffebfe64f876a30f4da769fa6c3e6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
version: "3.2.1"
shelf_packages_handler:
dependency: transitive
description:
@ -747,10 +715,26 @@ packages:
dependency: "direct dev"
description:
name: shelf_router_generator
sha256: dce5ad77c5db0271ffcc282ced6b106145c8f953e83f5074a13a07811c818793
sha256: "310416e0eb5a96c8b27f2586367f07b09dc480af06485c1f7951bbed1e8b8b08"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
version: "1.1.3"
shelf_routing:
dependency: transitive
description:
name: shelf_routing
sha256: "3ae2c56fc3428c081cae62280c4d60db79c5dbd8181af58aa2b0c6af961def7a"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
shelf_routing_generator:
dependency: transitive
description:
name: shelf_routing_generator
sha256: fb18551e3c648043bacdfa7a94471de9b15999471ea869cb90644758873ebde9
url: "https://pub.dev"
source: hosted
version: "2.0.1"
shelf_static:
dependency: "direct main"
description:
@ -787,18 +771,18 @@ packages:
dependency: transitive
description:
name: source_gen
sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832"
sha256: "07b277b67e0096c45196cbddddf2d8c6ffc49342e88bf31d460ce04605ddac75"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
version: "4.1.1"
source_helper:
dependency: transitive
description:
name: source_helper
sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd"
sha256: e82b1996c63da42aa3e6a34cc1ec17427728a1baf72ed017717a5669a7123f0d
url: "https://pub.dev"
source: hosted
version: "1.3.4"
version: "1.3.9"
source_map_stack_trace:
dependency: transitive
description:
@ -835,10 +819,10 @@ packages:
dependency: transitive
description:
name: sqlparser
sha256: "7b20045d1ccfb7bc1df7e8f9fee5ae58673fce6ff62cefbb0e0fd7214e90e5a0"
sha256: "162435ede92bcc793ea939fdc0452eef0a73d11f8ed053b58a89792fba749da5"
url: "https://pub.dev"
source: hosted
version: "0.34.1"
version: "0.42.1"
stack_trace:
dependency: transitive
description:
@ -903,14 +887,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.5"
timing:
dependency: transitive
description:
name: timing
sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
typed_data:
dependency: transitive
description:
@ -1016,4 +992,4 @@ packages:
source: hosted
version: "1.0.5"
sdks:
dart: ">=3.5.0 <4.0.0"
dart: ">=3.9.0 <4.0.0"

View file

@ -7,7 +7,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+5
environment:
sdk: '>=3.0.0 <4.0.0'
sdk: '>=3.8.0 <4.0.0'
dependencies:
@ -31,18 +31,17 @@ dependencies:
# path_provider: ^2.1.1
get_it: ^7.6.4
injectable: ^2.4.1
copy_with_extension_gen: ^5.0.4
copy_with_extension: ^11.0.0
shelf: ^1.4.1
shelf_enforces_ssl: ^1.2.1
shelf_router: ^1.1.4
shelf_open_api: ^1.1.0
shelf_open_api: ^3.0.0
shelf_swagger_ui: ^1.0.0+2
shelf_static: ^1.1.2
shelf_cors_headers: ^0.1.5
json_serializable:
json_annotation: ^4.8.1
json_annotation: ^4.9.0
dio: ^5.3.3
encrypt: ^5.0.3
basic_utils: ^5.7.0
@ -57,14 +56,19 @@ dependencies:
dev_dependencies:
build_runner: ^2.4.0
build_runner: ^2.10.4
json_serializable: ^6.8.0
copy_with_extension_gen: ^11.0.0
# Drift code generation
drift_dev: ^2.14.0
shelf_router_generator: ^1.1.0
shelf_router_generator: ^1.1.3
shelf_open_api_generator:
injectable_generator:
archive: ^3.4.6
test: ^1.25.0
mockito: ^5.4.4
mockito: ^5.4.4
dependency_overrides:
analyzer: ^8.4.1

Some files were not shown because too many files have changed in this diff Show more