Psyter - WindowsService Detailed Structure (Part 3 of 4)

Part 3 Overview

This document continues from Part 2 and covers:
- Payment Status Update Methods: Processing gateway responses and updating database
- API Authentication: OAuth token generation for PsyterAPI and SchedulingAPI
- Booking API Integration: Refund notifications and booking status updates


Payment Status Update Methods - Overview

After receiving payment gateway responses (from ProcessInquiryOrRefund()), the service updates the database and triggers additional actions. Four specialized update methods handle different payment types:

  1. UpdatePaymentInquiryStatus() - Booking payment inquiry responses
  2. UpdateBookingRefundRequestData() - Refund transaction responses
  3. UpdateWalletPurchasePaymentInquiryStatus() - Wallet purchase responses
  4. UpdatePackagePurchasePaymentInquiryStatus() - Package purchase responses

Each method has unique business logic for status transitions and side effects.


1. Update Booking Payment Inquiry Status (UpdatePaymentInquiryStatus)

File: WindowsService/PsyterPaymentInquiry/PaymentInquiryService.cs

Method Overview

public async Task UpdatePaymentInquiryStatus(RequestProcessInquiry requestParam, 
                                            SortedDictionary<string, string> result, 
                                            PendingPayment paymentDetail)

Purpose: Process payment gateway inquiry response for booking payments and update booking status

Parameters:
- requestParam: Original inquiry request (OrderId, PayForDataId, etc.)
- result: Parsed gateway response (StatusCode, card details, etc.)
- paymentDetail: Original pending payment record (booking details, inquiry count)

Code Analysis - Step by Step

Step 1: Build Update Object

try
{
    UpdateBookingOrderPayForData updateBookingPayForDataObj = new UpdateBookingOrderPayForData();
    updateBookingPayForDataObj.OrderId = requestParam.ORDER_ID;
    updateBookingPayForDataObj.PayForDataId = requestParam.PAYFORDATA_ID;
    updateBookingPayForDataObj.CardHolderName = result.ContainsKey("Response.CardHolderName") ? result["Response.CardHolderName"] : null;
    updateBookingPayForDataObj.CardNumber = result.ContainsKey("Response.CardNumber") ? result["Response.CardNumber"] : null;
    updateBookingPayForDataObj.CardExpiryDate = result.ContainsKey("Response.CardExpiryDate") ? result["Response.CardExpiryDate"] : null;
    updateBookingPayForDataObj.GatewayName = result.ContainsKey("Response.GatewayName") ? result["Response.GatewayName"] : null;
    updateBookingPayForDataObj.GatewayStatusCode = result.ContainsKey("Response.GatewayStatusCode") ? result["Response.GatewayStatusCode"] : result["Response.StatusCode"];
    updateBookingPayForDataObj.GatewayStatusDescription = result.ContainsKey("Response.GatewayStatusDescription") ? result["Response.GatewayStatusDescription"] : result["Response.StatusDescription"];
    updateBookingPayForDataObj.RRN = result.ContainsKey("Response.RRN") ? result["Response.RRN"] : null;
    updateBookingPayForDataObj.StatusCode = result["Response.StatusCode"];
    updateBookingPayForDataObj.StatusDescription = result["Response.StatusDescription"];

What It Does:
Extracts payment details from gateway response:

  1. OrderId, PayForDataId: Database identifiers
  2. Card Information:
    - CardHolderName: Cardholder’s name
    - CardNumber: Masked card number (e.g., “411111**1111”)
    -
    CardExpiryDate:** Card expiry (e.g., “12/25”)
  3. Gateway Information:
    - GatewayName: Payment gateway name (e.g., “MADA”, “VISA”, “Mastercard”)
    - GatewayStatusCode: Gateway-specific status code
    - GatewayStatusDescription: Gateway-specific status message
  4. Transaction Details:
    - RRN: Retrieval Reference Number (unique transaction identifier)
    - StatusCode: Standardized status code (“00000” = success)
    - StatusDescription: Standardized status message

Null-Safe Extraction:
- Uses ContainsKey() to check if response field exists
- Returns null if field is missing (some gateways don’t return all fields)
- GatewayStatusCode: Falls back to StatusCode if gateway-specific code unavailable

Status Code Examples:
- 00000: Transaction Success
- 00001: Transaction Pending
- 00002: Transaction Failed
- 00003: Transaction Cancelled
- 00019: Transaction not found (inquiry failed)
- 01111: Technical error

Step 2: Serialize to XML and Update Database

PaymentDataAccess dal = new PaymentDataAccess();
var xml = XmlHelper.ObjectToXml(updateBookingPayForDataObj);

What It Does:
1. Creates DAL instance
2. Serializes update object to XML string
3. XML will be passed to stored procedure

XML Output Example:

<UpdateBookingOrderPayForData>
  <OrderId>12345</OrderId>
  <PayForDataId>67890</PayForDataId>
  <CardHolderName>John Doe</CardHolderName>
  <CardNumber>411111******1111</CardNumber>
  <CardExpiryDate>12/25</CardExpiryDate>
  <GatewayName>MADA</GatewayName>
  <GatewayStatusCode>00</GatewayStatusCode>
  <GatewayStatusDescription>Approved</GatewayStatusDescription>
  <RRN>123456789012</RRN>
  <StatusCode>00000</StatusCode>
  <StatusDescription>Transaction Success</StatusDescription>
</UpdateBookingOrderPayForData>

Step 3: Authenticate with SchedulingAPI

//if (SchedulingAPIAuthToken == null)
//{
    var authResponse = await SchedulingApiAuthenticationToken(SchedulingAPIApplicationToken);
    SchedulingAPIAuthToken = authResponse.AccessToken.ToString();
//}

What It Does:
1. Commented Check: Previously checked if token already exists (commented out for fresh token each time)
2. Authentication: Calls SchedulingApiAuthenticationToken() with application token
3. Store Token: Saves OAuth access token for subsequent API calls

Why Fresh Token Each Time:
- Ensures token is always valid (no expiration issues)
- Commented code suggests previous approach cached token
- Current approach prioritizes reliability over performance

Step 4: Handle Successful Payment (StatusCode = “00000”)

if(SchedulingAPIAuthToken != null)
{
    if(updateBookingPayForDataObj.StatusCode == "00000")
    {
        UpdateScheduleBookingStatusRequest updateScheduleBookingStstus = new UpdateScheduleBookingStatusRequest();
        UpdateBookingStatus bookingStatusDetail = new UpdateBookingStatus();
        bookingStatusDetail.BookingId = paymentDetail.SlotBookingId;
        bookingStatusDetail.NewBookingStatusId = 1;
        bookingStatusDetail.StatusUpdateDateTime = DateTime.UtcNow;
        bookingStatusDetail.StatusUpdateAuthority = 2;
        bookingStatusDetail.UpdatedBy = paymentDetail.ConsumerId;
        updateScheduleBookingStstus.BookingStatusUpdateInfoList = new List<UpdateBookingStatus>();
        updateScheduleBookingStstus.BookingStatusUpdateInfoList.Add(bookingStatusDetail);

        var response = await UpdateBookingStatusInScheduling(JsonConvert.SerializeObject(updateScheduleBookingStstus));

        BookingStatusUpdateResponseWrapper updateStatus = response.ToObject<BookingStatusUpdateResponseWrapper>();
        if(updateStatus.reason == 1)
        {
            var responseStatus = dal.UpdateBookingOrderPayForData(xml);
        }
        else
        {
            if(updateStatus.data.BookingStatusUpdateInfoList[0].RejectCode == "7") // Status is already in Booked state
            {
                var responseStatus = dal.UpdateBookingOrderPayForData(xml);
            }
        }
    }

What It Does:

Step 4a: Build Booking Status Update Request

UpdateScheduleBookingStatusRequest updateScheduleBookingStstus = new UpdateScheduleBookingStatusRequest();
UpdateBookingStatus bookingStatusDetail = new UpdateBookingStatus();
bookingStatusDetail.BookingId = paymentDetail.SlotBookingId;
bookingStatusDetail.NewBookingStatusId = 1;  // 1 = Booked/Confirmed
bookingStatusDetail.StatusUpdateDateTime = DateTime.UtcNow;
bookingStatusDetail.StatusUpdateAuthority = 2;  // 2 = System/Automated
bookingStatusDetail.UpdatedBy = paymentDetail.ConsumerId;

Booking Status Transition:

Pending Payment (Status = 0) → Booked/Confirmed (Status = 1)

Status Update Fields:
- BookingId: Slot booking identifier
- NewBookingStatusId: 1 (Booked/Confirmed)
- StatusUpdateDateTime: UTC timestamp of status change
- StatusUpdateAuthority: 2 (System/Automated - not manual user action)
- UpdatedBy: Consumer/patient user ID

Step 4b: Call SchedulingAPI to Update Booking Status

var response = await UpdateBookingStatusInScheduling(JsonConvert.SerializeObject(updateScheduleBookingStstus));

What It Does:
- Serializes request to JSON
- Calls SchedulingAPI endpoint to update booking status
- Returns response indicating success or rejection

Step 4c: Process SchedulingAPI Response

BookingStatusUpdateResponseWrapper updateStatus = response.ToObject<BookingStatusUpdateResponseWrapper>();
if(updateStatus.reason == 1)
{
    var responseStatus = dal.UpdateBookingOrderPayForData(xml);
}
else
{
    if(updateStatus.data.BookingStatusUpdateInfoList[0].RejectCode == "7") // Status is already in Booked state
    {
        var responseStatus = dal.UpdateBookingOrderPayForData(xml);
    }
}

What It Does:

  1. Parse Response: Converts JObject to typed response wrapper
  2. Check Success: reason == 1 means booking status updated successfully
  3. Update Payment Data: Calls DAL to update PayForData table with payment details

Rejection Handling:
- RejectCode = “7”: Booking is already in “Booked” state
- Action: Still update payment data (payment was successful, booking status just already correct)
- Other Reject Codes: Do not update payment data (booking status update failed)

Why Two-Step Update:
1. First: Update booking status in SchedulingAPI (separate microservice)
2. Second: Update payment data in Psyter database (only if booking update succeeded)
3. Consistency: Ensures booking and payment data are in sync

Step 5: Handle Failed/Pending Payment (StatusCode != “00000”)

else
{
    if(paymentDetail.InquiryCount >= 3)
    {
        UpdateScheduleBookingStatusRequest updateScheduleBookingStstus = new UpdateScheduleBookingStatusRequest();
        UpdateBookingStatus bookingStatusDetail = new UpdateBookingStatus();
        bookingStatusDetail.BookingId = paymentDetail.SlotBookingId;
        bookingStatusDetail.NewBookingStatusId = 8;
        bookingStatusDetail.StatusUpdateDateTime = DateTime.UtcNow;
        bookingStatusDetail.StatusUpdateAuthority = 2;
        bookingStatusDetail.UpdatedBy = paymentDetail.ConsumerId;
        bookingStatusDetail.CancelledBy = paymentDetail.ConsumerId;
        updateScheduleBookingStstus.BookingStatusUpdateInfoList = new List<UpdateBookingStatus>();
        updateScheduleBookingStstus.BookingStatusUpdateInfoList.Add(bookingStatusDetail);

        var response = await UpdateBookingStatusInScheduling(JsonConvert.SerializeObject(updateScheduleBookingStstus));
    }

    var responseStatus = dal.UpdateBookingOrderPayForData(xml);
}

What It Does:

Retry Logic:
- InquiryCount >= 3: Payment inquiry attempted 3+ times
- Action: Cancel the booking (status = 8)

Booking Cancellation:

bookingStatusDetail.NewBookingStatusId = 8;  // 8 = Cancelled
bookingStatusDetail.CancelledBy = paymentDetail.ConsumerId;

Status Transition:

Pending Payment → Inquiry (1st attempt) → Inquiry (2nd attempt) → Inquiry (3rd attempt) → Cancelled

Why Cancel After 3 Attempts:
- Payment likely failed permanently
- Frees up time slot for other patients
- Patient notified to try booking again with different payment method

Update Payment Data:
- Always updates PayForData with latest inquiry result
- Records failure status and reason
- Enables customer support to troubleshoot

Step 6: Logging and Exception Handling

WriteToFile("Response Description :  " + result["Response.StatusDescription"], "Inquiry");
WriteToFile("-----------------------------END--------------------------------", "Inquiry");

What It Does:
1. Logs final status description
2. Logs end marker for this payment inquiry
3. Enables troubleshooting by reviewing logs

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

Exception Handling:
- Catches any errors during update process
- Logs exception message with timestamp
- Service continues processing other payments


2. Update Booking Refund Request Status (UpdateBookingRefundRequestData)

Method Overview

public async Task UpdateBookingRefundRequestData(RequestProcessInquiry requestParam, 
                                                SortedDictionary<string, string> result, 
                                                PendingPayment paymentDetail)

Purpose: Process refund gateway response and send refund notifications

Parameters:
- requestParam: Original refund request
- result: Gateway response
- paymentDetail: Original pending payment record

Code Analysis

Step 1: Prepare Refund Update Object

try
{
    decimal reconvertTheAmount = Convert.ToDecimal(requestParam.AMOUNT) / 100;

    UpdateBookingRefundRequestData updateBookingRefundStatus = new UpdateBookingRefundRequestData();
    updateBookingRefundStatus.OrderId = requestParam.ORDER_ID;
    updateBookingRefundStatus.PayForDataId = requestParam.PAYFORDATA_ID;

    if (result.ContainsKey("Response.TransactionID"))
    {
        updateBookingRefundStatus.TransactionId = result["Response.TransactionID"];
    }
    else
    {
        if (result.ContainsKey("Response.OriginalTransactionID"))
        {
            updateBookingRefundStatus.TransactionId = result["Response.OriginalTransactionID"];
        }
    }

    updateBookingRefundStatus.Amount = reconvertTheAmount.ToString();
    updateBookingRefundStatus.StatusCode = result["Response.StatusCode"];
    updateBookingRefundStatus.StatusDescription = result["Response.StatusDescription"];

What It Does:

  1. Amount Conversion:
    - requestParam.AMOUNT is in gateway format (e.g., 15050 for 150.50 SAR)
    - Divide by 100 to get actual amount
    - Convert back to string for database storage

  2. Transaction ID Extraction:
    - Response.TransactionID: New refund transaction ID (if refund was processed)
    - Response.OriginalTransactionID: Original payment transaction ID (if inquiry)
    - Priority: TransactionID (refund) > OriginalTransactionID (original payment)

  3. Status Information:
    - StatusCode: “00000” = Refund Success, other codes = Failed/Pending
    - StatusDescription: Human-readable status message

Example Update Object:

OrderId = 12345
PayForDataId = 67890
TransactionId = "PSY1634567890456" (refund transaction)
Amount = "150.50"
StatusCode = "00000"
StatusDescription = "Refund Success"

Step 2: Update Database

PaymentDataAccess dal = new PaymentDataAccess();
var xml = XmlHelper.ObjectToXml(updateBookingRefundStatus);
var responseStatus = dal.UpdateBookingRefundStatus(xml);

What It Does:
1. Serialize update object to XML
2. Call DAL method to update refund status
3. Returns true if status = 2 (success)

Database Updates:
- PayForData table: Update refund status
- Status change: “Pending For Refund” → “Refunded” (or “Refund Failed”)
- Records refund transaction ID and amount

Step 3: Send Refund Notification (if successful)

if (responseStatus && updateBookingRefundStatus.StatusCode == "00000")
{
    //if (PsyterAPIAuthToken == null)
    //{
        var authResponse = await PsyterApiAuthenticationToken(PsyterAPIApplicationToken);
        PsyterAPIAuthToken = authResponse.AccessToken.ToString();
    //}

    RefundBookingNotificationRequest notificationRequest = new RefundBookingNotificationRequest();
    notificationRequest.BookingId = paymentDetail.SlotBookingId;
    notificationRequest.CareProviderId = paymentDetail.PhysicianId;
    notificationRequest.PatientId = paymentDetail.ConsumerId;
    notificationRequest.NoNeedToCreateLog = true;

    var notificationResponse = CallRefundNotificationAPI(JsonConvert.SerializeObject(notificationRequest));
}

What It Does:

Condition Check:
- responseStatus == true: Database update succeeded
- StatusCode == "00000": Refund was successful

Authentication:
- Get fresh OAuth token from PsyterAPI
- Store token for notification API call

Build Notification Request:

BookingId = 12345
CareProviderId = 56789 (physician)
PatientId = 98765 (patient)
NoNeedToCreateLog = true (notification already logged in payment system)

Send Notification:
- Calls CallRefundNotificationAPI() to send refund notification
- Notifies both patient and physician about refund
- Email and push notifications sent to both parties

Notification Content:
- “Your booking payment has been refunded”
- Booking details (date, time, physician name)
- Refund amount
- Expected refund timeline (3-7 business days)

Step 4: Logging

WriteToFile("Response Description :  " + result["Response.StatusDescription"], "Refund");
WriteToFile("-----------------------------END--------------------------------", "Refund");

What It Does:
- Logs gateway response description
- Marks end of refund processing

catch(Exception ex)
{
    WriteToFile("Response Description :  " + result["Response.StatusDescription"], "Refund");
    WriteToFile("-----------------------------END--------------------------------", "Refund");
}

Exception Handling:
- Logs response description even if exception occurred
- Marks end of processing
- Service continues processing other refunds


3. Update Wallet Purchase Payment Status (UpdateWalletPurchasePaymentInquiryStatus)

Method Overview

public async Task UpdateWalletPurchasePaymentInquiryStatus(RequestProcessInquiry requestParam, 
                                                          SortedDictionary<string, string> result, 
                                                          PendingPayment paymentDetail)

Purpose: Process wallet credit purchase inquiry response

Code Analysis

try
{
    UpdateBookingOrderPayForData updateBookingPayForDataObj = new UpdateBookingOrderPayForData();
    updateBookingPayForDataObj.PayForDataId = requestParam.PAYFORDATA_ID;
    updateBookingPayForDataObj.UserWalletId = requestParam.USER_WALLET_ID;
    updateBookingPayForDataObj.CardHolderName = result.ContainsKey("Response.CardHolderName") ? result["Response.CardHolderName"] : null;
    updateBookingPayForDataObj.CardNumber = result.ContainsKey("Response.CardNumber") ? result["Response.CardNumber"] : null;
    updateBookingPayForDataObj.CardExpiryDate = result.ContainsKey("Response.CardExpiryDate") ? result["Response.CardExpiryDate"] : null;
    updateBookingPayForDataObj.GatewayName = result.ContainsKey("Response.GatewayName") ? result["Response.GatewayName"] : null;
    updateBookingPayForDataObj.GatewayStatusCode = result.ContainsKey("Response.GatewayStatusCode") ? result["Response.GatewayStatusCode"] : result["Response.StatusCode"];
    updateBookingPayForDataObj.GatewayStatusDescription = result.ContainsKey("Response.GatewayStatusDescription") ? result["Response.GatewayStatusDescription"] : result["Response.StatusDescription"];
    updateBookingPayForDataObj.RRN = result.ContainsKey("Response.RRN") ? result["Response.RRN"] : null;
    updateBookingPayForDataObj.StatusCode = result["Response.StatusCode"];
    updateBookingPayForDataObj.StatusDescription = result["Response.StatusDescription"];

    PaymentDataAccess dal = new PaymentDataAccess();
    var xml = XmlHelper.ObjectToXml(updateBookingPayForDataObj);

    var responseStatus = dal.UpdateWalletPurchasePayForData(xml);  

    WriteToFile("Response Description :  " + result["Response.StatusDescription"], "Inquiry");
    WriteToFile("-----------------------------END--------------------------------", "Inquiry");
}
catch (Exception ex)
{
    WriteToFile("Exception occur " + DateTime.Now + ", Error: " + ex.Message, "Inquiry");
    WriteToFile("-----------------------------END--------------------------------", "Inquiry");
}

What It Does:

Key Differences from Booking Payment:
1. No OrderId: Wallet purchases don’t have orders
2. UserWalletId: Identifies the wallet to credit
3. No SchedulingAPI Call: No booking to confirm
4. Simpler Logic: Just update payment status and credit wallet

Update Object Fields:
- PayForDataId, UserWalletId (instead of OrderId)
- Card information (same as booking)
- Gateway response data (same as booking)

Database Updates:
- PayForData: Update wallet purchase payment status
- UserWallet: Credit wallet balance if StatusCode = “00000”

Example:

User purchases 500 SAR wallet credit
→ Payment gateway inquiry returns success
→ Update PayForData status to "Paid Success"
→ Add 500 SAR to user's wallet balance
→ User can now use wallet funds for bookings

No Additional Actions:
- No booking to confirm
- No notifications sent (user sees wallet balance in app)
- No scheduling updates needed


4. Update Package Purchase Payment Status (UpdatePackagePurchasePaymentInquiryStatus)

Method Overview

public async Task UpdatePackagePurchasePaymentInquiryStatus(RequestProcessInquiry requestParam, 
                                                           SortedDictionary<string, string> result, 
                                                           PendingPayment paymentDetail)

Purpose: Process session package purchase inquiry response

Code Analysis

try
{
    UpdateBookingOrderPayForData updateBookingPayForDataObj = new UpdateBookingOrderPayForData();
    updateBookingPayForDataObj.PayForDataId = requestParam.PAYFORDATA_ID;
    updateBookingPayForDataObj.ClientPackageMainId = requestParam.CLIENT_PACKAGE_MAIN_ID;
    updateBookingPayForDataObj.TransactionID = result.ContainsKey("Response.TransactionID") ? result["Response.TransactionID"] : result.ContainsKey("Response.OriginalTransactionID") ? result["Response.OriginalTransactionID"] : null;
    updateBookingPayForDataObj.CardHolderName = result.ContainsKey("Response.CardHolderName") ? result["Response.CardHolderName"] : null;
    updateBookingPayForDataObj.CardNumber = result.ContainsKey("Response.CardNumber") ? result["Response.CardNumber"] : null;
    updateBookingPayForDataObj.CardExpiryDate = result.ContainsKey("Response.CardExpiryDate") ? result["Response.CardExpiryDate"] : null;
    updateBookingPayForDataObj.GatewayName = result.ContainsKey("Response.GatewayName") ? result["Response.GatewayName"] : null;
    updateBookingPayForDataObj.GatewayStatusCode = result.ContainsKey("Response.GatewayStatusCode") ? result["Response.GatewayStatusCode"] : result["Response.StatusCode"];
    updateBookingPayForDataObj.GatewayStatusDescription = result.ContainsKey("Response.GatewayStatusDescription") ? result["Response.GatewayStatusDescription"] : result["Response.StatusDescription"];
    updateBookingPayForDataObj.RRN = result.ContainsKey("Response.RRN") ? result["Response.RRN"] : null;
    updateBookingPayForDataObj.StatusCode = result["Response.StatusCode"];
    updateBookingPayForDataObj.StatusDescription = result["Response.StatusDescription"];

    PaymentDataAccess dal = new PaymentDataAccess();
    var xml = XmlHelper.ObjectToXml(updateBookingPayForDataObj);

    var responseStatus = dal.UpdatePackagePurchasePayForData(xml);

    WriteToFile("Response Description :  " + result["Response.StatusDescription"], "Inquiry");
    WriteToFile("-----------------------------END--------------------------------", "Inquiry");
}
catch (Exception ex)
{
    WriteToFile("Exception occur " + DateTime.Now + ", Error: " + ex.Message, "Inquiry");
    WriteToFile("-----------------------------END--------------------------------", "Inquiry");
}

What It Does:

Key Differences from Booking Payment:
1. No OrderId: Package purchases don’t have orders
2. ClientPackageMainId: Identifies the package to activate
3. TransactionID Included: Package records need transaction ID
4. No SchedulingAPI Call: No booking to confirm

Update Object Fields:
- PayForDataId, ClientPackageMainId (instead of OrderId)
- TransactionID (from gateway response)
- Card information and gateway data

Database Updates:
- PayForData: Update package purchase payment status
- ClientPackageMain: Activate package if StatusCode = “00000”
- ClientPackageDetail: Create session credits

Example:

User purchases "10 Sessions Package" for 2000 SAR
→ Payment gateway inquiry returns success
→ Update PayForData status to "Paid Success"
→ Activate package in ClientPackageMain
→ Add 10 session credits to ClientPackageDetail
→ User can now book appointments using package credits

Package Activation:
- Package status changed from “Pending Payment” to “Active”
- Session credits available for booking
- Package expiry date calculated (e.g., 6 months from activation)
- User notified of package activation (via app)


API Authentication Methods

The service integrates with two APIs: PsyterAPI and SchedulingAPI. Both use OAuth 2.0 authentication.


1. PsyterAPI Authentication (PsyterApiAuthenticationToken)

Method Overview

private static async Task<APIAuthTokenResponse> PsyterApiAuthenticationToken(string applicationToken)

Purpose: Authenticate with PsyterAPI and obtain OAuth bearer token

Parameter:
- applicationToken: Application-level token (retrieved from database)

Code Analysis

var userAuthResponse = new APIAuthTokenResponse();
var psyterAuthenticateAPIURL = ConfigurationManager.AppSettings["PsyterAPIBaseURL"] + "Authenticate";
var pairs = new List<KeyValuePair<string, string>>
{
    new KeyValuePair<string, string>("applicationtoken",applicationToken),
    new KeyValuePair<string, string>("grant_type", "password")
};
var content = new FormUrlEncodedContent(pairs);

using (var client = new HttpClient())
{
    var response = await client.PostAsync(psyterAuthenticateAPIURL, content);
    var jObject = JObject.Parse(response.Content.ReadAsStringAsync().Result);
    userAuthResponse.AccessToken = jObject.SelectToken("access_token").ToString();
    userAuthResponse.RefreshToken = jObject.SelectToken("refresh_token").ToString();
    userAuthResponse.TokenExpiresIn = jObject.SelectToken("expires_in").ToString();
}

return userAuthResponse;

What It Does:

Step 1: Build Authentication URL

var psyterAuthenticateAPIURL = ConfigurationManager.AppSettings["PsyterAPIBaseURL"] + "Authenticate";

URL Example:

https://dvx.innotech-sa.com/Psyter/Master/APIs/Authenticate

Base URL from App.config:

<add key="PsyterAPIBaseURL" value="https://dvx.innotech-sa.com/Psyter/Master/APIs/" />

Step 2: Prepare Request Parameters

var pairs = new List<KeyValuePair<string, string>>
{
    new KeyValuePair<string, string>("applicationtoken", applicationToken),
    new KeyValuePair<string, string>("grant_type", "password")
};

OAuth 2.0 Parameters:
- applicationtoken: Application-level credential (unique per service)
- grant_type: “password” (OAuth 2.0 grant type)

Application Token:
- Stored in database (AppConfigSetting table)
- Retrieved via GetPsyterApplicationToken() DAL method
- Unique identifier for WindowsService application

Step 3: Send HTTP POST Request

var content = new FormUrlEncodedContent(pairs);
using (var client = new HttpClient())
{
    var response = await client.PostAsync(psyterAuthenticateAPIURL, content);

HTTP Request:

POST https://dvx.innotech-sa.com/Psyter/Master/APIs/Authenticate
Content-Type: application/x-www-form-urlencoded

applicationtoken={token}&grant_type=password

Step 4: Parse OAuth Response

var jObject = JObject.Parse(response.Content.ReadAsStringAsync().Result);
userAuthResponse.AccessToken = jObject.SelectToken("access_token").ToString();
userAuthResponse.RefreshToken = jObject.SelectToken("refresh_token").ToString();
userAuthResponse.TokenExpiresIn = jObject.SelectToken("expires_in").ToString();

JSON Response Example:

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "bearer",
  "expires_in": 86400,
  "refresh_token": "8f7a6b5c4d3e2f1a..."
}

Response Fields:
- access_token: JWT bearer token (used in Authorization header)
- refresh_token: Token to refresh access token when expired
- expires_in: Token lifetime in seconds (e.g., 86400 = 24 hours)

Step 5: Return Token Response

return userAuthResponse;

APIAuthTokenResponse Object:

AccessToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
RefreshToken = "8f7a6b5c4d3e2f1a..."
TokenExpiresIn = "86400"

How It’s Used:

var authResponse = await PsyterApiAuthenticationToken(PsyterAPIApplicationToken);
PsyterAPIAuthToken = authResponse.AccessToken.ToString();

Subsequent API Calls:

mObjHttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", PsyterAPIAuthToken);


2. SchedulingAPI Authentication (SchedulingApiAuthenticationToken)

Method Overview

private static async Task<APIAuthTokenResponse> SchedulingApiAuthenticationToken(string applicationToken)

Purpose: Authenticate with SchedulingAPI and obtain OAuth bearer token

Code Analysis

var userAuthResponse = new APIAuthTokenResponse();
var psyterAuthenticateAPIURL = ConfigurationManager.AppSettings["SchedulingAPIBaseURL"] + "Authenticate";
var pairs = new List<KeyValuePair<string, string>>
{
    new KeyValuePair<string, string>("applicationtoken",applicationToken),
    new KeyValuePair<string, string>("grant_type", "password")
};
var content = new FormUrlEncodedContent(pairs);

using (var client = new HttpClient())
{
    var response = await client.PostAsync(psyterAuthenticateAPIURL, content);
    var jObject = JObject.Parse(response.Content.ReadAsStringAsync().Result);
    userAuthResponse.AccessToken = jObject.SelectToken("access_token").ToString();
    userAuthResponse.RefreshToken = jObject.SelectToken("refresh_token").ToString();
    userAuthResponse.TokenExpiresIn = jObject.SelectToken("expires_in").ToString();
}

return userAuthResponse;

What It Does:

Identical to PsyterAPI Authentication, except:
- Uses SchedulingAPIBaseURL instead of PsyterAPIBaseURL
- Authenticates with different API service

URL Example:

https://dvx.innotech-sa.com/Scheduling/SchedulingAPI/Authenticate

Base URL from App.config:

<add key="SchedulingAPIBaseURL" value="https://dvx.innotech-sa.com/Scheduling/SchedulingAPI/" />

Why Separate Authentication:
- Microservices Architecture: PsyterAPI and SchedulingAPI are separate services
- Different Tokens: Each service has its own authentication
- Security Isolation: Compromised token only affects one service

Usage Context:

// In UpdatePaymentInquiryStatus()
var authResponse = await SchedulingApiAuthenticationToken(SchedulingAPIApplicationToken);
SchedulingAPIAuthToken = authResponse.AccessToken.ToString();

// Then used to update booking status
var response = await UpdateBookingStatusInScheduling(JsonConvert.SerializeObject(updateScheduleBookingStstus));


Booking API Integration Methods


1. Update Booking Status in Scheduling (UpdateBookingStatusInScheduling)

Method Overview

public async Task<JObject> UpdateBookingStatusInScheduling(string requestData)

Purpose: Update booking status in SchedulingAPI (separate microservice)

Parameter:
- requestData: JSON string containing booking status update request

Code Analysis

var content = new StringContent(requestData, Encoding.UTF8, "application/json");

HttpClient mObjHttpClient = new HttpClient();
mObjHttpClient.BaseAddress = new Uri(ConfigurationManager.AppSettings["SchedulingAPIBaseURL"]);
mObjHttpClient.DefaultRequestHeaders.Clear();
mObjHttpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

mObjHttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", SchedulingAPIAuthToken);

HttpResponseMessage responseMessage = await mObjHttpClient.PostAsync("api/Schedule/UpdateBookingStatus", content);
var responseJson = await responseMessage.Content.ReadAsStringAsync();
return JObject.Parse(responseJson);

What It Does:

Step 1: Prepare Request Content

var content = new StringContent(requestData, Encoding.UTF8, "application/json");

Request Data Example:

{
  "BookingStatusUpdateInfoList": [
    {
      "BookingId": 12345,
      "NewBookingStatusId": 1,
      "StatusUpdateDateTime": "2025-11-05T10:30:00Z",
      "StatusUpdateAuthority": 2,
      "UpdatedBy": 98765
    }
  ]
}

Step 2: Configure HTTP Client

HttpClient mObjHttpClient = new HttpClient();
mObjHttpClient.BaseAddress = new Uri(ConfigurationManager.AppSettings["SchedulingAPIBaseURL"]);
mObjHttpClient.DefaultRequestHeaders.Clear();
mObjHttpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

Configuration:
- Base URL: https://dvx.innotech-sa.com/Scheduling/SchedulingAPI/
- Accept Header: application/json
- Clear Headers: Ensures no previous headers interfere

Step 3: Add OAuth Bearer Token

mObjHttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", SchedulingAPIAuthToken);

Authorization Header:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Step 4: Send POST Request

HttpResponseMessage responseMessage = await mObjHttpClient.PostAsync("api/Schedule/UpdateBookingStatus", content);

Full URL:

POST https://dvx.innotech-sa.com/Scheduling/SchedulingAPI/api/Schedule/UpdateBookingStatus

Step 5: Parse and Return Response

var responseJson = await responseMessage.Content.ReadAsStringAsync();
return JObject.Parse(responseJson);

Response Example (Success):

{
  "reason": 1,
  "data": {
    "BookingStatusUpdateInfoList": [
      {
        "BookingId": 12345,
        "NewBookingStatusId": 1,
        "StatusUpdateDateTime": "2025-11-05T10:30:00Z",
        "StatusUpdateAuthority": 2,
        "RejectCode": null,
        "RejectReason": null
      }
    ]
  }
}

Response Example (Failure):

{
  "reason": 0,
  "data": {
    "BookingStatusUpdateInfoList": [
      {
        "BookingId": 12345,
        "RejectCode": "7",
        "RejectReason": "Booking is already in Booked status"
      }
    ]
  }
}

Response Fields:
- reason: 1 = Success, 0 = Failure
- RejectCode: Error code if update failed
- “7” = Already in requested status
- Other codes = Different validation failures
- RejectReason: Human-readable error message

How It’s Used:

// In UpdatePaymentInquiryStatus()
var response = await UpdateBookingStatusInScheduling(JsonConvert.SerializeObject(updateScheduleBookingStstus));

BookingStatusUpdateResponseWrapper updateStatus = response.ToObject<BookingStatusUpdateResponseWrapper>();
if(updateStatus.reason == 1)
{
    // Success - update payment data
    var responseStatus = dal.UpdateBookingOrderPayForData(xml);
}


2. Call Refund Notification API (CallRefundNotificationAPI)

Method Overview

public async Task<JObject> CallRefundNotificationAPI(string requestData)

Purpose: Send refund notification to patient and physician via PsyterAPI

Parameter:
- requestData: JSON string containing refund notification request

Code Analysis

var content = new StringContent(requestData, Encoding.UTF8, "application/json");

HttpClient mObjHttpClient = new HttpClient();
mObjHttpClient.BaseAddress = new Uri(ConfigurationManager.AppSettings["PsyterAPIBaseURL"]);
mObjHttpClient.DefaultRequestHeaders.Clear();
mObjHttpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

mObjHttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", PsyterAPIAuthToken);

HttpResponseMessage responseMessage = await mObjHttpClient.PostAsync("Notification/SendRefundBookingNotification", content);
var responseJson = await responseMessage.Content.ReadAsStringAsync();
return JObject.Parse(responseJson);

What It Does:

Step 1: Prepare Request Content

var content = new StringContent(requestData, Encoding.UTF8, "application/json");

Request Data Example:

{
  "BookingId": 12345,
  "CareProviderId": 56789,
  "PatientId": 98765,
  "NoNeedToCreateLog": true
}

Request Fields:
- BookingId: Booking that was refunded
- CareProviderId: Physician user ID
- PatientId: Patient user ID
- NoNeedToCreateLog: true (notification already logged in payment system)

Step 2: Configure HTTP Client

HttpClient mObjHttpClient = new HttpClient();
mObjHttpClient.BaseAddress = new Uri(ConfigurationManager.AppSettings["PsyterAPIBaseURL"]);
mObjHttpClient.DefaultRequestHeaders.Clear();
mObjHttpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

Base URL:

https://dvx.innotech-sa.com/Psyter/Master/APIs/

Step 3: Add OAuth Bearer Token

mObjHttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", PsyterAPIAuthToken);

Authorization Header:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Step 4: Send POST Request

HttpResponseMessage responseMessage = await mObjHttpClient.PostAsync("Notification/SendRefundBookingNotification", content);

Full URL:

POST https://dvx.innotech-sa.com/Psyter/Master/APIs/Notification/SendRefundBookingNotification

Step 5: Parse and Return Response

var responseJson = await responseMessage.Content.ReadAsStringAsync();
return JObject.Parse(responseJson);

Response Example:

{
  "reason": 1,
  "data": {
    "notificationsSent": 2,
    "details": [
      {
        "userId": 98765,
        "userType": "Patient",
        "channels": ["Email", "PushNotification"]
      },
      {
        "userId": 56789,
        "userType": "Physician",
        "channels": ["Email", "PushNotification"]
      }
    ]
  }
}

What PsyterAPI Does:
1. Retrieves booking details (date, time, amount)
2. Sends email notification to patient
3. Sends email notification to physician
4. Sends push notification to patient’s mobile app
5. Sends push notification to physician’s mobile app
6. Creates notification history records

Notification Content (Patient):

Subject: Booking Refund Processed

Dear [Patient Name],

Your booking refund has been processed successfully.

Booking Details:
- Date: November 5, 2025
- Time: 10:30 AM
- Physician: Dr. [Physician Name]
- Refund Amount: 150.50 SAR

The refund will be credited to your original payment method within 3-7 business days.

Thank you for using Psyter.

Notification Content (Physician):

Subject: Booking Refund Notification

Dear Dr. [Physician Name],

A refund has been processed for a cancelled booking.

Booking Details:
- Date: November 5, 2025
- Time: 10:30 AM
- Patient: [Patient Name]
- Refund Amount: 150.50 SAR

This time slot is now available for other bookings.

How It’s Used:

// In UpdateBookingRefundRequestData()
if (responseStatus && updateBookingRefundStatus.StatusCode == "00000")
{
    var authResponse = await PsyterApiAuthenticationToken(PsyterAPIApplicationToken);
    PsyterAPIAuthToken = authResponse.AccessToken.ToString();

    RefundBookingNotificationRequest notificationRequest = new RefundBookingNotificationRequest();
    notificationRequest.BookingId = paymentDetail.SlotBookingId;
    notificationRequest.CareProviderId = paymentDetail.PhysicianId;
    notificationRequest.PatientId = paymentDetail.ConsumerId;
    notificationRequest.NoNeedToCreateLog = true;

    var notificationResponse = CallRefundNotificationAPI(JsonConvert.SerializeObject(notificationRequest));
}


Summary of Part 3

This document has covered:

Payment Status Update Methods (4 methods)
- UpdatePaymentInquiryStatus() - Complex booking confirmation logic
- UpdateBookingRefundRequestData() - Refund processing with notifications
- UpdateWalletPurchasePaymentInquiryStatus() - Wallet credit updates
- UpdatePackagePurchasePaymentInquiryStatus() - Package activation logic

API Authentication (2 methods)
- PsyterApiAuthenticationToken() - OAuth 2.0 token generation
- SchedulingApiAuthenticationToken() - Separate microservice authentication

Booking API Integration (2 methods)
- UpdateBookingStatusInScheduling() - Cross-service booking updates
- CallRefundNotificationAPI() - Multi-channel refund notifications

Key Concepts Covered:
- Payment status transitions and business logic
- Retry logic (3 attempts before cancellation)
- Cross-service communication (microservices)
- OAuth 2.0 authentication flow
- Multi-channel notification delivery


Coming in Part 4

The final document will cover:

🔜 FCM Notification System (2 methods)
- GetPendingFCMNotificationsAndReminders()
- CallSendFCMNotificationCommonAPI()

🔜 Supporting Features (2 methods)
- CallNotifySCHFSExpiryAPI()
- DeleteLogFilesOfPreviousMonth()

🔜 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 patterns


End of Part 3