Files
saqel/apps/teacher_app/lib/logic/cubits/teacher_monetization_cubit.dart
T

210 lines
7.8 KiB
Dart

import 'package:flutter_bloc/flutter_bloc.dart';
import '../../data/models/teacher_models.dart';
import '../../data/repositories/teacher_repository.dart';
/**
* ==============================================================================
* SAQEL TEACHER STUDIO - MONETIZATION CUBIT
* ==============================================================================
*
* إدارة المحفظة المالية والتسييل وسحوبات كليك (CliQ Payouts):
* - حساب عوائد النموذج المزدوج (1,420 مجاني مدعوم مقابل 380 مدفوع خارجي)
* - تقسيم الحصص: 55% للمعلم / 15% لمديرية الثقافة / 30% للمنصة
* - إرسال واعتماد طلبات السحب عبر كليك.
*/
class TeacherMonetizationState {
final double institutionalStudents;
final double studentSubscribers;
final double pricePerCourse;
final double grossRevenue;
final double teacherShare;
final double directorateShare;
final double platformShare;
final double availableBalance;
final double totalWithdrawn;
final String cliqAlias;
final List<TeacherCourseModel> courses;
final List<TeacherPayoutRequestModel> payoutRequests;
final bool isSubmittingPayout;
final bool isLoading;
final bool financialUnavailable;
final String financialMessage;
const TeacherMonetizationState({
required this.institutionalStudents,
required this.studentSubscribers,
required this.pricePerCourse,
required this.grossRevenue,
required this.teacherShare,
required this.directorateShare,
required this.platformShare,
required this.availableBalance,
required this.totalWithdrawn,
required this.cliqAlias,
required this.courses,
required this.payoutRequests,
required this.isSubmittingPayout,
this.isLoading = false,
this.financialUnavailable = false,
this.financialMessage = '',
});
TeacherMonetizationState copyWith({
double? institutionalStudents,
double? studentSubscribers,
double? pricePerCourse,
double? grossRevenue,
double? teacherShare,
double? directorateShare,
double? platformShare,
double? availableBalance,
double? totalWithdrawn,
String? cliqAlias,
List<TeacherCourseModel>? courses,
List<TeacherPayoutRequestModel>? payoutRequests,
bool? isSubmittingPayout,
bool? isLoading,
bool? financialUnavailable,
String? financialMessage,
}) {
return TeacherMonetizationState(
institutionalStudents:
institutionalStudents ?? this.institutionalStudents,
studentSubscribers: studentSubscribers ?? this.studentSubscribers,
pricePerCourse: pricePerCourse ?? this.pricePerCourse,
grossRevenue: grossRevenue ?? this.grossRevenue,
teacherShare: teacherShare ?? this.teacherShare,
directorateShare: directorateShare ?? this.directorateShare,
platformShare: platformShare ?? this.platformShare,
availableBalance: availableBalance ?? this.availableBalance,
totalWithdrawn: totalWithdrawn ?? this.totalWithdrawn,
cliqAlias: cliqAlias ?? this.cliqAlias,
courses: courses ?? this.courses,
payoutRequests: payoutRequests ?? this.payoutRequests,
isSubmittingPayout: isSubmittingPayout ?? this.isSubmittingPayout,
isLoading: isLoading ?? this.isLoading,
financialUnavailable: financialUnavailable ?? this.financialUnavailable,
financialMessage: financialMessage ?? this.financialMessage,
);
}
}
class TeacherMonetizationCubit extends Cubit<TeacherMonetizationState> {
final TeacherRepository repository;
TeacherMonetizationCubit({required this.repository})
: super(const TeacherMonetizationState(
institutionalStudents: 0,
studentSubscribers: 0,
pricePerCourse: 20.0,
grossRevenue: 0.0,
teacherShare: 0.0,
directorateShare: 0.0,
platformShare: 0.0,
availableBalance: 0.0,
totalWithdrawn: 0.0,
cliqAlias: '',
courses: [],
payoutRequests: [],
isSubmittingPayout: false,
isLoading: false,
));
Future<void> loadMonetization() async {
emit(state.copyWith(isLoading: true));
try {
final data = await repository.getMonetizationDashboard();
final courses = await repository.getPublishedCourses();
final audience =
data['audience_breakdown'] as Map<String, dynamic>? ?? {};
final inst =
audience['institutional_students'] as Map<String, dynamic>? ?? {};
final mkt =
audience['marketplace_students'] as Map<String, dynamic>? ?? {};
final wallet = data['wallet'] as Map<String, dynamic>? ?? {};
final unavailable = data['financial_status'] == 'unavailable';
final instCount = (inst['count'] as num?)?.toDouble() ?? 0.0;
final paidCount = (mkt['count'] as num?)?.toDouble() ?? 0.0;
final split = data['revenue_split'] as Map<String, dynamic>? ?? {};
// Never infer a wallet from subscriber counts, course prices, or old
// percentages. The backend returns null until a provider-backed ledger
// and eligible watch-minute policy are live.
final gross = (data['gross_revenue_jod'] as num?)?.toDouble() ?? 0.0;
final tShare = (split['teacher_amount_jod'] as num?)?.toDouble() ?? 0.0;
final dShare =
(split['directorate_amount_jod'] as num?)?.toDouble() ?? 0.0;
final pShare = (split['platform_amount_jod'] as num?)?.toDouble() ?? 0.0;
final avail =
(wallet['available_balance_jod'] as num?)?.toDouble() ?? 0.0;
final withdrawn =
(wallet['total_withdrawn_jod'] as num?)?.toDouble() ?? 0.0;
final alias = wallet['cliq_payout_alias']?.toString() ?? '';
List<TeacherPayoutRequestModel> payouts = [];
if (wallet['recent_payouts'] is List) {
payouts = (wallet['recent_payouts'] as List).map((p) {
return TeacherPayoutRequestModel(
cliqAlias: p['teacher_cliq_alias']?.toString() ?? alias,
amountJod: (p['amount_jod'] as num?)?.toDouble() ?? 0.0,
requestedAt: p['created_at']?.toString() ?? 'اليوم',
status: p['status']?.toString() ?? 'queued',
);
}).toList();
}
emit(state.copyWith(
institutionalStudents: instCount,
studentSubscribers: paidCount,
grossRevenue: gross,
teacherShare: tShare,
directorateShare: dShare,
platformShare: pShare,
availableBalance: avail,
totalWithdrawn: withdrawn,
cliqAlias: alias,
courses: courses,
payoutRequests: payouts,
financialUnavailable: unavailable,
financialMessage: data['financial_message']?.toString() ?? '',
isLoading: false,
));
} catch (_) {
emit(state.copyWith(
isLoading: false,
financialUnavailable: true,
financialMessage: 'قيد التفعيل المؤسسي — بانتظار اعتماد سياسة كليك والربط البنكي الرسمي ومطابقة الدقائق التعليمية المستحقة.',
));
}
}
void updateSubscribers(double subscribers) {
// Presentation must not fabricate revenue from a manually changed count.
emit(state.copyWith(studentSubscribers: subscribers));
}
Future<void> submitCliqPayout({
required String cliqAlias,
required double amountJod,
}) async {
emit(state.copyWith(isSubmittingPayout: true));
try {
final req = await repository.requestCliqPayout(
cliqAlias: cliqAlias,
amountJod: amountJod,
);
final updatedList =
List<TeacherPayoutRequestModel>.from(state.payoutRequests)
..insert(0, req);
emit(state.copyWith(
payoutRequests: updatedList,
isSubmittingPayout: false,
));
} catch (_) {
emit(state.copyWith(isSubmittingPayout: false));
}
}
}