/// <summary> /// Increments the prayer count. /// </summary> /// <param name="prayerRequestId">The prayer request identifier.</param> private void IncrementPrayerCount(int prayerRequestId) { using (var rockContext = new RockContext()) { var request = new PrayerRequestService(rockContext).Get(prayerRequestId); var count = request.PrayerCount ?? 0; request.PrayerCount = (count + 1); rockContext.SaveChanges(); Guid?workflowTypeGuid = GetAttributeValue("PrayerCountWorkflow").AsGuidOrNull(); if (workflowTypeGuid.HasValue) { var workflowType = WorkflowTypeCache.Get(workflowTypeGuid.Value); if (workflowType != null && (workflowType.IsActive ?? true)) { if (GetAttributeValue("ProcessWorkflowImmediately").AsBoolean()) { try { var workflow = Workflow.Activate(workflowType, request.Name); List <string> workflowErrors; new WorkflowService(rockContext).Process(workflow, request, out workflowErrors); } catch (Exception ex) { ExceptionLogService.LogException(ex, this.Context); } } else { var workflowDetails = new List <LaunchWorkflowDetails>(); workflowDetails.Add(new LaunchWorkflowDetails(request)); var transaction = new Rock.Transactions.LaunchWorkflowsTransaction(workflowTypeGuid.Value, workflowDetails); // NOTE: In v8, the initiator will always be null while using delayed start. //transaction.InitiatorPersonAliasId = CurrentPersonAliasId; // available in v9 Rock.Transactions.RockQueue.TransactionQueue.Enqueue(transaction); } } } } }
/// <summary> /// Processes the payments. /// </summary> /// <param name="gateway">The gateway.</param> /// <param name="batchNamePrefix">The batch name prefix.</param> /// <param name="payments">The payments.</param> /// <param name="batchUrlFormat">The batch URL format.</param> /// <param name="receiptEmail">The receipt email.</param> /// <param name="failedPaymentEmail">The failed payment email.</param> /// <param name="failedPaymentWorkflowType">Type of the failed payment workflow.</param> /// <returns></returns> public static string ProcessPayments(FinancialGateway gateway, string batchNamePrefix, List <Payment> payments, string batchUrlFormat, Guid?receiptEmail, Guid?failedPaymentEmail, Guid?failedPaymentWorkflowType) { int totalPayments = 0; int totalAlreadyDownloaded = 0; int totalNoMatchingTransaction = 0; int totalAdded = 0; int totalReversals = 0; int totalFailures = 0; int totalStatusChanges = 0; var batchSummary = new Dictionary <Guid, List <Decimal> >(); var initialControlAmounts = new Dictionary <Guid, decimal>(); var gatewayComponent = gateway.GetGatewayComponent(); var newTransactions = new List <FinancialTransaction>(); var failedPayments = new List <FinancialTransaction>(); var contributionTxnType = DefinedValueCache.Get(Rock.SystemGuid.DefinedValue.TRANSACTION_TYPE_CONTRIBUTION.AsGuid()); int?defaultAccountId = null; using (var rockContext2 = new RockContext()) { defaultAccountId = new FinancialAccountService(rockContext2).Queryable() .Where(a => a.IsActive && !a.ParentAccountId.HasValue && (!a.StartDate.HasValue || a.StartDate.Value <= RockDateTime.Now) && (!a.EndDate.HasValue || a.EndDate.Value >= RockDateTime.Now) ) .OrderBy(a => a.Order) .Select(a => a.Id) .FirstOrDefault(); } var batchTxnChanges = new Dictionary <Guid, List <string> >(); var batchBatchChanges = new Dictionary <Guid, List <string> >(); var scheduledTransactionIds = new List <int>(); List <FinancialTransaction> transactionsWithAttributes = new List <FinancialTransaction>(); foreach (var payment in payments.Where(p => p.Amount > 0.0M)) { using (var rockContext = new RockContext()) { totalPayments++; var financialTransactionService = new FinancialTransactionService(rockContext); // Find existing payments with same transaction code FinancialTransaction originalTxn = null; var txns = financialTransactionService .Queryable("TransactionDetails") .Where(t => t.FinancialGatewayId.HasValue && t.FinancialGatewayId.Value == gateway.Id && t.TransactionCode == payment.TransactionCode) .ToList(); if (txns.Any()) { originalTxn = txns.OrderBy(t => t.Id).First(); } var scheduledTransaction = new FinancialScheduledTransactionService(rockContext).GetByScheduleId(payment.GatewayScheduleId, gateway.Id); // Calculate whether a transaction needs to be added var txnAmount = CalculateTransactionAmount(payment, txns); if (txnAmount != 0.0M || (payment.IsFailure && originalTxn == null && scheduledTransaction != null)) { // Verify that the payment is for an existing scheduled transaction, or has the same transaction code as an existing payment if (scheduledTransaction != null || originalTxn != null) { var transaction = new FinancialTransaction(); transaction.Guid = Guid.NewGuid(); transaction.TransactionCode = payment.TransactionCode; transaction.TransactionDateTime = payment.TransactionDateTime; transaction.Status = payment.Status; transaction.IsSettled = payment.IsSettled; transaction.SettledGroupId = payment.SettledGroupId; transaction.SettledDate = payment.SettledDate; transaction.StatusMessage = payment.StatusMessage; transaction.FinancialPaymentDetail = new FinancialPaymentDetail(); if (payment.ForeignKey.IsNotNullOrWhiteSpace()) { transaction.ForeignKey = payment.ForeignKey; } FinancialPaymentDetail financialPaymentDetail = null; List <ITransactionDetail> originalTxnDetails = new List <ITransactionDetail>(); if (scheduledTransaction != null) { scheduledTransactionIds.Add(scheduledTransaction.Id); if (payment.ScheduleActive.HasValue) { scheduledTransaction.IsActive = payment.ScheduleActive.Value; } transaction.ScheduledTransactionId = scheduledTransaction.Id; transaction.AuthorizedPersonAliasId = scheduledTransaction.AuthorizedPersonAliasId; transaction.SourceTypeValueId = scheduledTransaction.SourceTypeValueId; financialPaymentDetail = scheduledTransaction.FinancialPaymentDetail; scheduledTransaction.ScheduledTransactionDetails.ToList().ForEach(d => originalTxnDetails.Add(d)); } else { transaction.AuthorizedPersonAliasId = originalTxn.AuthorizedPersonAliasId; transaction.SourceTypeValueId = originalTxn.SourceTypeValueId; financialPaymentDetail = originalTxn.FinancialPaymentDetail; originalTxn.TransactionDetails.ToList().ForEach(d => originalTxnDetails.Add(d)); } transaction.FinancialGatewayId = gateway.Id; transaction.TransactionTypeValueId = contributionTxnType.Id; if (txnAmount < 0.0M) { transaction.Summary = "Reversal created for previous transaction(s) to correct the total transaction amount." + Environment.NewLine; } // Set the attributes of the transaction if (payment.Attributes != null && payment.Attributes.Count > 0) { transaction.LoadAttributes(); foreach (var attribute in payment.Attributes) { transaction.SetAttributeValue(attribute.Key, attribute.Value); } transactionsWithAttributes.Add(transaction); } var currencyTypeValue = payment.CurrencyTypeValue; var creditCardTypevalue = payment.CreditCardTypeValue; if (financialPaymentDetail != null) { if (currencyTypeValue == null && financialPaymentDetail.CurrencyTypeValueId.HasValue) { currencyTypeValue = DefinedValueCache.Get(financialPaymentDetail.CurrencyTypeValueId.Value); } if (creditCardTypevalue == null && financialPaymentDetail.CreditCardTypeValueId.HasValue) { creditCardTypevalue = DefinedValueCache.Get(financialPaymentDetail.CreditCardTypeValueId.Value); } transaction.FinancialPaymentDetail.AccountNumberMasked = financialPaymentDetail.AccountNumberMasked; transaction.FinancialPaymentDetail.NameOnCardEncrypted = financialPaymentDetail.NameOnCardEncrypted; transaction.FinancialPaymentDetail.ExpirationMonthEncrypted = financialPaymentDetail.ExpirationMonthEncrypted; transaction.FinancialPaymentDetail.ExpirationYearEncrypted = financialPaymentDetail.ExpirationYearEncrypted; transaction.FinancialPaymentDetail.BillingLocationId = financialPaymentDetail.BillingLocationId; } if (currencyTypeValue != null) { transaction.FinancialPaymentDetail.CurrencyTypeValueId = currencyTypeValue.Id; } if (creditCardTypevalue != null) { transaction.FinancialPaymentDetail.CreditCardTypeValueId = creditCardTypevalue.Id; } // Try to allocate the amount of the transaction based on the current scheduled transaction accounts decimal remainingAmount = Math.Abs(txnAmount); foreach (var detail in originalTxnDetails.Where(d => d.Amount != 0.0M)) { if (remainingAmount <= 0.0M) { // If there's no amount left, break out of details break; } var transactionDetail = new FinancialTransactionDetail(); transactionDetail.AccountId = detail.AccountId; if (detail.Amount <= remainingAmount) { // If the configured amount for this account is less than or equal to the remaining // amount, allocate the configured amount transactionDetail.Amount = detail.Amount; remainingAmount -= detail.Amount; } else { // If the configured amount is greater than the remaining amount, only allocate // the remaining amount transaction.Summary += "Note: Downloaded transaction amount was less than the configured allocation amounts for the Scheduled Transaction."; transactionDetail.Amount = remainingAmount; transactionDetail.Summary = "Note: The downloaded amount was not enough to apply the configured amount to this account."; remainingAmount = 0.0M; } transaction.TransactionDetails.Add(transactionDetail); } // If there's still amount left after allocating based on current config, add the remainder // to the account that was configured for the most amount if (remainingAmount > 0.0M) { transaction.Summary += "Note: Downloaded transaction amount was greater than the configured allocation amounts for the Scheduled Transaction."; var transactionDetail = transaction.TransactionDetails .OrderByDescending(d => d.Amount) .FirstOrDefault(); if (transactionDetail == null && defaultAccountId.HasValue) { transactionDetail = new FinancialTransactionDetail(); transactionDetail.AccountId = defaultAccountId.Value; transaction.TransactionDetails.Add(transactionDetail); } if (transactionDetail != null) { transactionDetail.Amount += remainingAmount; transactionDetail.Summary = "Note: Extra amount was applied to this account."; } } // If the amount to apply was negative, update all details to be negative (absolute value was used when allocating to accounts) if (txnAmount < 0.0M) { foreach (var txnDetail in transaction.TransactionDetails) { txnDetail.Amount = 0 - txnDetail.Amount; } } // Get the batch var batch = new FinancialBatchService(rockContext).Get( batchNamePrefix, currencyTypeValue, creditCardTypevalue, transaction.TransactionDateTime.Value, gateway.GetBatchTimeOffset()); var batchChanges = new List <string>(); if (batch.Id != 0) { initialControlAmounts.AddOrIgnore(batch.Guid, batch.ControlAmount); transaction.BatchId = batch.Id; financialTransactionService.Add(transaction); } else { batch.Transactions.Add(transaction); } batch.ControlAmount += transaction.TotalAmount; batch.Transactions.Add(transaction); if (receiptEmail.HasValue && txnAmount > 0.0M) { newTransactions.Add(transaction); } if ( payment.IsFailure && ( (txnAmount == 0.0M && scheduledTransaction != null && originalTxn == null) || (txnAmount < 0.0M && originalTxn != null) )) { failedPayments.Add(transaction); } // Add summary if (!batchSummary.ContainsKey(batch.Guid)) { batchSummary.Add(batch.Guid, new List <Decimal>()); } batchSummary[batch.Guid].Add(txnAmount); totalAdded++; if (txnAmount < 0.0M) { totalReversals++; } else if (txnAmount == 0.0M) { totalFailures++; } } else { totalNoMatchingTransaction++; } } else { totalAlreadyDownloaded++; } foreach (var txn in txns .Where(t => t.Status != payment.Status || t.StatusMessage != payment.StatusMessage || t.IsSettled != payment.IsSettled || t.SettledGroupId != payment.SettledGroupId || t.SettledDate != payment.SettledDate)) { txn.IsSettled = payment.IsSettled; txn.SettledGroupId = payment.SettledGroupId; txn.SettledDate = payment.SettledDate; txn.Status = payment.Status; txn.StatusMessage = payment.StatusMessage; totalStatusChanges++; } rockContext.SaveChanges(); } } if (transactionsWithAttributes.Count > 0) { foreach (var transaction in transactionsWithAttributes) { using (var rockContext3 = new RockContext()) { transaction.SaveAttributeValues(rockContext3); rockContext3.SaveChanges(); } } } // Queue a transaction to update the status of all affected scheduled transactions var updatePaymentStatusTxn = new Rock.Transactions.UpdatePaymentStatusTransaction(gateway.Id, scheduledTransactionIds); Rock.Transactions.RockQueue.TransactionQueue.Enqueue(updatePaymentStatusTxn); if (receiptEmail.HasValue && newTransactions.Any()) { // Queue a transaction to send receipts var newTransactionIds = newTransactions.Select(t => t.Id).ToList(); var sendPaymentReceiptsTxn = new Rock.Transactions.SendPaymentReceipts(receiptEmail.Value, newTransactionIds); Rock.Transactions.RockQueue.TransactionQueue.Enqueue(sendPaymentReceiptsTxn); } // Queue transactions to launch failed payment workflow if (failedPayments.Any()) { if (failedPaymentEmail.HasValue) { // Queue a transaction to send payment failure var newTransactionIds = failedPayments.Select(t => t.Id).ToList(); var sendPaymentFailureTxn = new Rock.Transactions.SendPaymentReceipts(failedPaymentEmail.Value, newTransactionIds); Rock.Transactions.RockQueue.TransactionQueue.Enqueue(sendPaymentFailureTxn); } if (failedPaymentWorkflowType.HasValue) { // Queue a transaction to launch workflow var workflowDetails = failedPayments.Select(p => new LaunchWorkflowDetails(p)).ToList(); var launchWorkflowsTxn = new Rock.Transactions.LaunchWorkflowsTransaction(failedPaymentWorkflowType.Value, workflowDetails); Rock.Transactions.RockQueue.TransactionQueue.Enqueue(launchWorkflowsTxn); } } StringBuilder sb = new StringBuilder(); sb.AppendFormat("<li>{0} {1} downloaded.</li>", totalPayments.ToString("N0"), (totalPayments == 1 ? "payment" : "payments")); if (totalAlreadyDownloaded > 0) { sb.AppendFormat("<li>{0} {1} previously downloaded and {2} already been added.</li>", totalAlreadyDownloaded.ToString("N0"), (totalAlreadyDownloaded == 1 ? "payment was" : "payments were"), (totalAlreadyDownloaded == 1 ? "has" : "have")); } if (totalStatusChanges > 0) { sb.AppendFormat("<li>{0} {1} previously downloaded but had a change of status.</li>", totalStatusChanges.ToString("N0"), (totalStatusChanges == 1 ? "payment was" : "payments were")); } if (totalNoMatchingTransaction > 0) { sb.AppendFormat("<li>{0} {1} could not be matched to an existing scheduled payment profile or a previous transaction.</li>", totalNoMatchingTransaction.ToString("N0"), (totalNoMatchingTransaction == 1 ? "payment" : "payments")); } sb.AppendFormat("<li>{0} {1} successfully added.</li>", totalAdded.ToString("N0"), (totalAdded == 1 ? "payment was" : "payments were")); if (totalReversals > 0) { sb.AppendFormat("<li>{0} {1} added as a reversal to a previous transaction.</li>", totalReversals.ToString("N0"), (totalReversals == 1 ? "payment was" : "payments were")); } if (totalFailures > 0) { sb.AppendFormat("<li>{0} {1} recorded as a failed transaction.</li>", totalFailures.ToString("N0"), (totalFailures == 1 ? "payment was" : "payments were")); } using (var rockContext = new RockContext()) { var batches = new FinancialBatchService(rockContext) .Queryable().AsNoTracking() .Where(b => batchSummary.Keys.Contains(b.Guid)) .ToList(); foreach (var batchItem in batchSummary) { int items = batchItem.Value.Count; if (items > 0) { var batch = batches .Where(b => b.Guid.Equals(batchItem.Key)) .FirstOrDefault(); string batchName = string.Format("'{0} ({1})'", batch.Name, batch.BatchStartDateTime.Value.ToString("d")); if (!string.IsNullOrWhiteSpace(batchUrlFormat)) { batchName = string.Format("<a href='{0}'>{1}</a>", string.Format(batchUrlFormat, batch.Id), batchName); } decimal sum = batchItem.Value.Sum(); string summaryformat = items == 1 ? "<li>{0} transaction of {1} was added to the {2} batch.</li>" : "<li>{0} transactions totaling {1} were added to the {2} batch</li>"; sb.AppendFormat(summaryformat, items.ToString("N0"), sum.FormatAsCurrency(), batchName); } } } return(sb.ToString()); }