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:
- UpdatePaymentInquiryStatus() - Booking payment inquiry responses
- UpdateBookingRefundRequestData() - Refund transaction responses
- UpdateWalletPurchasePaymentInquiryStatus() - Wallet purchase responses
- 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:
- OrderId, PayForDataId: Database identifiers
- Card Information:
- CardHolderName: Cardholder’s name
- CardNumber: Masked card number (e.g., “411111**1111”)
- CardExpiryDate:** Card expiry (e.g., “12/25”) - Gateway Information:
- GatewayName: Payment gateway name (e.g., “MADA”, “VISA”, “Mastercard”)
- GatewayStatusCode: Gateway-specific status code
- GatewayStatusDescription: Gateway-specific status message - 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:
- Parse Response: Converts JObject to typed response wrapper
- Check Success:
reason == 1means booking status updated successfully - 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:
-
Amount Conversion:
-requestParam.AMOUNTis in gateway format (e.g., 15050 for 150.50 SAR)
- Divide by 100 to get actual amount
- Convert back to string for database storage -
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) -
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