Refund Module almost done

pull/322/head
faizatflutter 3 weeks ago
parent 198280fbde
commit 67bb3895e2

@ -209,8 +209,8 @@ class ApiClientImp implements ApiClient {
}
// TODO: REMOVE THIS MUST
body['TokenID'] = "@dm!n";
body['PatientID'] = 4768571;
// body['TokenID'] = "@dm!n";
// body['PatientID'] = 4768571;
// body['PatientTypeID'] = 1;
// body['PatientOutSA'] = 0;
// body['SessionID'] = "45786230487560q";

@ -33,7 +33,7 @@ class ValidationUtils {
isCorrectID = false;
}
if(nationalId!.length == 10) {
if (nationalId!.length == 10) {
if (Utils.isSAUDIIDValid(nationalId!) == false) {
_dialogService.showExceptionBottomSheet(message: LocaleKeys.enterValidNationalId.tr(), onOkPressed: onOkPress);
isCorrectID = false;
@ -77,10 +77,7 @@ class ValidationUtils {
// Check if "Others" country is selected and phone number starts with restricted codes
if (selectedCountry == CountryEnum.others) {
if (phoneNumber.startsWith('00966') || phoneNumber.startsWith('00971')) {
_dialogService.showExceptionBottomSheet(
message: LocaleKeys.cannotEnterSaudiOrUAENumber.tr(),
onOkPressed: onOkPress
);
_dialogService.showExceptionBottomSheet(message: LocaleKeys.cannotEnterSaudiOrUAENumber.tr(), onOkPressed: onOkPress);
return false;
}
}
@ -121,7 +118,8 @@ class ValidationUtils {
return regex.hasMatch(id);
}
static bool validateUaeRegistration({String? name, GenderTypeEnum? gender, NationalityCountries? country, MaritalStatusTypeEnum? maritalStatus, required Function() onOkPress}) {
static bool validateUaeRegistration(
{String? name, GenderTypeEnum? gender, NationalityCountries? country, MaritalStatusTypeEnum? maritalStatus, required Function() onOkPress}) {
if (name == null || name.isEmpty) {
_dialogService.showExceptionBottomSheet(message: LocaleKeys.pleaseEnterAValidName.tr(), onOkPressed: onOkPress);
return false;
@ -160,7 +158,8 @@ class ValidationUtils {
return true;
}
static bool isValidatedIdAndPhoneWithCountryValidation({String? nationalId, String? phoneNumber, required Function() onOkPress, CountryEnum? selectedCountry}) {
static bool isValidatedIdAndPhoneWithCountryValidation(
{String? nationalId, String? phoneNumber, required Function() onOkPress, CountryEnum? selectedCountry}) {
bool isCorrectID = true;
if (nationalId == null || nationalId.isEmpty) {
_dialogService.showExceptionBottomSheet(message: LocaleKeys.pleaseEnterAnationalID.tr(), onOkPressed: onOkPress);
@ -182,7 +181,7 @@ class ValidationUtils {
}
}
if (phoneNumber == null || phoneNumber.isEmpty) {
if (phoneNumber == null || phoneNumber.isEmpty) {
_dialogService.showExceptionBottomSheet(message: LocaleKeys.enterValidPhoneNumber.tr(), onOkPressed: onOkPress);
return false;
}
@ -190,9 +189,13 @@ class ValidationUtils {
return isCorrectID;
}
static bool isNullOrEmpty(String? value) {
return value == null || value
.trim()
.isEmpty;
static bool isNullOrEmpty(String? value) {
return value == null || value.trim().isEmpty;
}
}
/// Validates a KSA IBAN: must start with SA + 2 check digits + 20 alphanumeric chars (24 chars total).
static bool validateKsaIban(String iban) {
final ksaIbanRegex = RegExp(r'^SA\d{2}[0-9A-Z]{20}$', caseSensitive: false);
return ksaIbanRegex.hasMatch(iban.trim().replaceAll(' ', ''));
}
}

@ -1,8 +1,12 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/utils/validation_utils.dart';
import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_repo.dart';
import 'package:hmg_patient_app_new/features/habib_wallet/models/patient_advance_balance_response_model.dart';
import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/models/req_models/submit_refund_request_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refundable_invoices_response_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refund_request_list_response_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/refund_request_repo.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/services/error_handler_service.dart';
@ -28,6 +32,8 @@ class HabibWalletViewModel extends ChangeNotifier {
// Refundable Advances
bool isAdvancesLoading = false;
double totalAdvancesBalance = 0.0;
List<RefundableAdvancesItem> refundableAdvancesList = [];
bool isSubmittingWithdrawal = false;
bool isSearchedFileNumberDataShown = false;
@ -103,9 +109,7 @@ class HabibWalletViewModel extends ChangeNotifier {
_ibanError = LocaleKeys.ibanIsRequired.tr();
isValid = false;
} else {
// KSA IBAN format: SA followed by 2 check digits + 18 alphanumeric chars = 22 chars total
final ksaIbanRegex = RegExp(r'^SA\d{2}[0-9A-Z]{18}$', caseSensitive: false);
if (!ksaIbanRegex.hasMatch(iban.trim().replaceAll(' ', ''))) {
if (!ValidationUtils.validateKsaIban(iban.trim())) {
_ibanError = LocaleKeys.invalidIbanFormat.tr();
isValid = false;
}
@ -158,6 +162,7 @@ class HabibWalletViewModel extends ChangeNotifier {
Future<void> fetchAdvancesForHospital() async {
isAdvancesLoading = true;
totalAdvancesBalance = 0.0;
refundableAdvancesList = [];
notifyListeners();
final result = await refundRequestRepo.getRefundableAdvances();
@ -170,13 +175,86 @@ class HabibWalletViewModel extends ChangeNotifier {
(apiResponse) {
isAdvancesLoading = false;
if (apiResponse.data != null) {
totalAdvancesBalance = apiResponse.data!.refundableAdvancesItems.fold(0.0, (sum, item) => sum + (item.balanceAmount ?? 0.0));
refundableAdvancesList = apiResponse.data!.refundableAdvancesItems;
totalAdvancesBalance = refundableAdvancesList.fold(0.0, (sum, item) => sum + (item.balanceAmount ?? 0.0));
}
notifyListeners();
},
);
}
Future<void> submitWalletWithdrawal({
required String iban,
required String remarks,
required String patientName,
required String patientId,
required String patientMobileNumber,
required String patientNationalId,
required int projectId,
required String projectName,
Function(SubmitRefundResponseModel)? onSuccess,
Function(String)? onError,
}) async {
isSubmittingWithdrawal = true;
notifyListeners();
final advances = refundableAdvancesList
.map((a) => SubmitRefundAdvanceItem(
advanceNo: (a.advanceNo ?? 0).toString(),
paidAmount: a.paidAmount ?? 0.0,
balanceAmount: a.balanceAmount ?? 0.0,
advanceDate: a.advanceDate ?? '',
))
.toList();
final request = SubmitRefundRequestModel(
projectId: projectId,
refundType: '2', // wallet withdrawal
refundProject: projectName,
refundProjectId: projectId,
refundPatientId: patientId,
patientMobileNumber: patientMobileNumber,
refundPatientName: patientName,
refundNationalId: patientNationalId,
refundInvoiceNo: '',
refundAppointmentNo: '',
refundDoctorId: 0,
refundDoctorName: '',
refundClinicId: 0,
refundClinicName: '',
refundDestination: '2', // bank transfer
refundIban: iban,
refundRemarks: remarks,
refundTotalAmount: totalAdvancesBalance,
refundProceduresList: const [],
refundAdvancesList: advances,
);
final result = await refundRequestRepo.submitRefund(request: request);
result.fold(
(failure) {
isSubmittingWithdrawal = false;
notifyListeners();
if (onError != null) {
onError(failure.message);
} else {
errorHandlerService.handleError(failure: failure);
}
},
(apiResponse) {
isSubmittingWithdrawal = false;
notifyListeners();
if (apiResponse.data != null) {
if (onSuccess != null) onSuccess(apiResponse.data!);
} else {
if (onError != null) {
onError(apiResponse.errorMessage ?? LocaleKeys.failed.tr());
}
}
},
);
}
initHabibWalletProvider() {
isWalletAmountLoading = true;
isBottomSheetContentLoading = false;
@ -203,6 +281,20 @@ class HabibWalletViewModel extends ChangeNotifier {
notifyListeners();
}
/// Resets all state related to the wallet withdrawal form.
/// Pass [silent] = true when calling from dispose() to avoid notifyListeners
/// while the widget tree is locked.
void resetWithdrawForm({bool silent = false}) {
selectedHospital = null;
totalAdvancesBalance = 0.0;
refundableAdvancesList = [];
isAdvancesLoading = false;
isSubmittingWithdrawal = false;
_ibanError = null;
_withdrawDescriptionError = null;
if (!silent) notifyListeners();
}
setWalletRechargeAmount(num amount) {
walletRechargeAmount = amount;
notifyListeners();

@ -27,6 +27,33 @@ class SubmitRefundProcedureItem {
}
}
/// Represents a single advance item in the wallet withdrawal refund payload.
class SubmitRefundAdvanceItem {
final String advanceNo;
final double paidAmount;
final double balanceAmount;
final String advanceDate;
const SubmitRefundAdvanceItem({
required this.advanceNo,
required this.paidAmount,
required this.balanceAmount,
required this.advanceDate,
});
Map<String, dynamic> toJson() => {
"AdvanceNo": advanceNo,
"PaidAmount": paidAmount,
"BalanceAmount": balanceAmount,
"AdvanceDate": advanceDate,
};
@override
String toString() {
return 'SubmitRefundAdvanceItem{advanceNo: $advanceNo, paidAmount: $paidAmount, balanceAmount: $balanceAmount, advanceDate: $advanceDate}';
}
}
class SubmitRefundRequestModel {
final int projectId;
final String refundType;
@ -47,6 +74,8 @@ class SubmitRefundRequestModel {
final String refundRemarks;
final double refundTotalAmount;
final List<SubmitRefundProcedureItem> refundProceduresList;
/// Populated only for wallet withdrawal refunds (refundType == '2').
final List<SubmitRefundAdvanceItem> refundAdvancesList;
const SubmitRefundRequestModel({
required this.projectId,
@ -68,32 +97,46 @@ class SubmitRefundRequestModel {
required this.refundRemarks,
required this.refundTotalAmount,
required this.refundProceduresList,
this.refundAdvancesList = const [],
});
Map<String, dynamic> toJson() => {
"ProjectID": projectId,
"Refund_RefundType": refundType,
"Refund_Project": refundProject,
"Refund_ProjectId": refundProjectId,
"Refund_PatientId": refundPatientId,
"PatientMobileNumber": patientMobileNumber,
"Refund_PatientName": refundPatientName,
"Refund_NationalId": refundNationalId,
"Refund_InvoiceNo": refundInvoiceNo,
"Refund_AppointmentNo": refundAppointmentNo,
"Refund_DoctorId": refundDoctorId,
"Refund_DoctorName": refundDoctorName,
"Refund_ClinicId": refundClinicId,
"Refund_ClinicName": refundClinicName,
"Refund_RefundDestination": refundDestination,
"Refund_IBAN": refundIban,
"Refund_Remarks": refundRemarks,
"Refund_TotalRefundAmount": refundTotalAmount,
"Refund_ProceduresList": refundProceduresList.map((p) => p.toJson()).toList(),
};
/// refundType '1' = invoice refund sends Refund_ProceduresList
/// refundType '2' = wallet withdrawal sends Refund_AdvancesList
Map<String, dynamic> toJson() {
final Map<String, dynamic> json = {
"ProjectID": projectId,
"Refund_RefundType": refundType,
"Refund_Project": refundProject,
"Refund_ProjectId": refundProjectId,
"Refund_PatientId": refundPatientId,
"PatientMobileNumber": patientMobileNumber,
"Refund_PatientName": refundPatientName,
"Refund_NationalId": refundNationalId,
"Refund_DoctorId": refundDoctorId,
"Refund_DoctorName": refundDoctorName,
"Refund_RefundDestination": refundDestination,
"Refund_IBAN": refundIban,
"Refund_Remarks": refundRemarks,
"Refund_TotalRefundAmount": refundTotalAmount,
};
if (refundType == '2') {
// Wallet withdrawal include advances list, omit invoice-specific fields
json["Refund_AdvancesList"] = refundAdvancesList.map((a) => a.toJson()).toList();
} else {
// Invoice refund include invoice-specific fields and procedures list
json["Refund_InvoiceNo"] = refundInvoiceNo;
json["Refund_AppointmentNo"] = refundAppointmentNo;
json["Refund_ClinicId"] = refundClinicId;
json["Refund_ClinicName"] = refundClinicName;
json["Refund_ProceduresList"] = refundProceduresList.map((p) => p.toJson()).toList();
}
return json;
}
@override
String toString() {
return 'SubmitRefundRequestModel{projectId: $projectId, refundType: $refundType, refundProject: $refundProject, refundProjectId: $refundProjectId, refundPatientId: $refundPatientId, patientMobileNumber: $patientMobileNumber, refundPatientName: $refundPatientName, refundNationalId: $refundNationalId, refundInvoiceNo: $refundInvoiceNo, refundAppointmentNo: $refundAppointmentNo, refundDoctorId: $refundDoctorId, refundDoctorName: $refundDoctorName, refundClinicId: $refundClinicId, refundClinicName: $refundClinicName, refundDestination: $refundDestination, refundIban: $refundIban, refundRemarks: $refundRemarks, refundTotalAmount: $refundTotalAmount, refundProceduresList: $refundProceduresList}';
return 'SubmitRefundRequestModel{projectId: $projectId, refundType: $refundType, refundAdvancesList: $refundAdvancesList, refundProceduresList: $refundProceduresList}';
}
}

@ -1,6 +1,7 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/utils/validation_utils.dart';
import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/models/req_models/submit_refund_request_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refund_request_list_response_model.dart';
@ -275,8 +276,7 @@ class RefundRequestViewModel extends ChangeNotifier {
_ibanError = LocaleKeys.ibanIsRequired.tr();
isValid = false;
} else {
final ksaIbanRegex = RegExp(r'^SA\d{2}[0-9A-Z]{20}$', caseSensitive: false);
if (!ksaIbanRegex.hasMatch(iban.trim().replaceAll(' ', ''))) {
if (!ValidationUtils.validateKsaIban(iban.trim())) {
_ibanError = LocaleKeys.invalidIbanFormat.tr();
isValid = false;
}

@ -19,6 +19,7 @@ import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'package:hmg_patient_app_new/widgets/image_picker.dart';
import 'package:hmg_patient_app_new/widgets/input_widget.dart';
import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:provider/provider.dart';
@ -50,6 +51,7 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
@override
void dispose() {
_habibWalletViewModel.resetWithdrawForm(silent: true);
_bankDetailsController.dispose();
_descriptionController.dispose();
_ibanFocusNode.dispose();
@ -93,7 +95,7 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
description: _descriptionController.text,
);
if (isFormValid) {
// TODO: Submit withdraw request
_handleWalletWithdrawalSubmit(vm);
}
},
).paddingSymmetrical(24.h, 24.h),
@ -284,6 +286,88 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
);
}
// Submit
void _handleWalletWithdrawalSubmit(HabibWalletViewModel vm) {
showCommonBottomSheetWithoutHeight(
context,
title: LocaleKeys.notice.tr(context: context),
child: Utils.getWarningWidget(
loadingText: LocaleKeys.confirmSubmitRequest.tr(context: context),
isShowActionButtons: true,
onCancelTap: () {
Navigator.pop(context);
},
onConfirmTap: () async {
Navigator.pop(context);
LoaderBottomSheet.showLoader();
final user = _appState.getAuthenticatedUser();
final hospital = vm.selectedHospital;
await vm.submitWalletWithdrawal(
iban: _bankDetailsController.text.trim(),
remarks: _descriptionController.text.trim(),
patientName: '${user?.firstName ?? ''} ${user?.lastName ?? ''}'.trim(),
patientId: (user?.patientId ?? 0).toString(),
patientMobileNumber: user?.mobileNumber ?? '',
patientNationalId: user?.patientIdentificationNo?.toString() ?? '',
projectId: hospital?.mainProjectID ?? 0,
projectName: hospital?.name ?? '',
onSuccess: (response) {
LoaderBottomSheet.hideLoader();
_showWithdrawalSuccessBottomSheet(response.refNo ?? '');
},
onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err),
callBackFunc: () {},
);
},
);
},
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
void _showWithdrawalSuccessBottomSheet(String refNo) {
showCommonBottomSheetWithoutHeight(
context,
child: Padding(
padding: EdgeInsets.all(16.w),
child: Column(
children: [
Utils.getSuccessWidget(loadingText: LocaleKeys.requestSubmittedSuccessfully.tr(context: context)),
SizedBox(height: 24.h),
Row(
children: [
Expanded(
child: CustomButton(
height: 56.h,
text: LocaleKeys.ok.tr(context: context),
onPressed: () {
Navigator.pop(context);
Navigator.pop(context);
},
textColor: AppColors.whiteColor,
),
),
],
),
],
),
),
isCloseButtonVisible: false,
isDismissible: false,
isFullScreen: false,
callBackFunc: () {},
);
}
// Build
@override

Loading…
Cancel
Save