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:
-
Booking Payment Update:
UpdateBookingOrderPayForData updateObj = new UpdateBookingOrderPayForData(); // ... populate properties var xml = XmlHelper.ObjectToXml(updateObj); dal.UpdateBookingOrderPayForData(xml); -
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:
- Booking Payment Inquiry - Check status of pending booking payments
- Refund Processing - Process refund requests for cancelled bookings
- Wallet Purchase Inquiry - Check status of wallet credit purchases
- 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:
- SmartRoutingSecretKey: Shared secret for hash generation (security)
- SmartRoutingResponseBackURL: Callback URL for payment responses
- RedirectToPaymentMobileToken: Token for mobile payment redirection
- SmartRoutingRedirectURL: URL for payment gateway redirect
- SmartRoutingMerchantId: Merchant identifier in payment gateway
- SmartRoutingCurrencyISOCode: Currency code (e.g., “SAR” for Saudi Riyal)
- SmartRoutingRefundInquiryUrl: Gateway API endpoint for inquiry/refund
- SmartRoutingThemeId: UI theme identifier for payment page
- SmartRoutingVersion: Payment gateway API version
- 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:
- Log Transaction: Logs TransactionId and OrderId
- Prepare Hash Request:
- SECRET_KEY from configuration
- MESSAGE_ID = “2” (inquiry operation code)
- TRANSACTION_ID from pending payment record - Generate Secure Hash: Calls
GenerateSecureHash()to create SHA256 hash - Prepare Inquiry Request:
- SECURE_HASH from previous step
- MESSAGE_ID = “2”
- TRANSACTION_ID, ORDER_ID, PAYFORDATA_ID - 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:
- GetPendingRefundPayment(): Process refund requests
- GetPendingWalletPurchasedPayment(): Check wallet purchase statuses
- 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:
-
Pending For Refund: Initial refund request
- MESSAGE_ID = “4” (refund operation)
- Includes AMOUNT parameter
- Sends refund request to gateway
- Creates new refund transaction -
Pending Refund: Refund already initiated, checking status
- SetsIsRefundInquiry = 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:
- HASH_TRANSACTION_ID: New transaction ID for refund (generated by
GenerateSecureHash()) - SECURE_HASH: SHA256 hash for request validation
- MESSAGE_ID: “4” for refund operation
- TRANSACTION_ID: Original payment transaction ID
- AMOUNT: Refund amount in gateway format
- 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:
-
Generate New Transaction ID:
- Format: “PSY” + Unix timestamp in milliseconds
- Example: “PSY1634567890123”
- Used for new refund transactions -
Add MessageID:
- IfIsRefundInquiry = true: Use “2” (inquiry)
- Otherwise: Use request MESSAGE_ID (“2” or “4”) -
Add Refund-Specific Fields (if MESSAGE_ID = “4”):
- TransactionID (newly generated)
- Amount (refund amount)
- CurrencyISOCode (e.g., “SAR”) -
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:
-
Booking Refund:
ORDER_ID > 0andMESSAGE_ID = "4"
→UpdateBookingRefundRequestData() -
Booking Inquiry:
ORDER_ID > 0andMESSAGE_ID = "2"
→UpdatePaymentInquiryStatus() -
Wallet Purchase:
USER_WALLET_ID > 0
→UpdateWalletPurchasePaymentInquiryStatus() -
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