forked from eatvio/eatvio_app
Merge pull request 'sbp-alfa-bank-payment' (#998) from misha/eatvio_app:sbp_payments into main
Reviewed-on: eatvio/eatvio_app#998
This commit is contained in:
@@ -152,9 +152,10 @@ jobs:
|
|||||||
|
|
||||||
- name: Get dependencies
|
- name: Get dependencies
|
||||||
run: |
|
run: |
|
||||||
flutter clean
|
flutter clean
|
||||||
flutter pub get
|
flutter pub cache repair
|
||||||
dart run build_runner build --delete-conflicting-outputs
|
flutter pub get
|
||||||
|
dart run build_runner build --delete-conflicting-outputs
|
||||||
|
|
||||||
- name: Native Analyze
|
- name: Native Analyze
|
||||||
run: flutter analyze
|
run: flutter analyze
|
||||||
|
|||||||
2
Makefile
2
Makefile
@@ -10,4 +10,4 @@ build-generated-files:
|
|||||||
dart run build_runner build --delete-conflicting-outputs
|
dart run build_runner build --delete-conflicting-outputs
|
||||||
|
|
||||||
clean-coverage:
|
clean-coverage:
|
||||||
@./scripts/remove_generated_from_coverage.sh
|
@./scripts/remove_generated_from_coverage.sh
|
||||||
|
|||||||
@@ -88,5 +88,168 @@
|
|||||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||||
<data android:mimeType="text/plain"/>
|
<data android:mimeType="text/plain"/>
|
||||||
</intent>
|
</intent>
|
||||||
|
<!-- SBP bank packages for СБП payment detection -->
|
||||||
|
<package android:name="ru.sberbankmobile" />
|
||||||
|
<package android:name="com.idamob.tinkoff.android" />
|
||||||
|
<package android:name="ru.vtb24.mobilebanking.android" />
|
||||||
|
<package android:name="ru.alfabank.mobile.android" />
|
||||||
|
<package android:name="ru.raiffeisennews" />
|
||||||
|
<package android:name="com.openbank" />
|
||||||
|
<package android:name="ru.gazprombank.android.mobilebank.app" />
|
||||||
|
<package android:name="com.logo.mbanking" />
|
||||||
|
<package android:name="ru.sovcomcard.halva.v1" />
|
||||||
|
<package android:name="ru.rosbank.android" />
|
||||||
|
<package android:name="ru.rshb.dbo" />
|
||||||
|
<package android:name="ru.mkb.mobile" />
|
||||||
|
<package android:name="ru.ucb.android" />
|
||||||
|
<package android:name="ru.unicredit.android" />
|
||||||
|
<package android:name="ru.skbbank.ib" />
|
||||||
|
<package android:name="ru.gebank.ib" />
|
||||||
|
<package android:name="ru.avangard.sbpent" />
|
||||||
|
<package android:name="ru.elplat.elplat2" />
|
||||||
|
<package android:name="com.bifit.rncbbeta" />
|
||||||
|
<package android:name="ru.ftc.faktura.expobank" />
|
||||||
|
<package android:name="com.bssys.vbrrretail" />
|
||||||
|
<package android:name="ru.wildberries.razz" />
|
||||||
|
<package android:name="ru.artsofte.russiafl" />
|
||||||
|
<package android:name="ru.ftc.faktura.multibank" />
|
||||||
|
<package android:name="ru.bank_hlynov.xbank" />
|
||||||
|
<package android:name="ru.ftc.faktura.vesta" />
|
||||||
|
<package android:name="com.isimplelab.ibank.solidarnost" />
|
||||||
|
<package android:name="com.bssys.roscapretail" />
|
||||||
|
<package android:name="ru.ftc.faktura.kbhmb" />
|
||||||
|
<package android:name="ru.mts.money" />
|
||||||
|
<package android:name="ru.ftc.faktura.pskb" />
|
||||||
|
<package android:name="ru.ftc.faktura.nskbl" />
|
||||||
|
<package android:name="ru.akbars.mobile" />
|
||||||
|
<package android:name="ru.ftc.faktura.rostfinance" />
|
||||||
|
<package android:name="ru.ilb.bbmobile" />
|
||||||
|
<package android:name="ru.mcbankrus.MCBankRus" />
|
||||||
|
<package android:name="ru.albank.online.aebit" />
|
||||||
|
<package android:name="com.bifit.mobile.citizen.icbru" />
|
||||||
|
<package android:name="ru.ftc.faktura.orbank" />
|
||||||
|
<package android:name="ru.ftc.faktura.agropromkredit" />
|
||||||
|
<package android:name="com.energobank.digital" />
|
||||||
|
<package android:name="com.bifit.mobile.citizen.kbnk" />
|
||||||
|
<package android:name="ru.ftc.faktura.sdm" />
|
||||||
|
<package android:name="com.bifit.mobile.citizen.mbb" />
|
||||||
|
<package android:name="ru.ftc.faktura.absolutbank" />
|
||||||
|
<package android:name="ru.ftc.faktura.akcept" />
|
||||||
|
<package android:name="ru.faktura.etbank.person" />
|
||||||
|
<package android:name="ru.ftc.faktura.razvitiestolica" />
|
||||||
|
<package android:name="ru.ftc.faktura.resokreditbank" />
|
||||||
|
<package android:name="ru.yoo.money" />
|
||||||
|
<package android:name="ru.bspb" />
|
||||||
|
<package android:name="com.isimplelab.ibank.kubankredit" />
|
||||||
|
<package android:name="com.tochka.bank" />
|
||||||
|
<package android:name="com.bssys.novikomretail" />
|
||||||
|
<package android:name="ru.rosdorbank.physical" />
|
||||||
|
<package android:name="ru.ftc.faktura.primsoc" />
|
||||||
|
<package android:name="ru.ftc.faktura.banksaratov" />
|
||||||
|
<package android:name="ru.ftc.faktura.tavrich" />
|
||||||
|
<package android:name="com.bifit.mobile.citizen.thbank" />
|
||||||
|
<package android:name="ru.ftc.faktura.kremlevskiy" />
|
||||||
|
<package android:name="ru.ftc.faktura.tkbbank" />
|
||||||
|
<package android:name="ru.chelyabinvestbank.investpay" />
|
||||||
|
<package android:name="ru.ftc.faktura.derzhava" />
|
||||||
|
<package android:name="ru.nbd.android" />
|
||||||
|
<package android:name="ru.sngb.dbo.client.android" />
|
||||||
|
<package android:name="ru.ftc.faktura.united" />
|
||||||
|
<package android:name="ru.ftc.faktura.venetsbank" />
|
||||||
|
<package android:name="ru.letobank.Prometheus" />
|
||||||
|
<package android:name="ru.simpls.brs2.mobbank" />
|
||||||
|
<package android:name="com.bifit.dvbank" />
|
||||||
|
<package android:name="ru.ftc.faktura.intesabank" />
|
||||||
|
<package android:name="com.mmonline.mobile" />
|
||||||
|
<package android:name="cb.ibank.vuz" />
|
||||||
|
<package android:name="ru.uralprombank.mobilebanknew.googleplay" />
|
||||||
|
<package android:name="ru.ftc.faktura.nsbank" />
|
||||||
|
<package android:name="ru.emb.android" />
|
||||||
|
<package android:name="ru.ftc.faktura.maritimebank" />
|
||||||
|
<package android:name="com.credituralbank.CUBmobilenew" />
|
||||||
|
<package android:name="com.isimplelab.isimpleceo.uglemet" />
|
||||||
|
<package android:name="com.bssys.stroylesretail" />
|
||||||
|
<package android:name="ru.ftc.faktura.iturup" />
|
||||||
|
<package android:name="ru.ftc.faktura.finbank" />
|
||||||
|
<package android:name="ru.faktura.gaztransbank.person" />
|
||||||
|
<package android:name="com.isimplelab.ibank.chelind" />
|
||||||
|
<package android:name="ru.ftc.faktura.nokss" />
|
||||||
|
<package android:name="ru.ftc.faktura.vlbb" />
|
||||||
|
<package android:name="ru.ftc.faktura.kbb" />
|
||||||
|
<package android:name="ru.faktura.avtograd.person" />
|
||||||
|
<package android:name="ru.ftc.faktura.forshtadt" />
|
||||||
|
<package android:name="ru.ftc.faktura.solidbank" />
|
||||||
|
<package android:name="ru.ftc.faktura.alexbank" />
|
||||||
|
<package android:name="ru.ftc.faktura.akibank" />
|
||||||
|
<package android:name="ru.ftc.faktura.nipbank" />
|
||||||
|
<package android:name="ru.ftc.faktura.alefbank" />
|
||||||
|
<package android:name="com.bifit.vfbank" />
|
||||||
|
<package android:name="com.bssys.uralfdretail" />
|
||||||
|
<package android:name="com.snb.online" />
|
||||||
|
<package android:name="com.bifit.mobile.citizen.okbank" />
|
||||||
|
<package android:name="com.tatsotsbank.dbomobile" />
|
||||||
|
<package android:name="ru.vtkbank.android" />
|
||||||
|
<package android:name="ru.faktura.jtbank.person" />
|
||||||
|
<package android:name="ru.ftc.faktura.ns" />
|
||||||
|
<package android:name="ru.ftc.faktura.zemskybank" />
|
||||||
|
<package android:name="com.bssys.aversretail" />
|
||||||
|
<package android:name="ru.rusnarbank.correqts.retail" />
|
||||||
|
<package android:name="ru.ftc.faktura.baikalinvestbank" />
|
||||||
|
<package android:name="ru.sbi.android" />
|
||||||
|
<package android:name="com.metkombank.mobile" />
|
||||||
|
<package android:name="com.isimplelab.ionic.kazan.fl" />
|
||||||
|
<package android:name="com.intervale.sbpas" />
|
||||||
|
<package android:name="com.bifit.mobile.citizen.zarech" />
|
||||||
|
<package android:name="ru.centrinvest.mobilebanking2018" />
|
||||||
|
<package android:name="com.mifors.izhcombank" />
|
||||||
|
<package android:name="com.bifit.pmobile.isbank" />
|
||||||
|
<package android:name="ru.zenit.android" />
|
||||||
|
<package android:name="ru.briginvest.sense" />
|
||||||
|
<package android:name="com.bifit.mobile.citizen.cfb" />
|
||||||
|
<package android:name="com.isimplelab.ionic.gorbank.prod" />
|
||||||
|
<package android:name="ru.ftc.faktura.moscombank" />
|
||||||
|
<package android:name="com.timerbank.retail" />
|
||||||
|
<package android:name="com.mob.android" />
|
||||||
|
<package android:name="com.citibank.mobile.ru" />
|
||||||
|
<package android:name="com.bifit.atbbank" />
|
||||||
|
<package android:name="ru.mw" />
|
||||||
|
<package android:name="ru.bankuralsib.mb.android" />
|
||||||
|
<package android:name="com.banksoyuz.artsofte" />
|
||||||
|
<package android:name="ru.rubank.ubsmobile" />
|
||||||
|
<package android:name="cb.ibank" />
|
||||||
|
<package android:name="ru.ipb.ubsmobile" />
|
||||||
|
<package android:name="com.openwaygroup.ic.panda.poidem" />
|
||||||
|
<package android:name="com.bifit.mobile.citizen.tenderbank" />
|
||||||
|
<package android:name="ru.otpbank.mobile" />
|
||||||
|
<package android:name="ru.krk.ubsmobile" />
|
||||||
|
<package android:name="ru.homecredit.mycredit" />
|
||||||
|
<package android:name="cz.bsc.rc" />
|
||||||
|
<package android:name="com.bifit.mobile.citizen.belsocbank" />
|
||||||
|
<package android:name="com.hicebank.android" />
|
||||||
|
<package android:name="com.bpc.crossplatform_trading.bpc_trading" />
|
||||||
|
<package android:name="com.bifit.nrb" />
|
||||||
|
<package android:name="com.bifit.mobile.citizen.moskb" />
|
||||||
|
<package android:name="ru.isfront.android.kt" />
|
||||||
|
<package android:name="ru.ftc.faktura.novobank" />
|
||||||
|
<package android:name="com.smp.android" />
|
||||||
|
<package android:name="com.bifit.mobile.citizen.nkbank" />
|
||||||
|
<package android:name="com.isimplelab.ibank.toyota" />
|
||||||
|
<package android:name="com.bifit.mobile.private.bbr" />
|
||||||
|
<package android:name="ru.ftc.faktura.lanta" />
|
||||||
|
<package android:name="com.yandex.bank" />
|
||||||
|
<package android:name="ru.ftc.faktura.bgfbank" />
|
||||||
|
<package android:name="com.idamobile.android.LockoBank" />
|
||||||
|
<package android:name="com.bifit.mobile.citizen.MCBank" />
|
||||||
|
<package android:name="com.cetelem.cetelem_android" />
|
||||||
|
<package android:name="com.bssys.gutaretail" />
|
||||||
|
<package android:name="ru.ftc.faktura.siab" />
|
||||||
|
<package android:name="com.isimplelab.ionic.round.prod" />
|
||||||
|
<package android:name="com.priobank.prio" />
|
||||||
|
<package android:name="com.inbank.mobilebank" />
|
||||||
|
<package android:name="com.isimplelab.isimplemobile.payjet" />
|
||||||
|
<package android:name="ru.ftc.faktura.agroros" />
|
||||||
|
<package android:name="com.bifit.mobile.citizen.invb" />
|
||||||
|
<package android:name="com.compassplus.mobicash.customer" />
|
||||||
|
<package android:name="ru.finambank.app" />
|
||||||
</queries>
|
</queries>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -74,6 +74,57 @@
|
|||||||
<string>yandexauth</string>
|
<string>yandexauth</string>
|
||||||
<string>yandexauth2</string>
|
<string>yandexauth2</string>
|
||||||
<string>yandexauth4</string>
|
<string>yandexauth4</string>
|
||||||
|
<!-- SBP bank schemes (top ~50 by popularity, iOS 15+ caps at 50) -->
|
||||||
|
<string>bank100000000111</string>
|
||||||
|
<string>bank100000000004</string>
|
||||||
|
<string>bank100000000005</string>
|
||||||
|
<string>bank100000000008</string>
|
||||||
|
<string>bank100000000007</string>
|
||||||
|
<string>bank100000000015</string>
|
||||||
|
<string>bank100000000001</string>
|
||||||
|
<string>bank100000000010</string>
|
||||||
|
<string>bank100000000013</string>
|
||||||
|
<string>bank100000000012</string>
|
||||||
|
<string>bank100000000020</string>
|
||||||
|
<string>bank100000000025</string>
|
||||||
|
<string>bank100000000030</string>
|
||||||
|
<string>bank100000000030_new</string>
|
||||||
|
<string>bank100000000003</string>
|
||||||
|
<string>bank100000000043</string>
|
||||||
|
<string>bank100000000028</string>
|
||||||
|
<string>bank100000000086</string>
|
||||||
|
<string>bank100000000011</string>
|
||||||
|
<string>bank100000000044</string>
|
||||||
|
<string>bank100000000049</string>
|
||||||
|
<string>bank100000000500</string>
|
||||||
|
<string>bank100000000095</string>
|
||||||
|
<string>bank100000000900</string>
|
||||||
|
<string>bank100000000056</string>
|
||||||
|
<string>bank100000000053</string>
|
||||||
|
<string>bank100000000121</string>
|
||||||
|
<string>bank100000000082</string>
|
||||||
|
<string>bank100000000127</string>
|
||||||
|
<string>bank100000000017</string>
|
||||||
|
<string>bank100000000087</string>
|
||||||
|
<string>bank100000000052</string>
|
||||||
|
<string>bank100000000006</string>
|
||||||
|
<string>bank100000000098</string>
|
||||||
|
<string>bank100000000092</string>
|
||||||
|
<string>bank100000000229</string>
|
||||||
|
<string>bank100000000027</string>
|
||||||
|
<string>bank100000000080</string>
|
||||||
|
<string>bank100000000122</string>
|
||||||
|
<string>bank100000000124</string>
|
||||||
|
<string>bank100000000118</string>
|
||||||
|
<string>bank100000000159</string>
|
||||||
|
<string>bank100000000146</string>
|
||||||
|
<string>bank100000000069</string>
|
||||||
|
<string>bank100000000140</string>
|
||||||
|
<string>bank100000000047</string>
|
||||||
|
<string>bank100000000099</string>
|
||||||
|
<string>bank100000000135</string>
|
||||||
|
<string>bank100000000139</string>
|
||||||
|
<string>bank100000000022</string>
|
||||||
</array>
|
</array>
|
||||||
<key>YAClientId</key>
|
<key>YAClientId</key>
|
||||||
<string>5399b03636104c6d920b87c1ad329254</string>
|
<string>5399b03636104c6d920b87c1ad329254</string>
|
||||||
|
|||||||
@@ -123,7 +123,21 @@ class AppRouter {
|
|||||||
return '${InfoByOrderScreen.path}?planId=$consultationId';
|
return '${InfoByOrderScreen.path}?planId=$consultationId';
|
||||||
}
|
}
|
||||||
|
|
||||||
static String? _resolveDeepLinkUri(Uri uri) {
|
/// Deep link после оплаты Alfa: `finance/alfa/return/{orderId}` → чат с query `orderId`.
|
||||||
|
static String? _resolveFinanceAlfaReturnDeepLink(List<String> effectiveSegments) {
|
||||||
|
if (effectiveSegments.length != 4 ||
|
||||||
|
effectiveSegments[1] != 'alfa' ||
|
||||||
|
effectiveSegments[2] != 'return') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final orderId = effectiveSegments[3];
|
||||||
|
if (orderId.isEmpty) return null;
|
||||||
|
developer.log('Deep link: finance/alfa/return/$orderId', name: 'AppRouter');
|
||||||
|
return '${RouteConstants.chatScreen}?orderId=$orderId';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Разбирает deep link URI в внутренний route target (или null, если host не наш).
|
||||||
|
static String? resolveDeepLinkTarget(Uri uri) {
|
||||||
final host = uri.host.toLowerCase();
|
final host = uri.host.toLowerCase();
|
||||||
final segments = uri.pathSegments;
|
final segments = uri.pathSegments;
|
||||||
|
|
||||||
@@ -189,6 +203,10 @@ class AppRouter {
|
|||||||
final target = _resolveConsultationsDeepLink(effectiveSegments);
|
final target = _resolveConsultationsDeepLink(effectiveSegments);
|
||||||
if (target != null) return target;
|
if (target != null) return target;
|
||||||
break;
|
break;
|
||||||
|
case 'finance':
|
||||||
|
final target = _resolveFinanceAlfaReturnDeepLink(effectiveSegments);
|
||||||
|
if (target != null) return target;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
developer.log('Unknown deep link format: ${effectiveSegments.length} effective segments, type=$type, redirecting to feed', name: 'AppRouter');
|
developer.log('Unknown deep link format: ${effectiveSegments.length} effective segments, type=$type, redirecting to feed', name: 'AppRouter');
|
||||||
@@ -196,7 +214,7 @@ class AppRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static String? _handleRedirect(BuildContext context, GoRouterState state) {
|
static String? _handleRedirect(BuildContext context, GoRouterState state) {
|
||||||
return _resolveDeepLinkUri(state.uri);
|
return resolveDeepLinkTarget(state.uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void navigateByDeepLinkUrl(String url) {
|
static void navigateByDeepLinkUrl(String url) {
|
||||||
@@ -230,7 +248,7 @@ class AppRouter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final target = _resolveDeepLinkUri(uri);
|
final target = resolveDeepLinkTarget(uri);
|
||||||
FlutterNativeSplash.remove();
|
FlutterNativeSplash.remove();
|
||||||
if (target != null) {
|
if (target != null) {
|
||||||
appRouter.go(target);
|
appRouter.go(target);
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import 'package:eatvio_app/features/my_finance/presentation/cubit/user_cards_cub
|
|||||||
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart';
|
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart';
|
||||||
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
||||||
import 'package:eatvio_app/features/order_info/cubit/order_info_cubit.dart';
|
import 'package:eatvio_app/features/order_info/cubit/order_info_cubit.dart';
|
||||||
|
import 'package:eatvio_app/features/payment/presentation/cubit/consultation_payment_cubit/consultation_payment_cubit.dart';
|
||||||
import 'package:eatvio_app/features/personal_diet_suggestion/cubit/personal_diet_suggestion_cubit.dart';
|
import 'package:eatvio_app/features/personal_diet_suggestion/cubit/personal_diet_suggestion_cubit.dart';
|
||||||
import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_cubit.dart';
|
import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_cubit.dart';
|
||||||
import 'package:eatvio_app/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart' as profile_plans;
|
import 'package:eatvio_app/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart' as profile_plans;
|
||||||
@@ -135,6 +136,9 @@ class CubitRegistrations {
|
|||||||
sl.registerFactory<ComplainingCubit>(() => ComplainingCubit(repository: sl()));
|
sl.registerFactory<ComplainingCubit>(() => ComplainingCubit(repository: sl()));
|
||||||
sl.registerFactory<UserCardsCubit>(() => UserCardsCubit(cardsRepository: sl()));
|
sl.registerFactory<UserCardsCubit>(() => UserCardsCubit(cardsRepository: sl()));
|
||||||
sl.registerFactory<OrderInfoCubit>(() => OrderInfoCubit(chatRepository: sl(), profileRepository: sl()));
|
sl.registerFactory<OrderInfoCubit>(() => OrderInfoCubit(chatRepository: sl(), profileRepository: sl()));
|
||||||
|
sl.registerFactory<ConsultationPaymentCubit>(
|
||||||
|
() => ConsultationPaymentCubit(paymentRepository: sl(), sbpRepository: sl()),
|
||||||
|
);
|
||||||
sl.registerFactory<ConsultationsCubit>(() => ConsultationsCubit(consultationsRepository: sl()));
|
sl.registerFactory<ConsultationsCubit>(() => ConsultationsCubit(consultationsRepository: sl()));
|
||||||
sl.registerFactory<ConsultantCommentsCubit>(() => ConsultantCommentsCubit(diaryRepository: sl()));
|
sl.registerFactory<ConsultantCommentsCubit>(() => ConsultantCommentsCubit(diaryRepository: sl()));
|
||||||
sl.registerFactory<CancelPlanCubit>(() => CancelPlanCubit(cancelPlanRepository: sl()));
|
sl.registerFactory<CancelPlanCubit>(() => CancelPlanCubit(cancelPlanRepository: sl()));
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.da
|
|||||||
import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart';
|
import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart';
|
||||||
import 'package:eatvio_app/features/my_plans/repositories/my_diets_repository.dart';
|
import 'package:eatvio_app/features/my_plans/repositories/my_diets_repository.dart';
|
||||||
import 'package:eatvio_app/features/notifications/repository/notifications_repository.dart';
|
import 'package:eatvio_app/features/notifications/repository/notifications_repository.dart';
|
||||||
|
import 'package:eatvio_app/features/payment/repositories/payment_repository.dart';
|
||||||
|
import 'package:eatvio_app/features/payment/repositories/sbp_repository.dart';
|
||||||
import 'package:eatvio_app/features/personal_diet_suggestion/repository/personal_diet_suggestion_repository.dart';
|
import 'package:eatvio_app/features/personal_diet_suggestion/repository/personal_diet_suggestion_repository.dart';
|
||||||
import 'package:eatvio_app/features/plans/repositories/plan_repository.dart';
|
import 'package:eatvio_app/features/plans/repositories/plan_repository.dart';
|
||||||
import 'package:eatvio_app/features/plans/repositories/plans_repository.dart';
|
import 'package:eatvio_app/features/plans/repositories/plans_repository.dart';
|
||||||
@@ -74,6 +76,8 @@ class RepositoryRegistrations {
|
|||||||
sl.registerLazySingleton<BlockedUsersRepository>(() => BlockedUsersRepository(blockedUserService: sl()));
|
sl.registerLazySingleton<BlockedUsersRepository>(() => BlockedUsersRepository(blockedUserService: sl()));
|
||||||
sl.registerLazySingleton<CommentsRepository>(() => CommentsRepository(service: sl()));
|
sl.registerLazySingleton<CommentsRepository>(() => CommentsRepository(service: sl()));
|
||||||
sl.registerLazySingleton<NotificationSettingsRepository>(() => NotificationSettingsRepository(sl()));
|
sl.registerLazySingleton<NotificationSettingsRepository>(() => NotificationSettingsRepository(sl()));
|
||||||
|
sl.registerLazySingleton<SbpRepository>(() => SbpRepository(sbpService: sl()));
|
||||||
|
sl.registerLazySingleton<PaymentRepository>(() => PaymentRepository(chatService: sl()));
|
||||||
sl.registerLazySingleton<PersonalDietSuggestionRepository>(() => PersonalDietSuggestionRepository(personalDietSuggestionService: sl()));
|
sl.registerLazySingleton<PersonalDietSuggestionRepository>(() => PersonalDietSuggestionRepository(personalDietSuggestionService: sl()));
|
||||||
sl.registerLazySingleton<RegistrationRepositoryV2>(() => RegistrationRepositoryV2(registrationService: sl()));
|
sl.registerLazySingleton<RegistrationRepositoryV2>(() => RegistrationRepositoryV2(registrationService: sl()));
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart';
|
|||||||
import 'package:eatvio_app/features/my_finance/services/cards_service.dart';
|
import 'package:eatvio_app/features/my_finance/services/cards_service.dart';
|
||||||
import 'package:eatvio_app/features/my_plans/services/my_plans_service.dart';
|
import 'package:eatvio_app/features/my_plans/services/my_plans_service.dart';
|
||||||
import 'package:eatvio_app/features/notifications/service/notifications_service.dart';
|
import 'package:eatvio_app/features/notifications/service/notifications_service.dart';
|
||||||
|
import 'package:eatvio_app/features/payment/services/sbp_service.dart';
|
||||||
import 'package:eatvio_app/features/personal_diet_suggestion/services/personal_diet_suggestion_service.dart';
|
import 'package:eatvio_app/features/personal_diet_suggestion/services/personal_diet_suggestion_service.dart';
|
||||||
import 'package:eatvio_app/features/plans/services/plan_service.dart';
|
import 'package:eatvio_app/features/plans/services/plan_service.dart';
|
||||||
import 'package:eatvio_app/features/plans/services/plans_service.dart';
|
import 'package:eatvio_app/features/plans/services/plans_service.dart';
|
||||||
@@ -82,6 +83,7 @@ class ServiceRegistrations {
|
|||||||
sl.registerLazySingleton<ConsultantRatingService>(() => ConsultantRatingService(sl()));
|
sl.registerLazySingleton<ConsultantRatingService>(() => ConsultantRatingService(sl()));
|
||||||
sl.registerLazySingleton<NotificationSettingsService>(() => NotificationSettingsService(sl()));
|
sl.registerLazySingleton<NotificationSettingsService>(() => NotificationSettingsService(sl()));
|
||||||
sl.registerLazySingleton<ViewsService>(() => ViewsService(sl()));
|
sl.registerLazySingleton<ViewsService>(() => ViewsService(sl()));
|
||||||
|
sl.registerLazySingleton<SbpService>(() => const SbpServiceImpl());
|
||||||
sl.registerLazySingleton<PersonalDietSuggestionService>(() => PersonalDietSuggestionService(sl()));
|
sl.registerLazySingleton<PersonalDietSuggestionService>(() => PersonalDietSuggestionService(sl()));
|
||||||
sl.registerLazySingleton<RegistrationServiceV2>(() => RegistrationServiceV2(sl()));
|
sl.registerLazySingleton<RegistrationServiceV2>(() => RegistrationServiceV2(sl()));
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ class ErrorReportPolicy {
|
|||||||
case DioExceptionType.sendTimeout:
|
case DioExceptionType.sendTimeout:
|
||||||
case DioExceptionType.receiveTimeout:
|
case DioExceptionType.receiveTimeout:
|
||||||
case DioExceptionType.connectionError:
|
case DioExceptionType.connectionError:
|
||||||
|
case DioExceptionType.transformTimeout:
|
||||||
return true;
|
return true;
|
||||||
case DioExceptionType.unknown:
|
case DioExceptionType.unknown:
|
||||||
case DioExceptionType.badResponse:
|
case DioExceptionType.badResponse:
|
||||||
|
|||||||
@@ -211,13 +211,23 @@ class OtherRoutes {
|
|||||||
name: 'chatScreen',
|
name: 'chatScreen',
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final uuidFromQuery = state.uri.queryParameters['uuid'];
|
final uuidFromQuery = state.uri.queryParameters['uuid'];
|
||||||
if (uuidFromQuery != null) {
|
final orderIdFromQuery = state.uri.queryParameters['orderId'];
|
||||||
return ChatScreen(uuid: uuidFromQuery, sendInitialMessage: false);
|
if (uuidFromQuery != null || orderIdFromQuery != null) {
|
||||||
|
return ChatScreen(
|
||||||
|
uuid: uuidFromQuery,
|
||||||
|
orderId: orderIdFromQuery,
|
||||||
|
sendInitialMessage: false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
final data = state.extra as Map<String, dynamic>;
|
final data = state.extra as Map<String, dynamic>;
|
||||||
final uuid = data['uuid'] as String;
|
final uuid = data['uuid'] as String?;
|
||||||
|
final orderId = data['orderId'] as String?;
|
||||||
final sendInitialMessage = data['sendInitialMessage'] as bool?;
|
final sendInitialMessage = data['sendInitialMessage'] as bool?;
|
||||||
return ChatScreen(uuid: uuid, sendInitialMessage: sendInitialMessage);
|
return ChatScreen(
|
||||||
|
uuid: uuid,
|
||||||
|
orderId: orderId,
|
||||||
|
sendInitialMessage: sendInitialMessage,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
|
|||||||
15
lib/features/chat/models/order_id_to_conversation_model.dart
Normal file
15
lib/features/chat/models/order_id_to_conversation_model.dart
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'order_id_to_conversation_model.freezed.dart';
|
||||||
|
part 'order_id_to_conversation_model.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class OrderIdToConversationModel with _$OrderIdToConversationModel {
|
||||||
|
factory OrderIdToConversationModel({
|
||||||
|
@JsonKey(name: 'chosen_plan_id') required int chosenPlanId,
|
||||||
|
@JsonKey(name: 'conversation_uuid') required String conversationUuid,
|
||||||
|
}) = _OrderIdToConversationModel;
|
||||||
|
|
||||||
|
factory OrderIdToConversationModel.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$OrderIdToConversationModelFromJson(json);
|
||||||
|
}
|
||||||
@@ -5,10 +5,12 @@ part 'payment_model.g.dart';
|
|||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
abstract class PaymentModel with _$PaymentModel {
|
abstract class PaymentModel with _$PaymentModel {
|
||||||
|
|
||||||
factory PaymentModel({
|
factory PaymentModel({
|
||||||
@JsonKey(name: 'checkout_url') required String paymentUrl,
|
@JsonKey(name: 'checkout_url') required String paymentUrl,
|
||||||
|
@JsonKey(name: 'order_id') String? orderId,
|
||||||
|
@JsonKey(name: 'payment_provider') String? paymentProvider,
|
||||||
}) = _PaymentModel;
|
}) = _PaymentModel;
|
||||||
|
|
||||||
factory PaymentModel.fromJson(Map<String, dynamic> json) => _$PaymentModelFromJson(json);
|
factory PaymentModel.fromJson(Map<String, dynamic> json) =>
|
||||||
}
|
_$PaymentModelFromJson(json);
|
||||||
|
}
|
||||||
|
|||||||
@@ -91,17 +91,38 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initChat(String uuid, {bool sendInitialMessage = false}) async {
|
/// Инициализирует чат по [uuid] либо по [orderId] (resolve → conversation_uuid).
|
||||||
|
///
|
||||||
|
/// При наличии [orderId] сначала вызывается
|
||||||
|
/// `GET /api/consultation/resolve/order_id_to_conversation/{orderId}`.
|
||||||
|
///
|
||||||
|
/// Возвращает conversation uuid, которым инициализировали чат (для reload без повторного resolve).
|
||||||
|
Future<String?> initChat(String? uuid, {String? orderId, bool sendInitialMessage = false}) async {
|
||||||
try {
|
try {
|
||||||
emit(const ChatRoomState.loading());
|
emit(const ChatRoomState.loading());
|
||||||
final chatRoom = await chatRepository.getChatRoom(uuid);
|
|
||||||
ChosenPlanModel? chosenPlan;
|
String? resolvedUuid = uuid;
|
||||||
if (chatRoom.chosenPlanId != null) {
|
int? resolvedChosenPlanId;
|
||||||
chosenPlan = await chatRepository.getChosenPlan(id: chatRoom.chosenPlanId!);
|
|
||||||
|
if (orderId != null && orderId.isNotEmpty) {
|
||||||
|
final resolved = await chatRepository.resolveOrderIdToConversation(orderId: orderId);
|
||||||
|
resolvedUuid = resolved.conversationUuid;
|
||||||
|
resolvedChosenPlanId = resolved.chosenPlanId;
|
||||||
}
|
}
|
||||||
final chatMessages = await chatRepository.getChatMessages(uuid, currentPage);
|
|
||||||
|
if (resolvedUuid == null || resolvedUuid.isEmpty) {
|
||||||
|
throw ArgumentError('initChat requires uuid or orderId');
|
||||||
|
}
|
||||||
|
|
||||||
|
final chatRoom = await chatRepository.getChatRoom(resolvedUuid);
|
||||||
|
ChosenPlanModel? chosenPlan;
|
||||||
|
final planId = resolvedChosenPlanId ?? chatRoom.chosenPlanId;
|
||||||
|
if (planId != null) {
|
||||||
|
chosenPlan = await chatRepository.getChosenPlan(id: planId);
|
||||||
|
}
|
||||||
|
final chatMessages = await chatRepository.getChatMessages(resolvedUuid, currentPage);
|
||||||
lastPage = chatMessages.meta.lastPage;
|
lastPage = chatMessages.meta.lastPage;
|
||||||
await _subscribeToChatChannel(uuid: uuid, chatId: chatRoom.id);
|
await _subscribeToChatChannel(uuid: resolvedUuid, chatId: chatRoom.id);
|
||||||
emit(
|
emit(
|
||||||
ChatRoomState.loaded(
|
ChatRoomState.loaded(
|
||||||
chatRoom: chatRoom.copyWith(chosenPlan: chosenPlan),
|
chatRoom: chatRoom.copyWith(chosenPlan: chosenPlan),
|
||||||
@@ -115,6 +136,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
|||||||
if (chosenPlan?.consultantId == chatRoom.users.sender.id) {
|
if (chosenPlan?.consultantId == chatRoom.users.sender.id) {
|
||||||
await checkCards();
|
await checkCards();
|
||||||
}
|
}
|
||||||
|
return resolvedUuid;
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
await reportFrom(e, st, this);
|
await reportFrom(e, st, this);
|
||||||
log(e.toString());
|
log(e.toString());
|
||||||
@@ -122,6 +144,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
|||||||
messagesSubscription?.cancel();
|
messagesSubscription?.cancel();
|
||||||
wsManager.unsubscribeDirectChatChannel();
|
wsManager.unsubscribeDirectChatChannel();
|
||||||
emit(const ChatRoomState.error(errorMessage: 'Ошибка получеия данных'));
|
emit(const ChatRoomState.error(errorMessage: 'Ошибка получеия данных'));
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,7 +320,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
|||||||
try {
|
try {
|
||||||
final planId = (state as ChatRoomStateLoaded).chatRoom.chosenPlan?.id;
|
final planId = (state as ChatRoomStateLoaded).chatRoom.chosenPlan?.id;
|
||||||
if (planId == null) return null;
|
if (planId == null) return null;
|
||||||
final paymentModel = await chatRepository.getPaymentLink(id: planId);
|
final paymentModel = await chatRepository.getPaymentLink(id: planId, paymentProvider: 'alfa-bank');
|
||||||
return paymentModel.paymentUrl;
|
return paymentModel.paymentUrl;
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
await reportFrom(e, st, this);
|
await reportFrom(e, st, this);
|
||||||
|
|||||||
@@ -36,10 +36,11 @@ import 'package:intl/intl.dart';
|
|||||||
import 'package:visibility_detector/visibility_detector.dart';
|
import 'package:visibility_detector/visibility_detector.dart';
|
||||||
|
|
||||||
class ChatScreen extends StatefulWidget {
|
class ChatScreen extends StatefulWidget {
|
||||||
final String uuid;
|
final String? uuid;
|
||||||
|
final String? orderId;
|
||||||
final bool? sendInitialMessage;
|
final bool? sendInitialMessage;
|
||||||
|
|
||||||
const ChatScreen({required this.uuid, super.key, required this.sendInitialMessage});
|
const ChatScreen({this.uuid, this.orderId, super.key, required this.sendInitialMessage});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ChatScreen> createState() => _ChatScreenState();
|
State<ChatScreen> createState() => _ChatScreenState();
|
||||||
@@ -55,6 +56,11 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
late AppLifecycleListener _listener;
|
late AppLifecycleListener _listener;
|
||||||
List<File> pickedFiles = [];
|
List<File> pickedFiles = [];
|
||||||
|
|
||||||
|
/// Conversation uuid после успешного init (в т.ч. после resolve по orderId).
|
||||||
|
String? _effectiveUuid;
|
||||||
|
|
||||||
|
String? get _conversationUuid => _effectiveUuid ?? widget.uuid;
|
||||||
|
|
||||||
void _onHide() {
|
void _onHide() {
|
||||||
chatRoomCubit.cancelTimer();
|
chatRoomCubit.cancelTimer();
|
||||||
}
|
}
|
||||||
@@ -64,26 +70,41 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
chatRoomCubit.setTimer();
|
chatRoomCubit.setTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _initChatRoom() async {
|
||||||
|
final uuid = await chatRoomCubit.initChat(
|
||||||
|
_conversationUuid,
|
||||||
|
orderId: _effectiveUuid == null ? widget.orderId : null,
|
||||||
|
sendInitialMessage: widget.sendInitialMessage ?? false,
|
||||||
|
);
|
||||||
|
if (uuid != null && mounted) {
|
||||||
|
setState(() => _effectiveUuid = uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(ChatScreen oldWidget) {
|
void didUpdateWidget(ChatScreen oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (oldWidget.uuid != widget.uuid) {
|
if (oldWidget.uuid != widget.uuid || oldWidget.orderId != widget.orderId) {
|
||||||
chatRoomCubit.initChat(widget.uuid, sendInitialMessage: widget.sendInitialMessage ?? false);
|
_effectiveUuid = widget.uuid;
|
||||||
|
_initChatRoom();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_effectiveUuid = widget.uuid;
|
||||||
scrollController.addListener(_onScroll);
|
scrollController.addListener(_onScroll);
|
||||||
chatRoomCubit.initChat(widget.uuid, sendInitialMessage: widget.sendInitialMessage ?? false);
|
_initChatRoom();
|
||||||
chatRoomCubit.setTimer();
|
chatRoomCubit.setTimer();
|
||||||
_listener = AppLifecycleListener(onHide: _onHide, onResume: _onResume);
|
_listener = AppLifecycleListener(onHide: _onHide, onResume: _onResume);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onScroll() {
|
void _onScroll() {
|
||||||
|
final uuid = _conversationUuid;
|
||||||
|
if (uuid == null) return;
|
||||||
if (scrollController.offset >= scrollController.position.maxScrollExtent && !scrollController.position.outOfRange) {
|
if (scrollController.offset >= scrollController.position.maxScrollExtent && !scrollController.position.outOfRange) {
|
||||||
chatRoomCubit.loadNextPage(widget.uuid);
|
chatRoomCubit.loadNextPage(uuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +138,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: DataErrorWidget(onReload: () => chatRoomCubit.initChat(widget.uuid)),
|
body: DataErrorWidget(onReload: _initChatRoom),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -242,7 +263,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
GoRouter.of(context).pushNamed(
|
GoRouter.of(context).pushNamed(
|
||||||
VideoCallScreen.name,
|
VideoCallScreen.name,
|
||||||
extra: <String, dynamic>{
|
extra: <String, dynamic>{
|
||||||
'conversationUuid': widget.uuid,
|
'conversationUuid': _conversationUuid,
|
||||||
'counterpartName': state.chatRoom.users.receiver.name,
|
'counterpartName': state.chatRoom.users.receiver.name,
|
||||||
'localParticipantName': state.chatRoom.users.sender.name,
|
'localParticipantName': state.chatRoom.users.sender.name,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,15 +2,14 @@ import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
|||||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||||
import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart';
|
import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart';
|
||||||
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
|
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
|
||||||
import 'package:eatvio_app/core/widgets/loader.dart';
|
|
||||||
import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart';
|
import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart';
|
||||||
import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart';
|
import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart';
|
||||||
import 'package:eatvio_app/features/chat/presentation/pages/payment_screen.dart';
|
|
||||||
import 'package:eatvio_app/features/chat/presentation/widgets/chose_plan_date_widget.dart';
|
import 'package:eatvio_app/features/chat/presentation/widgets/chose_plan_date_widget.dart';
|
||||||
import 'package:eatvio_app/features/chat/presentation/widgets/complete_plan_bottom_sheet.dart';
|
import 'package:eatvio_app/features/chat/presentation/widgets/complete_plan_bottom_sheet.dart';
|
||||||
import 'package:eatvio_app/features/chat/presentation/widgets/plan_complain_bottom_sheet.dart';
|
import 'package:eatvio_app/features/chat/presentation/widgets/plan_complain_bottom_sheet.dart';
|
||||||
import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart';
|
import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart';
|
||||||
import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart';
|
import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart';
|
||||||
|
import 'package:eatvio_app/features/payment/presentation/payment_flow.dart';
|
||||||
import 'package:eatvio_app/generated/l10n.dart';
|
import 'package:eatvio_app/generated/l10n.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
@@ -29,7 +28,6 @@ class NotificationBanner extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _NotificationBannerState extends State<NotificationBanner> {
|
class _NotificationBannerState extends State<NotificationBanner> {
|
||||||
bool isLoading = false;
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final size = MediaQuery.sizeOf(context);
|
final size = MediaQuery.sizeOf(context);
|
||||||
@@ -54,13 +52,7 @@ class _NotificationBannerState extends State<NotificationBanner> {
|
|||||||
padding: EdgeInsets.only(top: 12, right: 8, left: 12),
|
padding: EdgeInsets.only(top: 12, right: 8, left: 12),
|
||||||
child: _StarIcon(),
|
child: _StarIcon(),
|
||||||
),
|
),
|
||||||
if (isLoading)
|
Padding(
|
||||||
const Padding(
|
|
||||||
padding: EdgeInsets.only(left: 40, top: 24),
|
|
||||||
child: Loader(),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 16),
|
padding: const EdgeInsets.only(top: 16),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -111,21 +103,10 @@ class _NotificationBannerState extends State<NotificationBanner> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () async {
|
onTap: () {
|
||||||
final planId = (context.read<ChatRoomCubit>().state as ChatRoomStateLoaded).chatRoom.chosenPlan?.id;
|
final planId = (context.read<ChatRoomCubit>().state as ChatRoomStateLoaded).chatRoom.chosenPlan?.id;
|
||||||
setState(() {
|
if (planId != null) {
|
||||||
isLoading = true;
|
showConsultationPaymentFlow(context, planId: planId);
|
||||||
});
|
|
||||||
final paymentLink = await context.read<ChatRoomCubit>().getPaymentLink();
|
|
||||||
setState(() {
|
|
||||||
isLoading = false;
|
|
||||||
});
|
|
||||||
if (paymentLink != null && planId != null) {
|
|
||||||
if (!context.mounted) return;
|
|
||||||
GoRouter.of(context).pushReplacement(
|
|
||||||
PaymentScreen.path,
|
|
||||||
extra: {'url': paymentLink, 'planId': planId},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Text(s.pay, style: TextStyles.textLSemibold.copyWith(color: appTheme.blueA400)),
|
child: Text(s.pay, style: TextStyles.textLSemibold.copyWith(color: appTheme.blueA400)),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:eatvio_app/features/chat/models/chat_message_model.dart';
|
|||||||
import 'package:eatvio_app/features/chat/models/chat_messages_model.dart';
|
import 'package:eatvio_app/features/chat/models/chat_messages_model.dart';
|
||||||
import 'package:eatvio_app/features/chat/models/chat_room_model.dart';
|
import 'package:eatvio_app/features/chat/models/chat_room_model.dart';
|
||||||
import 'package:eatvio_app/features/chat/models/chat_room_pagination_model.dart';
|
import 'package:eatvio_app/features/chat/models/chat_room_pagination_model.dart';
|
||||||
|
import 'package:eatvio_app/features/chat/models/order_id_to_conversation_model.dart';
|
||||||
import 'package:eatvio_app/features/chat/models/payment_model.dart';
|
import 'package:eatvio_app/features/chat/models/payment_model.dart';
|
||||||
import 'package:eatvio_app/features/chat/models/plan_complain_model.dart';
|
import 'package:eatvio_app/features/chat/models/plan_complain_model.dart';
|
||||||
import 'package:eatvio_app/features/chat/services/chat_service.dart';
|
import 'package:eatvio_app/features/chat/services/chat_service.dart';
|
||||||
@@ -98,8 +99,19 @@ class ChatRepository {
|
|||||||
|
|
||||||
Future<PaymentModel> getPaymentLink({
|
Future<PaymentModel> getPaymentLink({
|
||||||
required int id,
|
required int id,
|
||||||
|
required String paymentProvider,
|
||||||
}) async {
|
}) async {
|
||||||
return await chatService.getPaymentLink(id: id);
|
return await chatService.getPaymentLink(
|
||||||
|
id: id,
|
||||||
|
body: {'payment_provider': paymentProvider},
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<OrderIdToConversationModel> resolveOrderIdToConversation({
|
||||||
|
required String orderId,
|
||||||
|
}) async {
|
||||||
|
return await chatService.resolveOrderIdToConversation(orderId: orderId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:eatvio_app/features/chat/models/chat_message_model.dart';
|
|||||||
import 'package:eatvio_app/features/chat/models/chat_messages_model.dart';
|
import 'package:eatvio_app/features/chat/models/chat_messages_model.dart';
|
||||||
import 'package:eatvio_app/features/chat/models/chat_room_model.dart';
|
import 'package:eatvio_app/features/chat/models/chat_room_model.dart';
|
||||||
import 'package:eatvio_app/features/chat/models/chat_room_pagination_model.dart';
|
import 'package:eatvio_app/features/chat/models/chat_room_pagination_model.dart';
|
||||||
|
import 'package:eatvio_app/features/chat/models/order_id_to_conversation_model.dart';
|
||||||
import 'package:eatvio_app/features/chat/models/payment_model.dart';
|
import 'package:eatvio_app/features/chat/models/payment_model.dart';
|
||||||
import 'package:eatvio_app/features/chat/models/plan_complain_model.dart';
|
import 'package:eatvio_app/features/chat/models/plan_complain_model.dart';
|
||||||
import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart';
|
import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart';
|
||||||
@@ -65,6 +66,12 @@ abstract class ChatService {
|
|||||||
@POST('/api/chosen_plans/{id}/start_payment_process')
|
@POST('/api/chosen_plans/{id}/start_payment_process')
|
||||||
Future<PaymentModel> getPaymentLink({
|
Future<PaymentModel> getPaymentLink({
|
||||||
@Path() required int id,
|
@Path() required int id,
|
||||||
|
@Body() required Map<String, String> body,
|
||||||
|
});
|
||||||
|
|
||||||
|
@GET('/api/consultation/resolve/order_id_to_conversation/{orderId}')
|
||||||
|
Future<OrderIdToConversationModel> resolveOrderIdToConversation({
|
||||||
|
@Path() required String orderId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@GET('/api/chosen_plans/show/{id}')
|
@GET('/api/chosen_plans/show/{id}')
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class OrderInfoCubit extends Cubit<OrderInfoState> {
|
|||||||
try {
|
try {
|
||||||
final plan = state.chosenPlan;
|
final plan = state.chosenPlan;
|
||||||
if (plan == null) return null;
|
if (plan == null) return null;
|
||||||
final paymentModel = await chatRepository.getPaymentLink(id: plan.id);
|
final paymentModel = await chatRepository.getPaymentLink(id: plan.id, paymentProvider: 'alfa-bank');
|
||||||
return paymentModel.paymentUrl;
|
return paymentModel.paymentUrl;
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
await reportFrom(e, st, this);
|
await reportFrom(e, st, this);
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
|
import 'package:eatvio_app/core/error_reporting/report_from.dart';
|
||||||
|
import 'package:eatvio_app/features/payment/presentation/cubit/consultation_payment_cubit/consultation_payment_state.dart';
|
||||||
|
import 'package:eatvio_app/features/payment/repositories/payment_repository.dart';
|
||||||
|
import 'package:eatvio_app/features/payment/repositories/sbp_repository.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:sbp/models/c2bmembers_model.dart';
|
||||||
|
|
||||||
|
/// Cubit оплаты консультации через СБП: старт оплаты, список банков, редирект в банк.
|
||||||
|
///
|
||||||
|
/// Не хранит [planId] как поле — он передаётся в [startPayment].
|
||||||
|
/// Polling статуса оплаты остаётся в ChatRoomCubit.
|
||||||
|
class ConsultationPaymentCubit extends Cubit<ConsultationPaymentState> {
|
||||||
|
final PaymentRepository paymentRepository;
|
||||||
|
final SbpRepository sbpRepository;
|
||||||
|
|
||||||
|
ConsultationPaymentCubit({
|
||||||
|
required this.paymentRepository,
|
||||||
|
required this.sbpRepository,
|
||||||
|
}) : super(ConsultationPaymentState());
|
||||||
|
|
||||||
|
/// Запускает оплату для [planId], сохраняет checkout_url и загружает банки СБП.
|
||||||
|
Future<void> startPayment({required int planId}) async {
|
||||||
|
try {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
isLoading: true,
|
||||||
|
errorMessage: null,
|
||||||
|
banks: const [],
|
||||||
|
checkoutUrl: null,
|
||||||
|
orderId: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final payment = await paymentRepository.startPaymentProcess(planId: planId);
|
||||||
|
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
checkoutUrl: payment.paymentUrl,
|
||||||
|
orderId: payment.orderId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final banks = await sbpRepository.getInstalledBanks();
|
||||||
|
if (isClosed) return;
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
banks: banks,
|
||||||
|
isLoading: false,
|
||||||
|
errorMessage: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e, st) {
|
||||||
|
await reportFrom(e, st, this);
|
||||||
|
log(e.toString(), stackTrace: st);
|
||||||
|
if (isClosed) return;
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
isLoading: false,
|
||||||
|
banks: const [],
|
||||||
|
errorMessage: e.toString(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e, st) {
|
||||||
|
await reportFrom(e, st, this);
|
||||||
|
log(e.toString(), stackTrace: st);
|
||||||
|
if (isClosed) return;
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
isLoading: false,
|
||||||
|
banks: const [],
|
||||||
|
checkoutUrl: null,
|
||||||
|
orderId: null,
|
||||||
|
errorMessage: e.toString(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Открывает банковское приложение [bank] с текущим [ConsultationPaymentState.checkoutUrl].
|
||||||
|
Future<bool> openBank({required C2bmemberModel bank}) async {
|
||||||
|
final checkoutUrl = state.checkoutUrl;
|
||||||
|
if (checkoutUrl == null || checkoutUrl.isEmpty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await sbpRepository.openBank(url: checkoutUrl, bank: bank);
|
||||||
|
} catch (e, st) {
|
||||||
|
await reportFrom(e, st, this);
|
||||||
|
log(e.toString(), stackTrace: st);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:sbp/models/c2bmembers_model.dart';
|
||||||
|
|
||||||
|
part 'consultation_payment_state.freezed.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class ConsultationPaymentState with _$ConsultationPaymentState {
|
||||||
|
factory ConsultationPaymentState({
|
||||||
|
@Default(false) bool isLoading,
|
||||||
|
@Default([]) List<C2bmemberModel> banks,
|
||||||
|
String? checkoutUrl,
|
||||||
|
String? orderId,
|
||||||
|
String? errorMessage,
|
||||||
|
}) = _ConsultationPaymentState;
|
||||||
|
}
|
||||||
49
lib/features/payment/presentation/payment_flow.dart
Normal file
49
lib/features/payment/presentation/payment_flow.dart
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import 'package:eatvio_app/features/payment/presentation/widgets/promo_code_gate_bottom_sheet.dart';
|
||||||
|
import 'package:eatvio_app/features/payment/presentation/widgets/promo_code_modal.dart';
|
||||||
|
import 'package:eatvio_app/features/payment/presentation/widgets/sbp_banks_bottom_sheet.dart';
|
||||||
|
import 'package:eatvio_app/generated/l10n.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Оркестрирует флоу оплаты консультации:
|
||||||
|
/// гейт «У вас есть промокод?» → (Да → форма промокода) → SbpBanks.
|
||||||
|
Future<void> showConsultationPaymentFlow(
|
||||||
|
BuildContext context, {
|
||||||
|
required int planId,
|
||||||
|
Future<bool?> Function(BuildContext)? showGate,
|
||||||
|
Future<PromoCodeResult?> Function(BuildContext, {Future<PromoValidation> Function(String)? onValidate})? showForm,
|
||||||
|
Future<void> Function(BuildContext, {required int planId})? showSbp,
|
||||||
|
}) async {
|
||||||
|
final gate = showGate ?? _showGate;
|
||||||
|
final form = showForm ?? _showForm;
|
||||||
|
final sbp = showSbp ?? _showSbp;
|
||||||
|
final s = S.of(context);
|
||||||
|
|
||||||
|
final gateChoice = await gate(context);
|
||||||
|
if (gateChoice == null || !context.mounted) return;
|
||||||
|
|
||||||
|
if (gateChoice) {
|
||||||
|
final formResult = await form(
|
||||||
|
context,
|
||||||
|
onValidate: (_) async => PromoValidation.failure(s.promo_invalid),
|
||||||
|
);
|
||||||
|
if (formResult == null || !context.mounted) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.mounted) return;
|
||||||
|
await sbp(context, planId: planId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool?> _showGate(BuildContext context) {
|
||||||
|
return showPromoCodeGateBottomSheet(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<PromoCodeResult?> _showForm(
|
||||||
|
BuildContext context, {
|
||||||
|
Future<PromoValidation> Function(String)? onValidate,
|
||||||
|
}) {
|
||||||
|
return ItvioPromoCodeModal.show(context, onValidate: onValidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showSbp(BuildContext context, {required int planId}) {
|
||||||
|
return showSbpBanksBottomSheet(context, planId: planId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||||
|
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||||
|
import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart';
|
||||||
|
import 'package:eatvio_app/generated/l10n.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Показывает bottom sheet «У вас есть промокод?» с кнопками Да/Нет.
|
||||||
|
///
|
||||||
|
/// Возвращает `true` если пользователь нажал «Да», `false` если «Нет»,
|
||||||
|
/// `null` если закрыл шторку (бекдроп/крестик).
|
||||||
|
Future<bool?> showPromoCodeGateBottomSheet(BuildContext context) {
|
||||||
|
return showModalBottomSheet<bool?>(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
barrierColor: appTheme.transparentBlack,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
elevation: 0,
|
||||||
|
useSafeArea: true,
|
||||||
|
builder: (_) => Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||||
|
),
|
||||||
|
child: _PromoCodeGateSheet(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Виджет-гейт выбора промокода.
|
||||||
|
class PromoCodeGateBottomSheet extends StatelessWidget {
|
||||||
|
const PromoCodeGateBottomSheet({super.key, required this.onResult});
|
||||||
|
|
||||||
|
final ValueChanged<bool?> onResult;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final s = S.of(context);
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 20),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
const Center(child: BottomSheetHandlerBar()),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Text(
|
||||||
|
s.have_promo_code,
|
||||||
|
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
SizedBox(
|
||||||
|
height: 50,
|
||||||
|
child: FilledButton(
|
||||||
|
onPressed: () => onResult(true),
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
backgroundColor: appTheme.blueA400,
|
||||||
|
foregroundColor: appTheme.whiteA700,
|
||||||
|
shape: const StadiumBorder(),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
s.yes,
|
||||||
|
style: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SizedBox(
|
||||||
|
height: 50,
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: () => onResult(false),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: appTheme.blueA400,
|
||||||
|
side: BorderSide(color: appTheme.blueA400),
|
||||||
|
shape: const StadiumBorder(),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
s.no,
|
||||||
|
style: TextStyles.textMSemibold.copyWith(color: appTheme.blueA400),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Внутренняя реализация модалки — обёртка над [PromoCodeGateBottomSheet].
|
||||||
|
class _PromoCodeGateSheet extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return PromoCodeGateBottomSheet(
|
||||||
|
onResult: (result) => Navigator.of(context).pop(result),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
281
lib/features/payment/presentation/widgets/promo_code_modal.dart
Normal file
281
lib/features/payment/presentation/widgets/promo_code_modal.dart
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||||
|
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||||
|
import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart';
|
||||||
|
import 'package:eatvio_app/generated/l10n.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Результат работы модалки промокода.
|
||||||
|
class PromoCodeResult {
|
||||||
|
const PromoCodeResult({this.code});
|
||||||
|
|
||||||
|
/// Применённый промокод или null, если пользователь продолжил без него.
|
||||||
|
final String? code;
|
||||||
|
|
||||||
|
bool get hasPromoCode => code != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ответ проверки промокода.
|
||||||
|
class PromoValidation {
|
||||||
|
const PromoValidation.success() : error = null;
|
||||||
|
const PromoValidation.failure(this.error);
|
||||||
|
|
||||||
|
final String? error;
|
||||||
|
|
||||||
|
bool get isValid => error == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Модальное окно ввода промокода.
|
||||||
|
class ItvioPromoCodeModal extends StatefulWidget {
|
||||||
|
const ItvioPromoCodeModal({super.key, this.onValidate});
|
||||||
|
|
||||||
|
final Future<PromoValidation> Function(String code)? onValidate;
|
||||||
|
|
||||||
|
static Future<PromoCodeResult?> show(
|
||||||
|
BuildContext context, {
|
||||||
|
Future<PromoValidation> Function(String code)? onValidate,
|
||||||
|
}) {
|
||||||
|
return showModalBottomSheet<PromoCodeResult>(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
useSafeArea: true,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
builder: (_) => ItvioPromoCodeModal(onValidate: onValidate),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ItvioPromoCodeModal> createState() => _ItvioPromoCodeModalState();
|
||||||
|
}
|
||||||
|
|
||||||
|
enum _ModalState { input, loading }
|
||||||
|
|
||||||
|
class _ItvioPromoCodeModalState extends State<ItvioPromoCodeModal> {
|
||||||
|
final _controller = TextEditingController();
|
||||||
|
final _focusNode = FocusNode();
|
||||||
|
|
||||||
|
_ModalState _state = _ModalState.input;
|
||||||
|
String? _errorText;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
_focusNode.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _apply() async {
|
||||||
|
final code = _controller.text.trim().toUpperCase();
|
||||||
|
|
||||||
|
if (code.isEmpty) {
|
||||||
|
setState(() => _errorText = S.of(context).enter_promo_code);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_state = _ModalState.loading;
|
||||||
|
_errorText = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
PromoValidation validation;
|
||||||
|
try {
|
||||||
|
validation = widget.onValidate != null
|
||||||
|
? await widget.onValidate!(code)
|
||||||
|
: const PromoValidation.success();
|
||||||
|
} catch (_) {
|
||||||
|
validation = const PromoValidation.failure(
|
||||||
|
'Не удалось проверить промокод. Попробуйте позже',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
if (!validation.isValid) {
|
||||||
|
setState(() {
|
||||||
|
_state = _ModalState.input;
|
||||||
|
_errorText = validation.error;
|
||||||
|
});
|
||||||
|
_focusNode.requestFocus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Navigator.of(context).pop(PromoCodeResult(code: code));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSkip() {
|
||||||
|
Navigator.of(context).pop(const PromoCodeResult());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get _isLoading => _state == _ModalState.loading;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
|
||||||
|
final s = S.of(context);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: bottomInset),
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 8, 20, 20),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
const Center(child: BottomSheetHandlerBar()),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildHeader(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const Text(
|
||||||
|
'Промокод',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: Color(0xFF6B7684),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildField(),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildHint(),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildApplyButton(s),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
_buildSkipButton(s),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHeader() {
|
||||||
|
final s = S.of(context);
|
||||||
|
return Text(
|
||||||
|
s.have_promo_code_form,
|
||||||
|
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildField() {
|
||||||
|
final hasError = _errorText != null;
|
||||||
|
final s = S.of(context);
|
||||||
|
|
||||||
|
return AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 150),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: appTheme.gray100,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(
|
||||||
|
width: 2,
|
||||||
|
color: hasError
|
||||||
|
? appTheme.red
|
||||||
|
: (_focusNode.hasFocus
|
||||||
|
? appTheme.blueA400
|
||||||
|
: Colors.transparent),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: _controller,
|
||||||
|
focusNode: _focusNode,
|
||||||
|
enabled: !_isLoading,
|
||||||
|
textCapitalization: TextCapitalization.characters,
|
||||||
|
onChanged: (_) {
|
||||||
|
if (_errorText != null) {
|
||||||
|
setState(() => _errorText = null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSubmitted: (_) => _apply(),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: s.enter_promo_code,
|
||||||
|
hintStyle: const TextStyle(
|
||||||
|
color: Color(0xFF9AA3AF),
|
||||||
|
),
|
||||||
|
border: InputBorder.none,
|
||||||
|
isDense: true,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
color: Color(0xFF24303F),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHint() {
|
||||||
|
final String text;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
if (_errorText != null) {
|
||||||
|
text = _errorText!;
|
||||||
|
color = appTheme.red;
|
||||||
|
} else {
|
||||||
|
text = '';
|
||||||
|
color = Colors.transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
height: 19,
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: TextStyle(fontSize: 13, color: color),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildApplyButton(S s) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 50,
|
||||||
|
child: FilledButton(
|
||||||
|
onPressed: _isLoading ? null : _apply,
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
backgroundColor: appTheme.blueA400,
|
||||||
|
foregroundColor: appTheme.whiteA700,
|
||||||
|
disabledBackgroundColor: appTheme.blueA400.withValues(alpha: 0.55),
|
||||||
|
shape: const StadiumBorder(),
|
||||||
|
textStyle: const TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: _isLoading
|
||||||
|
? const SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2.5,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(s.apply),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSkipButton(S s) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 46,
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: _isLoading ? null : _onSkip,
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor: appTheme.blueA400,
|
||||||
|
shape: const StadiumBorder(),
|
||||||
|
textStyle: const TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
child: Text(s.skip),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:eatvio_app/core/locator_service.dart';
|
||||||
|
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||||
|
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||||
|
import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart';
|
||||||
|
import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart';
|
||||||
|
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
|
||||||
|
import 'package:eatvio_app/core/widgets/loader.dart';
|
||||||
|
import 'package:eatvio_app/features/payment/presentation/cubit/consultation_payment_cubit/consultation_payment_cubit.dart';
|
||||||
|
import 'package:eatvio_app/features/payment/presentation/cubit/consultation_payment_cubit/consultation_payment_state.dart';
|
||||||
|
import 'package:eatvio_app/generated/l10n.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:sbp/models/c2bmembers_model.dart';
|
||||||
|
|
||||||
|
/// Показывает bottom sheet выбора банка СБП для оплаты консультации.
|
||||||
|
Future<void> showSbpBanksBottomSheet(BuildContext context, {required int planId}) {
|
||||||
|
final height = MediaQuery.sizeOf(context).height * 0.55;
|
||||||
|
return showBottomSheetWithBlur(
|
||||||
|
context: context,
|
||||||
|
height: height,
|
||||||
|
child: SbpBanksBottomSheet(planId: planId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bottom sheet со списком установленных банков СБП.
|
||||||
|
///
|
||||||
|
/// При открытии вызывает [ConsultationPaymentCubit.startPayment] с [planId]
|
||||||
|
/// и показывает [Loader], пока загружаются checkout_url и список банков.
|
||||||
|
class SbpBanksBottomSheet extends StatefulWidget {
|
||||||
|
const SbpBanksBottomSheet({super.key, required this.planId, this.cubit});
|
||||||
|
|
||||||
|
final int planId;
|
||||||
|
final ConsultationPaymentCubit? cubit;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SbpBanksBottomSheet> createState() => _SbpBanksBottomSheetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SbpBanksBottomSheetState extends State<SbpBanksBottomSheet> {
|
||||||
|
late final ConsultationPaymentCubit _cubit;
|
||||||
|
late final bool _ownsCubit;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_ownsCubit = widget.cubit == null;
|
||||||
|
_cubit = widget.cubit ?? sl<ConsultationPaymentCubit>();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_cubit.startPayment(planId: widget.planId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
if (_ownsCubit) {
|
||||||
|
_cubit.close();
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final s = S.of(context);
|
||||||
|
return BlocProvider<ConsultationPaymentCubit>.value(
|
||||||
|
value: _cubit,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const BottomSheetHandlerBar(),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Text(
|
||||||
|
s.choose_bank_for_payment,
|
||||||
|
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Expanded(
|
||||||
|
child: BlocBuilder<ConsultationPaymentCubit, ConsultationPaymentState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
if (state.isLoading) {
|
||||||
|
return const Loader();
|
||||||
|
}
|
||||||
|
if (state.errorMessage != null) {
|
||||||
|
return _ErrorContent(
|
||||||
|
message: s.sbp_payment_error,
|
||||||
|
onOk: () {
|
||||||
|
final navigator = Navigator.of(context);
|
||||||
|
if (navigator.canPop()) {
|
||||||
|
navigator.pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (state.banks.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Text(
|
||||||
|
s.no_installed_banks,
|
||||||
|
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return ListView.separated(
|
||||||
|
itemCount: state.banks.length,
|
||||||
|
separatorBuilder: (_, _) => const SizedBox(height: 8),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final bank = state.banks[index];
|
||||||
|
return _BankTile(bank: bank, onTap: () => _onBankTap(bank));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onBankTap(C2bmemberModel bank) async {
|
||||||
|
final opened = await _cubit.openBank(bank: bank);
|
||||||
|
if (opened && mounted) {
|
||||||
|
final navigator = Navigator.of(context);
|
||||||
|
if (navigator.canPop()) {
|
||||||
|
navigator.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BankTile extends StatelessWidget {
|
||||||
|
const _BankTile({required this.bank, required this.onTap});
|
||||||
|
|
||||||
|
final C2bmemberModel bank;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final name = bank.localName ?? bank.bankName;
|
||||||
|
return InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 4),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
_BankIcon(bank: bank),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(name, style: TextStyles.textLSemibold.copyWith(color: appTheme.black)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BankIcon extends StatelessWidget {
|
||||||
|
const _BankIcon({required this.bank});
|
||||||
|
|
||||||
|
final C2bmemberModel bank;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (bank.icon.isNotEmpty) {
|
||||||
|
return ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: bank.logoURL,
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
placeholder: (context, url) => _placeholder(),
|
||||||
|
errorWidget: (context, url, error) => _placeholder(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _placeholder();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _placeholder() {
|
||||||
|
return Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(color: appTheme.blueA400.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(8)),
|
||||||
|
child: Icon(Icons.account_balance, color: appTheme.blueA400, size: 22),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ErrorContent extends StatelessWidget {
|
||||||
|
const _ErrorContent({required this.message, required this.onOk});
|
||||||
|
|
||||||
|
final String message;
|
||||||
|
final VoidCallback onOk;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final s = S.of(context);
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
message,
|
||||||
|
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
CustomOutlinedButton(
|
||||||
|
text: s.ok,
|
||||||
|
height: 52,
|
||||||
|
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||||
|
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
|
||||||
|
onPressed: onOk,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
lib/features/payment/repositories/payment_repository.dart
Normal file
18
lib/features/payment/repositories/payment_repository.dart
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import 'package:eatvio_app/features/chat/models/payment_model.dart';
|
||||||
|
import 'package:eatvio_app/features/chat/services/chat_service.dart';
|
||||||
|
|
||||||
|
/// Репозиторий оплаты консультаций (chosen_plan) через Alfa Bank / СБП.
|
||||||
|
class PaymentRepository {
|
||||||
|
final ChatService chatService;
|
||||||
|
|
||||||
|
PaymentRepository({required this.chatService});
|
||||||
|
|
||||||
|
/// Запускает процесс оплаты: POST `/api/chosen_plans/{id}/start_payment_process`
|
||||||
|
/// с `payment_provider: alfa-bank`.
|
||||||
|
Future<PaymentModel> startPaymentProcess({required int planId}) async {
|
||||||
|
return chatService.getPaymentLink(
|
||||||
|
id: planId,
|
||||||
|
body: const {'payment_provider': 'alfa-bank'},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
lib/features/payment/repositories/sbp_repository.dart
Normal file
27
lib/features/payment/repositories/sbp_repository.dart
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import 'package:eatvio_app/features/payment/services/sbp_service.dart';
|
||||||
|
import 'package:sbp/models/c2bmembers_model.dart';
|
||||||
|
|
||||||
|
/// Репозиторий оплаты по СБП: обёртка над [SbpService] с graceful-обработкой
|
||||||
|
/// ошибок нативного плагина (отсутствие установленных банков, недоступность
|
||||||
|
/// сервисов на устройстве, в т.ч. на Huawei HMS и т.п.).
|
||||||
|
///
|
||||||
|
/// Возвращает управляемые значения вместо проброса исключений, чтобы UI мог
|
||||||
|
/// показать fallback (например, открыть checkout_url в браузере/WebView).
|
||||||
|
class SbpRepository {
|
||||||
|
final SbpService sbpService;
|
||||||
|
|
||||||
|
SbpRepository({required this.sbpService});
|
||||||
|
|
||||||
|
/// Возвращает установленные банки СБП либо пустой список при ошибке плагина.
|
||||||
|
Future<List<C2bmemberModel>> getInstalledBanks() async {
|
||||||
|
return await sbpService.getInstalledBanks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Открывает банк для оплаты. Возвращает `false` при ошибке плагина.
|
||||||
|
Future<bool> openBank({
|
||||||
|
required String url,
|
||||||
|
required C2bmemberModel bank,
|
||||||
|
}) async {
|
||||||
|
return await sbpService.openBank(url: url, bank: bank);
|
||||||
|
}
|
||||||
|
}
|
||||||
43
lib/features/payment/services/sbp_service.dart
Normal file
43
lib/features/payment/services/sbp_service.dart
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import 'package:sbp/data/c2bmembers_data.dart';
|
||||||
|
import 'package:sbp/models/c2bmembers_model.dart';
|
||||||
|
import 'package:sbp/sbp.dart' as sbp;
|
||||||
|
|
||||||
|
/// Контракт обёртки над нативным плагином СБП (Система Быстрых Платежей).
|
||||||
|
///
|
||||||
|
/// Статичные методы класса [Sbp] из пакета `sbp` работают через MethodChannel,
|
||||||
|
/// поэтому не поддаются прямому мокированию в тестах. Этот интерфейс вводится
|
||||||
|
/// для тестируемости и возможности подмены реализации (см. [SbpServiceImpl]).
|
||||||
|
abstract class SbpService {
|
||||||
|
/// Возвращает список банков, установленных на устройстве пользователя
|
||||||
|
/// и поддерживающих оплату по СБП.
|
||||||
|
Future<List<C2bmemberModel>> getInstalledBanks();
|
||||||
|
|
||||||
|
/// Открывает банковское приложение [bank] для оплаты по ссылке [url]
|
||||||
|
/// (checkout_url вида `https://qr.nspk.ru/...`).
|
||||||
|
///
|
||||||
|
/// Возвращает `true`, если приложение банка успешно запущено.
|
||||||
|
Future<bool> openBank({required String url, required C2bmemberModel bank});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Реализация [SbpService], делегирующая вызовы в статичные методы пакета `sbp`.
|
||||||
|
class SbpServiceImpl implements SbpService {
|
||||||
|
const SbpServiceImpl();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<C2bmemberModel>> getInstalledBanks() {
|
||||||
|
// Используем встроенный справочник банков c2bmembersData (получаемый с
|
||||||
|
// https://qr.nspk.ru/proxyapp/c2bmembers.json) и фильтруем только те, что
|
||||||
|
// установлены на устройстве. Локальные иконки/названия включены, чтобы UI
|
||||||
|
// не зависел от сетевой загрузки логотипов.
|
||||||
|
return sbp.Sbp.getInstalledBanks(
|
||||||
|
C2bmembersModel.fromJson(c2bmembersData),
|
||||||
|
useAndroidLocalIcons: true,
|
||||||
|
useAndroidLocalNames: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> openBank({required String url, required C2bmemberModel bank}) {
|
||||||
|
return sbp.Sbp.openBank(url, bank);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,7 +30,9 @@ enum ChosenPlanStateEnum {
|
|||||||
@JsonValue('personal_diet_setted')
|
@JsonValue('personal_diet_setted')
|
||||||
personalDietSet,
|
personalDietSet,
|
||||||
@JsonValue('charge_was_collected')
|
@JsonValue('charge_was_collected')
|
||||||
stateChargeWasCollected;
|
stateChargeWasCollected,
|
||||||
|
@JsonValue('payment_failed')
|
||||||
|
statePaymentFailed;
|
||||||
|
|
||||||
static List<ChosenPlanStateEnum> getShowableChatBadgeStates() {
|
static List<ChosenPlanStateEnum> getShowableChatBadgeStates() {
|
||||||
return [
|
return [
|
||||||
@@ -53,6 +55,7 @@ enum ChosenPlanStateEnum {
|
|||||||
case ChosenPlanStateEnum.stateInCoordination:
|
case ChosenPlanStateEnum.stateInCoordination:
|
||||||
case ChosenPlanStateEnum.stateStartDateHasBeenSet:
|
case ChosenPlanStateEnum.stateStartDateHasBeenSet:
|
||||||
case ChosenPlanStateEnum.statePendingPayment:
|
case ChosenPlanStateEnum.statePendingPayment:
|
||||||
|
case ChosenPlanStateEnum.statePaymentFailed:
|
||||||
return false;
|
return false;
|
||||||
default:
|
default:
|
||||||
return true;
|
return true;
|
||||||
@@ -68,7 +71,7 @@ enum ChosenPlanStateEnum {
|
|||||||
ChosenPlanStateEnum.stateLastDayFinished,
|
ChosenPlanStateEnum.stateLastDayFinished,
|
||||||
ChosenPlanStateEnum.stateComplained,
|
ChosenPlanStateEnum.stateComplained,
|
||||||
ChosenPlanStateEnum.stateDone,
|
ChosenPlanStateEnum.stateDone,
|
||||||
ChosenPlanStateEnum.personalDietSet
|
ChosenPlanStateEnum.personalDietSet,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"another_form_of_minute": MessageLookupByLibrary.simpleMessage("минут"),
|
"another_form_of_minute": MessageLookupByLibrary.simpleMessage("минут"),
|
||||||
"another_form_of_stage": MessageLookupByLibrary.simpleMessage("этапа"),
|
"another_form_of_stage": MessageLookupByLibrary.simpleMessage("этапа"),
|
||||||
"answer": MessageLookupByLibrary.simpleMessage("Ответить"),
|
"answer": MessageLookupByLibrary.simpleMessage("Ответить"),
|
||||||
|
"apply": MessageLookupByLibrary.simpleMessage("Apply"),
|
||||||
"archived": MessageLookupByLibrary.simpleMessage("В архиве"),
|
"archived": MessageLookupByLibrary.simpleMessage("В архиве"),
|
||||||
"atLeast3Symbols": MessageLookupByLibrary.simpleMessage(
|
"atLeast3Symbols": MessageLookupByLibrary.simpleMessage(
|
||||||
"Минимум 3 символа",
|
"Минимум 3 символа",
|
||||||
@@ -173,6 +174,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
),
|
),
|
||||||
"chest": MessageLookupByLibrary.simpleMessage("Грудь"),
|
"chest": MessageLookupByLibrary.simpleMessage("Грудь"),
|
||||||
"chest_circumference": MessageLookupByLibrary.simpleMessage("Обхват груди"),
|
"chest_circumference": MessageLookupByLibrary.simpleMessage("Обхват груди"),
|
||||||
|
"choose_bank_for_payment": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Выберите банк",
|
||||||
|
),
|
||||||
"choose_date": MessageLookupByLibrary.simpleMessage("Выбрать дату"),
|
"choose_date": MessageLookupByLibrary.simpleMessage("Выбрать дату"),
|
||||||
"choose_diet_duration": MessageLookupByLibrary.simpleMessage(
|
"choose_diet_duration": MessageLookupByLibrary.simpleMessage(
|
||||||
"Выберите срок диеты",
|
"Выберите срок диеты",
|
||||||
@@ -378,6 +382,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"enter_information": MessageLookupByLibrary.simpleMessage(
|
"enter_information": MessageLookupByLibrary.simpleMessage(
|
||||||
"Введите информацию...",
|
"Введите информацию...",
|
||||||
),
|
),
|
||||||
|
"enter_promo_code": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Enter promo code",
|
||||||
|
),
|
||||||
"error_desc": MessageLookupByLibrary.simpleMessage(
|
"error_desc": MessageLookupByLibrary.simpleMessage(
|
||||||
"Произошла ошибка. Мы уже работаем над её устранением. Попробуйте позже",
|
"Произошла ошибка. Мы уже работаем над её устранением. Попробуйте позже",
|
||||||
),
|
),
|
||||||
@@ -463,6 +470,12 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"hatred_description": MessageLookupByLibrary.simpleMessage(
|
"hatred_description": MessageLookupByLibrary.simpleMessage(
|
||||||
"Враждебные высказывания",
|
"Враждебные высказывания",
|
||||||
),
|
),
|
||||||
|
"have_promo_code": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Do you have a promo code?",
|
||||||
|
),
|
||||||
|
"have_promo_code_form": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Have a promo code?",
|
||||||
|
),
|
||||||
"height": MessageLookupByLibrary.simpleMessage("Рост"),
|
"height": MessageLookupByLibrary.simpleMessage("Рост"),
|
||||||
"height_error": MessageLookupByLibrary.simpleMessage("Введите рост"),
|
"height_error": MessageLookupByLibrary.simpleMessage("Введите рост"),
|
||||||
"height_measurements": MessageLookupByLibrary.simpleMessage("Замер роста"),
|
"height_measurements": MessageLookupByLibrary.simpleMessage("Замер роста"),
|
||||||
@@ -588,6 +601,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"no_default_cards": MessageLookupByLibrary.simpleMessage(
|
"no_default_cards": MessageLookupByLibrary.simpleMessage(
|
||||||
"У вас нет привязанных карт. Что бы начать работу, привяжите карту",
|
"У вас нет привязанных карт. Что бы начать работу, привяжите карту",
|
||||||
),
|
),
|
||||||
|
"no_installed_banks": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Нет установленных банков",
|
||||||
|
),
|
||||||
"not_liked_groceries": MessageLookupByLibrary.simpleMessage(
|
"not_liked_groceries": MessageLookupByLibrary.simpleMessage(
|
||||||
"Не любимые продукты",
|
"Не любимые продукты",
|
||||||
),
|
),
|
||||||
@@ -699,6 +715,10 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"Разница в ценах, это комиссия Итвио",
|
"Разница в ценах, это комиссия Итвио",
|
||||||
),
|
),
|
||||||
"profile": MessageLookupByLibrary.simpleMessage("Профиль"),
|
"profile": MessageLookupByLibrary.simpleMessage("Профиль"),
|
||||||
|
"promo_code": MessageLookupByLibrary.simpleMessage("Promo code"),
|
||||||
|
"promo_invalid": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Promo code not found or expired",
|
||||||
|
),
|
||||||
"prostitution": MessageLookupByLibrary.simpleMessage("Проституция"),
|
"prostitution": MessageLookupByLibrary.simpleMessage("Проституция"),
|
||||||
"proteins": MessageLookupByLibrary.simpleMessage("Белки"),
|
"proteins": MessageLookupByLibrary.simpleMessage("Белки"),
|
||||||
"public_diet_state": MessageLookupByLibrary.simpleMessage("Публичная"),
|
"public_diet_state": MessageLookupByLibrary.simpleMessage("Публичная"),
|
||||||
@@ -737,6 +757,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"reply": MessageLookupByLibrary.simpleMessage("Ответить"),
|
"reply": MessageLookupByLibrary.simpleMessage("Ответить"),
|
||||||
"resend": MessageLookupByLibrary.simpleMessage("Отправить повторно"),
|
"resend": MessageLookupByLibrary.simpleMessage("Отправить повторно"),
|
||||||
"save": MessageLookupByLibrary.simpleMessage("Сохранить"),
|
"save": MessageLookupByLibrary.simpleMessage("Сохранить"),
|
||||||
|
"sbp_payment_error": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Не удалось начать оплату. Попробуйте позже",
|
||||||
|
),
|
||||||
"send": MessageLookupByLibrary.simpleMessage("Отправить"),
|
"send": MessageLookupByLibrary.simpleMessage("Отправить"),
|
||||||
"send_complain": MessageLookupByLibrary.simpleMessage("Отправить жалобу"),
|
"send_complain": MessageLookupByLibrary.simpleMessage("Отправить жалобу"),
|
||||||
"services": MessageLookupByLibrary.simpleMessage("Оказание услуг"),
|
"services": MessageLookupByLibrary.simpleMessage("Оказание услуг"),
|
||||||
@@ -944,6 +967,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"wrong_credentials": MessageLookupByLibrary.simpleMessage(
|
"wrong_credentials": MessageLookupByLibrary.simpleMessage(
|
||||||
"Неверные данные пользователя",
|
"Неверные данные пользователя",
|
||||||
),
|
),
|
||||||
|
"yes": MessageLookupByLibrary.simpleMessage("Yes"),
|
||||||
"youCanComplain": MessageLookupByLibrary.simpleMessage(
|
"youCanComplain": MessageLookupByLibrary.simpleMessage(
|
||||||
"Если вас что-то не устроило, вы можете подать жалобу",
|
"Если вас что-то не устроило, вы можете подать жалобу",
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
),
|
),
|
||||||
"another_form_of_day": MessageLookupByLibrary.simpleMessage("дня"),
|
"another_form_of_day": MessageLookupByLibrary.simpleMessage("дня"),
|
||||||
"answer": MessageLookupByLibrary.simpleMessage("Ответить"),
|
"answer": MessageLookupByLibrary.simpleMessage("Ответить"),
|
||||||
|
"apply": MessageLookupByLibrary.simpleMessage("Применить"),
|
||||||
"archived": MessageLookupByLibrary.simpleMessage("В архиве"),
|
"archived": MessageLookupByLibrary.simpleMessage("В архиве"),
|
||||||
"authentificate": MessageLookupByLibrary.simpleMessage("Войти"),
|
"authentificate": MessageLookupByLibrary.simpleMessage("Войти"),
|
||||||
"authorization": MessageLookupByLibrary.simpleMessage("Авторизация"),
|
"authorization": MessageLookupByLibrary.simpleMessage("Авторизация"),
|
||||||
@@ -62,6 +63,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"chat": MessageLookupByLibrary.simpleMessage("Чат"),
|
"chat": MessageLookupByLibrary.simpleMessage("Чат"),
|
||||||
"chest": MessageLookupByLibrary.simpleMessage("Грудь"),
|
"chest": MessageLookupByLibrary.simpleMessage("Грудь"),
|
||||||
"chest_circumference": MessageLookupByLibrary.simpleMessage("Обхват груди"),
|
"chest_circumference": MessageLookupByLibrary.simpleMessage("Обхват груди"),
|
||||||
|
"choose_bank_for_payment": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Выберите банк",
|
||||||
|
),
|
||||||
"choose_diet_duration": MessageLookupByLibrary.simpleMessage(
|
"choose_diet_duration": MessageLookupByLibrary.simpleMessage(
|
||||||
"Выберите срок диеты",
|
"Выберите срок диеты",
|
||||||
),
|
),
|
||||||
@@ -151,6 +155,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"enter_information": MessageLookupByLibrary.simpleMessage(
|
"enter_information": MessageLookupByLibrary.simpleMessage(
|
||||||
"Введите информацию...",
|
"Введите информацию...",
|
||||||
),
|
),
|
||||||
|
"enter_promo_code": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Введите промокод",
|
||||||
|
),
|
||||||
"exit": MessageLookupByLibrary.simpleMessage("Выйти"),
|
"exit": MessageLookupByLibrary.simpleMessage("Выйти"),
|
||||||
"expand": MessageLookupByLibrary.simpleMessage("Показать полностью"),
|
"expand": MessageLookupByLibrary.simpleMessage("Показать полностью"),
|
||||||
"fats": MessageLookupByLibrary.simpleMessage("Жиры"),
|
"fats": MessageLookupByLibrary.simpleMessage("Жиры"),
|
||||||
@@ -181,6 +188,12 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"gallery": MessageLookupByLibrary.simpleMessage("Галерея"),
|
"gallery": MessageLookupByLibrary.simpleMessage("Галерея"),
|
||||||
"gr": MessageLookupByLibrary.simpleMessage("гр"),
|
"gr": MessageLookupByLibrary.simpleMessage("гр"),
|
||||||
"grams": MessageLookupByLibrary.simpleMessage("гр"),
|
"grams": MessageLookupByLibrary.simpleMessage("гр"),
|
||||||
|
"have_promo_code": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"У вас есть промокод?",
|
||||||
|
),
|
||||||
|
"have_promo_code_form": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Есть промокод?",
|
||||||
|
),
|
||||||
"height_measurements": MessageLookupByLibrary.simpleMessage("Замер роста"),
|
"height_measurements": MessageLookupByLibrary.simpleMessage("Замер роста"),
|
||||||
"hide": MessageLookupByLibrary.simpleMessage("Свернуть"),
|
"hide": MessageLookupByLibrary.simpleMessage("Свернуть"),
|
||||||
"hip_circumference": MessageLookupByLibrary.simpleMessage("Обхват бедер"),
|
"hip_circumference": MessageLookupByLibrary.simpleMessage("Обхват бедер"),
|
||||||
@@ -243,6 +256,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
),
|
),
|
||||||
"no": MessageLookupByLibrary.simpleMessage("Нет"),
|
"no": MessageLookupByLibrary.simpleMessage("Нет"),
|
||||||
"no_account": MessageLookupByLibrary.simpleMessage("У меня нет аккаунта"),
|
"no_account": MessageLookupByLibrary.simpleMessage("У меня нет аккаунта"),
|
||||||
|
"no_installed_banks": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Нет установленных банков",
|
||||||
|
),
|
||||||
"notifications": MessageLookupByLibrary.simpleMessage("Уведомления"),
|
"notifications": MessageLookupByLibrary.simpleMessage("Уведомления"),
|
||||||
"one_option": MessageLookupByLibrary.simpleMessage("1 опция"),
|
"one_option": MessageLookupByLibrary.simpleMessage("1 опция"),
|
||||||
"option": MessageLookupByLibrary.simpleMessage("Опция"),
|
"option": MessageLookupByLibrary.simpleMessage("Опция"),
|
||||||
@@ -306,6 +322,10 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"Разница в ценах, это комиссия Итвио",
|
"Разница в ценах, это комиссия Итвио",
|
||||||
),
|
),
|
||||||
"profile": MessageLookupByLibrary.simpleMessage("Профиль"),
|
"profile": MessageLookupByLibrary.simpleMessage("Профиль"),
|
||||||
|
"promo_code": MessageLookupByLibrary.simpleMessage("Промокод"),
|
||||||
|
"promo_invalid": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Промокод не найден или истёк",
|
||||||
|
),
|
||||||
"proteins": MessageLookupByLibrary.simpleMessage("Белки"),
|
"proteins": MessageLookupByLibrary.simpleMessage("Белки"),
|
||||||
"public_diet_state": MessageLookupByLibrary.simpleMessage("Публичная"),
|
"public_diet_state": MessageLookupByLibrary.simpleMessage("Публичная"),
|
||||||
"publish": MessageLookupByLibrary.simpleMessage("Опубликовать"),
|
"publish": MessageLookupByLibrary.simpleMessage("Опубликовать"),
|
||||||
@@ -314,6 +334,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"Снять с публикации",
|
"Снять с публикации",
|
||||||
),
|
),
|
||||||
"save": MessageLookupByLibrary.simpleMessage("Сохранить"),
|
"save": MessageLookupByLibrary.simpleMessage("Сохранить"),
|
||||||
|
"sbp_payment_error": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Не удалось начать оплату. Попробуйте позже",
|
||||||
|
),
|
||||||
"services": MessageLookupByLibrary.simpleMessage("Оказание услуг"),
|
"services": MessageLookupByLibrary.simpleMessage("Оказание услуг"),
|
||||||
"settings": MessageLookupByLibrary.simpleMessage("Настройки"),
|
"settings": MessageLookupByLibrary.simpleMessage("Настройки"),
|
||||||
"share": MessageLookupByLibrary.simpleMessage("Поделиться"),
|
"share": MessageLookupByLibrary.simpleMessage("Поделиться"),
|
||||||
@@ -404,6 +427,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"wrong_credentials": MessageLookupByLibrary.simpleMessage(
|
"wrong_credentials": MessageLookupByLibrary.simpleMessage(
|
||||||
"Неверные данные пользователя",
|
"Неверные данные пользователя",
|
||||||
),
|
),
|
||||||
|
"yes": MessageLookupByLibrary.simpleMessage("Да"),
|
||||||
"you_will_get": MessageLookupByLibrary.simpleMessage("Вы получите"),
|
"you_will_get": MessageLookupByLibrary.simpleMessage("Вы получите"),
|
||||||
"your_blood_pressure": MessageLookupByLibrary.simpleMessage(
|
"your_blood_pressure": MessageLookupByLibrary.simpleMessage(
|
||||||
"Ваше артериальное давление?",
|
"Ваше артериальное давление?",
|
||||||
|
|||||||
@@ -2649,6 +2649,36 @@ class S {
|
|||||||
return Intl.message('Оплатить', name: 'pay', desc: '', args: []);
|
return Intl.message('Оплатить', name: 'pay', desc: '', args: []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `Выберите банк`
|
||||||
|
String get choose_bank_for_payment {
|
||||||
|
return Intl.message(
|
||||||
|
'Выберите банк',
|
||||||
|
name: 'choose_bank_for_payment',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Нет установленных банков`
|
||||||
|
String get no_installed_banks {
|
||||||
|
return Intl.message(
|
||||||
|
'Нет установленных банков',
|
||||||
|
name: 'no_installed_banks',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Не удалось начать оплату. Попробуйте позже`
|
||||||
|
String get sbp_payment_error {
|
||||||
|
return Intl.message(
|
||||||
|
'Не удалось начать оплату. Попробуйте позже',
|
||||||
|
name: 'sbp_payment_error',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// `Подробнее о заказе`
|
/// `Подробнее о заказе`
|
||||||
String get more_about_order {
|
String get more_about_order {
|
||||||
return Intl.message(
|
return Intl.message(
|
||||||
@@ -4148,6 +4178,61 @@ class S {
|
|||||||
args: [],
|
args: [],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `Yes`
|
||||||
|
String get yes {
|
||||||
|
return Intl.message('Yes', name: 'yes', desc: '', args: []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Apply`
|
||||||
|
String get apply {
|
||||||
|
return Intl.message('Apply', name: 'apply', desc: '', args: []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Do you have a promo code?`
|
||||||
|
String get have_promo_code {
|
||||||
|
return Intl.message(
|
||||||
|
'Do you have a promo code?',
|
||||||
|
name: 'have_promo_code',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Have a promo code?`
|
||||||
|
String get have_promo_code_form {
|
||||||
|
return Intl.message(
|
||||||
|
'Have a promo code?',
|
||||||
|
name: 'have_promo_code_form',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Promo code`
|
||||||
|
String get promo_code {
|
||||||
|
return Intl.message('Promo code', name: 'promo_code', desc: '', args: []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Enter promo code`
|
||||||
|
String get enter_promo_code {
|
||||||
|
return Intl.message(
|
||||||
|
'Enter promo code',
|
||||||
|
name: 'enter_promo_code',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Promo code not found or expired`
|
||||||
|
String get promo_invalid {
|
||||||
|
return Intl.message(
|
||||||
|
'Promo code not found or expired',
|
||||||
|
name: 'promo_invalid',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AppLocalizationDelegate extends LocalizationsDelegate<S> {
|
class AppLocalizationDelegate extends LocalizationsDelegate<S> {
|
||||||
|
|||||||
@@ -326,6 +326,9 @@
|
|||||||
"date_has_ben_set": "Дата старта начала консультации установлена",
|
"date_has_ben_set": "Дата старта начала консультации установлена",
|
||||||
"tap_on_link_to_pay": "Перейдите по ссылке для оплаты консультации",
|
"tap_on_link_to_pay": "Перейдите по ссылке для оплаты консультации",
|
||||||
"pay": "Оплатить",
|
"pay": "Оплатить",
|
||||||
|
"choose_bank_for_payment": "Выберите банк",
|
||||||
|
"no_installed_banks": "Нет установленных банков",
|
||||||
|
"sbp_payment_error": "Не удалось начать оплату. Попробуйте позже",
|
||||||
"more_about_order": "Подробнее о заказе",
|
"more_about_order": "Подробнее о заказе",
|
||||||
"cancel_order": "Отменить заказ",
|
"cancel_order": "Отменить заказ",
|
||||||
"refuse_order":"Отказаться от заказа",
|
"refuse_order":"Отказаться от заказа",
|
||||||
@@ -488,6 +491,13 @@
|
|||||||
"video_call_permission_camera_title": "Camera access",
|
"video_call_permission_camera_title": "Camera access",
|
||||||
"video_call_permission_camera_description": "Allow camera access so others can see you during the video call. If you declined, you can enable it in system settings.",
|
"video_call_permission_camera_description": "Allow camera access so others can see you during the video call. If you declined, you can enable it in system settings.",
|
||||||
"video_call_permission_microphone_title": "Microphone access",
|
"video_call_permission_microphone_title": "Microphone access",
|
||||||
"video_call_permission_microphone_description": "Allow microphone access so others can hear you during the video call. If you declined, you can enable it in system settings."
|
"video_call_permission_microphone_description": "Allow microphone access so others can hear you during the video call. If you declined, you can enable it in system settings.",
|
||||||
|
"yes": "Yes",
|
||||||
|
"apply": "Apply",
|
||||||
|
"have_promo_code": "Do you have a promo code?",
|
||||||
|
"have_promo_code_form": "Have a promo code?",
|
||||||
|
"promo_code": "Promo code",
|
||||||
|
"enter_promo_code": "Enter promo code",
|
||||||
|
"promo_invalid": "Promo code not found or expired"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -233,5 +233,15 @@
|
|||||||
"video_call_permission_camera_title": "Доступ к камере",
|
"video_call_permission_camera_title": "Доступ к камере",
|
||||||
"video_call_permission_camera_description": "Чтобы вас видели во время видеозвонка, разрешите доступ к камере. При отказе можно включить доступ в настройках устройства.",
|
"video_call_permission_camera_description": "Чтобы вас видели во время видеозвонка, разрешите доступ к камере. При отказе можно включить доступ в настройках устройства.",
|
||||||
"video_call_permission_microphone_title": "Доступ к микрофону",
|
"video_call_permission_microphone_title": "Доступ к микрофону",
|
||||||
"video_call_permission_microphone_description": "Чтобы вас слышали во время видеозвонка, разрешите доступ к микрофону. При отказе можно включить доступ в настройках устройства."
|
"video_call_permission_microphone_description": "Чтобы вас слышали во время видеозвонка, разрешите доступ к микрофону. При отказе можно включить доступ в настройках устройства.",
|
||||||
|
"choose_bank_for_payment": "Выберите банк",
|
||||||
|
"no_installed_banks": "Нет установленных банков",
|
||||||
|
"sbp_payment_error": "Не удалось начать оплату. Попробуйте позже",
|
||||||
|
"yes": "Да",
|
||||||
|
"apply": "Применить",
|
||||||
|
"have_promo_code": "У вас есть промокод?",
|
||||||
|
"have_promo_code_form": "Есть промокод?",
|
||||||
|
"promo_code": "Промокод",
|
||||||
|
"enter_promo_code": "Введите промокод",
|
||||||
|
"promo_invalid": "Промокод не найден или истёк"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
name: eatvio_app
|
name: eatvio_app
|
||||||
description: A new Flutter project.
|
description: A new Flutter project.
|
||||||
version: 1.3.23+105
|
version: 1.3.24+106
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
@@ -85,6 +85,7 @@ dependencies:
|
|||||||
intl_utils: ^2.8.13
|
intl_utils: ^2.8.13
|
||||||
sentry_dio: ^9.10.0
|
sentry_dio: ^9.10.0
|
||||||
livekit_client: ^2.6.1
|
livekit_client: ^2.6.1
|
||||||
|
sbp: 0.1.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
build_runner: ^2.10.4
|
build_runner: ^2.10.4
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:eatvio_app/core/app_router.dart';
|
import 'package:eatvio_app/core/app_router.dart';
|
||||||
|
import 'package:eatvio_app/core/routing/route_constants.dart';
|
||||||
import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart';
|
import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
@@ -6,6 +7,50 @@ import 'package:go_router/go_router.dart';
|
|||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('AppRouter', () {
|
group('AppRouter', () {
|
||||||
|
group('resolveDeepLinkTarget - finance/alfa/return', () {
|
||||||
|
const orderId = 'alfa_c29_k15138_t1712345678_r1234';
|
||||||
|
|
||||||
|
test('finance/alfa/return/{orderId} → chat-screen?orderId=', () {
|
||||||
|
final target = AppRouter.resolveDeepLinkTarget(
|
||||||
|
Uri.parse('https://eatvio.ru/finance/alfa/return/$orderId'),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(target, '${RouteConstants.chatScreen}?orderId=$orderId');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ru/finance/alfa/return/{orderId} → chat-screen?orderId= (metaSegments)', () {
|
||||||
|
final target = AppRouter.resolveDeepLinkTarget(
|
||||||
|
Uri.parse('https://eatvio.ru/ru/finance/alfa/return/$orderId'),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(target, '${RouteConstants.chatScreen}?orderId=$orderId');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('неполный path /finance/alfa → feed', () {
|
||||||
|
final target = AppRouter.resolveDeepLinkTarget(
|
||||||
|
Uri.parse('https://eatvio.ru/finance/alfa'),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(target, RouteConstants.feedScreen);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('неполный path /finance → feed', () {
|
||||||
|
final target = AppRouter.resolveDeepLinkTarget(
|
||||||
|
Uri.parse('https://eatvio.ru/finance'),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(target, RouteConstants.feedScreen);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('регресс: chat/{uuid} → chat-screen?uuid=', () {
|
||||||
|
final target = AppRouter.resolveDeepLinkTarget(
|
||||||
|
Uri.parse('https://eatvio.ru/ru/chat/uuid-1'),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(target, '${RouteConstants.chatScreen}?uuid=uuid-1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('navigatorKey инициализирован', () {
|
test('navigatorKey инициализирован', () {
|
||||||
expect(AppRouter.navigatorKey, isNotNull);
|
expect(AppRouter.navigatorKey, isNotNull);
|
||||||
expect(AppRouter.navigatorKey, isA<GlobalKey<NavigatorState>>());
|
expect(AppRouter.navigatorKey, isA<GlobalKey<NavigatorState>>());
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import 'package:eatvio_app/features/chat/models/order_id_to_conversation_model.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import '../../../fixtures/fixture_reader.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('OrderIdToConversationModel', () {
|
||||||
|
test('fromJson создаёт модель из JSON фикстуры', () {
|
||||||
|
final json = FixtureReader.readAsMap(
|
||||||
|
'chat/payments/order_id_to_chat_uuid_response.json',
|
||||||
|
);
|
||||||
|
|
||||||
|
final model = OrderIdToConversationModel.fromJson(json);
|
||||||
|
|
||||||
|
expect(model.chosenPlanId, 12);
|
||||||
|
expect(
|
||||||
|
model.conversationUuid,
|
||||||
|
'fbd81429-8019-499a-80e8-e3933cc4d872',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,25 +1,47 @@
|
|||||||
import 'package:eatvio_app/features/chat/models/payment_model.dart';
|
import 'package:eatvio_app/features/chat/models/payment_model.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import '../../../fixtures/fixture_reader.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('PaymentModel', () {
|
group('PaymentModel', () {
|
||||||
test('создает PaymentModel с правильными значениями', () {
|
test('создает PaymentModel с правильными значениями', () {
|
||||||
final model = PaymentModel(
|
final model = PaymentModel(paymentUrl: 'https://example.com/payment');
|
||||||
paymentUrl: 'https://example.com/payment',
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(model.paymentUrl, 'https://example.com/payment');
|
expect(model.paymentUrl, 'https://example.com/payment');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('fromJson создает PaymentModel из JSON', () {
|
test('fromJson создает PaymentModel из JSON', () {
|
||||||
final json = {
|
final json = {'checkout_url': 'https://example.com/payment'};
|
||||||
'checkout_url': 'https://example.com/payment',
|
|
||||||
};
|
|
||||||
|
|
||||||
final model = PaymentModel.fromJson(json);
|
final model = PaymentModel.fromJson(json);
|
||||||
|
|
||||||
expect(model.paymentUrl, 'https://example.com/payment');
|
expect(model.paymentUrl, 'https://example.com/payment');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'fromJson парсит ответ alfa-bank (checkout_url, order_id, payment_provider)',
|
||||||
|
() {
|
||||||
|
final json = FixtureReader.readAsMap(
|
||||||
|
'chat/payments/payment_response_alfa_bank.json',
|
||||||
|
);
|
||||||
|
|
||||||
|
final model = PaymentModel.fromJson(json);
|
||||||
|
|
||||||
|
expect(model.paymentUrl, 'https://qr.nspk.ru/ASDFASDFASDFASDF');
|
||||||
|
expect(model.orderId, 'alfa_c29_k15138_t1712345678_r1234');
|
||||||
|
expect(model.paymentProvider, 'alfa-bank');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('fromJson парсит mock-ответ (checkout_url без order_id)', () {
|
||||||
|
final json = FixtureReader.readAsMap('chat/payments/payment_response.json');
|
||||||
|
|
||||||
|
final model = PaymentModel.fromJson(json);
|
||||||
|
|
||||||
|
expect(model.paymentUrl, 'https://example.com/checkout/payment-123');
|
||||||
|
expect(model.orderId, isNull);
|
||||||
|
expect(model.paymentProvider, isNull);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_
|
|||||||
import 'package:eatvio_app/features/chat/repositories/chat_repository.dart';
|
import 'package:eatvio_app/features/chat/repositories/chat_repository.dart';
|
||||||
import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart';
|
import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart';
|
||||||
import 'package:eatvio_app/features/my_finance/models/card_model.dart';
|
import 'package:eatvio_app/features/my_finance/models/card_model.dart';
|
||||||
|
import 'package:eatvio_app/features/chat/models/order_id_to_conversation_model.dart';
|
||||||
import 'package:eatvio_app/features/chat/models/payment_model.dart';
|
import 'package:eatvio_app/features/chat/models/payment_model.dart';
|
||||||
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||||
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
|
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
|
||||||
@@ -102,6 +103,89 @@ void main() {
|
|||||||
// Let's check that at least the repository methods were called
|
// Let's check that at least the repository methods were called
|
||||||
verify(mockChatRepository.getChatRoom(uuid)).called(1);
|
verify(mockChatRepository.getChatRoom(uuid)).called(1);
|
||||||
verify(mockChatRepository.getChatMessages(uuid, 1)).called(1);
|
verify(mockChatRepository.getChatMessages(uuid, 1)).called(1);
|
||||||
|
verifyNever(
|
||||||
|
mockChatRepository.resolveOrderIdToConversation(orderId: anyNamed('orderId')),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('initChat - с orderId резолвит conversation и инициализирует чат', () async {
|
||||||
|
const orderId = 'alfa_c29_k15138_t1712345678_r1234';
|
||||||
|
const conversationUuid = 'fbd81429-8019-499a-80e8-e3933cc4d872';
|
||||||
|
const chosenPlanId = 12;
|
||||||
|
final resolved = OrderIdToConversationModel(
|
||||||
|
chosenPlanId: chosenPlanId,
|
||||||
|
conversationUuid: conversationUuid,
|
||||||
|
);
|
||||||
|
final chatRoomWithPlan = testChatRoom.copyWith(chosenPlanId: chosenPlanId);
|
||||||
|
const testPlan = PlanModel(
|
||||||
|
id: 1,
|
||||||
|
slug: 'test-plan',
|
||||||
|
name: 'Test Plan',
|
||||||
|
image: 'https://example.com/image.jpg',
|
||||||
|
duration: 7,
|
||||||
|
state: 'active',
|
||||||
|
type: PlanType(fit: true, slim: false, behavior: false, wellness: false),
|
||||||
|
details: 'Test details',
|
||||||
|
packetsCount: 3,
|
||||||
|
author: Author(
|
||||||
|
authorSlug: 'author',
|
||||||
|
authorName: 'Author Name',
|
||||||
|
authorProfileIcon: 'https://example.com/icon.jpg',
|
||||||
|
authorIsMe: false,
|
||||||
|
),
|
||||||
|
totalLikes: 10,
|
||||||
|
totalViews: 100,
|
||||||
|
totalComments: 5,
|
||||||
|
isFavorite: false,
|
||||||
|
minPriceOfOptions: 1000,
|
||||||
|
maxPriceOfOptions: 2000,
|
||||||
|
wasLikedByMe: false,
|
||||||
|
);
|
||||||
|
final chosenPlan = ChosenPlanModel(
|
||||||
|
id: chosenPlanId,
|
||||||
|
optionType: 'options1',
|
||||||
|
startDate: DateTime.now(),
|
||||||
|
chosenPlanState: ChosenPlanStateEnum.statePendingPayment,
|
||||||
|
chatId: 'chat-1',
|
||||||
|
consultantId: 2,
|
||||||
|
plan: testPlan,
|
||||||
|
);
|
||||||
|
|
||||||
|
when(
|
||||||
|
mockChatRepository.resolveOrderIdToConversation(orderId: orderId),
|
||||||
|
).thenAnswer((_) async => resolved);
|
||||||
|
when(mockChatRepository.getChatRoom(conversationUuid)).thenAnswer((_) async => chatRoomWithPlan);
|
||||||
|
when(mockChatRepository.getChatMessages(conversationUuid, 1)).thenAnswer((_) async => testChatMessages);
|
||||||
|
when(mockChatRepository.getChosenPlan(id: chosenPlanId)).thenAnswer((_) async => chosenPlan);
|
||||||
|
when(mockWsManager.client).thenReturn(null);
|
||||||
|
when(mockWsManager.initPrivateDirectChatChannel(any)).thenAnswer((_) async => Future.value());
|
||||||
|
when(mockWsManager.subscribeDirectChatChannel()).thenReturn(null);
|
||||||
|
when(mockWsManager.directChatStream).thenReturn(null);
|
||||||
|
when(mockWsManager.clientTypingStream).thenReturn(null);
|
||||||
|
|
||||||
|
await chatRoomCubit.initChat(null, orderId: orderId);
|
||||||
|
await Future.delayed(const Duration(milliseconds: 200));
|
||||||
|
|
||||||
|
verify(mockChatRepository.resolveOrderIdToConversation(orderId: orderId)).called(1);
|
||||||
|
verify(mockChatRepository.getChatRoom(conversationUuid)).called(1);
|
||||||
|
verify(mockChatRepository.getChosenPlan(id: chosenPlanId)).called(1);
|
||||||
|
verify(mockChatRepository.getChatMessages(conversationUuid, 1)).called(1);
|
||||||
|
expect(chatRoomCubit.state, isA<ChatRoomStateLoaded>());
|
||||||
|
final loaded = chatRoomCubit.state as ChatRoomStateLoaded;
|
||||||
|
expect(loaded.chatRoom.chosenPlan?.id, chosenPlanId);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('initChat - при ошибке resolve не вызывает getChatRoom', () async {
|
||||||
|
const orderId = 'alfa_c29_k15138_t1712345678_r1234';
|
||||||
|
when(
|
||||||
|
mockChatRepository.resolveOrderIdToConversation(orderId: orderId),
|
||||||
|
).thenThrow(Exception('Resolve failed'));
|
||||||
|
|
||||||
|
await chatRoomCubit.initChat(null, orderId: orderId);
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
expect(chatRoomCubit.state, isA<ChatRoomStateError>());
|
||||||
|
verifyNever(mockChatRepository.getChatRoom(any));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('initChat - обрабатывает ошибку', () async {
|
test('initChat - обрабатывает ошибку', () async {
|
||||||
@@ -678,7 +762,7 @@ void main() {
|
|||||||
when(mockChatRepository.getChatRoom(uuid)).thenAnswer((_) async => chatRoomWithPlan);
|
when(mockChatRepository.getChatRoom(uuid)).thenAnswer((_) async => chatRoomWithPlan);
|
||||||
when(mockChatRepository.getChatMessages(uuid, 1)).thenAnswer((_) async => testChatMessages);
|
when(mockChatRepository.getChatMessages(uuid, 1)).thenAnswer((_) async => testChatMessages);
|
||||||
when(mockChatRepository.getChosenPlan(id: planId)).thenAnswer((_) async => chosenPlan);
|
when(mockChatRepository.getChosenPlan(id: planId)).thenAnswer((_) async => chosenPlan);
|
||||||
when(mockChatRepository.getPaymentLink(id: planId)).thenAnswer((_) async => PaymentModel(paymentUrl: paymentUrl));
|
when(mockChatRepository.getPaymentLink(id: planId, paymentProvider: 'alfa-bank')).thenAnswer((_) async => PaymentModel(paymentUrl: paymentUrl));
|
||||||
when(mockWsManager.client).thenReturn(null);
|
when(mockWsManager.client).thenReturn(null);
|
||||||
when(mockWsManager.initPrivateDirectChatChannel(any)).thenAnswer((_) async => Future.value());
|
when(mockWsManager.initPrivateDirectChatChannel(any)).thenAnswer((_) async => Future.value());
|
||||||
when(mockWsManager.subscribeDirectChatChannel()).thenReturn(null);
|
when(mockWsManager.subscribeDirectChatChannel()).thenReturn(null);
|
||||||
@@ -691,7 +775,7 @@ void main() {
|
|||||||
final result = await chatRoomCubit.getPaymentLink();
|
final result = await chatRoomCubit.getPaymentLink();
|
||||||
|
|
||||||
expect(result, paymentUrl);
|
expect(result, paymentUrl);
|
||||||
verify(mockChatRepository.getPaymentLink(id: planId)).called(1);
|
verify(mockChatRepository.getPaymentLink(id: planId, paymentProvider: 'alfa-bank')).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getPaymentLink - обрабатывает ошибку', () async {
|
test('getPaymentLink - обрабатывает ошибку', () async {
|
||||||
@@ -730,7 +814,7 @@ void main() {
|
|||||||
when(mockChatRepository.getChatRoom(uuid)).thenAnswer((_) async => chatRoomWithPlan);
|
when(mockChatRepository.getChatRoom(uuid)).thenAnswer((_) async => chatRoomWithPlan);
|
||||||
when(mockChatRepository.getChatMessages(uuid, 1)).thenAnswer((_) async => testChatMessages);
|
when(mockChatRepository.getChatMessages(uuid, 1)).thenAnswer((_) async => testChatMessages);
|
||||||
when(mockChatRepository.getChosenPlan(id: planId)).thenAnswer((_) async => chosenPlan);
|
when(mockChatRepository.getChosenPlan(id: planId)).thenAnswer((_) async => chosenPlan);
|
||||||
when(mockChatRepository.getPaymentLink(id: planId)).thenThrow(Exception('Payment error'));
|
when(mockChatRepository.getPaymentLink(id: planId, paymentProvider: 'alfa-bank')).thenThrow(Exception('Payment error'));
|
||||||
when(mockWsManager.client).thenReturn(null);
|
when(mockWsManager.client).thenReturn(null);
|
||||||
when(mockWsManager.initPrivateDirectChatChannel(any)).thenAnswer((_) async => Future.value());
|
when(mockWsManager.initPrivateDirectChatChannel(any)).thenAnswer((_) async => Future.value());
|
||||||
when(mockWsManager.subscribeDirectChatChannel()).thenReturn(null);
|
when(mockWsManager.subscribeDirectChatChannel()).thenReturn(null);
|
||||||
|
|||||||
@@ -147,7 +147,8 @@ void main() {
|
|||||||
when(chatRoomCubit.state).thenReturn(state);
|
when(chatRoomCubit.state).thenReturn(state);
|
||||||
when(chatRoomCubit.stream).thenAnswer((_) => const Stream<ChatRoomState>.empty());
|
when(chatRoomCubit.stream).thenAnswer((_) => const Stream<ChatRoomState>.empty());
|
||||||
when(chatRoomCubit.close()).thenAnswer((_) async => Future<void>.value());
|
when(chatRoomCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||||
when(chatRoomCubit.initChat(any, sendInitialMessage: anyNamed('sendInitialMessage'))).thenAnswer((_) async => Future<void>.value());
|
when(chatRoomCubit.initChat(any, sendInitialMessage: anyNamed('sendInitialMessage'), orderId: anyNamed('orderId')))
|
||||||
|
.thenAnswer((_) async => 'uuid');
|
||||||
when(chatRoomCubit.setTimer()).thenReturn(null);
|
when(chatRoomCubit.setTimer()).thenReturn(null);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ void main() {
|
|||||||
when(cubit.state).thenReturn(const ChatRoomState.loading());
|
when(cubit.state).thenReturn(const ChatRoomState.loading());
|
||||||
when(cubit.stream).thenAnswer((_) => const Stream<ChatRoomState>.empty());
|
when(cubit.stream).thenAnswer((_) => const Stream<ChatRoomState>.empty());
|
||||||
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
|
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||||
when(cubit.initChat(any, sendInitialMessage: anyNamed('sendInitialMessage'))).thenAnswer((_) async => Future<void>.value());
|
when(cubit.initChat(any, sendInitialMessage: anyNamed('sendInitialMessage'), orderId: anyNamed('orderId')))
|
||||||
|
.thenAnswer((_) async => 'uuid');
|
||||||
when(cubit.setTimer()).thenReturn(null);
|
when(cubit.setTimer()).thenReturn(null);
|
||||||
sl.registerFactory<ChatRoomCubit>(() => cubit);
|
sl.registerFactory<ChatRoomCubit>(() => cubit);
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,15 @@ import 'package:dio/dio.dart';
|
|||||||
import 'package:eatvio_app/core/models/pagination_model.dart';
|
import 'package:eatvio_app/core/models/pagination_model.dart';
|
||||||
import 'package:eatvio_app/features/chat/models/chat_message_model.dart';
|
import 'package:eatvio_app/features/chat/models/chat_message_model.dart';
|
||||||
import 'package:eatvio_app/features/chat/models/chat_messages_model.dart';
|
import 'package:eatvio_app/features/chat/models/chat_messages_model.dart';
|
||||||
import 'package:eatvio_app/features/chat/models/chat_room_model.dart' as chat_room;
|
import 'package:eatvio_app/features/chat/models/chat_room_model.dart'
|
||||||
|
as chat_room;
|
||||||
import 'package:eatvio_app/features/chat/models/chat_room_pagination_model.dart';
|
import 'package:eatvio_app/features/chat/models/chat_room_pagination_model.dart';
|
||||||
|
import 'package:eatvio_app/features/chat/models/order_id_to_conversation_model.dart';
|
||||||
import 'package:eatvio_app/features/chat/models/payment_model.dart';
|
import 'package:eatvio_app/features/chat/models/payment_model.dart';
|
||||||
import 'package:eatvio_app/features/chat/models/plan_complain_model.dart';
|
import 'package:eatvio_app/features/chat/models/plan_complain_model.dart';
|
||||||
import 'package:eatvio_app/features/chat/repositories/chat_repository.dart';
|
import 'package:eatvio_app/features/chat/repositories/chat_repository.dart';
|
||||||
import 'package:eatvio_app/features/chat/services/chat_service.dart';
|
import 'package:eatvio_app/features/chat/services/chat_service.dart';
|
||||||
|
import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
|
||||||
import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart';
|
import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:mockito/annotations.dart';
|
import 'package:mockito/annotations.dart';
|
||||||
@@ -32,7 +35,9 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('emitNewMessageToChats - эмитит новое сообщение в поток', () async {
|
test('emitNewMessageToChats - эмитит новое сообщение в поток', () async {
|
||||||
final messageFixture = FixtureReader.readAsMap('chat/chat_message_response.json');
|
final messageFixture = FixtureReader.readAsMap(
|
||||||
|
'chat/chat_message_response.json',
|
||||||
|
);
|
||||||
final message = ChatMessageModel.fromJson(messageFixture);
|
final message = ChatMessageModel.fromJson(messageFixture);
|
||||||
|
|
||||||
chatRepository.emitNewMessageToChats(message);
|
chatRepository.emitNewMessageToChats(message);
|
||||||
@@ -45,12 +50,14 @@ void main() {
|
|||||||
const data = 'Test message';
|
const data = 'Test message';
|
||||||
const socketId = 'socket-id-123';
|
const socketId = 'socket-id-123';
|
||||||
|
|
||||||
when(mockChatService.sendMessage(
|
when(
|
||||||
slug: slug,
|
mockChatService.sendMessage(
|
||||||
data: data,
|
slug: slug,
|
||||||
mediaData: null,
|
data: data,
|
||||||
socketId: socketId,
|
mediaData: null,
|
||||||
)).thenAnswer((_) async => Future.value());
|
socketId: socketId,
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
// Note: SocketIdStorage is static and can't be easily mocked
|
// Note: SocketIdStorage is static and can't be easily mocked
|
||||||
// This test verifies the repository calls the service correctly
|
// This test verifies the repository calls the service correctly
|
||||||
@@ -68,12 +75,14 @@ void main() {
|
|||||||
const data = 'Test message with files';
|
const data = 'Test message with files';
|
||||||
const socketId = 'socket-id-123';
|
const socketId = 'socket-id-123';
|
||||||
|
|
||||||
when(mockChatService.sendMessage(
|
when(
|
||||||
slug: slug,
|
mockChatService.sendMessage(
|
||||||
data: data,
|
slug: slug,
|
||||||
mediaData: anyNamed('mediaData'),
|
data: data,
|
||||||
socketId: socketId,
|
mediaData: anyNamed('mediaData'),
|
||||||
)).thenAnswer((_) async => Future.value());
|
socketId: socketId,
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
// Создаем временные файлы для теста
|
// Создаем временные файлы для теста
|
||||||
final tempFile1 = File('test1.jpg');
|
final tempFile1 = File('test1.jpg');
|
||||||
@@ -113,11 +122,14 @@ void main() {
|
|||||||
|
|
||||||
test('getChatRoom - получает комнату чата по UUID', () async {
|
test('getChatRoom - получает комнату чата по UUID', () async {
|
||||||
const uuid = 'chat-1';
|
const uuid = 'chat-1';
|
||||||
final roomFixture = FixtureReader.readAsMap('chat/chat_room_response.json');
|
final roomFixture = FixtureReader.readAsMap(
|
||||||
|
'chat/chat_room_response.json',
|
||||||
|
);
|
||||||
final expectedRoom = chat_room.ChatRoomModel.fromJson(roomFixture);
|
final expectedRoom = chat_room.ChatRoomModel.fromJson(roomFixture);
|
||||||
|
|
||||||
when(mockChatService.getChatRoom(uuid: uuid))
|
when(
|
||||||
.thenAnswer((_) async => expectedRoom);
|
mockChatService.getChatRoom(uuid: uuid),
|
||||||
|
).thenAnswer((_) async => expectedRoom);
|
||||||
|
|
||||||
final result = await chatRepository.getChatRoom(uuid);
|
final result = await chatRepository.getChatRoom(uuid);
|
||||||
|
|
||||||
@@ -132,18 +144,20 @@ void main() {
|
|||||||
|
|
||||||
when(mockChatService.getChatRoom(uuid: uuid)).thenThrow(error);
|
when(mockChatService.getChatRoom(uuid: uuid)).thenThrow(error);
|
||||||
|
|
||||||
expect(
|
expect(() => chatRepository.getChatRoom(uuid), throwsException);
|
||||||
() => chatRepository.getChatRoom(uuid),
|
|
||||||
throwsException,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getChatRooms - получает список комнат чата', () async {
|
test('getChatRooms - получает список комнат чата', () async {
|
||||||
final paginationFixture = FixtureReader.readAsMap('chat/chat_room_pagination_response.json');
|
final paginationFixture = FixtureReader.readAsMap(
|
||||||
final expectedPagination = ChatRoomPaginationModel.fromJson(paginationFixture);
|
'chat/chat_room_pagination_response.json',
|
||||||
|
);
|
||||||
|
final expectedPagination = ChatRoomPaginationModel.fromJson(
|
||||||
|
paginationFixture,
|
||||||
|
);
|
||||||
|
|
||||||
when(mockChatService.getChatRooms(query: null, page: 1))
|
when(
|
||||||
.thenAnswer((_) async => expectedPagination);
|
mockChatService.getChatRooms(query: null, page: 1),
|
||||||
|
).thenAnswer((_) async => expectedPagination);
|
||||||
|
|
||||||
final result = await chatRepository.getChatRooms();
|
final result = await chatRepository.getChatRooms();
|
||||||
|
|
||||||
@@ -154,11 +168,16 @@ void main() {
|
|||||||
|
|
||||||
test('getChatRooms - получает список комнат с запросом', () async {
|
test('getChatRooms - получает список комнат с запросом', () async {
|
||||||
const query = 'test query';
|
const query = 'test query';
|
||||||
final paginationFixture = FixtureReader.readAsMap('chat/chat_room_pagination_response.json');
|
final paginationFixture = FixtureReader.readAsMap(
|
||||||
final expectedPagination = ChatRoomPaginationModel.fromJson(paginationFixture);
|
'chat/chat_room_pagination_response.json',
|
||||||
|
);
|
||||||
|
final expectedPagination = ChatRoomPaginationModel.fromJson(
|
||||||
|
paginationFixture,
|
||||||
|
);
|
||||||
|
|
||||||
when(mockChatService.getChatRooms(query: query, page: 1))
|
when(
|
||||||
.thenAnswer((_) async => expectedPagination);
|
mockChatService.getChatRooms(query: query, page: 1),
|
||||||
|
).thenAnswer((_) async => expectedPagination);
|
||||||
|
|
||||||
final result = await chatRepository.getChatRooms(query: query);
|
final result = await chatRepository.getChatRooms(query: query);
|
||||||
|
|
||||||
@@ -181,8 +200,9 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
when(mockChatService.getChatRooms(query: null, page: 2))
|
when(
|
||||||
.thenAnswer((_) async => expectedPagination);
|
mockChatService.getChatRooms(query: null, page: 2),
|
||||||
|
).thenAnswer((_) async => expectedPagination);
|
||||||
|
|
||||||
final result = await chatRepository.getChatRooms(page: 2);
|
final result = await chatRepository.getChatRooms(page: 2);
|
||||||
|
|
||||||
@@ -193,11 +213,14 @@ void main() {
|
|||||||
test('getChatMessages - получает сообщения чата', () async {
|
test('getChatMessages - получает сообщения чата', () async {
|
||||||
const chatId = 'chat-1';
|
const chatId = 'chat-1';
|
||||||
const page = 1;
|
const page = 1;
|
||||||
final messagesFixture = FixtureReader.readAsMap('chat/chat_messages_response.json');
|
final messagesFixture = FixtureReader.readAsMap(
|
||||||
|
'chat/chat_messages_response.json',
|
||||||
|
);
|
||||||
final expectedMessages = ChatMessagesModel.fromJson(messagesFixture);
|
final expectedMessages = ChatMessagesModel.fromJson(messagesFixture);
|
||||||
|
|
||||||
when(mockChatService.getChatMessages(id: chatId, page: page))
|
when(
|
||||||
.thenAnswer((_) async => expectedMessages);
|
mockChatService.getChatMessages(id: chatId, page: page),
|
||||||
|
).thenAnswer((_) async => expectedMessages);
|
||||||
|
|
||||||
final result = await chatRepository.getChatMessages(chatId, page);
|
final result = await chatRepository.getChatMessages(chatId, page);
|
||||||
|
|
||||||
@@ -206,42 +229,53 @@ void main() {
|
|||||||
verify(mockChatService.getChatMessages(id: chatId, page: page)).called(1);
|
verify(mockChatService.getChatMessages(id: chatId, page: page)).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getChatRoomIdByUserSlug - получает комнату по slug пользователя', () async {
|
test(
|
||||||
const slug = 'user-1';
|
'getChatRoomIdByUserSlug - получает комнату по slug пользователя',
|
||||||
final roomFixture = FixtureReader.readAsMap('chat/chat_room_response.json');
|
() async {
|
||||||
final expectedRoom = chat_room.ChatRoomModel.fromJson(roomFixture);
|
const slug = 'user-1';
|
||||||
|
final roomFixture = FixtureReader.readAsMap(
|
||||||
|
'chat/chat_room_response.json',
|
||||||
|
);
|
||||||
|
final expectedRoom = chat_room.ChatRoomModel.fromJson(roomFixture);
|
||||||
|
|
||||||
when(mockChatService.getChatRoomIdByUserSlug(user: slug))
|
when(
|
||||||
.thenAnswer((_) async => expectedRoom);
|
mockChatService.getChatRoomIdByUserSlug(user: slug),
|
||||||
|
).thenAnswer((_) async => expectedRoom);
|
||||||
|
|
||||||
final result = await chatRepository.getChatRoomIdByUserSlug(slug: slug);
|
final result = await chatRepository.getChatRoomIdByUserSlug(slug: slug);
|
||||||
|
|
||||||
expect(result, isA<chat_room.ChatRoomModel>());
|
expect(result, isA<chat_room.ChatRoomModel>());
|
||||||
verify(mockChatService.getChatRoomIdByUserSlug(user: slug)).called(1);
|
verify(mockChatService.getChatRoomIdByUserSlug(user: slug)).called(1);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('markMessagesAsRead - отмечает сообщения как прочитанные', () async {
|
test('markMessagesAsRead - отмечает сообщения как прочитанные', () async {
|
||||||
const chatId = 'chat-id-123';
|
const chatId = 'chat-id-123';
|
||||||
final messageIds = [1, 2, 3];
|
final messageIds = [1, 2, 3];
|
||||||
|
|
||||||
when(mockChatService.markMessagesAsRead(
|
when(
|
||||||
id: chatId,
|
mockChatService.markMessagesAsRead(
|
||||||
body: {'messages_ids': messageIds},
|
id: chatId,
|
||||||
)).thenAnswer((_) async => Future.value());
|
body: {'messages_ids': messageIds},
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
await chatRepository.markMessagesAsRead(chatId, messageIds);
|
await chatRepository.markMessagesAsRead(chatId, messageIds);
|
||||||
|
|
||||||
verify(mockChatService.markMessagesAsRead(
|
verify(
|
||||||
id: chatId,
|
mockChatService.markMessagesAsRead(
|
||||||
body: {'messages_ids': messageIds},
|
id: chatId,
|
||||||
)).called(1);
|
body: {'messages_ids': messageIds},
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('cancelChosenPlan - отменяет выбранный план', () async {
|
test('cancelChosenPlan - отменяет выбранный план', () async {
|
||||||
const planId = 123;
|
const planId = 123;
|
||||||
|
|
||||||
when(mockChatService.cancelChosenPlan(id: planId))
|
when(
|
||||||
.thenAnswer((_) async => Future.value());
|
mockChatService.cancelChosenPlan(id: planId),
|
||||||
|
).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
await chatRepository.cancelChosenPlan(id: planId);
|
await chatRepository.cancelChosenPlan(id: planId);
|
||||||
|
|
||||||
@@ -252,36 +286,114 @@ void main() {
|
|||||||
const planId = 123;
|
const planId = 123;
|
||||||
final date = {'start_date': '2024-01-15'};
|
final date = {'start_date': '2024-01-15'};
|
||||||
|
|
||||||
when(mockChatService.choseStartPlanDate(id: planId, date: date))
|
when(
|
||||||
.thenAnswer((_) async => Future.value());
|
mockChatService.choseStartPlanDate(id: planId, date: date),
|
||||||
|
).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
await chatRepository.choseStartPlanDate(id: planId, date: date);
|
await chatRepository.choseStartPlanDate(id: planId, date: date);
|
||||||
|
|
||||||
verify(mockChatService.choseStartPlanDate(id: planId, date: date)).called(1);
|
verify(
|
||||||
|
mockChatService.choseStartPlanDate(id: planId, date: date),
|
||||||
|
).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getPaymentLink - получает ссылку на оплату', () async {
|
test('getPaymentLink - получает ссылку на оплату', () async {
|
||||||
const planId = 123;
|
const planId = 123;
|
||||||
final paymentFixture = FixtureReader.readAsMap('chat/payment_response.json');
|
final paymentFixture = FixtureReader.readAsMap(
|
||||||
|
'chat/payments/payment_response.json',
|
||||||
|
);
|
||||||
final expectedPayment = PaymentModel.fromJson(paymentFixture);
|
final expectedPayment = PaymentModel.fromJson(paymentFixture);
|
||||||
|
|
||||||
when(mockChatService.getPaymentLink(id: planId))
|
when(
|
||||||
.thenAnswer((_) async => expectedPayment);
|
mockChatService.getPaymentLink(
|
||||||
|
id: planId,
|
||||||
|
body: {'payment_provider': 'mock'},
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => expectedPayment);
|
||||||
|
|
||||||
final result = await chatRepository.getPaymentLink(id: planId);
|
final result = await chatRepository.getPaymentLink(
|
||||||
|
id: planId,
|
||||||
|
paymentProvider: 'mock',
|
||||||
|
);
|
||||||
|
|
||||||
expect(result, isA<PaymentModel>());
|
expect(result, isA<PaymentModel>());
|
||||||
expect(result.paymentUrl, 'https://example.com/checkout/payment-123');
|
expect(result.paymentUrl, 'https://example.com/checkout/payment-123');
|
||||||
verify(mockChatService.getPaymentLink(id: planId)).called(1);
|
verify(
|
||||||
|
mockChatService.getPaymentLink(
|
||||||
|
id: planId,
|
||||||
|
body: {'payment_provider': 'mock'},
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'getPaymentLink - передаёт payment_provider в тело запроса alfa-bank',
|
||||||
|
() async {
|
||||||
|
const planId = 123;
|
||||||
|
final paymentFixture = FixtureReader.readAsMap(
|
||||||
|
'chat/payments/payment_response_alfa_bank.json',
|
||||||
|
);
|
||||||
|
final expectedPayment = PaymentModel.fromJson(paymentFixture);
|
||||||
|
|
||||||
|
when(
|
||||||
|
mockChatService.getPaymentLink(
|
||||||
|
id: planId,
|
||||||
|
body: {'payment_provider': 'alfa-bank'},
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => expectedPayment);
|
||||||
|
|
||||||
|
final result = await chatRepository.getPaymentLink(
|
||||||
|
id: planId,
|
||||||
|
paymentProvider: 'alfa-bank',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result, isA<PaymentModel>());
|
||||||
|
expect(result.paymentUrl, 'https://qr.nspk.ru/ASDFASDFASDFASDF');
|
||||||
|
expect(result.orderId, 'alfa_c29_k15138_t1712345678_r1234');
|
||||||
|
expect(result.paymentProvider, 'alfa-bank');
|
||||||
|
verify(
|
||||||
|
mockChatService.getPaymentLink(
|
||||||
|
id: planId,
|
||||||
|
body: {'payment_provider': 'alfa-bank'},
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('resolveOrderIdToConversation - получает модель по orderId', () async {
|
||||||
|
const orderId = 'alfa_c29_k15138_t1712345678_r1234';
|
||||||
|
final fixture = FixtureReader.readAsMap(
|
||||||
|
'chat/payments/order_id_to_chat_uuid_response.json',
|
||||||
|
);
|
||||||
|
final expected = OrderIdToConversationModel.fromJson(fixture);
|
||||||
|
|
||||||
|
when(
|
||||||
|
mockChatService.resolveOrderIdToConversation(orderId: orderId),
|
||||||
|
).thenAnswer((_) async => expected);
|
||||||
|
|
||||||
|
final result = await chatRepository.resolveOrderIdToConversation(
|
||||||
|
orderId: orderId,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result, isA<OrderIdToConversationModel>());
|
||||||
|
expect(result.chosenPlanId, 12);
|
||||||
|
expect(result.conversationUuid, "fbd81429-8019-499a-80e8-e3933cc4d872");
|
||||||
|
|
||||||
|
verify(
|
||||||
|
mockChatService.resolveOrderIdToConversation(orderId: orderId),
|
||||||
|
).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getChosenPlan - получает выбранный план', () async {
|
test('getChosenPlan - получает выбранный план', () async {
|
||||||
const planId = 1;
|
const planId = 1;
|
||||||
final chosenPlanFixture = FixtureReader.readAsMap('chat/chosen_plan_response.json');
|
final chosenPlanFixture = FixtureReader.readAsMap(
|
||||||
|
'chat/chosen_plan/chosen_plan_in_progress_response.json',
|
||||||
|
);
|
||||||
final expectedPlan = ChosenPlanModel.fromJson(chosenPlanFixture);
|
final expectedPlan = ChosenPlanModel.fromJson(chosenPlanFixture);
|
||||||
|
|
||||||
when(mockChatService.getChosenPlan(id: planId))
|
when(
|
||||||
.thenAnswer((_) async => expectedPlan);
|
mockChatService.getChosenPlan(id: planId),
|
||||||
|
).thenAnswer((_) async => expectedPlan);
|
||||||
|
|
||||||
final result = await chatRepository.getChosenPlan(id: planId);
|
final result = await chatRepository.getChosenPlan(id: planId);
|
||||||
|
|
||||||
@@ -290,66 +402,140 @@ void main() {
|
|||||||
verify(mockChatService.getChosenPlan(id: planId)).called(1);
|
verify(mockChatService.getChosenPlan(id: planId)).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('getChosenPlan payment polling statuses', () {
|
||||||
|
test('getChosenPlan - возвращает план в статусе pending_payment', () async {
|
||||||
|
const planId = 1;
|
||||||
|
final fixture = FixtureReader.readAsMap(
|
||||||
|
'chat/chosen_plan/chosen_plan_pending_payment_response.json',
|
||||||
|
);
|
||||||
|
final expectedPlan = ChosenPlanModel.fromJson(fixture);
|
||||||
|
|
||||||
|
when(mockChatService.getChosenPlan(id: planId)).thenAnswer((_) async => expectedPlan);
|
||||||
|
|
||||||
|
final result = await chatRepository.getChosenPlan(id: planId);
|
||||||
|
|
||||||
|
expect(result.chosenPlanState, ChosenPlanStateEnum.statePendingPayment);
|
||||||
|
verify(mockChatService.getChosenPlan(id: planId)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getChosenPlan - возвращает план в статусе paid', () async {
|
||||||
|
const planId = 1;
|
||||||
|
final fixture = FixtureReader.readAsMap(
|
||||||
|
'chat/chosen_plan/chosen_plan_paid_response.json',
|
||||||
|
);
|
||||||
|
final expectedPlan = ChosenPlanModel.fromJson(fixture);
|
||||||
|
|
||||||
|
when(mockChatService.getChosenPlan(id: planId)).thenAnswer((_) async => expectedPlan);
|
||||||
|
|
||||||
|
final result = await chatRepository.getChosenPlan(id: planId);
|
||||||
|
|
||||||
|
expect(result.chosenPlanState, ChosenPlanStateEnum.statePaid);
|
||||||
|
verify(mockChatService.getChosenPlan(id: planId)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getChosenPlan - возвращает план в статусе payment_failed', () async {
|
||||||
|
const planId = 1;
|
||||||
|
final fixture = FixtureReader.readAsMap(
|
||||||
|
'chat/chosen_plan/chosen_plan_payment_failed_response.json',
|
||||||
|
);
|
||||||
|
final expectedPlan = ChosenPlanModel.fromJson(fixture);
|
||||||
|
|
||||||
|
when(mockChatService.getChosenPlan(id: planId)).thenAnswer((_) async => expectedPlan);
|
||||||
|
|
||||||
|
final result = await chatRepository.getChosenPlan(id: planId);
|
||||||
|
|
||||||
|
expect(result.chosenPlanState, ChosenPlanStateEnum.statePaymentFailed);
|
||||||
|
verify(mockChatService.getChosenPlan(id: planId)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getChosenPlan - возвращает план в статусе canceled_by_the_initiator', () async {
|
||||||
|
const planId = 1;
|
||||||
|
final fixture = FixtureReader.readAsMap(
|
||||||
|
'chat/chosen_plan/chosen_plan_canceled_by_the_initiator_response.json',
|
||||||
|
);
|
||||||
|
final expectedPlan = ChosenPlanModel.fromJson(fixture);
|
||||||
|
|
||||||
|
when(mockChatService.getChosenPlan(id: planId)).thenAnswer((_) async => expectedPlan);
|
||||||
|
|
||||||
|
final result = await chatRepository.getChosenPlan(id: planId);
|
||||||
|
|
||||||
|
expect(result.chosenPlanState, ChosenPlanStateEnum.stateCanceledByTheInitiator);
|
||||||
|
verify(mockChatService.getChosenPlan(id: planId)).called(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('finishChosenPlan - завершает выбранный план', () async {
|
test('finishChosenPlan - завершает выбранный план', () async {
|
||||||
const planId = 123;
|
const planId = 123;
|
||||||
|
|
||||||
when(mockChatService.finishChosenPlan(id: planId))
|
when(
|
||||||
.thenAnswer((_) async => Future.value());
|
mockChatService.finishChosenPlan(id: planId),
|
||||||
|
).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
await chatRepository.finishChosenPlan(id: planId);
|
await chatRepository.finishChosenPlan(id: planId);
|
||||||
|
|
||||||
verify(mockChatService.finishChosenPlan(id: planId)).called(1);
|
verify(mockChatService.finishChosenPlan(id: planId)).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('complainAboutConsultation - отправляет жалобу на консультацию', () async {
|
test(
|
||||||
const planId = 123;
|
'complainAboutConsultation - отправляет жалобу на консультацию',
|
||||||
final complainFixture = FixtureReader.readAsMap('chat/plan_complain_response.json');
|
() async {
|
||||||
final complainModel = PlanComplainModel.fromJson(complainFixture);
|
const planId = 123;
|
||||||
|
final complainFixture = FixtureReader.readAsMap(
|
||||||
|
'chat/plan_complain_response.json',
|
||||||
|
);
|
||||||
|
final complainModel = PlanComplainModel.fromJson(complainFixture);
|
||||||
|
|
||||||
when(mockChatService.complainAboutConsultation(id: planId, body: complainModel))
|
when(
|
||||||
.thenAnswer((_) async => Future.value());
|
mockChatService.complainAboutConsultation(
|
||||||
|
id: planId,
|
||||||
|
body: complainModel,
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
await chatRepository.complainAboutConsultation(
|
await chatRepository.complainAboutConsultation(
|
||||||
id: planId,
|
id: planId,
|
||||||
body: complainModel,
|
body: complainModel,
|
||||||
);
|
);
|
||||||
|
|
||||||
verify(mockChatService.complainAboutConsultation(id: planId, body: complainModel))
|
verify(
|
||||||
.called(1);
|
mockChatService.complainAboutConsultation(
|
||||||
});
|
id: planId,
|
||||||
|
body: complainModel,
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('getChatByUserSlug - возвращает ID чата при успешном ответе', () async {
|
test(
|
||||||
const slug = 'user-slug';
|
'getChatByUserSlug - возвращает ID чата при успешном ответе',
|
||||||
const chatId = 'chat-id-123';
|
() async {
|
||||||
final response = HttpResponse(
|
const slug = 'user-slug';
|
||||||
{'id': chatId},
|
const chatId = 'chat-id-123';
|
||||||
Response(
|
final response = HttpResponse({
|
||||||
requestOptions: RequestOptions(path: ''),
|
'id': chatId,
|
||||||
statusCode: 200,
|
}, Response(requestOptions: RequestOptions(path: ''), statusCode: 200));
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
when(mockChatService.getChatByUserSlug(slug: slug))
|
when(
|
||||||
.thenAnswer((_) async => response);
|
mockChatService.getChatByUserSlug(slug: slug),
|
||||||
|
).thenAnswer((_) async => response);
|
||||||
|
|
||||||
final result = await chatRepository.getChatByUserSlug(slug);
|
final result = await chatRepository.getChatByUserSlug(slug);
|
||||||
|
|
||||||
expect(result, chatId);
|
expect(result, chatId);
|
||||||
verify(mockChatService.getChatByUserSlug(slug: slug)).called(1);
|
verify(mockChatService.getChatByUserSlug(slug: slug)).called(1);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('getChatByUserSlug - возвращает 404 при отсутствии чата', () async {
|
test('getChatByUserSlug - возвращает 404 при отсутствии чата', () async {
|
||||||
const slug = 'user-slug';
|
const slug = 'user-slug';
|
||||||
final response = HttpResponse(
|
final response = HttpResponse(
|
||||||
{},
|
{},
|
||||||
Response(
|
Response(requestOptions: RequestOptions(path: ''), statusCode: 404),
|
||||||
requestOptions: RequestOptions(path: ''),
|
|
||||||
statusCode: 404,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
when(mockChatService.getChatByUserSlug(slug: slug))
|
when(
|
||||||
.thenAnswer((_) async => response);
|
mockChatService.getChatByUserSlug(slug: slug),
|
||||||
|
).thenAnswer((_) async => response);
|
||||||
|
|
||||||
final result = await chatRepository.getChatByUserSlug(slug);
|
final result = await chatRepository.getChatByUserSlug(slug);
|
||||||
|
|
||||||
@@ -357,17 +543,20 @@ void main() {
|
|||||||
verify(mockChatService.getChatByUserSlug(slug: slug)).called(1);
|
verify(mockChatService.getChatByUserSlug(slug: slug)).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getChatByUserSlug - пробрасывает исключение при ошибке сети', () async {
|
test(
|
||||||
const slug = 'user-slug';
|
'getChatByUserSlug - пробрасывает исключение при ошибке сети',
|
||||||
|
() async {
|
||||||
|
const slug = 'user-slug';
|
||||||
|
|
||||||
when(mockChatService.getChatByUserSlug(slug: slug))
|
when(
|
||||||
.thenThrow(Exception('Network error'));
|
mockChatService.getChatByUserSlug(slug: slug),
|
||||||
|
).thenThrow(Exception('Network error'));
|
||||||
|
|
||||||
await expectLater(
|
await expectLater(
|
||||||
chatRepository.getChatByUserSlug(slug),
|
chatRepository.getChatByUserSlug(slug),
|
||||||
throwsA(isA<Exception>()),
|
throwsA(isA<Exception>()),
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:eatvio_app/features/chat/models/chat_message_model.dart';
|
import 'package:eatvio_app/features/chat/models/chat_message_model.dart';
|
||||||
import 'package:eatvio_app/features/chat/models/chat_messages_model.dart';
|
import 'package:eatvio_app/features/chat/models/chat_messages_model.dart';
|
||||||
import 'package:eatvio_app/features/chat/models/chat_room_model.dart' as chat_room;
|
import 'package:eatvio_app/features/chat/models/chat_room_model.dart'
|
||||||
|
as chat_room;
|
||||||
import 'package:eatvio_app/features/chat/models/chat_room_pagination_model.dart';
|
import 'package:eatvio_app/features/chat/models/chat_room_pagination_model.dart';
|
||||||
|
import 'package:eatvio_app/features/chat/models/order_id_to_conversation_model.dart';
|
||||||
import 'package:eatvio_app/features/chat/models/payment_model.dart';
|
import 'package:eatvio_app/features/chat/models/payment_model.dart';
|
||||||
import 'package:eatvio_app/features/chat/models/plan_complain_model.dart';
|
import 'package:eatvio_app/features/chat/models/plan_complain_model.dart';
|
||||||
import 'package:eatvio_app/features/chat/services/chat_service.dart';
|
import 'package:eatvio_app/features/chat/services/chat_service.dart';
|
||||||
@@ -24,10 +26,16 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('getChatRooms - возвращает список комнат чата', () async {
|
test('getChatRooms - возвращает список комнат чата', () async {
|
||||||
final paginationFixture = FixtureReader.readAsMap('chat/chat_room_pagination_response.json');
|
final paginationFixture = FixtureReader.readAsMap(
|
||||||
final expectedPagination = ChatRoomPaginationModel.fromJson(paginationFixture);
|
'chat/chat_room_pagination_response.json',
|
||||||
|
);
|
||||||
|
final expectedPagination = ChatRoomPaginationModel.fromJson(
|
||||||
|
paginationFixture,
|
||||||
|
);
|
||||||
|
|
||||||
when(mockChatService.getChatRooms(query: null, page: 1)).thenAnswer((_) async => expectedPagination);
|
when(
|
||||||
|
mockChatService.getChatRooms(query: null, page: 1),
|
||||||
|
).thenAnswer((_) async => expectedPagination);
|
||||||
|
|
||||||
final result = await mockChatService.getChatRooms(query: null, page: 1);
|
final result = await mockChatService.getChatRooms(query: null, page: 1);
|
||||||
|
|
||||||
@@ -38,10 +46,16 @@ void main() {
|
|||||||
|
|
||||||
test('getChatRooms - возвращает список комнат с запросом', () async {
|
test('getChatRooms - возвращает список комнат с запросом', () async {
|
||||||
const query = 'test query';
|
const query = 'test query';
|
||||||
final paginationFixture = FixtureReader.readAsMap('chat/chat_room_pagination_response.json');
|
final paginationFixture = FixtureReader.readAsMap(
|
||||||
final expectedPagination = ChatRoomPaginationModel.fromJson(paginationFixture);
|
'chat/chat_room_pagination_response.json',
|
||||||
|
);
|
||||||
|
final expectedPagination = ChatRoomPaginationModel.fromJson(
|
||||||
|
paginationFixture,
|
||||||
|
);
|
||||||
|
|
||||||
when(mockChatService.getChatRooms(query: query, page: 1)).thenAnswer((_) async => expectedPagination);
|
when(
|
||||||
|
mockChatService.getChatRooms(query: query, page: 1),
|
||||||
|
).thenAnswer((_) async => expectedPagination);
|
||||||
|
|
||||||
final result = await mockChatService.getChatRooms(query: query, page: 1);
|
final result = await mockChatService.getChatRooms(query: query, page: 1);
|
||||||
|
|
||||||
@@ -51,10 +65,14 @@ void main() {
|
|||||||
|
|
||||||
test('getChatRoom - возвращает комнату чата по UUID', () async {
|
test('getChatRoom - возвращает комнату чата по UUID', () async {
|
||||||
const uuid = 'chat-1';
|
const uuid = 'chat-1';
|
||||||
final roomFixture = FixtureReader.readAsMap('chat/chat_room_response.json');
|
final roomFixture = FixtureReader.readAsMap(
|
||||||
|
'chat/chat_room_response.json',
|
||||||
|
);
|
||||||
final expectedRoom = chat_room.ChatRoomModel.fromJson(roomFixture);
|
final expectedRoom = chat_room.ChatRoomModel.fromJson(roomFixture);
|
||||||
|
|
||||||
when(mockChatService.getChatRoom(uuid: uuid)).thenAnswer((_) async => expectedRoom);
|
when(
|
||||||
|
mockChatService.getChatRoom(uuid: uuid),
|
||||||
|
).thenAnswer((_) async => expectedRoom);
|
||||||
|
|
||||||
final result = await mockChatService.getChatRoom(uuid: uuid);
|
final result = await mockChatService.getChatRoom(uuid: uuid);
|
||||||
|
|
||||||
@@ -67,62 +85,114 @@ void main() {
|
|||||||
const slug = 'user-slug';
|
const slug = 'user-slug';
|
||||||
const data = 'Test message';
|
const data = 'Test message';
|
||||||
const socketId = 'socket-id-123';
|
const socketId = 'socket-id-123';
|
||||||
final messageFixture = FixtureReader.readAsMap('chat/chat_message_response.json');
|
final messageFixture = FixtureReader.readAsMap(
|
||||||
|
'chat/chat_message_response.json',
|
||||||
|
);
|
||||||
final expectedMessage = ChatMessageModel.fromJson(messageFixture);
|
final expectedMessage = ChatMessageModel.fromJson(messageFixture);
|
||||||
|
|
||||||
// ignore: null_argument_to_non_null_type
|
// ignore: null_argument_to_non_null_type
|
||||||
when(mockChatService.sendMessage(slug: slug, data: data, mediaData: null, socketId: socketId)).thenAnswer((_) async => expectedMessage);
|
when(
|
||||||
|
mockChatService.sendMessage(
|
||||||
|
slug: slug,
|
||||||
|
data: data,
|
||||||
|
mediaData: null,
|
||||||
|
socketId: socketId,
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => expectedMessage);
|
||||||
|
|
||||||
final result = await mockChatService.sendMessage(slug: slug, data: data, mediaData: null, socketId: socketId);
|
final result = await mockChatService.sendMessage(
|
||||||
|
slug: slug,
|
||||||
|
data: data,
|
||||||
|
mediaData: null,
|
||||||
|
socketId: socketId,
|
||||||
|
);
|
||||||
|
|
||||||
expect(result, isA<ChatMessageModel>());
|
expect(result, isA<ChatMessageModel>());
|
||||||
expect(result.data, data);
|
expect(result.data, data);
|
||||||
verify(mockChatService.sendMessage(slug: slug, data: data, mediaData: null, socketId: socketId)).called(1);
|
verify(
|
||||||
|
mockChatService.sendMessage(
|
||||||
|
slug: slug,
|
||||||
|
data: data,
|
||||||
|
mediaData: null,
|
||||||
|
socketId: socketId,
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getChatMessages - возвращает сообщения чата', () async {
|
test('getChatMessages - возвращает сообщения чата', () async {
|
||||||
const chatId = 'chat-1';
|
const chatId = 'chat-1';
|
||||||
const page = 1;
|
const page = 1;
|
||||||
final messagesFixture = FixtureReader.readAsMap('chat/chat_messages_response.json');
|
final messagesFixture = FixtureReader.readAsMap(
|
||||||
|
'chat/chat_messages_response.json',
|
||||||
|
);
|
||||||
final expectedMessages = ChatMessagesModel.fromJson(messagesFixture);
|
final expectedMessages = ChatMessagesModel.fromJson(messagesFixture);
|
||||||
|
|
||||||
when(mockChatService.getChatMessages(id: chatId, page: page)).thenAnswer((_) async => expectedMessages);
|
when(
|
||||||
|
mockChatService.getChatMessages(id: chatId, page: page),
|
||||||
|
).thenAnswer((_) async => expectedMessages);
|
||||||
|
|
||||||
final result = await mockChatService.getChatMessages(id: chatId, page: page);
|
final result = await mockChatService.getChatMessages(
|
||||||
|
id: chatId,
|
||||||
|
page: page,
|
||||||
|
);
|
||||||
|
|
||||||
expect(result, isA<ChatMessagesModel>());
|
expect(result, isA<ChatMessagesModel>());
|
||||||
expect(result.messagesList.length, 2);
|
expect(result.messagesList.length, 2);
|
||||||
verify(mockChatService.getChatMessages(id: chatId, page: page)).called(1);
|
verify(mockChatService.getChatMessages(id: chatId, page: page)).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getChatRoomIdByUserSlug - возвращает комнату по slug пользователя', () async {
|
test(
|
||||||
const slug = 'user-1';
|
'getChatRoomIdByUserSlug - возвращает комнату по slug пользователя',
|
||||||
final roomFixture = FixtureReader.readAsMap('chat/chat_room_response.json');
|
() async {
|
||||||
final expectedRoom = chat_room.ChatRoomModel.fromJson(roomFixture);
|
const slug = 'user-1';
|
||||||
|
final roomFixture = FixtureReader.readAsMap(
|
||||||
|
'chat/chat_room_response.json',
|
||||||
|
);
|
||||||
|
final expectedRoom = chat_room.ChatRoomModel.fromJson(roomFixture);
|
||||||
|
|
||||||
when(mockChatService.getChatRoomIdByUserSlug(user: slug)).thenAnswer((_) async => expectedRoom);
|
when(
|
||||||
|
mockChatService.getChatRoomIdByUserSlug(user: slug),
|
||||||
|
).thenAnswer((_) async => expectedRoom);
|
||||||
|
|
||||||
final result = await mockChatService.getChatRoomIdByUserSlug(user: slug);
|
final result = await mockChatService.getChatRoomIdByUserSlug(
|
||||||
|
user: slug,
|
||||||
|
);
|
||||||
|
|
||||||
expect(result, isA<chat_room.ChatRoomModel>());
|
expect(result, isA<chat_room.ChatRoomModel>());
|
||||||
verify(mockChatService.getChatRoomIdByUserSlug(user: slug)).called(1);
|
verify(mockChatService.getChatRoomIdByUserSlug(user: slug)).called(1);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('markMessagesAsRead - отмечает сообщения как прочитанные', () async {
|
test('markMessagesAsRead - отмечает сообщения как прочитанные', () async {
|
||||||
const chatId = 'chat-id-123';
|
const chatId = 'chat-id-123';
|
||||||
final messageIds = [1, 2, 3];
|
final messageIds = [1, 2, 3];
|
||||||
|
|
||||||
when(mockChatService.markMessagesAsRead(id: chatId, body: {'messages_ids': messageIds})).thenAnswer((_) async => Future.value());
|
when(
|
||||||
|
mockChatService.markMessagesAsRead(
|
||||||
|
id: chatId,
|
||||||
|
body: {'messages_ids': messageIds},
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
await mockChatService.markMessagesAsRead(id: chatId, body: {'messages_ids': messageIds});
|
await mockChatService.markMessagesAsRead(
|
||||||
|
id: chatId,
|
||||||
|
body: {'messages_ids': messageIds},
|
||||||
|
);
|
||||||
|
|
||||||
verify(mockChatService.markMessagesAsRead(id: chatId, body: {'messages_ids': messageIds})).called(1);
|
verify(
|
||||||
|
mockChatService.markMessagesAsRead(
|
||||||
|
id: chatId,
|
||||||
|
body: {'messages_ids': messageIds},
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('cancelChosenPlan - отменяет выбранный план', () async {
|
test('cancelChosenPlan - отменяет выбранный план', () async {
|
||||||
const planId = 123;
|
const planId = 123;
|
||||||
|
|
||||||
when(mockChatService.cancelChosenPlan(id: planId)).thenAnswer((_) async => Future.value());
|
when(
|
||||||
|
mockChatService.cancelChosenPlan(id: planId),
|
||||||
|
).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
await mockChatService.cancelChosenPlan(id: planId);
|
await mockChatService.cancelChosenPlan(id: planId);
|
||||||
|
|
||||||
@@ -133,33 +203,80 @@ void main() {
|
|||||||
const planId = 123;
|
const planId = 123;
|
||||||
final date = {'start_date': '2024-01-15'};
|
final date = {'start_date': '2024-01-15'};
|
||||||
|
|
||||||
when(mockChatService.choseStartPlanDate(id: planId, date: date)).thenAnswer((_) async => Future.value());
|
when(
|
||||||
|
mockChatService.choseStartPlanDate(id: planId, date: date),
|
||||||
|
).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
await mockChatService.choseStartPlanDate(id: planId, date: date);
|
await mockChatService.choseStartPlanDate(id: planId, date: date);
|
||||||
|
|
||||||
verify(mockChatService.choseStartPlanDate(id: planId, date: date)).called(1);
|
verify(
|
||||||
|
mockChatService.choseStartPlanDate(id: planId, date: date),
|
||||||
|
).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getPaymentLink - возвращает ссылку на оплату', () async {
|
test('getPaymentLink - возвращает ссылку на оплату', () async {
|
||||||
const planId = 123;
|
const planId = 123;
|
||||||
final paymentFixture = FixtureReader.readAsMap('chat/payment_response.json');
|
final paymentFixture = FixtureReader.readAsMap(
|
||||||
|
'chat/payments/payment_response.json',
|
||||||
|
);
|
||||||
final expectedPayment = PaymentModel.fromJson(paymentFixture);
|
final expectedPayment = PaymentModel.fromJson(paymentFixture);
|
||||||
|
|
||||||
when(mockChatService.getPaymentLink(id: planId)).thenAnswer((_) async => expectedPayment);
|
when(
|
||||||
|
mockChatService.getPaymentLink(id: planId, body: {'payment_provider': 'mock'}),
|
||||||
|
).thenAnswer((_) async => expectedPayment);
|
||||||
|
|
||||||
final result = await mockChatService.getPaymentLink(id: planId);
|
final result = await mockChatService.getPaymentLink(
|
||||||
|
id: planId,
|
||||||
|
body: {'payment_provider': 'mock'},
|
||||||
|
);
|
||||||
|
|
||||||
expect(result, isA<PaymentModel>());
|
expect(result, isA<PaymentModel>());
|
||||||
expect(result.paymentUrl, 'https://example.com/checkout/payment-123');
|
expect(result.paymentUrl, 'https://example.com/checkout/payment-123');
|
||||||
verify(mockChatService.getPaymentLink(id: planId)).called(1);
|
verify(mockChatService.getPaymentLink(id: planId, body: {'payment_provider': 'mock'})).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'getPaymentLink - возвращает ссылку на оплату alfa-bank',
|
||||||
|
() async {
|
||||||
|
const planId = 123;
|
||||||
|
final requestFixture = FixtureReader.readAsMap(
|
||||||
|
'chat/payments/payment_alfabank_request.json',
|
||||||
|
);
|
||||||
|
final requestBody = Map<String, String>.from(requestFixture);
|
||||||
|
final paymentFixture = FixtureReader.readAsMap(
|
||||||
|
'chat/payments/payment_response_alfa_bank.json',
|
||||||
|
);
|
||||||
|
final expectedPayment = PaymentModel.fromJson(paymentFixture);
|
||||||
|
|
||||||
|
when(
|
||||||
|
mockChatService.getPaymentLink(id: planId, body: requestBody),
|
||||||
|
).thenAnswer((_) async => expectedPayment);
|
||||||
|
|
||||||
|
final result = await mockChatService.getPaymentLink(
|
||||||
|
id: planId,
|
||||||
|
body: requestBody,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result, isA<PaymentModel>());
|
||||||
|
expect(result.paymentUrl, 'https://qr.nspk.ru/ASDFASDFASDFASDF');
|
||||||
|
expect(result.orderId, 'alfa_c29_k15138_t1712345678_r1234');
|
||||||
|
expect(result.paymentProvider, 'alfa-bank');
|
||||||
|
verify(
|
||||||
|
mockChatService.getPaymentLink(id: planId, body: requestBody),
|
||||||
|
).called(1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('getChosenPlan - возвращает выбранный план', () async {
|
test('getChosenPlan - возвращает выбранный план', () async {
|
||||||
const planId = 1;
|
const planId = 1;
|
||||||
final chosenPlanFixture = FixtureReader.readAsMap('chat/chosen_plan_response.json');
|
final chosenPlanFixture = FixtureReader.readAsMap(
|
||||||
|
'chat/chosen_plan/chosen_plan_in_progress_response.json',
|
||||||
|
);
|
||||||
final expectedPlan = ChosenPlanModel.fromJson(chosenPlanFixture);
|
final expectedPlan = ChosenPlanModel.fromJson(chosenPlanFixture);
|
||||||
|
|
||||||
when(mockChatService.getChosenPlan(id: planId)).thenAnswer((_) async => expectedPlan);
|
when(
|
||||||
|
mockChatService.getChosenPlan(id: planId),
|
||||||
|
).thenAnswer((_) async => expectedPlan);
|
||||||
|
|
||||||
final result = await mockChatService.getChosenPlan(id: planId);
|
final result = await mockChatService.getChosenPlan(id: planId);
|
||||||
|
|
||||||
@@ -171,31 +288,80 @@ void main() {
|
|||||||
test('finishChosenPlan - завершает выбранный план', () async {
|
test('finishChosenPlan - завершает выбранный план', () async {
|
||||||
const planId = 123;
|
const planId = 123;
|
||||||
|
|
||||||
when(mockChatService.finishChosenPlan(id: planId)).thenAnswer((_) async => Future.value());
|
when(
|
||||||
|
mockChatService.finishChosenPlan(id: planId),
|
||||||
|
).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
await mockChatService.finishChosenPlan(id: planId);
|
await mockChatService.finishChosenPlan(id: planId);
|
||||||
|
|
||||||
verify(mockChatService.finishChosenPlan(id: planId)).called(1);
|
verify(mockChatService.finishChosenPlan(id: planId)).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('complainAboutConsultation - отправляет жалобу на консультацию', () async {
|
test(
|
||||||
const planId = 123;
|
'complainAboutConsultation - отправляет жалобу на консультацию',
|
||||||
final complainFixture = FixtureReader.readAsMap('chat/plan_complain_response.json');
|
() async {
|
||||||
final complainModel = PlanComplainModel.fromJson(complainFixture);
|
const planId = 123;
|
||||||
|
final complainFixture = FixtureReader.readAsMap(
|
||||||
|
'chat/plan_complain_response.json',
|
||||||
|
);
|
||||||
|
final complainModel = PlanComplainModel.fromJson(complainFixture);
|
||||||
|
|
||||||
when(mockChatService.complainAboutConsultation(id: planId, body: complainModel)).thenAnswer((_) async => Future.value());
|
when(
|
||||||
|
mockChatService.complainAboutConsultation(
|
||||||
|
id: planId,
|
||||||
|
body: complainModel,
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => Future.value());
|
||||||
|
|
||||||
await mockChatService.complainAboutConsultation(id: planId, body: complainModel);
|
await mockChatService.complainAboutConsultation(
|
||||||
|
id: planId,
|
||||||
|
body: complainModel,
|
||||||
|
);
|
||||||
|
|
||||||
verify(mockChatService.complainAboutConsultation(id: planId, body: complainModel)).called(1);
|
verify(
|
||||||
});
|
mockChatService.complainAboutConsultation(
|
||||||
|
id: planId,
|
||||||
|
body: complainModel,
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'resolveOrderIdToConversation - возвращает модель по orderId',
|
||||||
|
() async {
|
||||||
|
const orderId = 'alfa_c29_k15138_t1712345678_r1234';
|
||||||
|
final fixture = FixtureReader.readAsMap(
|
||||||
|
'chat/payments/order_id_to_chat_uuid_response.json',
|
||||||
|
);
|
||||||
|
final expected = OrderIdToConversationModel.fromJson(fixture);
|
||||||
|
|
||||||
|
when(
|
||||||
|
mockChatService.resolveOrderIdToConversation(orderId: orderId),
|
||||||
|
).thenAnswer((_) async => expected);
|
||||||
|
|
||||||
|
final result = await mockChatService.resolveOrderIdToConversation(
|
||||||
|
orderId: orderId,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result, isA<OrderIdToConversationModel>());
|
||||||
|
expect(result.chosenPlanId, 12);
|
||||||
|
verify(
|
||||||
|
mockChatService.resolveOrderIdToConversation(orderId: orderId),
|
||||||
|
).called(1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('getChatByUserSlug - возвращает HttpResponse', () async {
|
test('getChatByUserSlug - возвращает HttpResponse', () async {
|
||||||
const slug = 'user-slug';
|
const slug = 'user-slug';
|
||||||
const chatId = 'chat-id-123';
|
const chatId = 'chat-id-123';
|
||||||
final response = HttpResponse({'id': chatId}, Response(requestOptions: RequestOptions(path: ''), statusCode: 200));
|
final response = HttpResponse({
|
||||||
|
'id': chatId,
|
||||||
|
}, Response(requestOptions: RequestOptions(path: ''), statusCode: 200));
|
||||||
|
|
||||||
when(mockChatService.getChatByUserSlug(slug: slug)).thenAnswer((_) async => response);
|
when(
|
||||||
|
mockChatService.getChatByUserSlug(slug: slug),
|
||||||
|
).thenAnswer((_) async => response);
|
||||||
|
|
||||||
final result = await mockChatService.getChatByUserSlug(slug: slug);
|
final result = await mockChatService.getChatByUserSlug(slug: slug);
|
||||||
|
|
||||||
|
|||||||
@@ -200,20 +200,20 @@ void main() {
|
|||||||
.thenAnswer((_) async => chosenPlan);
|
.thenAnswer((_) async => chosenPlan);
|
||||||
await cubit.getChosenPlan(id: 1);
|
await cubit.getChosenPlan(id: 1);
|
||||||
|
|
||||||
when(mockChatRepository.getPaymentLink(id: 1))
|
when(mockChatRepository.getPaymentLink(id: 1, paymentProvider: 'alfa-bank'))
|
||||||
.thenAnswer((_) async => paymentModel);
|
.thenAnswer((_) async => paymentModel);
|
||||||
|
|
||||||
final result = await cubit.getPaymentLink();
|
final result = await cubit.getPaymentLink();
|
||||||
|
|
||||||
expect(result, equals('https://payment.url'));
|
expect(result, equals('https://payment.url'));
|
||||||
verify(mockChatRepository.getPaymentLink(id: 1)).called(1);
|
verify(mockChatRepository.getPaymentLink(id: 1, paymentProvider: 'alfa-bank')).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getPaymentLink - возвращает null, если план не выбран', () async {
|
test('getPaymentLink - возвращает null, если план не выбран', () async {
|
||||||
final result = await cubit.getPaymentLink();
|
final result = await cubit.getPaymentLink();
|
||||||
|
|
||||||
expect(result, isNull);
|
expect(result, isNull);
|
||||||
verifyNever(mockChatRepository.getPaymentLink(id: anyNamed('id')));
|
verifyNever(mockChatRepository.getPaymentLink(id: anyNamed('id'), paymentProvider: anyNamed('paymentProvider')));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getPaymentLink - обработка ошибки', () async {
|
test('getPaymentLink - обработка ошибки', () async {
|
||||||
@@ -254,7 +254,7 @@ void main() {
|
|||||||
.thenAnswer((_) async => chosenPlan);
|
.thenAnswer((_) async => chosenPlan);
|
||||||
await cubit.getChosenPlan(id: 1);
|
await cubit.getChosenPlan(id: 1);
|
||||||
|
|
||||||
when(mockChatRepository.getPaymentLink(id: 1))
|
when(mockChatRepository.getPaymentLink(id: 1, paymentProvider: 'alfa-bank'))
|
||||||
.thenThrow(Exception('Ошибка'));
|
.thenThrow(Exception('Ошибка'));
|
||||||
|
|
||||||
final result = await cubit.getPaymentLink();
|
final result = await cubit.getPaymentLink();
|
||||||
|
|||||||
131
test/features/payment/consultation_payment_flow_test.dart
Normal file
131
test/features/payment/consultation_payment_flow_test.dart
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import 'package:eatvio_app/generated/l10n.dart';
|
||||||
|
import 'package:eatvio_app/features/payment/presentation/payment_flow.dart';
|
||||||
|
import 'package:eatvio_app/features/payment/presentation/widgets/promo_code_modal.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
Widget createTestApp() {
|
||||||
|
return const MaterialApp(
|
||||||
|
localizationsDelegates: [
|
||||||
|
S.delegate,
|
||||||
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
GlobalCupertinoLocalizations.delegate,
|
||||||
|
],
|
||||||
|
supportedLocales: [Locale('ru')],
|
||||||
|
home: Scaffold(body: SizedBox()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
group('showConsultationPaymentFlow', () {
|
||||||
|
late bool gateCalled;
|
||||||
|
late bool? gateResult;
|
||||||
|
late bool formCalled;
|
||||||
|
late PromoCodeResult? formResult;
|
||||||
|
late bool sbpCalled;
|
||||||
|
late int sbpPlanId;
|
||||||
|
|
||||||
|
Future<bool?> mockGate(BuildContext context) async {
|
||||||
|
gateCalled = true;
|
||||||
|
return gateResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<PromoCodeResult?> mockForm(
|
||||||
|
BuildContext context, {
|
||||||
|
Future<PromoValidation> Function(String)? onValidate,
|
||||||
|
}) async {
|
||||||
|
formCalled = true;
|
||||||
|
return formResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> mockSbp(BuildContext context, {required int planId}) async {
|
||||||
|
sbpCalled = true;
|
||||||
|
sbpPlanId = planId;
|
||||||
|
}
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
gateCalled = false;
|
||||||
|
gateResult = null;
|
||||||
|
formCalled = false;
|
||||||
|
formResult = null;
|
||||||
|
sbpCalled = false;
|
||||||
|
sbpPlanId = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Нет → вызывает SbpBanks (не форму)', (tester) async {
|
||||||
|
gateResult = false;
|
||||||
|
await tester.pumpWidget(createTestApp());
|
||||||
|
|
||||||
|
await showConsultationPaymentFlow(
|
||||||
|
tester.element(find.byType(SizedBox)),
|
||||||
|
planId: 42,
|
||||||
|
showGate: mockGate,
|
||||||
|
showForm: mockForm,
|
||||||
|
showSbp: mockSbp,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(gateCalled, isTrue);
|
||||||
|
expect(formCalled, isFalse);
|
||||||
|
expect(sbpCalled, isTrue);
|
||||||
|
expect(sbpPlanId, 42);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Да → форма → Пропустить → вызывает SbpBanks', (tester) async {
|
||||||
|
gateResult = true;
|
||||||
|
formResult = const PromoCodeResult();
|
||||||
|
await tester.pumpWidget(createTestApp());
|
||||||
|
|
||||||
|
await showConsultationPaymentFlow(
|
||||||
|
tester.element(find.byType(SizedBox)),
|
||||||
|
planId: 42,
|
||||||
|
showGate: mockGate,
|
||||||
|
showForm: mockForm,
|
||||||
|
showSbp: mockSbp,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(gateCalled, isTrue);
|
||||||
|
expect(formCalled, isTrue);
|
||||||
|
expect(sbpCalled, isTrue);
|
||||||
|
expect(sbpPlanId, 42);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Да → форма отменена (null) → Sbp не вызывается', (tester) async {
|
||||||
|
gateResult = true;
|
||||||
|
formResult = null;
|
||||||
|
await tester.pumpWidget(createTestApp());
|
||||||
|
|
||||||
|
await showConsultationPaymentFlow(
|
||||||
|
tester.element(find.byType(SizedBox)),
|
||||||
|
planId: 42,
|
||||||
|
showGate: mockGate,
|
||||||
|
showForm: mockForm,
|
||||||
|
showSbp: mockSbp,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(gateCalled, isTrue);
|
||||||
|
expect(formCalled, isTrue);
|
||||||
|
expect(sbpCalled, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('гейт отменён (null) → ничего не вызывается', (tester) async {
|
||||||
|
gateResult = null;
|
||||||
|
await tester.pumpWidget(createTestApp());
|
||||||
|
|
||||||
|
await showConsultationPaymentFlow(
|
||||||
|
tester.element(find.byType(SizedBox)),
|
||||||
|
planId: 42,
|
||||||
|
showGate: mockGate,
|
||||||
|
showForm: mockForm,
|
||||||
|
showSbp: mockSbp,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(gateCalled, isTrue);
|
||||||
|
expect(formCalled, isFalse);
|
||||||
|
expect(sbpCalled, isFalse);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
import 'package:eatvio_app/features/chat/models/payment_model.dart';
|
||||||
|
import 'package:eatvio_app/features/payment/presentation/cubit/consultation_payment_cubit/consultation_payment_cubit.dart';
|
||||||
|
import 'package:eatvio_app/features/payment/presentation/cubit/consultation_payment_cubit/consultation_payment_state.dart';
|
||||||
|
import 'package:eatvio_app/features/payment/repositories/payment_repository.dart';
|
||||||
|
import 'package:eatvio_app/features/payment/repositories/sbp_repository.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mockito/annotations.dart';
|
||||||
|
import 'package:mockito/mockito.dart';
|
||||||
|
import 'package:sbp/models/c2bmembers_model.dart';
|
||||||
|
|
||||||
|
import '../../../../fixtures/fixture_reader.dart';
|
||||||
|
import 'consultation_payment_cubit_test.mocks.dart';
|
||||||
|
|
||||||
|
@GenerateNiceMocks([
|
||||||
|
MockSpec<PaymentRepository>(),
|
||||||
|
MockSpec<SbpRepository>(),
|
||||||
|
])
|
||||||
|
void main() {
|
||||||
|
group('ConsultationPaymentCubit', () {
|
||||||
|
late ConsultationPaymentCubit cubit;
|
||||||
|
late MockPaymentRepository mockPaymentRepository;
|
||||||
|
late MockSbpRepository mockSbpRepository;
|
||||||
|
|
||||||
|
final installedBank = C2bmemberModel.fromJson(const {
|
||||||
|
'bankName': 'Сбербанк',
|
||||||
|
'logoURL': 'https://qr.nspk.ru/proxyapp/logo/bank100000000111.png',
|
||||||
|
'schema': 'bank100000000111',
|
||||||
|
'package_name': 'ru.sberbankmobile',
|
||||||
|
'icon': 'packages/sbp/assets/icons/sber.png',
|
||||||
|
});
|
||||||
|
|
||||||
|
late PaymentModel alfaPayment;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
mockPaymentRepository = MockPaymentRepository();
|
||||||
|
mockSbpRepository = MockSbpRepository();
|
||||||
|
cubit = ConsultationPaymentCubit(
|
||||||
|
paymentRepository: mockPaymentRepository,
|
||||||
|
sbpRepository: mockSbpRepository,
|
||||||
|
);
|
||||||
|
alfaPayment = PaymentModel.fromJson(
|
||||||
|
FixtureReader.readAsMap('chat/payments/payment_response_alfa_bank.json'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() {
|
||||||
|
cubit.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('начальный state - isLoading=false, banks=[], checkoutUrl=null', () {
|
||||||
|
expect(cubit.state, isA<ConsultationPaymentState>());
|
||||||
|
expect(cubit.state.isLoading, isFalse);
|
||||||
|
expect(cubit.state.banks, isEmpty);
|
||||||
|
expect(cubit.state.checkoutUrl, isNull);
|
||||||
|
expect(cubit.state.orderId, isNull);
|
||||||
|
expect(cubit.state.errorMessage, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'startPayment - успешно получает checkout_url и загружает банки',
|
||||||
|
() async {
|
||||||
|
const planId = 123;
|
||||||
|
|
||||||
|
when(
|
||||||
|
mockPaymentRepository.startPaymentProcess(planId: planId),
|
||||||
|
).thenAnswer((_) async => alfaPayment);
|
||||||
|
when(
|
||||||
|
mockSbpRepository.getInstalledBanks(),
|
||||||
|
).thenAnswer((_) async => [installedBank]);
|
||||||
|
|
||||||
|
await cubit.startPayment(planId: planId);
|
||||||
|
|
||||||
|
expect(cubit.state.isLoading, isFalse);
|
||||||
|
expect(cubit.state.checkoutUrl, alfaPayment.paymentUrl);
|
||||||
|
expect(cubit.state.orderId, alfaPayment.orderId);
|
||||||
|
expect(cubit.state.banks, [installedBank]);
|
||||||
|
expect(cubit.state.errorMessage, isNull);
|
||||||
|
verify(
|
||||||
|
mockPaymentRepository.startPaymentProcess(planId: planId),
|
||||||
|
).called(1);
|
||||||
|
verify(mockSbpRepository.getInstalledBanks()).called(1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('startPayment - при ошибке API emit errorMessage', () async {
|
||||||
|
const planId = 123;
|
||||||
|
|
||||||
|
when(
|
||||||
|
mockPaymentRepository.startPaymentProcess(planId: planId),
|
||||||
|
).thenThrow(Exception('Payment start failed'));
|
||||||
|
|
||||||
|
await cubit.startPayment(planId: planId);
|
||||||
|
|
||||||
|
expect(cubit.state.isLoading, isFalse);
|
||||||
|
expect(cubit.state.errorMessage, isNotNull);
|
||||||
|
expect(cubit.state.banks, isEmpty);
|
||||||
|
expect(cubit.state.checkoutUrl, isNull);
|
||||||
|
verifyNever(mockSbpRepository.getInstalledBanks());
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'startPayment - при ошибке SBP после успешного payment emit error',
|
||||||
|
() async {
|
||||||
|
const planId = 123;
|
||||||
|
|
||||||
|
when(
|
||||||
|
mockPaymentRepository.startPaymentProcess(planId: planId),
|
||||||
|
).thenAnswer((_) async => alfaPayment);
|
||||||
|
when(
|
||||||
|
mockSbpRepository.getInstalledBanks(),
|
||||||
|
).thenThrow(Exception('SBP plugin error'));
|
||||||
|
|
||||||
|
await cubit.startPayment(planId: planId);
|
||||||
|
|
||||||
|
expect(cubit.state.isLoading, isFalse);
|
||||||
|
expect(cubit.state.errorMessage, isNotNull);
|
||||||
|
expect(cubit.state.checkoutUrl, alfaPayment.paymentUrl);
|
||||||
|
expect(cubit.state.orderId, alfaPayment.orderId);
|
||||||
|
expect(cubit.state.banks, isEmpty);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('openBank - успешно открывает банк после startPayment', () async {
|
||||||
|
const planId = 123;
|
||||||
|
|
||||||
|
when(
|
||||||
|
mockPaymentRepository.startPaymentProcess(planId: planId),
|
||||||
|
).thenAnswer((_) async => alfaPayment);
|
||||||
|
when(
|
||||||
|
mockSbpRepository.getInstalledBanks(),
|
||||||
|
).thenAnswer((_) async => [installedBank]);
|
||||||
|
when(
|
||||||
|
mockSbpRepository.openBank(
|
||||||
|
url: alfaPayment.paymentUrl,
|
||||||
|
bank: installedBank,
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => true);
|
||||||
|
|
||||||
|
await cubit.startPayment(planId: planId);
|
||||||
|
final result = await cubit.openBank(bank: installedBank);
|
||||||
|
|
||||||
|
expect(result, isTrue);
|
||||||
|
verify(
|
||||||
|
mockSbpRepository.openBank(
|
||||||
|
url: alfaPayment.paymentUrl,
|
||||||
|
bank: installedBank,
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'openBank - без checkoutUrl возвращает false и не вызывает SbpRepository',
|
||||||
|
() async {
|
||||||
|
final result = await cubit.openBank(bank: installedBank);
|
||||||
|
|
||||||
|
expect(result, isFalse);
|
||||||
|
verifyNever(
|
||||||
|
mockSbpRepository.openBank(
|
||||||
|
url: anyNamed('url'),
|
||||||
|
bank: anyNamed('bank'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('openBank - возвращает false когда банк не открылся', () async {
|
||||||
|
const planId = 123;
|
||||||
|
|
||||||
|
when(
|
||||||
|
mockPaymentRepository.startPaymentProcess(planId: planId),
|
||||||
|
).thenAnswer((_) async => alfaPayment);
|
||||||
|
when(
|
||||||
|
mockSbpRepository.getInstalledBanks(),
|
||||||
|
).thenAnswer((_) async => [installedBank]);
|
||||||
|
when(
|
||||||
|
mockSbpRepository.openBank(
|
||||||
|
url: alfaPayment.paymentUrl,
|
||||||
|
bank: installedBank,
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => false);
|
||||||
|
|
||||||
|
await cubit.startPayment(planId: planId);
|
||||||
|
final result = await cubit.openBank(bank: installedBank);
|
||||||
|
|
||||||
|
expect(result, isFalse);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||||
|
import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart';
|
||||||
|
import 'package:eatvio_app/features/payment/presentation/widgets/promo_code_gate_bottom_sheet.dart';
|
||||||
|
import 'package:eatvio_app/generated/l10n.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
group('PromoCodeGateBottomSheet', () {
|
||||||
|
Widget createTestWidget({required ValueChanged<bool?> onResult}) {
|
||||||
|
return MaterialApp(
|
||||||
|
theme: ThemeHelper().themeData(),
|
||||||
|
localizationsDelegates: const [
|
||||||
|
S.delegate,
|
||||||
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
GlobalCupertinoLocalizations.delegate,
|
||||||
|
],
|
||||||
|
supportedLocales: const [Locale('ru')],
|
||||||
|
home: Scaffold(
|
||||||
|
body: PromoCodeGateBottomSheet(onResult: onResult),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
testWidgets('показывает заголовок, BottomSheetHandlerBar и кнопки Да/Нет', (tester) async {
|
||||||
|
await tester.pumpWidget(createTestWidget(onResult: (_) {}));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.byType(BottomSheetHandlerBar), findsOneWidget);
|
||||||
|
expect(find.text('У вас есть промокод?'), findsOneWidget);
|
||||||
|
expect(find.text('Да'), findsOneWidget);
|
||||||
|
expect(find.text('Нет'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('tap Да вызывает onResult(true)', (tester) async {
|
||||||
|
bool? result;
|
||||||
|
await tester.pumpWidget(
|
||||||
|
createTestWidget(onResult: (v) => result = v),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await tester.tap(find.text('Да'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(result, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('tap Нет вызывает onResult(false)', (tester) async {
|
||||||
|
bool? result;
|
||||||
|
await tester.pumpWidget(
|
||||||
|
createTestWidget(onResult: (v) => result = v),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await tester.tap(find.text('Нет'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(result, isFalse);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||||
|
import 'package:eatvio_app/features/payment/presentation/widgets/promo_code_modal.dart';
|
||||||
|
import 'package:eatvio_app/generated/l10n.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
group('ItvioPromoCodeModal', () {
|
||||||
|
Widget createTestWidget({
|
||||||
|
Future<PromoValidation> Function(String)? onValidate,
|
||||||
|
}) {
|
||||||
|
return MaterialApp(
|
||||||
|
theme: ThemeHelper().themeData(),
|
||||||
|
localizationsDelegates: const [
|
||||||
|
S.delegate,
|
||||||
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
GlobalCupertinoLocalizations.delegate,
|
||||||
|
],
|
||||||
|
supportedLocales: const [Locale('ru')],
|
||||||
|
home: Builder(
|
||||||
|
builder: (context) => ElevatedButton(
|
||||||
|
onPressed: () => ItvioPromoCodeModal.show(
|
||||||
|
context,
|
||||||
|
onValidate: onValidate,
|
||||||
|
),
|
||||||
|
child: const Text('Open'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
testWidgets('показывает заголовок Есть промокод? и кнопки Применить/Пропустить', (tester) async {
|
||||||
|
await tester.pumpWidget(createTestWidget());
|
||||||
|
await tester.tap(find.text('Open'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Есть промокод?'), findsOneWidget);
|
||||||
|
expect(find.text('Применить'), findsOneWidget);
|
||||||
|
expect(find.text('Пропустить'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Применить с пустым полем показывает ошибку и не закрывает', (tester) async {
|
||||||
|
await tester.pumpWidget(createTestWidget());
|
||||||
|
await tester.tap(find.text('Open'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.text('Применить'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.text('Введите промокод'), findsAtLeast(1));
|
||||||
|
expect(find.text('Применить'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Применить с вводом вызывает onValidate и показывает ошибку', (tester) async {
|
||||||
|
PromoValidation? captured;
|
||||||
|
await tester.pumpWidget(
|
||||||
|
createTestWidget(
|
||||||
|
onValidate: (code) async {
|
||||||
|
captured ??= const PromoValidation.failure('Промокод не найден или истёк');
|
||||||
|
return captured!;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.tap(find.text('Open'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.enterText(find.byType(TextField), 'TEST123');
|
||||||
|
await tester.tap(find.text('Применить'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(captured, isNotNull);
|
||||||
|
expect(find.text('Промокод не найден или истёк'), findsOneWidget);
|
||||||
|
expect(find.text('Применить'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Пропустить закрывает шторку', (tester) async {
|
||||||
|
await tester.pumpWidget(createTestWidget());
|
||||||
|
await tester.tap(find.text('Open'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.text('Пропустить'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Есть промокод?'), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('отсутствует UI applied-состояния (Промокод применён)', (tester) async {
|
||||||
|
await tester.pumpWidget(createTestWidget());
|
||||||
|
await tester.tap(find.text('Open'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Промокод применён!'), findsNothing);
|
||||||
|
expect(find.text('Продолжить'), findsNothing);
|
||||||
|
expect(find.text('Убрать промокод'), findsNothing);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||||
|
import 'package:eatvio_app/core/widgets/loader.dart';
|
||||||
|
import 'package:eatvio_app/features/chat/models/payment_model.dart';
|
||||||
|
import 'package:eatvio_app/features/payment/presentation/cubit/consultation_payment_cubit/consultation_payment_cubit.dart';
|
||||||
|
import 'package:eatvio_app/features/payment/presentation/widgets/sbp_banks_bottom_sheet.dart';
|
||||||
|
import 'package:eatvio_app/features/payment/repositories/payment_repository.dart';
|
||||||
|
import 'package:eatvio_app/features/payment/repositories/sbp_repository.dart';
|
||||||
|
import 'package:eatvio_app/generated/l10n.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mockito/annotations.dart';
|
||||||
|
import 'package:mockito/mockito.dart';
|
||||||
|
import 'package:sbp/models/c2bmembers_model.dart';
|
||||||
|
|
||||||
|
import '../../../../fixtures/fixture_reader.dart';
|
||||||
|
import 'sbp_banks_bottom_sheet_test.mocks.dart';
|
||||||
|
|
||||||
|
@GenerateNiceMocks([
|
||||||
|
MockSpec<PaymentRepository>(),
|
||||||
|
MockSpec<SbpRepository>(),
|
||||||
|
])
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
group('SbpBanksBottomSheet', () {
|
||||||
|
late MockPaymentRepository mockPaymentRepository;
|
||||||
|
late MockSbpRepository mockSbpRepository;
|
||||||
|
late ConsultationPaymentCubit cubit;
|
||||||
|
late PaymentModel alfaPayment;
|
||||||
|
|
||||||
|
final installedBank = C2bmemberModel.fromJson(const {
|
||||||
|
'bankName': 'Сбербанк',
|
||||||
|
'logoURL': 'https://qr.nspk.ru/proxyapp/logo/bank100000000111.png',
|
||||||
|
'schema': 'bank100000000111',
|
||||||
|
'package_name': 'ru.sberbankmobile',
|
||||||
|
'icon': '',
|
||||||
|
});
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
mockPaymentRepository = MockPaymentRepository();
|
||||||
|
mockSbpRepository = MockSbpRepository();
|
||||||
|
cubit = ConsultationPaymentCubit(
|
||||||
|
paymentRepository: mockPaymentRepository,
|
||||||
|
sbpRepository: mockSbpRepository,
|
||||||
|
);
|
||||||
|
alfaPayment = PaymentModel.fromJson(
|
||||||
|
FixtureReader.readAsMap('chat/payments/payment_response_alfa_bank.json'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() {
|
||||||
|
cubit.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
Widget createTestWidget({required int planId}) {
|
||||||
|
return MaterialApp(
|
||||||
|
theme: ThemeHelper().themeData(),
|
||||||
|
localizationsDelegates: const [
|
||||||
|
S.delegate,
|
||||||
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
GlobalCupertinoLocalizations.delegate,
|
||||||
|
],
|
||||||
|
supportedLocales: const [Locale('ru')],
|
||||||
|
home: Scaffold(
|
||||||
|
body: SbpBanksBottomSheet(
|
||||||
|
planId: planId,
|
||||||
|
cubit: cubit,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
testWidgets('показывает Loader пока идёт startPayment', (tester) async {
|
||||||
|
const planId = 12;
|
||||||
|
final completer = Completer<PaymentModel>();
|
||||||
|
|
||||||
|
when(
|
||||||
|
mockPaymentRepository.startPaymentProcess(planId: planId),
|
||||||
|
).thenAnswer((_) => completer.future);
|
||||||
|
|
||||||
|
await tester.pumpWidget(createTestWidget(planId: planId));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.byType(Loader), findsOneWidget);
|
||||||
|
expect(find.text('Сбербанк'), findsNothing);
|
||||||
|
|
||||||
|
completer.complete(alfaPayment);
|
||||||
|
when(mockSbpRepository.getInstalledBanks()).thenAnswer((_) async => [installedBank]);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('вызывает startPaymentProcess с переданным planId', (tester) async {
|
||||||
|
const planId = 12;
|
||||||
|
|
||||||
|
when(
|
||||||
|
mockPaymentRepository.startPaymentProcess(planId: planId),
|
||||||
|
).thenAnswer((_) async => alfaPayment);
|
||||||
|
when(mockSbpRepository.getInstalledBanks()).thenAnswer((_) async => [installedBank]);
|
||||||
|
|
||||||
|
await tester.pumpWidget(createTestWidget(planId: planId));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
verify(mockPaymentRepository.startPaymentProcess(planId: planId)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('после успеха показывает список банков и скрывает Loader', (tester) async {
|
||||||
|
const planId = 12;
|
||||||
|
|
||||||
|
when(
|
||||||
|
mockPaymentRepository.startPaymentProcess(planId: planId),
|
||||||
|
).thenAnswer((_) async => alfaPayment);
|
||||||
|
when(mockSbpRepository.getInstalledBanks()).thenAnswer((_) async => [installedBank]);
|
||||||
|
|
||||||
|
await tester.pumpWidget(createTestWidget(planId: planId));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(Loader), findsNothing);
|
||||||
|
expect(find.text('Выберите банк'), findsOneWidget);
|
||||||
|
expect(find.text('Сбербанк'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('tap по банку вызывает openBank с checkout_url', (tester) async {
|
||||||
|
const planId = 12;
|
||||||
|
|
||||||
|
when(
|
||||||
|
mockPaymentRepository.startPaymentProcess(planId: planId),
|
||||||
|
).thenAnswer((_) async => alfaPayment);
|
||||||
|
when(mockSbpRepository.getInstalledBanks()).thenAnswer((_) async => [installedBank]);
|
||||||
|
when(
|
||||||
|
mockSbpRepository.openBank(
|
||||||
|
url: alfaPayment.paymentUrl,
|
||||||
|
bank: installedBank,
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => true);
|
||||||
|
|
||||||
|
await tester.pumpWidget(createTestWidget(planId: planId));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.text('Сбербанк'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
verify(
|
||||||
|
mockSbpRepository.openBank(
|
||||||
|
url: alfaPayment.paymentUrl,
|
||||||
|
bank: installedBank,
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('при ошибке API показывает сообщение об ошибке', (tester) async {
|
||||||
|
const planId = 12;
|
||||||
|
|
||||||
|
when(
|
||||||
|
mockPaymentRepository.startPaymentProcess(planId: planId),
|
||||||
|
).thenThrow(Exception('Payment start failed'));
|
||||||
|
|
||||||
|
await tester.pumpWidget(createTestWidget(planId: planId));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(Loader), findsNothing);
|
||||||
|
expect(find.text('Сбербанк'), findsNothing);
|
||||||
|
expect(
|
||||||
|
find.text('Не удалось начать оплату. Попробуйте позже'),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('при пустом списке банков показывает empty-сообщение', (tester) async {
|
||||||
|
const planId = 12;
|
||||||
|
|
||||||
|
when(
|
||||||
|
mockPaymentRepository.startPaymentProcess(planId: planId),
|
||||||
|
).thenAnswer((_) async => alfaPayment);
|
||||||
|
when(mockSbpRepository.getInstalledBanks()).thenAnswer((_) async => []);
|
||||||
|
|
||||||
|
await tester.pumpWidget(createTestWidget(planId: planId));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(Loader), findsNothing);
|
||||||
|
expect(find.text('Нет установленных банков'), findsOneWidget);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import 'package:eatvio_app/features/chat/models/payment_model.dart';
|
||||||
|
import 'package:eatvio_app/features/chat/services/chat_service.dart';
|
||||||
|
import 'package:eatvio_app/features/payment/repositories/payment_repository.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mockito/annotations.dart';
|
||||||
|
import 'package:mockito/mockito.dart';
|
||||||
|
|
||||||
|
import '../../../fixtures/fixture_reader.dart';
|
||||||
|
import 'payment_repository_test.mocks.dart';
|
||||||
|
|
||||||
|
@GenerateNiceMocks([MockSpec<ChatService>()])
|
||||||
|
void main() {
|
||||||
|
group('PaymentRepository', () {
|
||||||
|
late PaymentRepository paymentRepository;
|
||||||
|
late MockChatService mockChatService;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
mockChatService = MockChatService();
|
||||||
|
paymentRepository = PaymentRepository(chatService: mockChatService);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'startPaymentProcess - возвращает PaymentModel из ответа alfa-bank',
|
||||||
|
() async {
|
||||||
|
const planId = 123;
|
||||||
|
final paymentFixture = FixtureReader.readAsMap(
|
||||||
|
'chat/payments/payment_response_alfa_bank.json',
|
||||||
|
);
|
||||||
|
final expectedPayment = PaymentModel.fromJson(paymentFixture);
|
||||||
|
final requestBody = Map<String, String>.from(
|
||||||
|
FixtureReader.readAsMap('chat/payments/payment_alfabank_request.json'),
|
||||||
|
);
|
||||||
|
|
||||||
|
when(
|
||||||
|
mockChatService.getPaymentLink(id: planId, body: requestBody),
|
||||||
|
).thenAnswer((_) async => expectedPayment);
|
||||||
|
|
||||||
|
final result = await paymentRepository.startPaymentProcess(planId: planId);
|
||||||
|
|
||||||
|
expect(result, isA<PaymentModel>());
|
||||||
|
expect(result.paymentUrl, 'https://qr.nspk.ru/ASDFASDFASDFASDF');
|
||||||
|
expect(result.orderId, 'alfa_c29_k15138_t1712345678_r1234');
|
||||||
|
expect(result.paymentProvider, 'alfa-bank');
|
||||||
|
verify(
|
||||||
|
mockChatService.getPaymentLink(
|
||||||
|
id: planId,
|
||||||
|
body: {'payment_provider': 'alfa-bank'},
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'startPaymentProcess - передаёт payment_provider alfa-bank в тело запроса',
|
||||||
|
() async {
|
||||||
|
const planId = 42;
|
||||||
|
final paymentFixture = FixtureReader.readAsMap(
|
||||||
|
'chat/payments/payment_response_alfa_bank.json',
|
||||||
|
);
|
||||||
|
final expectedPayment = PaymentModel.fromJson(paymentFixture);
|
||||||
|
|
||||||
|
when(
|
||||||
|
mockChatService.getPaymentLink(
|
||||||
|
id: planId,
|
||||||
|
body: {'payment_provider': 'alfa-bank'},
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => expectedPayment);
|
||||||
|
|
||||||
|
await paymentRepository.startPaymentProcess(planId: planId);
|
||||||
|
|
||||||
|
verify(
|
||||||
|
mockChatService.getPaymentLink(
|
||||||
|
id: planId,
|
||||||
|
body: {'payment_provider': 'alfa-bank'},
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('startPaymentProcess - пробрасывает ошибку сервиса', () async {
|
||||||
|
const planId = 123;
|
||||||
|
final error = Exception('Payment start failed');
|
||||||
|
|
||||||
|
when(
|
||||||
|
mockChatService.getPaymentLink(
|
||||||
|
id: planId,
|
||||||
|
body: {'payment_provider': 'alfa-bank'},
|
||||||
|
),
|
||||||
|
).thenThrow(error);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => paymentRepository.startPaymentProcess(planId: planId),
|
||||||
|
throwsA(isA<Exception>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
118
test/features/payment/repositories/sbp_repository_test.dart
Normal file
118
test/features/payment/repositories/sbp_repository_test.dart
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import 'package:eatvio_app/features/payment/repositories/sbp_repository.dart';
|
||||||
|
import 'package:eatvio_app/features/payment/services/sbp_service.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mockito/annotations.dart';
|
||||||
|
import 'package:mockito/mockito.dart';
|
||||||
|
import 'package:sbp/models/c2bmembers_model.dart';
|
||||||
|
|
||||||
|
import 'sbp_repository_test.mocks.dart';
|
||||||
|
|
||||||
|
@GenerateNiceMocks([MockSpec<SbpService>()])
|
||||||
|
void main() {
|
||||||
|
group('SbpRepository', () {
|
||||||
|
late SbpRepository sbpRepository;
|
||||||
|
late MockSbpService mockSbpService;
|
||||||
|
|
||||||
|
final installedBank = C2bmemberModel.fromJson(const {
|
||||||
|
'bankName': 'Сбербанк',
|
||||||
|
'logoURL': 'https://qr.nspk.ru/proxyapp/logo/bank100000000111.png',
|
||||||
|
'schema': 'bank100000000111',
|
||||||
|
'package_name': 'ru.sberbankmobile',
|
||||||
|
'icon': 'packages/sbp/assets/icons/sber.png',
|
||||||
|
});
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
mockSbpService = MockSbpService();
|
||||||
|
sbpRepository = SbpRepository(sbpService: mockSbpService);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('getInstalledBanks', () {
|
||||||
|
test(
|
||||||
|
'делегирует сервису и возвращает список установленных банков',
|
||||||
|
() async {
|
||||||
|
when(
|
||||||
|
mockSbpService.getInstalledBanks(),
|
||||||
|
).thenAnswer((_) async => [installedBank]);
|
||||||
|
|
||||||
|
final result = await sbpRepository.getInstalledBanks();
|
||||||
|
|
||||||
|
expect(result, [installedBank]);
|
||||||
|
verify(mockSbpService.getInstalledBanks()).called(1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'возвращает пустой список, когда сервис вернул пустой список',
|
||||||
|
() async {
|
||||||
|
when(mockSbpService.getInstalledBanks()).thenAnswer((_) async => []);
|
||||||
|
|
||||||
|
final result = await sbpRepository.getInstalledBanks();
|
||||||
|
|
||||||
|
expect(result, isEmpty);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'бросает exception при ошибке нативного плагина (graceful)',
|
||||||
|
() async {
|
||||||
|
when(
|
||||||
|
mockSbpService.getInstalledBanks(),
|
||||||
|
).thenThrow(MissingPluginException('no sbp handler'));
|
||||||
|
|
||||||
|
expect(() => sbpRepository.getInstalledBanks(), throwsException);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('openBank', () {
|
||||||
|
const checkoutUrl = 'https://qr.nspk.ru/ASDFASDFASDFASDF';
|
||||||
|
|
||||||
|
test(
|
||||||
|
'делегирует сервису и возвращает true при успешном запуске',
|
||||||
|
() async {
|
||||||
|
when(
|
||||||
|
mockSbpService.openBank(url: checkoutUrl, bank: installedBank),
|
||||||
|
).thenAnswer((_) async => true);
|
||||||
|
|
||||||
|
final result = await sbpRepository.openBank(
|
||||||
|
url: checkoutUrl,
|
||||||
|
bank: installedBank,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result, isTrue);
|
||||||
|
verify(
|
||||||
|
mockSbpService.openBank(url: checkoutUrl, bank: installedBank),
|
||||||
|
).called(1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('возвращает false, когда сервис вернул false', () async {
|
||||||
|
when(
|
||||||
|
mockSbpService.openBank(url: checkoutUrl, bank: installedBank),
|
||||||
|
).thenAnswer((_) async => false);
|
||||||
|
|
||||||
|
final result = await sbpRepository.openBank(
|
||||||
|
url: checkoutUrl,
|
||||||
|
bank: installedBank,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'возвращает false при ошибке нативного плагина (graceful)',
|
||||||
|
() async {
|
||||||
|
when(
|
||||||
|
mockSbpService.openBank(url: checkoutUrl, bank: installedBank),
|
||||||
|
).thenThrow(MissingPluginException('no sbp handler'));
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => sbpRepository.openBank(url: checkoutUrl, bank: installedBank),
|
||||||
|
throwsException,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
119
test/features/payment/services/sbp_service_test.dart
Normal file
119
test/features/payment/services/sbp_service_test.dart
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import 'package:eatvio_app/features/payment/services/sbp_service.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:sbp/models/c2bmembers_model.dart';
|
||||||
|
|
||||||
|
/// Тесты реализации [SbpServiceImpl] — обёртки над статичным классом [Sbp].
|
||||||
|
///
|
||||||
|
/// Поскольку unit-тесты исполняются на хост-машине (не iOS), срабатывает
|
||||||
|
/// Android-ветка плагина: `getInstalledBanks` возвращает список банков через
|
||||||
|
/// `package_name`, а `openBank` подставляет схему банка в начало checkout_url.
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
const channel = MethodChannel('sbp');
|
||||||
|
|
||||||
|
/// Банк-фикстура: Сбербанк (присутствует в справочнике c2bmembersData пакета).
|
||||||
|
final sberbank = C2bmemberModel.fromJson(const {
|
||||||
|
'bankName': 'Сбербанк',
|
||||||
|
'logoURL': 'https://qr.nspk.ru/proxyapp/logo/bank100000000111.png',
|
||||||
|
'schema': 'bank100000000111',
|
||||||
|
'package_name': 'ru.sberbankmobile',
|
||||||
|
'icon': 'packages/sbp/assets/icons/sber.png',
|
||||||
|
});
|
||||||
|
|
||||||
|
late SbpService sbpService;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
sbpService = const SbpServiceImpl();
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() {
|
||||||
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
|
.setMockMethodCallHandler(channel, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('SbpServiceImpl.getInstalledBanks', () {
|
||||||
|
test('возвращает список банков, установленных на устройстве (Android)', () async {
|
||||||
|
// Плагин (Android) возвращает список установленных банков в формате
|
||||||
|
// {name, bitmap, package_name}; обёртка сопоставляет со справочником.
|
||||||
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
|
.setMockMethodCallHandler(channel, (MethodCall call) async {
|
||||||
|
return [
|
||||||
|
{'name': 'Сбербанк', 'bitmap': null, 'package_name': 'ru.sberbankmobile'},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
final result = await sbpService.getInstalledBanks();
|
||||||
|
|
||||||
|
expect(result, isNotEmpty);
|
||||||
|
expect(
|
||||||
|
result.any(
|
||||||
|
(bank) =>
|
||||||
|
bank.bankName == 'Сбербанк' && bank.packageName == 'ru.sberbankmobile',
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('возвращает пустой список, если нет установленных банков', () async {
|
||||||
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
|
.setMockMethodCallHandler(channel, (MethodCall call) async {
|
||||||
|
return <Map<String, dynamic>>[];
|
||||||
|
});
|
||||||
|
|
||||||
|
final result = await sbpService.getInstalledBanks();
|
||||||
|
|
||||||
|
expect(result, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('пробрасывает ошибку плагина при отсутствии нативного обработчика', () async {
|
||||||
|
// Обработчик не зарегистрирован → invokeMethod выбрасывает
|
||||||
|
// MissingPluginException. Сервис должен пробросить её наверх.
|
||||||
|
expect(
|
||||||
|
sbpService.getInstalledBanks(),
|
||||||
|
throwsA(isA<MissingPluginException>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('SbpServiceImpl.openBank', () {
|
||||||
|
test('вызывает нативный openBank с подстановкой схемы банка в url и возвращает true', () async {
|
||||||
|
const checkoutUrl = 'https://qr.nspk.ru/ASDFASDFASDFASDF';
|
||||||
|
String? receivedUrl;
|
||||||
|
String? receivedPackageName;
|
||||||
|
|
||||||
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
|
.setMockMethodCallHandler(channel, (MethodCall call) async {
|
||||||
|
if (call.method == 'openBank') {
|
||||||
|
final args = call.arguments as Map<dynamic, dynamic>;
|
||||||
|
receivedUrl = args['url'] as String?;
|
||||||
|
receivedPackageName = args['package_name'] as String?;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
final result = await sbpService.openBank(url: checkoutUrl, bank: sberbank);
|
||||||
|
|
||||||
|
expect(result, isTrue);
|
||||||
|
// Android-ветка заменяет схему "https" на schema банка в начале url.
|
||||||
|
expect(receivedUrl, 'bank100000000111://qr.nspk.ru/ASDFASDFASDFASDF');
|
||||||
|
expect(receivedPackageName, 'ru.sberbankmobile');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('возвращает false при неудачном запуске банковского приложения', () async {
|
||||||
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
|
.setMockMethodCallHandler(channel, (MethodCall call) async {
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
final result = await sbpService.openBank(
|
||||||
|
url: 'https://qr.nspk.ru/ASDFASDFASDFASDF',
|
||||||
|
bank: sberbank,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result, isFalse);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
void main() {
|
void main() {
|
||||||
group('ChosenPlanStateEnum', () {
|
group('ChosenPlanStateEnum', () {
|
||||||
test('has all expected values', () {
|
test('has all expected values', () {
|
||||||
expect(ChosenPlanStateEnum.values.length, 15);
|
expect(ChosenPlanStateEnum.values.length, 16);
|
||||||
expect(ChosenPlanStateEnum.values, contains(ChosenPlanStateEnum.stateInited));
|
expect(ChosenPlanStateEnum.values, contains(ChosenPlanStateEnum.stateInited));
|
||||||
expect(ChosenPlanStateEnum.values, contains(ChosenPlanStateEnum.stateStartDateHasBeenSet));
|
expect(ChosenPlanStateEnum.values, contains(ChosenPlanStateEnum.stateStartDateHasBeenSet));
|
||||||
expect(ChosenPlanStateEnum.values, contains(ChosenPlanStateEnum.stateInCoordination));
|
expect(ChosenPlanStateEnum.values, contains(ChosenPlanStateEnum.stateInCoordination));
|
||||||
@@ -20,6 +20,7 @@ void main() {
|
|||||||
expect(ChosenPlanStateEnum.values, contains(ChosenPlanStateEnum.stateDone));
|
expect(ChosenPlanStateEnum.values, contains(ChosenPlanStateEnum.stateDone));
|
||||||
expect(ChosenPlanStateEnum.values, contains(ChosenPlanStateEnum.personalDietSet));
|
expect(ChosenPlanStateEnum.values, contains(ChosenPlanStateEnum.personalDietSet));
|
||||||
expect(ChosenPlanStateEnum.values, contains(ChosenPlanStateEnum.stateChargeWasCollected));
|
expect(ChosenPlanStateEnum.values, contains(ChosenPlanStateEnum.stateChargeWasCollected));
|
||||||
|
expect(ChosenPlanStateEnum.values, contains(ChosenPlanStateEnum.statePaymentFailed));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getShowableChatBadgeStates returns correct states', () {
|
test('getShowableChatBadgeStates returns correct states', () {
|
||||||
@@ -35,6 +36,7 @@ void main() {
|
|||||||
expect(states, contains(ChosenPlanStateEnum.stateDone));
|
expect(states, contains(ChosenPlanStateEnum.stateDone));
|
||||||
expect(states, contains(ChosenPlanStateEnum.stateInProgress));
|
expect(states, contains(ChosenPlanStateEnum.stateInProgress));
|
||||||
expect(states, contains(ChosenPlanStateEnum.personalDietSet));
|
expect(states, contains(ChosenPlanStateEnum.personalDietSet));
|
||||||
|
expect(states, isNot(contains(ChosenPlanStateEnum.statePaymentFailed)));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('isStateAfterPaidState returns false for states before paid', () {
|
test('isStateAfterPaidState returns false for states before paid', () {
|
||||||
@@ -43,6 +45,7 @@ void main() {
|
|||||||
expect(ChosenPlanStateEnum.stateInCoordination.isStateAfterPaidState(), isFalse);
|
expect(ChosenPlanStateEnum.stateInCoordination.isStateAfterPaidState(), isFalse);
|
||||||
expect(ChosenPlanStateEnum.stateStartDateHasBeenSet.isStateAfterPaidState(), isFalse);
|
expect(ChosenPlanStateEnum.stateStartDateHasBeenSet.isStateAfterPaidState(), isFalse);
|
||||||
expect(ChosenPlanStateEnum.statePendingPayment.isStateAfterPaidState(), isFalse);
|
expect(ChosenPlanStateEnum.statePendingPayment.isStateAfterPaidState(), isFalse);
|
||||||
|
expect(ChosenPlanStateEnum.statePaymentFailed.isStateAfterPaidState(), isFalse);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('isStateAfterPaidState returns true for states after paid', () {
|
test('isStateAfterPaidState returns true for states after paid', () {
|
||||||
@@ -70,6 +73,7 @@ void main() {
|
|||||||
expect(activeStates, contains(ChosenPlanStateEnum.stateComplained));
|
expect(activeStates, contains(ChosenPlanStateEnum.stateComplained));
|
||||||
expect(activeStates, contains(ChosenPlanStateEnum.stateDone));
|
expect(activeStates, contains(ChosenPlanStateEnum.stateDone));
|
||||||
expect(activeStates, contains(ChosenPlanStateEnum.personalDietSet));
|
expect(activeStates, contains(ChosenPlanStateEnum.personalDietSet));
|
||||||
|
expect(activeStates, isNot(contains(ChosenPlanStateEnum.statePaymentFailed)));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
|
|||||||
import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart';
|
import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import '../../../fixtures/fixture_reader.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('ChosenPlanModel', () {
|
group('ChosenPlanModel', () {
|
||||||
test('создает ChosenPlanModel с правильными значениями', () {
|
test('создает ChosenPlanModel с правильными значениями', () {
|
||||||
@@ -140,6 +142,60 @@ void main() {
|
|||||||
expect(json['consultant_id'], 1);
|
expect(json['consultant_id'], 1);
|
||||||
expect(json.containsKey('plan'), isTrue);
|
expect(json.containsKey('plan'), isTrue);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('fromJson payment polling statuses', () {
|
||||||
|
test('fromJson парсит pending_payment из фикстуры', () {
|
||||||
|
final json = FixtureReader.readAsMap(
|
||||||
|
'chat/chosen_plan/chosen_plan_pending_payment_response.json',
|
||||||
|
);
|
||||||
|
|
||||||
|
final model = ChosenPlanModel.fromJson(json);
|
||||||
|
|
||||||
|
expect(model.id, 1);
|
||||||
|
expect(model.optionType, '1');
|
||||||
|
expect(model.chosenPlanState, ChosenPlanStateEnum.statePendingPayment);
|
||||||
|
expect(model.chatId, 'chat-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fromJson парсит paid из фикстуры', () {
|
||||||
|
final json = FixtureReader.readAsMap(
|
||||||
|
'chat/chosen_plan/chosen_plan_paid_response.json',
|
||||||
|
);
|
||||||
|
|
||||||
|
final model = ChosenPlanModel.fromJson(json);
|
||||||
|
|
||||||
|
expect(model.id, 1);
|
||||||
|
expect(model.optionType, '1');
|
||||||
|
expect(model.chosenPlanState, ChosenPlanStateEnum.statePaid);
|
||||||
|
expect(model.chatId, 'chat-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fromJson парсит payment_failed из фикстуры', () {
|
||||||
|
final json = FixtureReader.readAsMap(
|
||||||
|
'chat/chosen_plan/chosen_plan_payment_failed_response.json',
|
||||||
|
);
|
||||||
|
|
||||||
|
final model = ChosenPlanModel.fromJson(json);
|
||||||
|
|
||||||
|
expect(model.id, 1);
|
||||||
|
expect(model.optionType, '1');
|
||||||
|
expect(model.chosenPlanState, ChosenPlanStateEnum.statePaymentFailed);
|
||||||
|
expect(model.chatId, 'chat-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fromJson парсит canceled_by_the_initiator из фикстуры', () {
|
||||||
|
final json = FixtureReader.readAsMap(
|
||||||
|
'chat/chosen_plan/chosen_plan_canceled_by_the_initiator_response.json',
|
||||||
|
);
|
||||||
|
|
||||||
|
final model = ChosenPlanModel.fromJson(json);
|
||||||
|
|
||||||
|
expect(model.id, 1);
|
||||||
|
expect(model.optionType, '1');
|
||||||
|
expect(model.chosenPlanState, ChosenPlanStateEnum.stateCanceledByTheInitiator);
|
||||||
|
expect(model.chatId, 'chat-1');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group('parseChosenPlan', () {
|
group('parseChosenPlan', () {
|
||||||
|
|||||||
47
test/fixtures/chat/chosen_plan/chosen_plan_canceled_by_the_initiator_response.json
vendored
Normal file
47
test/fixtures/chat/chosen_plan/chosen_plan_canceled_by_the_initiator_response.json
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"option_type": "1",
|
||||||
|
"start_date": "2024-01-01T00:00:00Z",
|
||||||
|
"last_state": "canceled_by_the_initiator",
|
||||||
|
"chat_id": "chat-1",
|
||||||
|
"consultant_id": 1,
|
||||||
|
"plan": {
|
||||||
|
"id": 1,
|
||||||
|
"slug": "test-plan",
|
||||||
|
"name": "Test Plan",
|
||||||
|
"image": "https://example.com/plan.jpg",
|
||||||
|
"duration": 30,
|
||||||
|
"state": "active",
|
||||||
|
"type": {
|
||||||
|
"fit": true,
|
||||||
|
"slim": false,
|
||||||
|
"behavior": false,
|
||||||
|
"wellness": false
|
||||||
|
},
|
||||||
|
"description_md": "Plan description",
|
||||||
|
"packets_count": 1,
|
||||||
|
"packet1_options": {
|
||||||
|
"items": ["Item 1", "Item 2"],
|
||||||
|
"price": 1500.0,
|
||||||
|
"short": "Option 1",
|
||||||
|
"description_md": "Description",
|
||||||
|
"has_personal_diet": false
|
||||||
|
},
|
||||||
|
"packet2_options": null,
|
||||||
|
"packet3_options": null,
|
||||||
|
"author": {
|
||||||
|
"author_slug": "author-1",
|
||||||
|
"author_name": "Author Name",
|
||||||
|
"author_profile_icon": "https://example.com/author.jpg",
|
||||||
|
"author_is_me": false
|
||||||
|
},
|
||||||
|
"total_likes": 10,
|
||||||
|
"total_views": 100,
|
||||||
|
"total_comments": 5,
|
||||||
|
"is_favorite": false,
|
||||||
|
"min_price_of_options": 1000.0,
|
||||||
|
"max_price_of_options": 2000.0,
|
||||||
|
"was_liked_by_me": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
47
test/fixtures/chat/chosen_plan/chosen_plan_charge_was_callected_response.json
vendored
Normal file
47
test/fixtures/chat/chosen_plan/chosen_plan_charge_was_callected_response.json
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"option_type": "1",
|
||||||
|
"start_date": "2024-01-01T00:00:00Z",
|
||||||
|
"last_state": "charge_was_collected",
|
||||||
|
"chat_id": "chat-1",
|
||||||
|
"consultant_id": 1,
|
||||||
|
"plan": {
|
||||||
|
"id": 1,
|
||||||
|
"slug": "test-plan",
|
||||||
|
"name": "Test Plan",
|
||||||
|
"image": "https://example.com/plan.jpg",
|
||||||
|
"duration": 30,
|
||||||
|
"state": "active",
|
||||||
|
"type": {
|
||||||
|
"fit": true,
|
||||||
|
"slim": false,
|
||||||
|
"behavior": false,
|
||||||
|
"wellness": false
|
||||||
|
},
|
||||||
|
"description_md": "Plan description",
|
||||||
|
"packets_count": 1,
|
||||||
|
"packet1_options": {
|
||||||
|
"items": ["Item 1", "Item 2"],
|
||||||
|
"price": 1500.0,
|
||||||
|
"short": "Option 1",
|
||||||
|
"description_md": "Description",
|
||||||
|
"has_personal_diet": false
|
||||||
|
},
|
||||||
|
"packet2_options": null,
|
||||||
|
"packet3_options": null,
|
||||||
|
"author": {
|
||||||
|
"author_slug": "author-1",
|
||||||
|
"author_name": "Author Name",
|
||||||
|
"author_profile_icon": "https://example.com/author.jpg",
|
||||||
|
"author_is_me": false
|
||||||
|
},
|
||||||
|
"total_likes": 10,
|
||||||
|
"total_views": 100,
|
||||||
|
"total_comments": 5,
|
||||||
|
"is_favorite": false,
|
||||||
|
"min_price_of_options": 1000.0,
|
||||||
|
"max_price_of_options": 2000.0,
|
||||||
|
"was_liked_by_me": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
47
test/fixtures/chat/chosen_plan/chosen_plan_complained_response.json
vendored
Normal file
47
test/fixtures/chat/chosen_plan/chosen_plan_complained_response.json
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"option_type": "1",
|
||||||
|
"start_date": "2024-01-01T00:00:00Z",
|
||||||
|
"last_state": "complained",
|
||||||
|
"chat_id": "chat-1",
|
||||||
|
"consultant_id": 1,
|
||||||
|
"plan": {
|
||||||
|
"id": 1,
|
||||||
|
"slug": "test-plan",
|
||||||
|
"name": "Test Plan",
|
||||||
|
"image": "https://example.com/plan.jpg",
|
||||||
|
"duration": 30,
|
||||||
|
"state": "active",
|
||||||
|
"type": {
|
||||||
|
"fit": true,
|
||||||
|
"slim": false,
|
||||||
|
"behavior": false,
|
||||||
|
"wellness": false
|
||||||
|
},
|
||||||
|
"description_md": "Plan description",
|
||||||
|
"packets_count": 1,
|
||||||
|
"packet1_options": {
|
||||||
|
"items": ["Item 1", "Item 2"],
|
||||||
|
"price": 1500.0,
|
||||||
|
"short": "Option 1",
|
||||||
|
"description_md": "Description",
|
||||||
|
"has_personal_diet": false
|
||||||
|
},
|
||||||
|
"packet2_options": null,
|
||||||
|
"packet3_options": null,
|
||||||
|
"author": {
|
||||||
|
"author_slug": "author-1",
|
||||||
|
"author_name": "Author Name",
|
||||||
|
"author_profile_icon": "https://example.com/author.jpg",
|
||||||
|
"author_is_me": false
|
||||||
|
},
|
||||||
|
"total_likes": 10,
|
||||||
|
"total_views": 100,
|
||||||
|
"total_comments": 5,
|
||||||
|
"is_favorite": false,
|
||||||
|
"min_price_of_options": 1000.0,
|
||||||
|
"max_price_of_options": 2000.0,
|
||||||
|
"was_liked_by_me": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
47
test/fixtures/chat/chosen_plan/chosen_plan_confirmed_by_consultant_response.json
vendored
Normal file
47
test/fixtures/chat/chosen_plan/chosen_plan_confirmed_by_consultant_response.json
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"option_type": "1",
|
||||||
|
"start_date": "2024-01-01T00:00:00Z",
|
||||||
|
"last_state": "confirmed_by_consultant",
|
||||||
|
"chat_id": "chat-1",
|
||||||
|
"consultant_id": 1,
|
||||||
|
"plan": {
|
||||||
|
"id": 1,
|
||||||
|
"slug": "test-plan",
|
||||||
|
"name": "Test Plan",
|
||||||
|
"image": "https://example.com/plan.jpg",
|
||||||
|
"duration": 30,
|
||||||
|
"state": "active",
|
||||||
|
"type": {
|
||||||
|
"fit": true,
|
||||||
|
"slim": false,
|
||||||
|
"behavior": false,
|
||||||
|
"wellness": false
|
||||||
|
},
|
||||||
|
"description_md": "Plan description",
|
||||||
|
"packets_count": 1,
|
||||||
|
"packet1_options": {
|
||||||
|
"items": ["Item 1", "Item 2"],
|
||||||
|
"price": 1500.0,
|
||||||
|
"short": "Option 1",
|
||||||
|
"description_md": "Description",
|
||||||
|
"has_personal_diet": false
|
||||||
|
},
|
||||||
|
"packet2_options": null,
|
||||||
|
"packet3_options": null,
|
||||||
|
"author": {
|
||||||
|
"author_slug": "author-1",
|
||||||
|
"author_name": "Author Name",
|
||||||
|
"author_profile_icon": "https://example.com/author.jpg",
|
||||||
|
"author_is_me": false
|
||||||
|
},
|
||||||
|
"total_likes": 10,
|
||||||
|
"total_views": 100,
|
||||||
|
"total_comments": 5,
|
||||||
|
"is_favorite": false,
|
||||||
|
"min_price_of_options": 1000.0,
|
||||||
|
"max_price_of_options": 2000.0,
|
||||||
|
"was_liked_by_me": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
47
test/fixtures/chat/chosen_plan/chosen_plan_done_response.json
vendored
Normal file
47
test/fixtures/chat/chosen_plan/chosen_plan_done_response.json
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"option_type": "1",
|
||||||
|
"start_date": "2024-01-01T00:00:00Z",
|
||||||
|
"last_state": "done",
|
||||||
|
"chat_id": "chat-1",
|
||||||
|
"consultant_id": 1,
|
||||||
|
"plan": {
|
||||||
|
"id": 1,
|
||||||
|
"slug": "test-plan",
|
||||||
|
"name": "Test Plan",
|
||||||
|
"image": "https://example.com/plan.jpg",
|
||||||
|
"duration": 30,
|
||||||
|
"state": "active",
|
||||||
|
"type": {
|
||||||
|
"fit": true,
|
||||||
|
"slim": false,
|
||||||
|
"behavior": false,
|
||||||
|
"wellness": false
|
||||||
|
},
|
||||||
|
"description_md": "Plan description",
|
||||||
|
"packets_count": 1,
|
||||||
|
"packet1_options": {
|
||||||
|
"items": ["Item 1", "Item 2"],
|
||||||
|
"price": 1500.0,
|
||||||
|
"short": "Option 1",
|
||||||
|
"description_md": "Description",
|
||||||
|
"has_personal_diet": false
|
||||||
|
},
|
||||||
|
"packet2_options": null,
|
||||||
|
"packet3_options": null,
|
||||||
|
"author": {
|
||||||
|
"author_slug": "author-1",
|
||||||
|
"author_name": "Author Name",
|
||||||
|
"author_profile_icon": "https://example.com/author.jpg",
|
||||||
|
"author_is_me": false
|
||||||
|
},
|
||||||
|
"total_likes": 10,
|
||||||
|
"total_views": 100,
|
||||||
|
"total_comments": 5,
|
||||||
|
"is_favorite": false,
|
||||||
|
"min_price_of_options": 1000.0,
|
||||||
|
"max_price_of_options": 2000.0,
|
||||||
|
"was_liked_by_me": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
47
test/fixtures/chat/chosen_plan/chosen_plan_in_progress_response.json
vendored
Normal file
47
test/fixtures/chat/chosen_plan/chosen_plan_in_progress_response.json
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"option_type": "1",
|
||||||
|
"start_date": "2024-01-01T00:00:00Z",
|
||||||
|
"last_state": "in_progress",
|
||||||
|
"chat_id": "chat-1",
|
||||||
|
"consultant_id": 1,
|
||||||
|
"plan": {
|
||||||
|
"id": 1,
|
||||||
|
"slug": "test-plan",
|
||||||
|
"name": "Test Plan",
|
||||||
|
"image": "https://example.com/plan.jpg",
|
||||||
|
"duration": 30,
|
||||||
|
"state": "active",
|
||||||
|
"type": {
|
||||||
|
"fit": true,
|
||||||
|
"slim": false,
|
||||||
|
"behavior": false,
|
||||||
|
"wellness": false
|
||||||
|
},
|
||||||
|
"description_md": "Plan description",
|
||||||
|
"packets_count": 1,
|
||||||
|
"packet1_options": {
|
||||||
|
"items": ["Item 1", "Item 2"],
|
||||||
|
"price": 1500.0,
|
||||||
|
"short": "Option 1",
|
||||||
|
"description_md": "Description",
|
||||||
|
"has_personal_diet": false
|
||||||
|
},
|
||||||
|
"packet2_options": null,
|
||||||
|
"packet3_options": null,
|
||||||
|
"author": {
|
||||||
|
"author_slug": "author-1",
|
||||||
|
"author_name": "Author Name",
|
||||||
|
"author_profile_icon": "https://example.com/author.jpg",
|
||||||
|
"author_is_me": false
|
||||||
|
},
|
||||||
|
"total_likes": 10,
|
||||||
|
"total_views": 100,
|
||||||
|
"total_comments": 5,
|
||||||
|
"is_favorite": false,
|
||||||
|
"min_price_of_options": 1000.0,
|
||||||
|
"max_price_of_options": 2000.0,
|
||||||
|
"was_liked_by_me": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
47
test/fixtures/chat/chosen_plan/chosen_plan_initied_response.json
vendored
Normal file
47
test/fixtures/chat/chosen_plan/chosen_plan_initied_response.json
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"option_type": "1",
|
||||||
|
"start_date": "2024-01-01T00:00:00Z",
|
||||||
|
"last_state": "initied",
|
||||||
|
"chat_id": "chat-1",
|
||||||
|
"consultant_id": 1,
|
||||||
|
"plan": {
|
||||||
|
"id": 1,
|
||||||
|
"slug": "test-plan",
|
||||||
|
"name": "Test Plan",
|
||||||
|
"image": "https://example.com/plan.jpg",
|
||||||
|
"duration": 30,
|
||||||
|
"state": "active",
|
||||||
|
"type": {
|
||||||
|
"fit": true,
|
||||||
|
"slim": false,
|
||||||
|
"behavior": false,
|
||||||
|
"wellness": false
|
||||||
|
},
|
||||||
|
"description_md": "Plan description",
|
||||||
|
"packets_count": 1,
|
||||||
|
"packet1_options": {
|
||||||
|
"items": ["Item 1", "Item 2"],
|
||||||
|
"price": 1500.0,
|
||||||
|
"short": "Option 1",
|
||||||
|
"description_md": "Description",
|
||||||
|
"has_personal_diet": false
|
||||||
|
},
|
||||||
|
"packet2_options": null,
|
||||||
|
"packet3_options": null,
|
||||||
|
"author": {
|
||||||
|
"author_slug": "author-1",
|
||||||
|
"author_name": "Author Name",
|
||||||
|
"author_profile_icon": "https://example.com/author.jpg",
|
||||||
|
"author_is_me": false
|
||||||
|
},
|
||||||
|
"total_likes": 10,
|
||||||
|
"total_views": 100,
|
||||||
|
"total_comments": 5,
|
||||||
|
"is_favorite": false,
|
||||||
|
"min_price_of_options": 1000.0,
|
||||||
|
"max_price_of_options": 2000.0,
|
||||||
|
"was_liked_by_me": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
47
test/fixtures/chat/chosen_plan/chosen_plan_last_day_finished_response.json
vendored
Normal file
47
test/fixtures/chat/chosen_plan/chosen_plan_last_day_finished_response.json
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"option_type": "1",
|
||||||
|
"start_date": "2024-01-01T00:00:00Z",
|
||||||
|
"last_state": "last_day_finished",
|
||||||
|
"chat_id": "chat-1",
|
||||||
|
"consultant_id": 1,
|
||||||
|
"plan": {
|
||||||
|
"id": 1,
|
||||||
|
"slug": "test-plan",
|
||||||
|
"name": "Test Plan",
|
||||||
|
"image": "https://example.com/plan.jpg",
|
||||||
|
"duration": 30,
|
||||||
|
"state": "active",
|
||||||
|
"type": {
|
||||||
|
"fit": true,
|
||||||
|
"slim": false,
|
||||||
|
"behavior": false,
|
||||||
|
"wellness": false
|
||||||
|
},
|
||||||
|
"description_md": "Plan description",
|
||||||
|
"packets_count": 1,
|
||||||
|
"packet1_options": {
|
||||||
|
"items": ["Item 1", "Item 2"],
|
||||||
|
"price": 1500.0,
|
||||||
|
"short": "Option 1",
|
||||||
|
"description_md": "Description",
|
||||||
|
"has_personal_diet": false
|
||||||
|
},
|
||||||
|
"packet2_options": null,
|
||||||
|
"packet3_options": null,
|
||||||
|
"author": {
|
||||||
|
"author_slug": "author-1",
|
||||||
|
"author_name": "Author Name",
|
||||||
|
"author_profile_icon": "https://example.com/author.jpg",
|
||||||
|
"author_is_me": false
|
||||||
|
},
|
||||||
|
"total_likes": 10,
|
||||||
|
"total_views": 100,
|
||||||
|
"total_comments": 5,
|
||||||
|
"is_favorite": false,
|
||||||
|
"min_price_of_options": 1000.0,
|
||||||
|
"max_price_of_options": 2000.0,
|
||||||
|
"was_liked_by_me": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
47
test/fixtures/chat/chosen_plan/chosen_plan_last_day_started_response.json
vendored
Normal file
47
test/fixtures/chat/chosen_plan/chosen_plan_last_day_started_response.json
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"option_type": "1",
|
||||||
|
"start_date": "2024-01-01T00:00:00Z",
|
||||||
|
"last_state": "last_day_started",
|
||||||
|
"chat_id": "chat-1",
|
||||||
|
"consultant_id": 1,
|
||||||
|
"plan": {
|
||||||
|
"id": 1,
|
||||||
|
"slug": "test-plan",
|
||||||
|
"name": "Test Plan",
|
||||||
|
"image": "https://example.com/plan.jpg",
|
||||||
|
"duration": 30,
|
||||||
|
"state": "active",
|
||||||
|
"type": {
|
||||||
|
"fit": true,
|
||||||
|
"slim": false,
|
||||||
|
"behavior": false,
|
||||||
|
"wellness": false
|
||||||
|
},
|
||||||
|
"description_md": "Plan description",
|
||||||
|
"packets_count": 1,
|
||||||
|
"packet1_options": {
|
||||||
|
"items": ["Item 1", "Item 2"],
|
||||||
|
"price": 1500.0,
|
||||||
|
"short": "Option 1",
|
||||||
|
"description_md": "Description",
|
||||||
|
"has_personal_diet": false
|
||||||
|
},
|
||||||
|
"packet2_options": null,
|
||||||
|
"packet3_options": null,
|
||||||
|
"author": {
|
||||||
|
"author_slug": "author-1",
|
||||||
|
"author_name": "Author Name",
|
||||||
|
"author_profile_icon": "https://example.com/author.jpg",
|
||||||
|
"author_is_me": false
|
||||||
|
},
|
||||||
|
"total_likes": 10,
|
||||||
|
"total_views": 100,
|
||||||
|
"total_comments": 5,
|
||||||
|
"is_favorite": false,
|
||||||
|
"min_price_of_options": 1000.0,
|
||||||
|
"max_price_of_options": 2000.0,
|
||||||
|
"was_liked_by_me": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
47
test/fixtures/chat/chosen_plan/chosen_plan_paid_response.json
vendored
Normal file
47
test/fixtures/chat/chosen_plan/chosen_plan_paid_response.json
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"option_type": "1",
|
||||||
|
"start_date": "2024-01-01T00:00:00Z",
|
||||||
|
"last_state": "paid",
|
||||||
|
"chat_id": "chat-1",
|
||||||
|
"consultant_id": 1,
|
||||||
|
"plan": {
|
||||||
|
"id": 1,
|
||||||
|
"slug": "test-plan",
|
||||||
|
"name": "Test Plan",
|
||||||
|
"image": "https://example.com/plan.jpg",
|
||||||
|
"duration": 30,
|
||||||
|
"state": "active",
|
||||||
|
"type": {
|
||||||
|
"fit": true,
|
||||||
|
"slim": false,
|
||||||
|
"behavior": false,
|
||||||
|
"wellness": false
|
||||||
|
},
|
||||||
|
"description_md": "Plan description",
|
||||||
|
"packets_count": 1,
|
||||||
|
"packet1_options": {
|
||||||
|
"items": ["Item 1", "Item 2"],
|
||||||
|
"price": 1500.0,
|
||||||
|
"short": "Option 1",
|
||||||
|
"description_md": "Description",
|
||||||
|
"has_personal_diet": false
|
||||||
|
},
|
||||||
|
"packet2_options": null,
|
||||||
|
"packet3_options": null,
|
||||||
|
"author": {
|
||||||
|
"author_slug": "author-1",
|
||||||
|
"author_name": "Author Name",
|
||||||
|
"author_profile_icon": "https://example.com/author.jpg",
|
||||||
|
"author_is_me": false
|
||||||
|
},
|
||||||
|
"total_likes": 10,
|
||||||
|
"total_views": 100,
|
||||||
|
"total_comments": 5,
|
||||||
|
"is_favorite": false,
|
||||||
|
"min_price_of_options": 1000.0,
|
||||||
|
"max_price_of_options": 2000.0,
|
||||||
|
"was_liked_by_me": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
47
test/fixtures/chat/chosen_plan/chosen_plan_payment_failed_response.json
vendored
Normal file
47
test/fixtures/chat/chosen_plan/chosen_plan_payment_failed_response.json
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"option_type": "1",
|
||||||
|
"start_date": "2024-01-01T00:00:00Z",
|
||||||
|
"last_state": "payment_failed",
|
||||||
|
"chat_id": "chat-1",
|
||||||
|
"consultant_id": 1,
|
||||||
|
"plan": {
|
||||||
|
"id": 1,
|
||||||
|
"slug": "test-plan",
|
||||||
|
"name": "Test Plan",
|
||||||
|
"image": "https://example.com/plan.jpg",
|
||||||
|
"duration": 30,
|
||||||
|
"state": "active",
|
||||||
|
"type": {
|
||||||
|
"fit": true,
|
||||||
|
"slim": false,
|
||||||
|
"behavior": false,
|
||||||
|
"wellness": false
|
||||||
|
},
|
||||||
|
"description_md": "Plan description",
|
||||||
|
"packets_count": 1,
|
||||||
|
"packet1_options": {
|
||||||
|
"items": ["Item 1", "Item 2"],
|
||||||
|
"price": 1500.0,
|
||||||
|
"short": "Option 1",
|
||||||
|
"description_md": "Description",
|
||||||
|
"has_personal_diet": false
|
||||||
|
},
|
||||||
|
"packet2_options": null,
|
||||||
|
"packet3_options": null,
|
||||||
|
"author": {
|
||||||
|
"author_slug": "author-1",
|
||||||
|
"author_name": "Author Name",
|
||||||
|
"author_profile_icon": "https://example.com/author.jpg",
|
||||||
|
"author_is_me": false
|
||||||
|
},
|
||||||
|
"total_likes": 10,
|
||||||
|
"total_views": 100,
|
||||||
|
"total_comments": 5,
|
||||||
|
"is_favorite": false,
|
||||||
|
"min_price_of_options": 1000.0,
|
||||||
|
"max_price_of_options": 2000.0,
|
||||||
|
"was_liked_by_me": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
47
test/fixtures/chat/chosen_plan/chosen_plan_pending_payment_response.json
vendored
Normal file
47
test/fixtures/chat/chosen_plan/chosen_plan_pending_payment_response.json
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"option_type": "1",
|
||||||
|
"start_date": "2024-01-01T00:00:00Z",
|
||||||
|
"last_state": "pending_payment",
|
||||||
|
"chat_id": "chat-1",
|
||||||
|
"consultant_id": 1,
|
||||||
|
"plan": {
|
||||||
|
"id": 1,
|
||||||
|
"slug": "test-plan",
|
||||||
|
"name": "Test Plan",
|
||||||
|
"image": "https://example.com/plan.jpg",
|
||||||
|
"duration": 30,
|
||||||
|
"state": "active",
|
||||||
|
"type": {
|
||||||
|
"fit": true,
|
||||||
|
"slim": false,
|
||||||
|
"behavior": false,
|
||||||
|
"wellness": false
|
||||||
|
},
|
||||||
|
"description_md": "Plan description",
|
||||||
|
"packets_count": 1,
|
||||||
|
"packet1_options": {
|
||||||
|
"items": ["Item 1", "Item 2"],
|
||||||
|
"price": 1500.0,
|
||||||
|
"short": "Option 1",
|
||||||
|
"description_md": "Description",
|
||||||
|
"has_personal_diet": false
|
||||||
|
},
|
||||||
|
"packet2_options": null,
|
||||||
|
"packet3_options": null,
|
||||||
|
"author": {
|
||||||
|
"author_slug": "author-1",
|
||||||
|
"author_name": "Author Name",
|
||||||
|
"author_profile_icon": "https://example.com/author.jpg",
|
||||||
|
"author_is_me": false
|
||||||
|
},
|
||||||
|
"total_likes": 10,
|
||||||
|
"total_views": 100,
|
||||||
|
"total_comments": 5,
|
||||||
|
"is_favorite": false,
|
||||||
|
"min_price_of_options": 1000.0,
|
||||||
|
"max_price_of_options": 2000.0,
|
||||||
|
"was_liked_by_me": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
47
test/fixtures/chat/chosen_plan/chosen_plan_presonal_diet_set_response.json
vendored
Normal file
47
test/fixtures/chat/chosen_plan/chosen_plan_presonal_diet_set_response.json
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"option_type": "1",
|
||||||
|
"start_date": "2024-01-01T00:00:00Z",
|
||||||
|
"last_state": "personal_diet_setted",
|
||||||
|
"chat_id": "chat-1",
|
||||||
|
"consultant_id": 1,
|
||||||
|
"plan": {
|
||||||
|
"id": 1,
|
||||||
|
"slug": "test-plan",
|
||||||
|
"name": "Test Plan",
|
||||||
|
"image": "https://example.com/plan.jpg",
|
||||||
|
"duration": 30,
|
||||||
|
"state": "active",
|
||||||
|
"type": {
|
||||||
|
"fit": true,
|
||||||
|
"slim": false,
|
||||||
|
"behavior": false,
|
||||||
|
"wellness": false
|
||||||
|
},
|
||||||
|
"description_md": "Plan description",
|
||||||
|
"packets_count": 1,
|
||||||
|
"packet1_options": {
|
||||||
|
"items": ["Item 1", "Item 2"],
|
||||||
|
"price": 1500.0,
|
||||||
|
"short": "Option 1",
|
||||||
|
"description_md": "Description",
|
||||||
|
"has_personal_diet": false
|
||||||
|
},
|
||||||
|
"packet2_options": null,
|
||||||
|
"packet3_options": null,
|
||||||
|
"author": {
|
||||||
|
"author_slug": "author-1",
|
||||||
|
"author_name": "Author Name",
|
||||||
|
"author_profile_icon": "https://example.com/author.jpg",
|
||||||
|
"author_is_me": false
|
||||||
|
},
|
||||||
|
"total_likes": 10,
|
||||||
|
"total_views": 100,
|
||||||
|
"total_comments": 5,
|
||||||
|
"is_favorite": false,
|
||||||
|
"min_price_of_options": 1000.0,
|
||||||
|
"max_price_of_options": 2000.0,
|
||||||
|
"was_liked_by_me": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
47
test/fixtures/chat/chosen_plan/chosen_plan_start_date_has_been_set_response.json
vendored
Normal file
47
test/fixtures/chat/chosen_plan/chosen_plan_start_date_has_been_set_response.json
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"option_type": "1",
|
||||||
|
"start_date": "2024-01-01T00:00:00Z",
|
||||||
|
"last_state": "start_date_has_been_set",
|
||||||
|
"chat_id": "chat-1",
|
||||||
|
"consultant_id": 1,
|
||||||
|
"plan": {
|
||||||
|
"id": 1,
|
||||||
|
"slug": "test-plan",
|
||||||
|
"name": "Test Plan",
|
||||||
|
"image": "https://example.com/plan.jpg",
|
||||||
|
"duration": 30,
|
||||||
|
"state": "active",
|
||||||
|
"type": {
|
||||||
|
"fit": true,
|
||||||
|
"slim": false,
|
||||||
|
"behavior": false,
|
||||||
|
"wellness": false
|
||||||
|
},
|
||||||
|
"description_md": "Plan description",
|
||||||
|
"packets_count": 1,
|
||||||
|
"packet1_options": {
|
||||||
|
"items": ["Item 1", "Item 2"],
|
||||||
|
"price": 1500.0,
|
||||||
|
"short": "Option 1",
|
||||||
|
"description_md": "Description",
|
||||||
|
"has_personal_diet": false
|
||||||
|
},
|
||||||
|
"packet2_options": null,
|
||||||
|
"packet3_options": null,
|
||||||
|
"author": {
|
||||||
|
"author_slug": "author-1",
|
||||||
|
"author_name": "Author Name",
|
||||||
|
"author_profile_icon": "https://example.com/author.jpg",
|
||||||
|
"author_is_me": false
|
||||||
|
},
|
||||||
|
"total_likes": 10,
|
||||||
|
"total_views": 100,
|
||||||
|
"total_comments": 5,
|
||||||
|
"is_favorite": false,
|
||||||
|
"min_price_of_options": 1000.0,
|
||||||
|
"max_price_of_options": 2000.0,
|
||||||
|
"was_liked_by_me": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
47
test/fixtures/chat/chosen_plan/chosen_plan_started_response.json
vendored
Normal file
47
test/fixtures/chat/chosen_plan/chosen_plan_started_response.json
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"option_type": "1",
|
||||||
|
"start_date": "2024-01-01T00:00:00Z",
|
||||||
|
"last_state": "started",
|
||||||
|
"chat_id": "chat-1",
|
||||||
|
"consultant_id": 1,
|
||||||
|
"plan": {
|
||||||
|
"id": 1,
|
||||||
|
"slug": "test-plan",
|
||||||
|
"name": "Test Plan",
|
||||||
|
"image": "https://example.com/plan.jpg",
|
||||||
|
"duration": 30,
|
||||||
|
"state": "active",
|
||||||
|
"type": {
|
||||||
|
"fit": true,
|
||||||
|
"slim": false,
|
||||||
|
"behavior": false,
|
||||||
|
"wellness": false
|
||||||
|
},
|
||||||
|
"description_md": "Plan description",
|
||||||
|
"packets_count": 1,
|
||||||
|
"packet1_options": {
|
||||||
|
"items": ["Item 1", "Item 2"],
|
||||||
|
"price": 1500.0,
|
||||||
|
"short": "Option 1",
|
||||||
|
"description_md": "Description",
|
||||||
|
"has_personal_diet": false
|
||||||
|
},
|
||||||
|
"packet2_options": null,
|
||||||
|
"packet3_options": null,
|
||||||
|
"author": {
|
||||||
|
"author_slug": "author-1",
|
||||||
|
"author_name": "Author Name",
|
||||||
|
"author_profile_icon": "https://example.com/author.jpg",
|
||||||
|
"author_is_me": false
|
||||||
|
},
|
||||||
|
"total_likes": 10,
|
||||||
|
"total_views": 100,
|
||||||
|
"total_comments": 5,
|
||||||
|
"is_favorite": false,
|
||||||
|
"min_price_of_options": 1000.0,
|
||||||
|
"max_price_of_options": 2000.0,
|
||||||
|
"was_liked_by_me": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
5
test/fixtures/chat/payments/order_id_to_chat_uuid_response.json
vendored
Normal file
5
test/fixtures/chat/payments/order_id_to_chat_uuid_response.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
{
|
||||||
|
"chosen_plan_id": 12,
|
||||||
|
"conversation_uuid": "fbd81429-8019-499a-80e8-e3933cc4d872"
|
||||||
|
}
|
||||||
3
test/fixtures/chat/payments/payment_alfabank_request.json
vendored
Normal file
3
test/fixtures/chat/payments/payment_alfabank_request.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"payment_provider": "alfa-bank"
|
||||||
|
}
|
||||||
3
test/fixtures/chat/payments/payment_mock_requeste.json
vendored
Normal file
3
test/fixtures/chat/payments/payment_mock_requeste.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"payment_provider": "mock"
|
||||||
|
}
|
||||||
5
test/fixtures/chat/payments/payment_response_alfa_bank.json
vendored
Normal file
5
test/fixtures/chat/payments/payment_response_alfa_bank.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"checkout_url": "https://qr.nspk.ru/ASDFASDFASDFASDF",
|
||||||
|
"payment_provider": "alfa-bank",
|
||||||
|
"order_id": "alfa_c29_k15138_t1712345678_r1234"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user