Psyter - WindowsService Detailed Structure (Part 2 of 3)

Part 2 Overview

This document continues from Part 1 and covers:
- Data Access Layer (DAL) - Part 2: Update methods, notification methods, XmlHelper
- Payment Processing Workflows: Core inquiry and refund processing methods
- Payment Gateway Integration: Secure hash generation and gateway communication


DAL - PaymentDataAccess.cs (Part 2 - Update Methods)

File: WindowsService/PsyterPaymentInquiry/DAL/PaymentDataAccess.cs

5. Update Booking Order Payment Status

public bool UpdateBookingOrderPayForData(string xmlPaymentData)
{
    bool response = false;

    using (DbCommand dbCommand = dataBase.GetStoredProcCommand(DBConstants.UPDATE_PENDING_PAYMENTS_STATUS))
    {
        dataBase.AddInParameter(dbCommand, "@PaymentData", DbType.Xml, xmlPaymentData);
        dataBase.AddOutParameter(dbCommand, "@Status", DbType.Int16, 0);
        dataBase.ExecuteNonQuery(dbCommand);

        int statusInt = Convert.ToInt16(dbCommand.Parameters["@Status"].Value);
        if(statusInt == 2)
        {
            response = true;
        }
    }

    return response;
}

What It Does:
1. Stored Procedure: Calls ws_UpdatePaymentInquiryStatus
2. Input Parameter: XML string containing payment inquiry response data
3. Output Parameter: Status code (2 = success)
4. Return: Boolean indicating success or failure

XML Input Structure:
The xmlPaymentData parameter contains serialized UpdateBookingOrderPayForData object with:
- OrderId, PayForDataId
- CardHolderName, CardNumber, CardExpiryDate
- GatewayName, GatewayStatusCode, GatewayStatusDescription
- RRN (Retrieval Reference Number)
- StatusCode, StatusDescription

Database Operations:
- Updates PayForData table with payment gateway response
- Updates payment status (Pending → Paid Success or Failed)
- Updates booking order status
- Stores card information (masked)
- Records gateway transaction details

How It Connects:
- Called by UpdatePaymentInquiryStatus() in service layer
- Executed after successful payment gateway inquiry response
- Updates database with final payment status

6. Update Booking Refund Status

public bool UpdateBookingRefundStatus(string xmlPaymentData)
{
    bool response = false;

    using (DbCommand dbCommand = dataBase.GetStoredProcCommand(DBConstants.UPDATE_PENDING_REFUND_PAYMENTS_STATUS))
    {
        dataBase.AddInParameter(dbCommand, "@PaymentData", DbType.Xml, xmlPaymentData);
        dataBase.AddOutParameter(dbCommand, "@Status", DbType.Int16, 0);
        dataBase.ExecuteNonQuery(dbCommand);

        int statusInt = Convert.ToInt16(dbCommand.Parameters["@Status"].Value);
        if (statusInt == 2)
        {
            response = true;
        }
    }

    return response;
}

What It Does:
1. Stored Procedure: Calls ws_UpdatePendingPaymentRefundStatus
2. Input Parameter: XML string containing refund response data
3. Output Parameter: Status code (2 = success)
4. Return: Boolean indicating refund update success

XML Input Structure:
The xmlPaymentData contains serialized UpdateBookingRefundRequestData object with:
- OrderId, PayForDataId
- TransactionId (refund transaction ID)
- Amount (refunded amount)
- StatusCode, StatusDescription

Database Operations:
- Updates PayForData refund status
- Changes payment status from “Pending For Refund” → “Refunded” or “Refund Failed”
- Records refund transaction ID and amount
- Updates refund timestamp

How It Connects:
- Called by UpdateBookingRefundRequestData() in service layer
- Executed after refund gateway response
- Triggers refund notification if successful (status 2 returned)

7. Update Wallet Purchase Payment Status

public bool UpdateWalletPurchasePayForData(string xmlPaymentData)
{
    bool response = false;

    using (DbCommand dbCommand = dataBase.GetStoredProcCommand(DBConstants.UPDATE_WALLET_PURCHASE_PENDING_PAYMENTS_STATUS))
    {
        dataBase.AddInParameter(dbCommand, "@PaymentData", DbType.Xml, xmlPaymentData);
        dataBase.AddOutParameter(dbCommand, "@Status", DbType.Int16, 0);
        dataBase.ExecuteNonQuery(dbCommand);

        int statusInt = Convert.ToInt16(dbCommand.Parameters["@Status"].Value);
        if (statusInt == 2)
        {
            response = true;
        }
    }

    return response;
}

What It Does:
1. Stored Procedure: Calls ws_UpdateWalletPurchasePaymentInquiryStatus
2. Input Parameter: XML containing wallet payment inquiry response
3. Output Parameter: Status code (2 = success)
4. Return: Boolean indicating update success

XML Input Structure:
Contains serialized UpdateBookingOrderPayForData object with:
- PayForDataId, UserWalletId
- Card information and gateway response data
- StatusCode, StatusDescription

Database Operations:
- Updates PayForData for wallet transactions
- Changes wallet payment status (Pending → Success/Failed)
- Credits user wallet if payment successful
- Records transaction details

How It Connects:
- Called by UpdateWalletPurchasePaymentInquiryStatus() in service layer
- Wallet credit is applied only on successful payment status

8. Update Package Purchase Payment Status

public bool UpdatePackagePurchasePayForData(string xmlPaymentData)
{
    bool response = false;

    using (DbCommand dbCommand = dataBase.GetStoredProcCommand(DBConstants.UPDATE_PACKAGE_PURCHASE_PENDING_PAYMENTS_STATUS))
    {
        dataBase.AddInParameter(dbCommand, "@PackagePaymentData", DbType.Xml, xmlPaymentData);
        dataBase.AddOutParameter(dbCommand, "@Status", DbType.Int16, 0);
        dataBase.ExecuteNonQuery(dbCommand);

        int statusInt = Convert.ToInt16(dbCommand.Parameters["@Status"].Value);
        if (statusInt == 2)
        {
            response = true;
        }
    }

    return response;
}

What It Does:
1. Stored Procedure: Calls ws_UpdatePackagePaymentInquiryStatus
2. Input Parameter: XML containing package payment inquiry response
3. Output Parameter: Status code (2 = success)
4. Return: Boolean indicating update success

XML Input Structure:
Contains serialized UpdateBookingOrderPayForData object with:
- PayForDataId, ClientPackageMainId
- TransactionID, card information
- Gateway response data

Database Operations:
- Updates PayForData for package transactions
- Changes package payment status (Pending → Success/Failed)
- Activates client package if payment successful
- Records transaction details

How It Connects:
- Called by UpdatePackagePurchasePaymentInquiryStatus() in service layer
- Package is activated only on successful payment


DAL - PaymentDataAccess.cs (Part 3 - Configuration & Notification Methods)

9. Get Psyter Application Token

public string GetPsyterApplicationToken()
{
    string applicationToken = "";
    using (DbCommand dbCommand = dataBase.GetStoredProcCommand(DBConstants.GET_APPLICATION_CONFIG_BY_GROUP_ID))
    {
        dataBase.AddInParameter(dbCommand, "@GroupId", DbType.Int64, 2);

        DataSet dataSet = dataBase.ExecuteDataSet(dbCommand);

        if (dataSet.Tables != null && dataSet.Tables.Count > 0)
        {
            var appConfig = MapDataTableToList<ApplicationConfiguration>(dataSet.Tables[0]).ToList();
            applicationToken = appConfig.FirstOrDefault(s => s.Id == 30)?.PropertyValue;
        }
    }

    return applicationToken;
}

What It Does:
1. Stored Procedure: Calls AppConfig_GetAppConfigSettingsByGroupId
2. Input Parameter: GroupId = 2 (API configuration group)
3. Configuration Lookup: Finds configuration with Id = 30 (PsyterAPI application token)
4. Return: Application token string for API authentication

Application Token Purpose:
- Used for OAuth authentication with PsyterAPI
- Required to obtain bearer token (access token)
- Stored in database for centralized configuration management

How It Connects:
- Called by CallNotifySCHFSExpiryAPI() and GetPendingFCMNotificationsAndReminders()
- Token is used to call PsyterApiAuthenticationToken() method
- Enables service to authenticate with PsyterAPI without hardcoded credentials

10. Get Pending FCM Notifications and Reminders

public UserNotificationsListWrapper GetPendingFCMNotificationsAndRemindersToSend()
{
    UserNotificationsListWrapper response = new UserNotificationsListWrapper();
    using (DbCommand dbCommand = dataBase.GetStoredProcCommand(DBConstants.GET_PENDING_NOTIFICATIONS_FOR_FCM))
    {
        DataSet dataSet = dataBase.ExecuteDataSet(dbCommand);

        if (dataSet.Tables != null && dataSet.Tables.Count > 0)
        {
            response.UserNotificationsList = MapDataTableToList<UserNotification>(dataSet.Tables[0]).ToList();
            response.FCMConfiguration = MapDataTableToObject<FCMConfiguration>(dataSet.Tables[1]);
            response.UserRemindersList = MapDataTableToList<UserReminder>(dataSet.Tables[2]).ToList();
        }
    }

    return response;
}

What It Does:
1. Stored Procedure: Calls Notification_GetNotificationsListToSentUsingFCM
2. Result Sets:
- Table 0: List of pending notifications (UserNotification objects)
- Table 1: FCM configuration (API URL, authorization key)
- Table 2: List of pending reminders (UserReminder objects)
3. Return: Wrapper containing all three result sets

Database Query Purpose:
- Notifications: System-generated notifications (payment success, refund, etc.)
- Reminders: Appointment reminders based on user preferences (5 min, 15 min, 1 hour, 1 day before)
- Configuration: Firebase Cloud Messaging API credentials

Notification Types:
- General notifications (NotificationType = 11)
- Booking reminders (NotificationType = 12)

Reminder Details:
Each reminder includes:
- Patient and physician information
- Booking details (SlotBookingId, SlotDateTimeUTC)
- FCM topic for targeted delivery
- Reminder timing (ReminderBeforeMinutes)
- Video SDK meeting ID

How It Connects:
- Called by GetPendingFCMNotificationsAndReminders() in service layer
- Results are processed and sent via Firebase Cloud Messaging
- Reminders are sent to specific user FCM topics

11. Mark FCM Notification as Sent

public bool MarkFCMNotificationSent(long id)
{
    bool response = false;

    using (DbCommand dbCommand = dataBase.GetStoredProcCommand(DBConstants.UPDATE_NOTIFICATIONS_SENT_STATUS))
    {
        dataBase.AddInParameter(dbCommand, "@UserNotificationId", DbType.Int64, id);
        dataBase.AddOutParameter(dbCommand, "@Status", DbType.Int16, 0);
        dataBase.ExecuteNonQuery(dbCommand);

        int statusInt = Convert.ToInt16(dbCommand.Parameters["@Status"].Value);
        if (statusInt == 2)
        {
            response = true;
        }
    }

    return response;
}

What It Does:
1. Stored Procedure: Calls Notification_UpdateNotificationSentStatus
2. Input Parameter: UserNotificationId
3. Output Parameter: Status code (2 = success)
4. Return: Boolean indicating update success

Database Operations:
- Updates notification record with “Sent” status
- Records sent timestamp
- Prevents duplicate notification sending

How It Connects:
- Called after successfully sending FCM notification
- Ensures each notification is sent only once

12. Mark Reminder as Sent

public bool MarkReminderSent(long id, long userLoginInfoId, string titlePLang, string titleSLang, 
                             string textPLang, string textSLang)
{
    bool response = false;

    using (DbCommand dbCommand = dataBase.GetStoredProcCommand(DBConstants.UPDATE_REMINDER_SENT_STATUS))
    {
        dataBase.AddInParameter(dbCommand, "@ReminderId", DbType.Int64, id);
        dataBase.AddInParameter(dbCommand, "@UserLoginInfoId", DbType.Int64, userLoginInfoId);
        dataBase.AddInParameter(dbCommand, "@SubjectPLang", DbType.String, titlePLang);
        dataBase.AddInParameter(dbCommand, "@SubjectSLang", DbType.String, titleSLang);
        dataBase.AddInParameter(dbCommand, "@BodyTextPLang", DbType.String, textPLang);
        dataBase.AddInParameter(dbCommand, "@BodyTextSLang", DbType.String, textSLang);
        dataBase.AddOutParameter(dbCommand, "@Status", DbType.Int16, 0);

        dataBase.ExecuteNonQuery(dbCommand);

        int statusInt = Convert.ToInt16(dbCommand.Parameters["@Status"].Value);
        if (statusInt == 2)
        {
            response = true;
        }
    }

    return response;
}

What It Does:
1. Stored Procedure: Calls Reminder_UpdateReminderSentStatus
2. Input Parameters:
- ReminderId, UserLoginInfoId
- Reminder title and text in both languages (PLang = Primary, SLang = Secondary)
3. Output Parameter: Status code (2 = success)
4. Return: Boolean indicating update success

Database Operations:
- Updates reminder record with “Sent” status
- Records sent timestamp and notification content
- Creates notification history for user
- Prevents duplicate reminder sending

Bilingual Support:
- PLang: Primary language (typically Arabic)
- SLang: Secondary language (typically English)
- Both versions stored for notification history

How It Connects:
- Called after successfully sending reminder FCM notification
- Records notification in user’s notification history
- Enables notification tracking and audit trail

13. Get User Reminders List

public List<UserReminderForFCMPayLoad> GetUserRemindersList(long userLoginInfoId, long slotBookingId)
{
    var response = new List<UserReminderForFCMPayLoad>();

    using (DbCommand dbCommand = dataBase.GetStoredProcCommand(DBConstants.GET_USER_REMINDERS_LIST))
    {
        dataBase.AddInParameter(dbCommand, "@UserLoginInfoId", DbType.Int64, userLoginInfoId);
        dataBase.AddInParameter(dbCommand, "@SlotBookingId", DbType.Int64, slotBookingId);

        DataSet dataSet = dataBase.ExecuteDataSet(dbCommand);

        if (dataSet.Tables != null && dataSet.Tables.Count > 0)
        {
            response = MapDataTableToList<UserReminderForFCMPayLoad>(dataSet.Tables[0]).ToList();
        }
    }

    return response;
}

What It Does:
1. Stored Procedure: Calls Reminder_GetUserReminderList
2. Input Parameters:
- UserLoginInfoId (patient or physician ID)
- SlotBookingId (specific booking)
3. Return: List of reminders for the user and booking

Purpose:
- Retrieves all reminder configurations for a specific user and booking
- Used to build comprehensive FCM notification payload
- Shows all reminder times (e.g., 5 min, 15 min, 1 hour, 1 day before appointment)

FCM Payload Integration:
- Reminder list is embedded in FCM notification data
- Mobile app displays all upcoming reminders for the booking
- User can see when next reminders will arrive

How It Connects:
- Called by GetPendingFCMNotificationsAndReminders() for each reminder
- Results are included in FCM notification template
- Enables rich notification experience in mobile app


DAL - XmlHelper.cs

File: WindowsService/PsyterPaymentInquiry/DAL/XmlHelper.cs

Purpose: Utility class for XML serialization of objects (used for stored procedure parameters)

Code Analysis

public class XmlHelper
{
    public static string ObjectToXml(object obj, bool includeEncodingInfo = false)
    {
        if (includeEncodingInfo)
            return ObjectToXmlWithEncoding(obj);

        return ObjectToXmlWithOutEncoding(obj);
    }
}

Public Method:
- ObjectToXml(): Main entry point for XML serialization
- includeEncodingInfo: Optional parameter to include XML declaration (default: false)
- Return: XML string representation of object

XML Serialization Without Encoding Declaration

private static string ObjectToXmlWithOutEncoding(object obj)
{
    try
    {
        XmlSerializerNamespaces ns = new XmlSerializerNamespaces();
        ns.Add("", "");

        using (StringWriter sw = new StringWriter())
        {
            using (XmlWriter writer = XmlWriter.Create(sw, new XmlWriterSettings {OmitXmlDeclaration = true}))
            {
                XmlSerializer serializer = new XmlSerializer(obj.GetType());
                serializer.Serialize(writer, obj, ns);
            }

            return sw.ToString();
        }
    }
    catch (Exception e)
    {
        throw new Exception("Error occured while converting object to Xml.", e);
    }
}

What It Does:
1. Namespace Removal: Creates empty namespace to prevent XML namespace attributes
2. XML Writer Configuration: OmitXmlDeclaration = true (no <?xml version="1.0"?> header)
3. Serialization: Uses XmlSerializer to convert object to XML
4. Return: Clean XML string without declaration

Output Example:

<UpdateBookingOrderPayForData>
  <OrderId>12345</OrderId>
  <PayForDataId>67890</PayForDataId>
  <StatusCode>00000</StatusCode>
  <StatusDescription>Transaction Success</StatusDescription>
</UpdateBookingOrderPayForData>

Why No Encoding Declaration:
- SQL Server stored procedures prefer clean XML
- Easier to read in logs and debugging
- No need for encoding declaration in stored procedure parameters

XML Serialization With Encoding Declaration

private static string ObjectToXmlWithEncoding(object obj)
{
    StringWriter sw = new StringWriter();
    XmlTextWriter writer = null;

    try
    {
        XmlSerializer serializer = new XmlSerializer(obj.GetType());
        writer = new XmlTextWriter(sw);
        serializer.Serialize(writer, obj);
    }
    catch (Exception e)
    {
        throw new Exception("Error occured while converting object to Xml.", e);
    }
    finally
    {
        sw.Close();
        writer?.Close();
    }

    return sw.ToString();
}

What It Does:
1. XML Text Writer: Uses XmlTextWriter (includes XML declaration)
2. Serialization: Converts object to XML with full formatting
3. Return: XML string with <?xml version="1.0" encoding="utf-16"?> header

Output Example:

<?xml version="1.0" encoding="utf-16"?>
<UpdateBookingOrderPayForData>
  <OrderId>12345</OrderId>
  <PayForDataId>67890</PayForDataId>
</UpdateBookingOrderPayForData>

Usage Scenarios:
- Used when XML needs to be saved to file
- Required for XML documents that need encoding specification
- Not commonly used in this service (most calls use default without encoding)

How It Connects to the Service

XML Serialization Flow:

Payment Update Object (C#)
    ↓
XmlHelper.ObjectToXml()
    ↓
XML String
    ↓
Stored Procedure Parameter (@PaymentData as XML)
    ↓
SQL Server parses and updates database

Usage Examples in Service:

  1. Booking Payment Update:

    UpdateBookingOrderPayForData updateObj = new UpdateBookingOrderPayForData();
    // ... populate properties
    var xml = XmlHelper.ObjectToXml(updateObj);
    dal.UpdateBookingOrderPayForData(xml);
    

  2. Refund Update:

    UpdateBookingRefundRequestData refundObj = new UpdateBookingRefundRequestData();
    // ... populate properties
    var xml = XmlHelper.ObjectToXml(refundObj);
    dal.UpdateBookingRefundStatus(xml);
    

Why XML for Stored Procedure Parameters:
- Flexibility: Can pass complex objects with multiple properties
- Type Safety: SQL Server validates XML structure
- Maintainability: Easier to add new properties without changing stored procedure signature
- Performance: SQL Server efficiently parses XML parameters


Payment Processing Workflows - Overview

The service implements 4 main payment processing workflows:

  1. Booking Payment Inquiry - Check status of pending booking payments
  2. Refund Processing - Process refund requests for cancelled bookings
  3. Wallet Purchase Inquiry - Check status of wallet credit purchases
  4. Package Purchase Inquiry - Check status of session package purchases

Each workflow follows a similar pattern but has unique business logic.


1. Booking Payment Inquiry Workflow (GetPendingPayment)

File: WindowsService/PsyterPaymentInquiry/PaymentInquiryService.cs

Method Overview

public async Task<PendingPaymentResponse> GetPendingPayment()

Purpose: Main method for processing pending booking payment inquiries

Execution Flow:
1. Retrieve pending payments from database
2. Load payment gateway configuration
3. For each pending payment:
- Generate secure hash
- Send inquiry to payment gateway
- Process gateway response
4. Process refund requests
5. Process wallet purchase inquiries
6. Process package purchase inquiries

Code Analysis - Step by Step

Step 1: Retrieve Pending Payments

PendingPaymentResponse response = new PendingPaymentResponse();
try
{
    PaymentDataAccess dal = new PaymentDataAccess();
    var paymentResponse = dal.GETPendingPayments();
    WriteToFile("Pending Payments Count :  " + paymentResponse.PendingPaymentsList.Count, "Inquiry");
    WriteToFile("Waiting until pending payments inquiry is being processed " + DateTime.Now, "Refund");

    SchedulingAPIApplicationToken = paymentResponse.ApplicationAPIToken;

What It Does:
1. Creates DAL instance
2. Calls GETPendingPayments() to retrieve:
- List of pending booking payments
- Payment gateway configuration settings
- SchedulingAPI application token
3. Logs pending payment count
4. Stores SchedulingAPI token for later use

Payment Selection Criteria:
- Payment status = “Pending” or “Processing”
- Transaction created but status not confirmed
- InquiryCount < 3 (prevents infinite retry loops)

Step 2: Load Payment Gateway Configuration

if (paymentResponse.AppConfigSettingList.Count > 0 && paymentResponse.PendingPaymentsList.Count > 0)
{
    response.PendingPaymentsList = paymentResponse.PendingPaymentsList;

    response.AppConfigSetting = new PaymentApplicationConfiguration();
    response.AppConfigSetting.SmartRoutingSecretKey = paymentResponse.AppConfigSettingList
        .FirstOrDefault(s => s.TitlePLang == "SmartRoutingSecretKey")?.PropertyValue;
    response.AppConfigSetting.SmartRoutingResponseBackURL = paymentResponse.AppConfigSettingList
        .FirstOrDefault(s => s.TitlePLang == "SmartRoutingResponseBackURL")?.PropertyValue;
    response.AppConfigSetting.RedirectToPaymentMobileToken = paymentResponse.AppConfigSettingList
        .FirstOrDefault(s => s.TitlePLang == "RedirectToPaymentMobileToken")?.PropertyValue;
    response.AppConfigSetting.SmartRoutingRedirectURL = paymentResponse.AppConfigSettingList
        .FirstOrDefault(s => s.TitlePLang == "SmartRoutingRedirectURL")?.PropertyValue;
    response.AppConfigSetting.SmartRoutingMerchantId = paymentResponse.AppConfigSettingList
        .FirstOrDefault(s => s.TitlePLang == "SmartRoutingMerchantId")?.PropertyValue;
    response.AppConfigSetting.SmartRoutingCurrencyISOCode = paymentResponse.AppConfigSettingList
        .FirstOrDefault(s => s.TitlePLang == "SmartRoutingCurrencyISOCode")?.PropertyValue;
    response.AppConfigSetting.SmartRoutingRefundInquiryUrl = paymentResponse.AppConfigSettingList
        .FirstOrDefault(s => s.TitlePLang == "SmartRoutingRefundInquiryUrl")?.PropertyValue;
    response.AppConfigSetting.SmartRoutingThemeId = paymentResponse.AppConfigSettingList
        .FirstOrDefault(s => s.TitlePLang == "SmartRoutingThemeId")?.PropertyValue;
    response.AppConfigSetting.SmartRoutingVersion = paymentResponse.AppConfigSettingList
        .FirstOrDefault(s => s.TitlePLang == "SmartRoutingVersion")?.PropertyValue;
    response.AppConfigSetting.SmartRoutingItemId = paymentResponse.AppConfigSettingList
        .FirstOrDefault(s => s.TitlePLang == "SmartRoutingItemId")?.PropertyValue;
}

What It Does:
Populates PaymentApplicationConfiguration object with 10 gateway settings:

  1. SmartRoutingSecretKey: Shared secret for hash generation (security)
  2. SmartRoutingResponseBackURL: Callback URL for payment responses
  3. RedirectToPaymentMobileToken: Token for mobile payment redirection
  4. SmartRoutingRedirectURL: URL for payment gateway redirect
  5. SmartRoutingMerchantId: Merchant identifier in payment gateway
  6. SmartRoutingCurrencyISOCode: Currency code (e.g., “SAR” for Saudi Riyal)
  7. SmartRoutingRefundInquiryUrl: Gateway API endpoint for inquiry/refund
  8. SmartRoutingThemeId: UI theme identifier for payment page
  9. SmartRoutingVersion: Payment gateway API version
  10. SmartRoutingItemId: Item identifier in gateway system

Why Configuration is Database-Driven:
- Easy to change without recompiling service
- Different environments (Dev/Staging/Prod) use different configurations
- Security: Secret key not hardcoded in source code

Step 3: Process Each Pending Payment

if (paymentResponse.PendingPaymentsList.Count > 0)
{
    foreach (var pendingpay in response.PendingPaymentsList)
    {
        WriteToFile("TransactionId =" + pendingpay.TransactionId + ", OrderId =" + pendingpay.OrderId, "Inquiry");

        RequestSecureHash requestHash = new RequestSecureHash();
        requestHash.SECRECT_KEY = response.AppConfigSetting.SmartRoutingSecretKey;
        requestHash.MESSAGE_ID = "2"; // 2 for Inquiry
        requestHash.TRANSACTION_ID = pendingpay.TransactionId;
        SecureHashResponse hashResponse = GenerateSecureHash(requestHash, response.AppConfigSetting);

        RequestProcessInquiry requestInquiry = new RequestProcessInquiry();
        requestInquiry.SECURE_HASH = hashResponse.SECURE_HASH;
        requestInquiry.SECRECT_KEY = response.AppConfigSetting.SmartRoutingSecretKey;
        requestInquiry.MESSAGE_ID = "2"; // 2 for Inquiry
        requestInquiry.TRANSACTION_ID = pendingpay.TransactionId;
        requestInquiry.ORDER_ID = pendingpay.OrderId;
        requestInquiry.PAYFORDATA_ID = pendingpay.PayForDataId;

        var processResponse = await ProcessInquiryOrRefund(requestInquiry, response.AppConfigSetting, pendingpay);
    }
}
else
{
    WriteToFile("-----------------------------END--------------------------------", "Inquiry");
}

What It Does:
For each pending payment:

  1. Log Transaction: Logs TransactionId and OrderId
  2. Prepare Hash Request:
    - SECRET_KEY from configuration
    - MESSAGE_ID = “2” (inquiry operation code)
    - TRANSACTION_ID from pending payment record
  3. Generate Secure Hash: Calls GenerateSecureHash() to create SHA256 hash
  4. Prepare Inquiry Request:
    - SECURE_HASH from previous step
    - MESSAGE_ID = “2”
    - TRANSACTION_ID, ORDER_ID, PAYFORDATA_ID
  5. Process Inquiry: Calls ProcessInquiryOrRefund() to:
    - Send HTTP request to payment gateway
    - Parse gateway response
    - Update database with payment status

Message ID Values:
- 2: Inquiry operation (check transaction status)
- 4: Refund operation (process refund request)

Step 4: Chain Additional Processing

WriteToFile("Pending payments Refund process started: " + DateTime.Now, "Refund");
await GetPendingRefundPayment();

WriteToFile("Pending Wallet purchase payments inquire process started: " + DateTime.Now, "Inquiry");
await GetPendingWalletPurchasedPayment();

WriteToFile("Pending Package purchase payments inquire process started: " + DateTime.Now, "Inquiry");
await GetPendingPackagePurchasedPayment();

What It Does:
After processing booking payment inquiries, chains additional operations:

  1. GetPendingRefundPayment(): Process refund requests
  2. GetPendingWalletPurchasedPayment(): Check wallet purchase statuses
  3. GetPendingPackagePurchasedPayment(): Check package purchase statuses

Why Sequential:
- Each operation is independent
- Prevents database lock conflicts
- Enables targeted logging per operation type
- Clearer error handling and troubleshooting

Step 5: Exception Handling

catch(Exception ex)
{
    WriteToFile("Exception occur " + DateTime.Now +", Error: "+ ex.Message, "Inquiry");
    WriteToFile("-----------------------------END--------------------------------", "Inquiry");
}

return response;

What It Does:
1. Catches any exceptions during processing
2. Logs exception message with timestamp
3. Logs end marker
4. Returns response (may be empty if exception occurred early)

Error Recovery:
- Service continues running (exception doesn’t crash service)
- Next timer cycle will retry failed payments
- All errors logged for troubleshooting


2. Refund Processing Workflow (GetPendingRefundPayment)

Method Overview

public async Task<PendingPaymentResponse> GetPendingRefundPayment()

Purpose: Process refund requests for cancelled or rejected bookings

Execution Flow:
1. Retrieve pending refund requests from database
2. Load payment gateway configuration
3. For each refund request:
- Calculate refund amount
- Generate secure hash
- Send refund request to payment gateway
- Process gateway response
- Send refund notification if successful

Code Analysis - Refund-Specific Logic

Refund Amount Calculation

foreach (var pendingpay in response.PendingPaymentsList)
{
    WriteToFile("TransactionId =" + pendingpay.TransactionId + ", OrderId =" + pendingpay.OrderId, "Refund");

    int amountFormat = Convert.ToInt32(Convert.ToDecimal(pendingpay.TotalAmount) * 100);

What It Does:
- Amount Format: Converts amount to integer format expected by gateway
- Example: 150.50 SAR → 15050 (multiply by 100, no decimal point)
- Why: Payment gateways typically use integer amounts (smallest currency unit)

Refund vs Refund Inquiry

RequestSecureHash requestHash = new RequestSecureHash();
requestHash.SECRECT_KEY = response.AppConfigSetting.SmartRoutingSecretKey;
requestHash.MESSAGE_ID = "4"; // 4 for refund
requestHash.TRANSACTION_ID = pendingpay.TransactionId;
requestHash.AMOUNT = amountFormat.ToString();

if(pendingpay.PaymentStatus == "Pending Refund")
{
    requestHash.IsRefundInquiry = true;
}

SecureHashResponse hashResponse = GenerateSecureHash(requestHash, response.AppConfigSetting);

What It Does:
Handles two refund scenarios:

  1. Pending For Refund: Initial refund request
    - MESSAGE_ID = “4” (refund operation)
    - Includes AMOUNT parameter
    - Sends refund request to gateway
    - Creates new refund transaction

  2. Pending Refund: Refund already initiated, checking status
    - Sets IsRefundInquiry = true
    - Changes MESSAGE_ID internally to “2” (inquiry)
    - Checks status of existing refund transaction
    - No new refund transaction created

Status Progression:

Paid Success → Pending For Refund → Pending Refund → Refunded
                  (refund initiated)   (checking status)  (completed)

Refund Request Preparation

RequestProcessInquiry requestInquiry = new RequestProcessInquiry();
requestInquiry.HASH_TRANSACTION_ID = hashResponse.TRANSACTION_ID;
requestInquiry.SECURE_HASH = hashResponse.SECURE_HASH;
requestInquiry.SECRECT_KEY = response.AppConfigSetting.SmartRoutingSecretKey;
requestInquiry.MESSAGE_ID = "4"; // 4 for refund
requestInquiry.TRANSACTION_ID = pendingpay.TransactionId;
requestInquiry.AMOUNT = amountFormat.ToString();
requestInquiry.ORDER_ID = pendingpay.OrderId;
requestInquiry.PAYFORDATA_ID = pendingpay.PayForDataId;

var processResponse = await ProcessInquiryOrRefund(requestInquiry, response.AppConfigSetting, pendingpay);

What It Does:
Prepares refund request with:

  1. HASH_TRANSACTION_ID: New transaction ID for refund (generated by GenerateSecureHash())
  2. SECURE_HASH: SHA256 hash for request validation
  3. MESSAGE_ID: “4” for refund operation
  4. TRANSACTION_ID: Original payment transaction ID
  5. AMOUNT: Refund amount in gateway format
  6. ORDER_ID, PAYFORDATA_ID: Database identifiers

Two Transaction IDs:
- TRANSACTION_ID: Original payment transaction (e.g., “PSY1634567890123”)
- HASH_TRANSACTION_ID: New refund transaction (e.g., “PSY1634567890456”)


3. Wallet Purchase Inquiry Workflow (GetPendingWalletPurchasedPayment)

Method Overview

public async Task<PendingPaymentResponse> GetPendingWalletPurchasedPayment()

Purpose: Check payment status for wallet credit purchases

Key Differences from Booking Inquiry:

foreach (var pendingpay in response.PendingPaymentsList)
{
    WriteToFile("TransactionId =" + pendingpay.TransactionId + ", WalletId =" + pendingpay.UserWalletId, "Inquiry");

    // ... secure hash generation (same as booking inquiry)

    RequestProcessInquiry requestInquiry = new RequestProcessInquiry();
    requestInquiry.SECURE_HASH = hashResponse.SECURE_HASH;
    requestInquiry.SECRECT_KEY = response.AppConfigSetting.SmartRoutingSecretKey;
    requestInquiry.MESSAGE_ID = "2"; // 2 for Inquiry
    requestInquiry.TRANSACTION_ID = pendingpay.TransactionId;
    requestInquiry.PAYFORDATA_ID = pendingpay.PayForDataId;
    requestInquiry.USER_WALLET_ID = pendingpay.UserWalletId;  // Wallet-specific

    var processResponse = await ProcessInquiryOrRefund(requestInquiry, response.AppConfigSetting, pendingpay);
}

What It Does:
1. Logs with WalletId instead of OrderId
2. Includes USER_WALLET_ID in request (instead of ORDER_ID)
3. Same inquiry process as booking payments

Wallet Purchase Flow:

User adds funds → Payment gateway → Pending status → Inquiry checks status → 
→ If Success: Credit wallet → Update status to "Paid Success"
→ If Failed: Update status to "Failed"

No Refund Processing:
- Wallet purchases are inquiry-only
- No refund workflow for wallet credits
- User can use wallet funds for future bookings


4. Package Purchase Inquiry Workflow (GetPendingPackagePurchasedPayment)

Method Overview

public async Task<PendingPaymentResponse> GetPendingPackagePurchasedPayment()

Purpose: Check payment status for session package purchases

Key Differences:

foreach (var pendingpay in response.PendingPaymentsList)
{
    WriteToFile("TransactionId =" + pendingpay.TransactionId + ", ClientPackageMainId =" + pendingpay.ClientPackageMainId, "Inquiry");

    // ... secure hash generation (same as booking inquiry)

    RequestProcessInquiry requestInquiry = new RequestProcessInquiry();
    requestInquiry.SECURE_HASH = hashResponse.SECURE_HASH;
    requestInquiry.SECRECT_KEY = response.AppConfigSetting.SmartRoutingSecretKey;
    requestInquiry.MESSAGE_ID = "2"; // 2 for Inquiry
    requestInquiry.TRANSACTION_ID = pendingpay.TransactionId;
    requestInquiry.PAYFORDATA_ID = pendingpay.PayForDataId;
    requestInquiry.CLIENT_PACKAGE_MAIN_ID = pendingpay.ClientPackageMainId;  // Package-specific

    var processResponse = await ProcessInquiryOrRefund(requestInquiry, response.AppConfigSetting, pendingpay);
}

What It Does:
1. Logs with ClientPackageMainId
2. Includes CLIENT_PACKAGE_MAIN_ID in request
3. Same inquiry process as booking payments

Package Purchase Flow:

User buys package → Payment gateway → Pending status → Inquiry checks status → 
→ If Success: Activate package → Update status to "Paid Success"
→ If Failed: Update status to "Failed"

Package Benefits:
- Multiple sessions at discounted rate
- Pre-purchased sessions stored in package
- User books appointments using package credits
- No individual payment per booking


Secure Hash Generation (GenerateSecureHash)

Method Overview

public SecureHashResponse GenerateSecureHash(RequestSecureHash request, PaymentApplicationConfiguration appConfigSetting)

Purpose: Generate SHA256 secure hash for payment gateway API authentication

Security Purpose:
- Ensures request authenticity (prevents tampering)
- Validates request came from legitimate merchant
- Gateway verifies hash before processing transaction

Code Analysis

Step 1: Build Sorted Dictionary

SecureHashResponse response = new SecureHashResponse();
try
{
    SortedDictionary<string, string> dictionary = new SortedDictionary<String, String>();
    string transactionId = "PSY" + (DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond).ToString();

    if (request.IsRefundInquiry)
    {
        dictionary["MessageID"] = "2"; // Inquiry for refund
    }
    else
    {
        dictionary["MessageID"] = request.MESSAGE_ID;
    }

    if (request.MESSAGE_ID == "4" && !request.IsRefundInquiry)
    {
        dictionary.Add("TransactionID", transactionId);
        dictionary.Add("Amount", request.AMOUNT);
        dictionary.Add("CurrencyISOCode", appConfigSetting.SmartRoutingCurrencyISOCode);
    }

    dictionary.Add("OriginalTransactionID", request.TRANSACTION_ID);
    dictionary.Add("MerchantID", appConfigSetting.SmartRoutingMerchantId);
    dictionary.Add("Version", appConfigSetting.SmartRoutingVersion);

What It Does:

  1. Generate New Transaction ID:
    - Format: “PSY” + Unix timestamp in milliseconds
    - Example: “PSY1634567890123”
    - Used for new refund transactions

  2. Add MessageID:
    - If IsRefundInquiry = true: Use “2” (inquiry)
    - Otherwise: Use request MESSAGE_ID (“2” or “4”)

  3. Add Refund-Specific Fields (if MESSAGE_ID = “4”):
    - TransactionID (newly generated)
    - Amount (refund amount)
    - CurrencyISOCode (e.g., “SAR”)

  4. Add Common Fields:
    - OriginalTransactionID (transaction to inquire/refund)
    - MerchantID (merchant identifier)
    - Version (API version)

Why SortedDictionary:
- Hash generation requires fields in alphabetical order
- SortedDictionary automatically sorts keys
- Ensures consistent hash generation

Field Order Example (Inquiry):

1. MerchantID
2. MessageID
3. OriginalTransactionID
4. Version

Field Order Example (Refund):

1. Amount
2. CurrencyISOCode
3. MerchantID
4. MessageID
5. OriginalTransactionID
6. TransactionID
7. Version

Step 2: Build String for Hashing

StringBuilder orderedString = new StringBuilder();
orderedString.Append(request.SECRECT_KEY);
foreach (KeyValuePair<string, string> kv in dictionary)
{
    orderedString.Append(kv.Value);
}

What It Does:
1. Start with SECRET_KEY
2. Append all dictionary values in sorted order
3. Creates single concatenated string

Example String (Inquiry):

{SECRET_KEY}12345222PSY16345678901231.0
           ^     ^  ^              ^
           |     |  |              Version
           |     |  OriginalTransactionID
           |     MessageID
           MerchantID

Example String (Refund):

{SECRET_KEY}15050SAR123452PSY1634567890456PSY16345678901231.0
           ^    ^  ^     ^ ^                ^              ^
           |    |  |     | |                |              Version
           |    |  |     | |                OriginalTransactionID
           |    |  |     | TransactionID (new)
           |    |  |     MessageID
           |    |  MerchantID
           |    CurrencyISOCode
           Amount

Step 3: Generate SHA256 Hash

SHA256 sha256;
byte[] bytes, hash;
string secureHash = string.Empty;

bytes = Encoding.UTF8.GetBytes(orderedString.ToString());
sha256 = SHA256Managed.Create();
hash = sha256.ComputeHash(bytes);

foreach (byte x in hash)
{
    secureHash += String.Format("{0:x2}", x);
}

response.SECURE_HASH = secureHash;
response.TRANSACTION_ID = transactionId.ToString();

What It Does:
1. Convert to Bytes: Convert concatenated string to UTF-8 bytes
2. Compute Hash: Generate SHA256 hash (32-byte output)
3. Convert to Hex: Format each byte as 2-digit hex string
4. Return: 64-character hex string (lowercase)

Example Hash Output:

a3f5c8d9e2b1f4a6c8d9e2b1f4a6c8d9e2b1f4a6c8d9e2b1f4a6c8d9e2b1f4a6

Response Object:
- SECURE_HASH: 64-character hex string
- TRANSACTION_ID: Newly generated transaction ID (for refunds)

Exception Handling

catch (Exception ex)
{
    if (request.MESSAGE_ID == "4")
    {
        WriteToFile("Exception occur during creating Secure Hash" + DateTime.Now + ", Error: " + ex.Message, "Refund");
        WriteToFile("-----------------------------END--------------------------------", "Refund");
    }
    else
    {
        WriteToFile("Exception occur during creating Secure Hash" + DateTime.Now + ", Error: " + ex.Message, "Inquiry");
        WriteToFile("-----------------------------END--------------------------------", "Inquiry");
    }
}

return response;

What It Does:
- Logs exception to appropriate log file (Inquiry or Refund)
- Returns response (may contain empty SECURE_HASH if exception occurred)
- Service continues processing other payments


Payment Gateway Communication (ProcessInquiryOrRefund)

Method Overview

public async Task<ProcessResponse> ProcessInquiryOrRefund(RequestProcessInquiry requestParam, 
                                                          PaymentApplicationConfiguration appConfigSetting, 
                                                          PendingPayment paymentDetail = null)

Purpose: Send HTTP request to payment gateway and process response

Parameters:
- requestParam: Request data (transaction ID, secure hash, etc.)
- appConfigSetting: Gateway configuration (URL, merchant ID, etc.)
- paymentDetail: Original payment details (for logging and status update)

Code Analysis

Step 1: SSL/TLS Configuration

ServicePointManager.ServerCertificateValidationCallback += new RemoteCertificateValidationCallback(AllwaysGoodCertificate);
ServicePointManager.Expect100Continue = true;
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;

What It Does:
1. Certificate Validation: Accepts all SSL certificates (bypass validation)
2. HTTP 100-Continue: Enables HTTP 100-Continue behavior
3. TLS 1.2: Forces TLS 1.2 protocol for secure communication

Certificate Bypass Method:

private static bool AllwaysGoodCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors policyErrors)
{
    return true;
}

Why Bypass Certificate Validation:
- Some payment gateways use self-signed certificates in test environments
- Production should use proper certificate validation
- Security Note: This is a potential security risk in production

Step 2: Build Request Parameters

var dictionaryForWebClient = new NameValueCollection();
bool isRefundInquiry = false;

if (paymentDetail != null)
{
    if(paymentDetail.PaymentStatus == "Pending Refund")
    {
        isRefundInquiry = true;
        dictionaryForWebClient["MessageID"] = "2"; // Inquiry for refund
    }
    else
    {
        dictionaryForWebClient["MessageID"] = requestParam.MESSAGE_ID;
    }
}
else
{
    dictionaryForWebClient["MessageID"] = requestParam.MESSAGE_ID;
}

if (requestParam.MESSAGE_ID == "4" && !isRefundInquiry)
{
    dictionaryForWebClient["TransactionID"] = requestParam.HASH_TRANSACTION_ID;
    dictionaryForWebClient["Amount"] = requestParam.AMOUNT;
    dictionaryForWebClient["CurrencyISOCode"] = appConfigSetting.SmartRoutingCurrencyISOCode;
}

dictionaryForWebClient["MerchantID"] = appConfigSetting.SmartRoutingMerchantId;
dictionaryForWebClient["OriginalTransactionID"] = requestParam.TRANSACTION_ID;
dictionaryForWebClient["SecureHash"] = requestParam.SECURE_HASH;
dictionaryForWebClient["Version"] = appConfigSetting.SmartRoutingVersion;

What It Does:
Builds HTTP POST parameters:

For Inquiry (MESSAGE_ID = “2”):

MessageID=2
MerchantID=12345
OriginalTransactionID=PSY1634567890123
SecureHash=a3f5c8d9e2b1f4a6...
Version=1.0

For Refund (MESSAGE_ID = “4”):

MessageID=4
TransactionID=PSY1634567890456
Amount=15050
CurrencyISOCode=SAR
MerchantID=12345
OriginalTransactionID=PSY1634567890123
SecureHash=a3f5c8d9e2b1f4a6...
Version=1.0

For Refund Inquiry (PaymentStatus = “Pending Refund”):

MessageID=2
MerchantID=12345
OriginalTransactionID=PSY1634567890123
SecureHash=a3f5c8d9e2b1f4a6...
Version=1.0

Step 3: Send HTTP Request

String output = string.Empty;
string urlPath = appConfigSetting.SmartRoutingRefundInquiryUrl;

using (WebClient wb = new WebClient())
{
    var webresponse = wb.UploadValues(urlPath, "POST", dictionaryForWebClient);
    output = System.Text.Encoding.UTF8.GetString(webresponse);
}

if (requestParam.MESSAGE_ID == "4")
{
    WriteToFile("Payment Gateway Response :  " + output, "Refund");
}
else
{
    WriteToFile("Payment Gateway Response :  " + output, "Inquiry");
}

What It Does:
1. URL: Uses SmartRoutingRefundInquiryUrl from configuration
2. HTTP POST: Sends parameters using WebClient
3. Response: Receives query string response from gateway
4. Logging: Logs raw response to appropriate log file

Gateway Response Format:

Response.StatusCode=00000&Response.StatusDescription=Transaction Success&Response.TransactionID=PSY1634567890123&Response.CardNumber=411111******1111&Response.CardHolderName=John Doe&Response.RRN=123456789012&Response.GatewayName=PaymentGateway&Response.GatewayStatusCode=00&Response.GatewayStatusDescription=Approved

Step 4: Parse Gateway Response

SortedDictionary<string, string> result = new SortedDictionary<String, String>();
DBHelper helper = new DBHelper();
NameValueCollection qscoll = helper.ParseQueryString(output);

foreach (var kv in qscoll.AllKeys)
{
    result.Add(kv, qscoll[kv]);
}

What It Does:
1. Parse Query String: Uses DBHelper.ParseQueryString() to split response
2. Convert to Dictionary: Stores in SortedDictionary for easy access
3. Key-Value Access: Enables result["Response.StatusCode"] syntax

Parsed Result Example:

result["Response.StatusCode"] = "00000"
result["Response.StatusDescription"] = "Transaction Success"
result["Response.TransactionID"] = "PSY1634567890123"
result["Response.CardNumber"] = "411111******1111"
result["Response.CardHolderName"] = "John Doe"
result["Response.RRN"] = "123456789012"
result["Response.GatewayName"] = "PaymentGateway"
result["Response.GatewayStatusCode"] = "00"
result["Response.GatewayStatusDescription"] = "Approved"

Step 5: Route to Update Method

if (result.ContainsKey("Response.StatusCode"))
{
    if(requestParam.ORDER_ID > 0 && requestParam.CLIENT_PACKAGE_MAIN_ID == 0)
    {
        if (requestParam.MESSAGE_ID == "4")
        {
            await UpdateBookingRefundRequestData(requestParam, result, paymentDetail);
        }
        else
        {
            await UpdatePaymentInquiryStatus(requestParam, result, paymentDetail);
        }
    }
    else if(requestParam.ORDER_ID == 0 && requestParam.CLIENT_PACKAGE_MAIN_ID == 0 && requestParam.USER_WALLET_ID > 0)
    {
        await UpdateWalletPurchasePaymentInquiryStatus(requestParam, result, paymentDetail);
    }
    else if (requestParam.ORDER_ID == 0 && requestParam.CLIENT_PACKAGE_MAIN_ID > 0 && requestParam.USER_WALLET_ID == 0)
    {
        await UpdatePackagePurchasePaymentInquiryStatus(requestParam, result, paymentDetail);
    }
}

What It Does:
Routes to appropriate update method based on transaction type:

  1. Booking Refund: ORDER_ID > 0 and MESSAGE_ID = "4"
    UpdateBookingRefundRequestData()

  2. Booking Inquiry: ORDER_ID > 0 and MESSAGE_ID = "2"
    UpdatePaymentInquiryStatus()

  3. Wallet Purchase: USER_WALLET_ID > 0
    UpdateWalletPurchasePaymentInquiryStatus()

  4. Package Purchase: CLIENT_PACKAGE_MAIN_ID > 0
    UpdatePackagePurchasePaymentInquiryStatus()

Update Methods (covered in Part 3):
- Process gateway response
- Update database with payment status
- Trigger additional actions (booking confirmation, notifications, etc.)

Exception Handling

catch (Exception ex)
{
    if (requestParam.MESSAGE_ID == "4")
    {
        WriteToFile("Exception occur " + DateTime.Now + ", Error: " + ex.Message, "Refund");
        WriteToFile("-----------------------------END--------------------------------", "Refund");
    }
    else
    {
        WriteToFile("Exception occur " + DateTime.Now + ", Error: " + ex.Message, "Inquiry");
        WriteToFile("-----------------------------END--------------------------------", "Inquiry");
    }
}

return response;

What It Does:
- Logs exception to appropriate log file
- Returns empty response
- Service continues processing other payments


Summary of Part 2

This document has covered:

Data Access Layer (DAL) - Part 2
- 4 Update methods (booking, refund, wallet, package)
- 4 Notification/reminder methods
- Configuration retrieval method
- XmlHelper utility for object serialization

Payment Processing Workflows
- GetPendingPayment() - Main booking inquiry
- GetPendingRefundPayment() - Refund processing
- GetPendingWalletPurchasedPayment() - Wallet inquiry
- GetPendingPackagePurchasedPayment() - Package inquiry

Payment Gateway Integration
- GenerateSecureHash() - SHA256 hash generation
- ProcessInquiryOrRefund() - Gateway communication
- Request/response handling
- Transaction status parsing


Coming in Part 3

The final document will cover:

🔜 Payment Status Update Methods (4 methods)
- UpdatePaymentInquiryStatus()
- UpdateBookingRefundRequestData()
- UpdateWalletPurchasePaymentInquiryStatus()
- UpdatePackagePurchasePaymentInquiryStatus()

🔜 API Integration
- PsyterAPI authentication
- SchedulingAPI authentication
- CallRefundNotificationAPI()
- UpdateBookingStatusInScheduling()
- CallNotifySCHFSExpiryAPI()

🔜 FCM Notification System
- GetPendingFCMNotificationsAndReminders()
- CallSendFCMNotificationCommonAPI()
- Reminder processing and payload construction

🔜 Supporting Features
- DeleteLogFilesOfPreviousMonth()
- Log cleanup logic

🔜 Data Transfer Objects (DTO) - All 5 Files
- AppConfigSetting.cs
- FCMNotificationModal.cs
- InquiryPayment.cs
- PendingPaymentModal.cs
- PsyterAPIAuth.cs

🔜 Deployment
- Advanced Installer project
- Service installation guide

🔜 System Architecture Summary
- Complete workflow diagrams
- Inter-component communication
- Error handling and retry logic


End of Part 2