///<summary>Creates and returns the HPF URL and validation OTK which can be used to make a payment for an unspecified credit card. Throws exceptions.</summary> public static string GetHpfUrlForPayment(Patient pat, string accountToken, string payNote, bool isMobile, double amount, bool saveToken, CreditCardSource ccSource) { if (pat == null) { throw new ODException("No Patient Found", ODException.ErrorCodes.NoPatientFound); } if (string.IsNullOrWhiteSpace(accountToken)) { throw new ODException("Invalid Account Token", ODException.ErrorCodes.OtkArgsInvalid); } if (amount < 0.00 || amount > 99999.99) { throw new ODException("Invalid Amount", ODException.ErrorCodes.OtkArgsInvalid); } if (string.IsNullOrEmpty(payNote)) { throw new ODException("Invalid PayNote", ODException.ErrorCodes.OtkArgsInvalid); } PayConnectResponseWeb responseWeb = new PayConnectResponseWeb() { Amount = amount, AccountToken = accountToken, PatNum = pat.PatNum, ProcessingStatus = PayConnectWebStatus.Created, PayNote = payNote, CCSource = ccSource, IsTokenSaved = saveToken, }; PayConnectResponseWebs.Insert(responseWeb); try { string url; PayConnectREST.PostPaymentRequest(responseWeb, out url); PayConnectResponseWebs.Update(responseWeb); WakeupWebPaymentsMonitor?.Invoke(url, new EventArgs()); return(url); } catch (Exception e) { PayConnectResponseWebs.HandleResponseError(responseWeb, "Error calling PostPaymentRequest: " + e.Message); PayConnectResponseWebs.Update(responseWeb); throw; } }
///<summary>Poll the existing PayConnectResponseWeb for status changes. ///This method will update the ResponseJSON/ProcessingStatus with any changes</summary> public static void GetPaymentStatus(PayConnectResponseWeb responseWeb) { #region Response Object var resObj = new { Amount = -1.00, TransactionType = "", TransactionStatus = "", TransactionDate = DateTime.MinValue, StatusDescription = "", //Used to poll for getting the payment status to see if the user has made a payment. PayToken = "", CreditCardNumber = "", CreditCardExpireDate = "", TransactionID = -1, RefNumber = "", Pending = true, Status = new { code = -1, description = "", }, Messages = new { Message = new string[0] }, //Used for future payments with this card. Do not confuse this with PayToken. PaymentToken = new { TokenId = "", Expiration = new { month = "", year = "", }, Messages = new { Message = new string[0] }, }, }; #endregion List <string> listHeaders = GetClientRequestHeadersForWebURL(); listHeaders.Add("AccountToken: " + responseWeb.AccountToken); try { var res = Request(ApiRoute.PaymentStatus, HttpMethod.Get, listHeaders, "", resObj, $"?payToken={responseWeb.PayToken}"); if (res == null) { PayConnectResponseWebs.HandleResponseError(responseWeb, JsonConvert.SerializeObject(res)); throw new ODException("Invalid response from PayConnect."); } int code = -1; string codeMsg = ""; if (res.Status != null) { code = res.Status.code; codeMsg = "Response code: " + res.Status.code + "\r\n"; } if (code > 0) { string err = "Invalid response from PayConnect.\r\nResponse code: " + code; if (res.Messages != null && res.Messages.Message != null && res.Messages.Message.Length > 0) { err += "\r\nError retrieving payment status.\r\nResponse message(s):\r\n" + string.Join("\r\n", res.Messages.Message); } PayConnectResponseWebs.HandleResponseError(responseWeb, JsonConvert.SerializeObject(res)); throw new ODException(err); } if (res.Pending) { HandleResponseSuccess(responseWeb, JsonConvert.SerializeObject(res), true); } else if (res.TransactionStatus != null && (res.TransactionStatus.Contains("Timeout") || res.TransactionStatus.Contains("Cancelled") || res.TransactionStatus.Contains("Declined"))) { responseWeb.LastResponseStr = JsonConvert.SerializeObject(res); responseWeb.ProcessingStatus = res.TransactionStatus.Contains("Declined") ? PayConnectWebStatus.Declined : (res.TransactionStatus.Contains("Cancelled") ? PayConnectWebStatus.Cancelled : PayConnectWebStatus.Expired); responseWeb.DateTimeExpired = MiscData.GetNowDateTime(); } else if (res.TransactionStatus != null && res.TransactionStatus.Contains("Approved")) { string expYear = res.PaymentToken.Expiration.year.Substring(res.PaymentToken.Expiration.year.Length - 2); //Last 2 digits only string expMonth = res.PaymentToken.Expiration.month.PadLeft(2, '0'); //2 digit month with leading 0 if needed responseWeb.PaymentToken = res.PaymentToken.TokenId; responseWeb.ExpDateToken = expYear + expMonth; //yyMM format responseWeb.RefNumber = res.RefNumber; responseWeb.TransType = PayConnectService.transType.SALE; HandleResponseSuccess(responseWeb, JsonConvert.SerializeObject(res), res.Pending); } else { responseWeb.LastResponseStr = JsonConvert.SerializeObject(res); responseWeb.ProcessingStatus = PayConnectWebStatus.Unknown; responseWeb.DateTimeLastError = MiscData.GetNowDateTime(); } } catch (Exception ex) { PayConnectResponseWebs.HandleResponseError(responseWeb, ex.Message); throw; } }
///<summary>Returns number of pending transactions remaining after completion.</summary> private static int ProcessOutstandingTransactions() { OnLoggerEvent("Checking for outstanding PaymentTokens."); List <PayConnectResponseWeb> listPendingPaymentsAll = PayConnectResponseWebs.GetAllPending(); //Only process if it's been >= 5 seconds. List <PayConnectResponseWeb> listPendingDue = listPendingPaymentsAll.FindAll(x => DateTime.Now.Subtract(x.GetLastPendingUpdateDateTime()) > TimeSpan.FromSeconds(5)); OnLoggerEvent("Found " + listPendingPaymentsAll.Count + " PaymentTokens. " + listPendingDue.Count.ToString() + " are due to be processed."); if (listPendingDue.Count <= 0) //None are due this time around but we may have some still pending, return count of those. { return(listPendingPaymentsAll.Count); } OnLoggerEvent("Processing " + listPendingDue.Count.ToString() + " outstanding PaymentTokens.", LogLevel.Information); //Seed total remaining with any that we won't be processing this time around. int remaining = listPendingPaymentsAll.Count - listPendingDue.Count; foreach (PayConnectResponseWeb responseWebCur in listPendingDue) { try { //This method will update the responseWebCur with all of the data from the /paymentStatus API call PayConnectREST.GetPaymentStatus(responseWebCur); switch (responseWebCur.ProcessingStatus) { case PayConnectWebStatus.Pending: //No new status to report. Try again next time. OnLoggerEvent("PaymentToken still pending: " + responseWebCur.PayToken, LogLevel.Information); if (DateTime.Now.AddMinutes(30) < responseWebCur.GetLastPendingUpdateDateTime()) { //Expire this transaction ourselves after 30 minutes. In testing, PayConnect expires them after 15 minutes. responseWebCur.ProcessingStatus = PayConnectWebStatus.Expired; responseWebCur.DateTimeExpired = MiscData.GetNowDateTime(); OnLoggerEvent("PaymentToken has expired: " + responseWebCur.PayToken, LogLevel.Information); } break; case PayConnectWebStatus.CreatedError: case PayConnectWebStatus.PendingError: OnLoggerEvent("PaymentToken returned an error when retreiving a status update: " + responseWebCur.PayToken, LogLevel.Information); break; case PayConnectWebStatus.Expired: OnLoggerEvent("PaymentToken has expired: " + responseWebCur.PayToken, LogLevel.Information); break; case PayConnectWebStatus.Completed: OnLoggerEvent("PaymentToken has been completed: " + responseWebCur.PayToken, LogLevel.Information); if (responseWebCur.IsTokenSaved) { CreditCards.InsertFromPayConnect(responseWebCur); } Patient pat = Patients.GetPat(responseWebCur.PatNum); long clinicNum = 0; if (PrefC.HasClinicsEnabled) { clinicNum = pat.ClinicNum; } string receipt = ""; if (!string.IsNullOrWhiteSpace(responseWebCur.LastResponseStr)) { var pcInfo = Newtonsoft.Json.JsonConvert.DeserializeAnonymousType(responseWebCur.LastResponseStr, new { Amount = (decimal)0.0, CreditCardNumber = "", Messages = new { Message = new string[0] }, RefNumber = "", Status = new { description = "" }, } ); receipt = BuildReceiptString(PayConnectService.transType.SALE, pcInfo.RefNumber, "", pcInfo.CreditCardNumber, "", "", pcInfo.Status?.description ?? "", pcInfo.Messages.Message.ToList(), pcInfo.Amount, 0, clinicNum); } responseWebCur.PayNum = Payments.InsertFromPayConnect(pat.PatNum, pat.PriProv, clinicNum, responseWebCur.Amount, responseWebCur.GetFormattedNote(true), receipt, responseWebCur.CCSource); break; case PayConnectWebStatus.Cancelled: OnLoggerEvent("PaymentToken has been cancelled: " + responseWebCur.PayToken, LogLevel.Information); break; case PayConnectWebStatus.Declined: OnLoggerEvent("PaymentToken has been declined: " + responseWebCur.PayToken, LogLevel.Information); break; case PayConnectWebStatus.Created: case PayConnectWebStatus.Unknown: case PayConnectWebStatus.UnknownError: default: OnLoggerEvent($"PaymentToken {responseWebCur.PayToken} returned unsupported state: {responseWebCur.ProcessingStatus.ToString()}", LogLevel.Information); break; } } catch (Exception e) { e.DoNothing(); } finally { PayConnectResponseWebs.Update(responseWebCur); } } remaining = listPendingPaymentsAll.FindAll(x => x.ProcessingStatus == PayConnectWebStatus.Pending).Count; OnLoggerEvent(remaining.ToString() + " PaymentTokens still pending after processing.", LogLevel.Information); return(remaining); }
///<summary>Make a payment using HPF directly. Throws exceptions.</summary> public static void MakePaymentWithAlias(Patient pat, string payNote, double amount, CreditCard cc) { if (pat == null) { throw new ODException("No Patient Found", ODException.ErrorCodes.NoPatientFound); } if (amount < 0.00 || amount > 99999.99) { throw new ODException("Invalid Amount", ODException.ErrorCodes.OtkArgsInvalid); } if (string.IsNullOrEmpty(payNote)) { throw new ODException("Invalid PayNote", ODException.ErrorCodes.OtkArgsInvalid); } if (cc == null) { throw new ODException("No Credit Card Found", ODException.ErrorCodes.OtkArgsInvalid); } if (string.IsNullOrEmpty(cc.PayConnectToken)) { throw new ODException("Invalid CC Alias", ODException.ErrorCodes.OtkArgsInvalid); } //request a PayConnect token, if a token was already saved PayConnect will return the same token, //otherwise replace CCNumberMasked with the returned token if the sale successful PayConnectService.creditCardRequest payConnectRequest = PayConnect.BuildSaleRequest( (decimal)amount, cc.PayConnectToken, cc.CCExpiration.Year, cc.CCExpiration.Month, pat.GetNameFLnoPref(), "", cc.Zip, null, PayConnectService.transType.SALE, "", true); //clinicNumCur could be 0, and the practice level or 'Headquarters' PayConnect credentials would be used for this charge PayConnectService.transResponse payConnectResponse = PayConnect.ProcessCreditCard(payConnectRequest, pat.ClinicNum, x => throw new ODException(x)); if (payConnectRequest != null && payConnectResponse.Status.code == 0) //Success { string receipt = BuildReceiptString(payConnectRequest.TransType, payConnectResponse.RefNumber, payConnectRequest.NameOnCard, payConnectRequest.CardNumber, payConnectRequest.MagData, payConnectResponse.AuthCode, payConnectResponse.Status.description, payConnectResponse.Messages.ToList(), payConnectRequest.Amount, 0, pat.ClinicNum); DateTime dateTimeProcessed = DateTime.Now; string formattedNote = Lans.g("PayConnect", "Amount:") + " " + amount.ToString("f") + "\r\n" + Lans.g("PayConnect", "Card Number:") + " " + cc.CCNumberMasked + "\r\n" + Lans.g("PayConnect", "Transaction ID:") + " " + payConnectRequest.RefNumber + "\r\n" + Lans.g("PayConnect", "Processed:") + " " + dateTimeProcessed.ToShortDateString() + " " + dateTimeProcessed.ToShortTimeString() + "\r\n" + Lans.g("PayConnect", "Note:") + " " + payNote; long payNum = Payments.InsertFromPayConnect(pat.PatNum, pat.PriProv, pat.ClinicNum, amount, formattedNote, receipt, CreditCardSource.PayConnectPortal); PayConnectResponseWeb responseWeb = new PayConnectResponseWeb() { Amount = amount, PatNum = pat.PatNum, ProcessingStatus = PayConnectWebStatus.Completed, PayNote = payNote, CCSource = cc.CCSource, IsTokenSaved = true, PayNum = payNum, DateTimeCompleted = MiscData.GetNowDateTime(), RefNumber = payConnectResponse.RefNumber, TransType = PayConnectService.transType.SALE, PaymentToken = cc.PayConnectToken, }; //Insert a new payconnectresponse row for historical record in the transaction window. PayConnectResponseWebs.Insert(responseWeb); } else //Failed { throw new ODException("Unable to process payment for this credit card: " + (payConnectResponse == null ? "Unknown error" : payConnectResponse.Status.description)); } }