2025-11-16 11:25:27 +00:00
|
|
|
|
import 'package:injectable/injectable.dart';
|
|
|
|
|
|
import 'package:mnemo_cards_backend/api/ads/ads_manager.dart';
|
|
|
|
|
|
import 'package:mnemo_cards_backend/extensions.dart';
|
2026-01-09 17:21:18 +00:00
|
|
|
|
import 'package:mnemo_cards_backend/repository/export.dart';
|
2025-11-16 11:25:27 +00:00
|
|
|
|
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
|
|
|
|
|
|
import 'package:mnemo_cards_backend/packs/card_model_extension.dart';
|
|
|
|
|
|
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
|
|
|
|
|
|
|
|
|
|
|
import 'products_price_resolver.dart';
|
|
|
|
|
|
|
2025-12-13 20:55:50 +00:00
|
|
|
|
bool canOpenForAd(String? id) => id != null;
|
2025-11-16 11:25:27 +00:00
|
|
|
|
|
|
|
|
|
|
@LazySingleton()
|
|
|
|
|
|
class PackDtoConverter {
|
|
|
|
|
|
final ProductsPriceResolver _productsPriceResolver;
|
|
|
|
|
|
final AdsManager _adsManager;
|
2026-01-09 17:21:18 +00:00
|
|
|
|
final UserRepository _userRepository;
|
2025-11-16 11:25:27 +00:00
|
|
|
|
|
2026-01-09 17:21:18 +00:00
|
|
|
|
PackDtoConverter(
|
|
|
|
|
|
this._productsPriceResolver,
|
|
|
|
|
|
this._adsManager,
|
|
|
|
|
|
this._userRepository,
|
|
|
|
|
|
);
|
2025-11-16 11:25:27 +00:00
|
|
|
|
|
|
|
|
|
|
Future<CardPackPreviewDto> toCardPackPreviewDto(
|
|
|
|
|
|
CardPackModel model,
|
|
|
|
|
|
UserModel? userModel,
|
|
|
|
|
|
) async {
|
2026-01-08 16:58:13 +00:00
|
|
|
|
bool available =
|
|
|
|
|
|
userModel != null &&
|
|
|
|
|
|
(userModel.packs.contains(model.id?.toString()) || userModel.admin);
|
2026-01-09 17:21:18 +00:00
|
|
|
|
if (userModel != null && !available && userModel.id != null) {
|
|
|
|
|
|
final subscription = await _userRepository.getUserSubscription(
|
|
|
|
|
|
userModel.id!,
|
|
|
|
|
|
);
|
2025-12-20 18:26:15 +00:00
|
|
|
|
available =
|
2026-01-09 17:21:18 +00:00
|
|
|
|
subscription != null &&
|
|
|
|
|
|
subscription.isActive &&
|
|
|
|
|
|
subscription.features.contains(SubscriptionFeatureEnum.packs);
|
2025-11-16 11:25:27 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
String? price = model.price;
|
|
|
|
|
|
if (userModel != null && !available) {
|
|
|
|
|
|
price = await _productsPriceResolver.userPackPrice(userModel, model);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
final canOpenForAdVal = !available && canOpenForAd(model.id);
|
|
|
|
|
|
|
2025-12-20 19:27:32 +00:00
|
|
|
|
// Generate cover image URL
|
|
|
|
|
|
String? coverUrl;
|
|
|
|
|
|
if (model.cover != null && model.cover!.isNotEmpty) {
|
|
|
|
|
|
final coverValue = model.cover!.trim();
|
2026-01-08 13:02:47 +00:00
|
|
|
|
|
2025-12-20 19:27:32 +00:00
|
|
|
|
// If it's already a remote URL, use it directly
|
2026-01-08 13:02:47 +00:00
|
|
|
|
if (coverValue.startsWith('http://') ||
|
|
|
|
|
|
coverValue.startsWith('https://')) {
|
2025-12-20 19:27:32 +00:00
|
|
|
|
coverUrl = coverValue;
|
|
|
|
|
|
}
|
|
|
|
|
|
// If it's a UUID or any other value, use API endpoint
|
|
|
|
|
|
else if (coverValue.isNotEmpty) {
|
|
|
|
|
|
coverUrl = '/api/v2/packs/${model.id.toString()}/cover';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-16 11:25:27 +00:00
|
|
|
|
return CardPackPreviewDto(
|
|
|
|
|
|
id: model.id.toString(),
|
|
|
|
|
|
title: model.title,
|
|
|
|
|
|
subtitle: model.subtitle,
|
|
|
|
|
|
cards: model.cards.length,
|
|
|
|
|
|
tests: model.tests.length,
|
|
|
|
|
|
price: price,
|
2025-12-20 19:27:32 +00:00
|
|
|
|
imageUrl: coverUrl,
|
2025-11-16 11:25:27 +00:00
|
|
|
|
color: model.color,
|
|
|
|
|
|
isAvailable: available,
|
|
|
|
|
|
// trail: 'asset:icons/gift.png',
|
|
|
|
|
|
tip: available
|
|
|
|
|
|
? null
|
|
|
|
|
|
: canOpenForAdVal
|
2025-12-20 18:26:15 +00:00
|
|
|
|
? AssetPackTip('icons/ad.webp', position: PackTipPosition.bottomRight)
|
|
|
|
|
|
: AssetPackTip(
|
|
|
|
|
|
'icons/lock.webp',
|
|
|
|
|
|
position: PackTipPosition.bottomRight,
|
|
|
|
|
|
),
|
2025-11-16 11:25:27 +00:00
|
|
|
|
version: model.version,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Future<CardPackBuyDto> toCardPackBuyDto(
|
|
|
|
|
|
CardPackModel model,
|
|
|
|
|
|
UserModel? userModel,
|
|
|
|
|
|
) async {
|
|
|
|
|
|
final price = userModel == null
|
|
|
|
|
|
? model.price
|
|
|
|
|
|
: await _productsPriceResolver.userPackPrice(userModel, model);
|
|
|
|
|
|
|
|
|
|
|
|
final canOpenForAdVal = userModel != null && canOpenForAd(model.id);
|
|
|
|
|
|
String? _adsKey;
|
|
|
|
|
|
|
|
|
|
|
|
if (canOpenForAdVal) {
|
|
|
|
|
|
_adsKey = _adsManager.getAdsKey([model], userModel);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return CardPackBuyDto(
|
|
|
|
|
|
id: model.id.toString(),
|
|
|
|
|
|
title: model.title,
|
|
|
|
|
|
subtitle: model.subtitle,
|
|
|
|
|
|
appStoreId: model.appStoreId,
|
|
|
|
|
|
rustoreId: model.rustoreId,
|
|
|
|
|
|
googlePlayId: model.googlePlayId,
|
|
|
|
|
|
cards: await Future.wait(
|
|
|
|
|
|
(model.previewCards.isNotEmpty
|
|
|
|
|
|
? model.previewCards
|
|
|
|
|
|
: model.cards.take(3))
|
|
|
|
|
|
.toDtosList(model.cardsOrder)
|
|
|
|
|
|
.map(
|
|
|
|
|
|
(dto) async => dto.copyWith(
|
2025-12-20 18:26:15 +00:00
|
|
|
|
image:
|
|
|
|
|
|
null, // Legacy: card images should be stored in MinIO, not read from disk
|
2025-11-16 11:25:27 +00:00
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
items: [
|
2025-12-20 18:26:15 +00:00
|
|
|
|
if (model.description != null) TextItem(title: model.description),
|
2025-11-16 11:25:27 +00:00
|
|
|
|
if (_adsKey == null) ...[
|
|
|
|
|
|
SpacerItem(),
|
|
|
|
|
|
TextItem(
|
|
|
|
|
|
subtitle: 'Открыть все темы с подпиской!',
|
|
|
|
|
|
deeplink: SubscriptionOpenDeeplink().uri.toString(),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
if (_adsKey != null) ...[
|
|
|
|
|
|
SpacerItem(),
|
|
|
|
|
|
TextItem(
|
|
|
|
|
|
subtitle: 'Эту тему можно открыть, посмотрев рекламу',
|
|
|
|
|
|
deeplink: ProductForAdDeeplink(
|
|
|
|
|
|
model.toDto(),
|
|
|
|
|
|
_adsKey,
|
|
|
|
|
|
).uri.toString(),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
2025-12-20 18:26:15 +00:00
|
|
|
|
SpacerItem(flex: 0, height: 10),
|
2025-11-16 11:25:27 +00:00
|
|
|
|
if (_adsKey == null) ...[
|
|
|
|
|
|
TextItem(
|
|
|
|
|
|
subtitle: 'Оплатить российской картой',
|
|
|
|
|
|
deeplink: PurchaseInitDeeplink(
|
|
|
|
|
|
id: model.id.toString(),
|
|
|
|
|
|
productType: MnemoCardsProductType.pack,
|
|
|
|
|
|
paymentId: model.id.toString(),
|
|
|
|
|
|
paymentSystem: PaymentSystem.yookassa,
|
|
|
|
|
|
).uri.toString(),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
2025-12-20 18:26:15 +00:00
|
|
|
|
SpacerItem(height: 20, flex: 0),
|
2025-11-16 11:25:27 +00:00
|
|
|
|
],
|
|
|
|
|
|
color: model.color,
|
|
|
|
|
|
version: model.version,
|
|
|
|
|
|
price: price,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Future<EditCardPackDto> toEditDto(
|
|
|
|
|
|
CardPackModel model, {
|
|
|
|
|
|
bool withCards = true,
|
|
|
|
|
|
bool withTests = false,
|
|
|
|
|
|
bool withUsers = false,
|
|
|
|
|
|
}) async {
|
|
|
|
|
|
return EditCardPackDto(
|
|
|
|
|
|
id: model.id.toString(),
|
|
|
|
|
|
title: model.title,
|
|
|
|
|
|
subtitle: model.subtitle,
|
|
|
|
|
|
color: model.color,
|
|
|
|
|
|
version: model.version,
|
2025-12-20 18:26:15 +00:00
|
|
|
|
cover:
|
|
|
|
|
|
null, // Legacy: covers should be stored in MinIO, not read from disk
|
2025-11-16 11:25:27 +00:00
|
|
|
|
size: model.size,
|
|
|
|
|
|
googlePlayId: model.googlePlayId,
|
|
|
|
|
|
rustoreId: model.rustoreId,
|
|
|
|
|
|
appStoreId: model.appStoreId,
|
|
|
|
|
|
price: model.price,
|
|
|
|
|
|
description: model.description,
|
|
|
|
|
|
enabled: model.enabled,
|
2025-12-20 18:26:15 +00:00
|
|
|
|
addCardIds: withCards
|
|
|
|
|
|
? model.cards.map((c) => c.id.toString()).toList()
|
|
|
|
|
|
: null,
|
|
|
|
|
|
addTestIds: withTests
|
|
|
|
|
|
? model.tests.map((t) => t.id.toString()).toList()
|
|
|
|
|
|
: null,
|
2025-11-16 11:25:27 +00:00
|
|
|
|
removeCardIds: null,
|
|
|
|
|
|
removeTestIds: null,
|
|
|
|
|
|
previewCards: model.previewCards
|
2025-12-19 01:09:04 +00:00
|
|
|
|
.map((card) => card.id.toString())
|
2025-11-16 11:25:27 +00:00
|
|
|
|
.whereNotNull()
|
|
|
|
|
|
.toList(),
|
|
|
|
|
|
order: model.order,
|
|
|
|
|
|
cardsOrder: model.cardsOrder,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
CardPackDto toDto(CardPackModel model) {
|
|
|
|
|
|
return CardPackDto(
|
|
|
|
|
|
id: model.id.toString(),
|
|
|
|
|
|
title: model.title,
|
|
|
|
|
|
subtitle: model.subtitle,
|
|
|
|
|
|
cards: model.cards.toDtosList(model.cardsOrder),
|
|
|
|
|
|
color: model.color,
|
|
|
|
|
|
version: model.version,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
CardPackModel toModel(EditCardPackDto dto, CardPackModel? model) {
|
|
|
|
|
|
return CardPackModel(
|
|
|
|
|
|
title: dto.title ?? model?.title ?? '',
|
|
|
|
|
|
subtitle: dto.subtitle ?? model?.subtitle ?? '',
|
|
|
|
|
|
size: dto.size ?? model?.size ?? 0,
|
|
|
|
|
|
color: dto.color ?? model?.color,
|
|
|
|
|
|
cover: dto.cover ?? model?.cover,
|
|
|
|
|
|
description: dto.description ?? model?.description,
|
|
|
|
|
|
version: dto.version ?? model?.version,
|
2025-12-13 20:55:50 +00:00
|
|
|
|
id: model?.id ?? dto.id,
|
2025-11-16 11:25:27 +00:00
|
|
|
|
googlePlayId: dto.googlePlayId ?? model?.googlePlayId,
|
|
|
|
|
|
rustoreId: dto.rustoreId ?? model?.rustoreId,
|
|
|
|
|
|
appStoreId: dto.appStoreId ?? model?.appStoreId,
|
|
|
|
|
|
price: dto.price ?? model?.price,
|
|
|
|
|
|
enabled: dto.enabled ?? model?.enabled ?? false,
|
|
|
|
|
|
order: dto.order ?? model?.order ?? 0,
|
|
|
|
|
|
cardsOrder: dto.cardsOrder ?? model?.cardsOrder ?? const [],
|
2026-01-08 16:07:56 +00:00
|
|
|
|
currency: model?.currency ?? 'RUB',
|
2025-11-16 11:25:27 +00:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|