diff --git a/com.bemaservices.RemoteCheckDeposit/App.config b/com.bemaservices.RemoteCheckDeposit/App.config index 19a4a79..7c18f22 100644 --- a/com.bemaservices.RemoteCheckDeposit/App.config +++ b/com.bemaservices.RemoteCheckDeposit/App.config @@ -1,34 +1,36 @@ - + - -
- + +
+ - + - + - + + + - - + + - - + + - - + + - + \ No newline at end of file diff --git a/com.bemaservices.RemoteCheckDeposit/FileFormatTypes/X937/CassCommercialBank.cs b/com.bemaservices.RemoteCheckDeposit/FileFormatTypes/X937/CassCommercialBank.cs new file mode 100644 index 0000000..0d5bc82 --- /dev/null +++ b/com.bemaservices.RemoteCheckDeposit/FileFormatTypes/X937/CassCommercialBank.cs @@ -0,0 +1,403 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.Composition; +using System.IO; +using System.Linq; + +using Rock; +using Rock.Attribute; +using Rock.Model; + +using com.bemaservices.RemoteCheckDeposit.Model; +using com.bemaservices.RemoteCheckDeposit.Records.X937; +using com.bemaservices.RemoteCheckDeposit; +using System.Text; + +namespace com.bemaservices.RemoteCheckDeposit.FileFormatTypes +{ + + /// + /// Defines the x937 File export for Cass Commercial Bank + /// + [Description("Processes a batch export for Cass Commercial Bank.")] + [Export(typeof(FileFormatTypeComponent))] + [ExportMetadata("ComponentName", "Cass Commercial Bank")] + [EncryptedTextField("Destination Routing Number", "", true, "081000605", key: "DestinationRoutingNumber")] + [BooleanField(name: "Count the Deposit Ticket", + description: "Set this to true to include the deposit slip in the bundle count. Default is *usually* false.", + defaultValue: false, + key: "CountDepositSlip")] + //[EncryptedTextField("Origin Routing Number", "Used on Type 10 Record 3 for Account Routing", true, key: "OriginRoutingNumber")] + public class CassCommercialBank : X937DSTU + { + + #region Private Members + + /// + /// Counts the deposit slip in the item counts + /// + private bool countDepositSlip = false; + #endregion + + + #region System Setting Keys + /// + /// The system setting for the next cash header identifier. These should never be + /// repeated. Ever. + /// + protected const string SystemSettingNextCashHeaderId = "CassCommercial.NextCashHeaderId"; + + /// + /// The system setting that contains the last file modifier we used. + /// + protected const string SystemSettingLastFileModifier = "CassCommercial.LastFileModifier"; + + /// + /// The last item sequence number used for items. + /// + protected const string LastItemSequenceNumberKey = "CassCommercial.LastItemSequenceNumber"; + #endregion + + /// + /// Gets the File Header Record (type 01) + /// + protected override FileHeader GetFileHeaderRecord(ExportOptions options) + { + var header = base.GetFileHeaderRecord(options); + + var originRoutingNumber = Rock.Security.Encryption.DecryptString(GetAttributeValue(options.FileFormat, "InstitutionRoutingNumber")); + + // Override account number with institution routing number for Field 5 + header.ImmediateOriginRoutingNumber = originRoutingNumber; + + return header; + } + + /// + /// Gets the Cash Letter Header Record (type 10) + /// + protected override CashLetterHeader GetCashLetterHeaderRecord(ExportOptions options) + { + int cashHeaderId = GetSystemSetting(SystemSettingNextCashHeaderId).AsIntegerOrNull() ?? 0; + var originRoutingNumber = Rock.Security.Encryption.DecryptString(GetAttributeValue(options.FileFormat, "InstitutionRoutingNumber")); + + var header = base.GetCashLetterHeaderRecord(options); + header.ID = cashHeaderId.ToString("D8"); + SetSystemSetting(SystemSettingNextCashHeaderId, (cashHeaderId + 1).ToString()); + + // Override routing number with institution routing number for Field 4 + header.ClientInstitutionRoutingNumber = originRoutingNumber; + + return header; + } + + /// + /// Gets the Bundle Header Record (type 20) + /// + protected override BundleHeader GetBundleHeader(ExportOptions options, int bundleIndex) + { + var header = base.GetBundleHeader(options, bundleIndex); + var originRoutingNumber = Rock.Security.Encryption.DecryptString(GetAttributeValue(options.FileFormat, "InstitutionRoutingNumber")); + + // Override routing number with institution routing number for Field 4 + header.ClientInstitutionRoutingNumber = originRoutingNumber; + + // Set Bundle ID (should be same as Bundle Sequence Number ) + header.ID = (bundleIndex + 1).ToString(); + + return header; + } + + /// + /// Gets the item detail records (type 25) + /// + protected override List GetItemDetailRecords(ExportOptions options, FinancialTransaction transaction) + { + //var accountNumber = Rock.Security.Encryption.DecryptString(GetAttributeValue(options.FileFormat, "AccountNumber")); + //var routingNumber = Rock.Security.Encryption.DecryptString(GetAttributeValue(options.FileFormat, "RoutingNumber")); + + // + // Parse the MICR data from the transaction. + // + var micr = GetMicrInstance(transaction.CheckMicrEncrypted); + + var transactionRoutingNumber = micr.GetRoutingNumber(); + + // Check Detail Record Type (25) + var detail = new Records.X937.CheckDetail + { + AuxiliaryOnUs = micr.GetAuxOnUs(), + ExternalProcessingCode = micr.GetExternalProcessingCode(), + PayorBankRoutingNumber = transactionRoutingNumber.Substring(0, 8), + PayorBankRoutingNumberCheckDigit = transactionRoutingNumber.Substring(8, 1), + OnUs = string.Format("{0}/{1}", micr.GetAccountNumber(), micr.GetCheckNumber()), + ItemAmount = transaction.TotalAmount, + ClientInstitutionItemSequenceNumber = transaction.Id.ToString(), + DocumentationTypeIndicator = "G", // Field Value must be "G" - Meaning there are 2 images present + ElectronicReturnAcceptanceIndicator = string.Empty, + MICRValidIndicator = null, + BankOfFirstDepositIndicator = "Y", + CheckDetailRecordAddendumCount = 00, // From Veronica + CorrectionIndicator = string.Empty, + ArchiveTypeIndicator = string.Empty + + }; + + return new List { detail }; + } + + + /// + /// Gets the image record for a specific transaction image (type 50 and 52). + /// + protected override List GetImageRecords(ExportOptions options, FinancialTransaction transaction, FinancialTransactionImage image, bool isFront) + { + var institutionRoutingNumber = Rock.Security.Encryption.DecryptString(GetAttributeValue(options.FileFormat, "InstitutionRoutingNumber")); + var records = base.GetImageRecords(options, transaction, image, isFront); + + foreach (var imageData in records.Where(r => r.RecordType == 52).Cast()) + { + imageData.InstitutionRoutingNumber = institutionRoutingNumber; + } + + foreach (var imageData in records.Where(r => r.RecordType == 50).Cast()) + { + imageData.ImageCreatorRoutingNumber = institutionRoutingNumber; + } + + return records; + } + + /// + /// Gets the credit detail deposit record (type 61). + /// + protected override List GetCreditDetailRecords(ExportOptions options, int bundleIndex, List transactions) + { + var accountNumber = Rock.Security.Encryption.DecryptString(GetAttributeValue(options.FileFormat, "AccountNumber")); + var routingNumber = Rock.Security.Encryption.DecryptString(GetAttributeValue(options.FileFormat, "RoutingNumber")); + //var payorRoutingNumber = Rock.Security.Encryption.DecryptString(GetAttributeValue(options.FileFormat, "PayorRoutingNumber")); + var destinationRoutingNumber = Rock.Security.Encryption.DecryptString(GetAttributeValue(options.FileFormat, "DestinationRoutingNumber")); + var records = new List(); + + var creditDetail = new CreditDetail + { + PayorRoutingNumber = routingNumber,//payorRoutingNumber, + CreditAccountNumber = accountNumber, //accountNumber + "/", + Amount = transactions.Sum(t => t.TotalAmount), + InstitutionItemSequenceNumber = GetNextItemSequenceNumber().ToString("000000000000000"), + DocumentTypeIndicator = "G", + SourceOfWorkCode = "3", + DebitCreditIndicator = "2" + }; + records.Add(creditDetail); + + for (int i = 0; i < 2; i++) + { + using (var ms = GetDepositSlipImage(options, creditDetail, i == 0)) + { + var tiffImageBytes = ConvertImageToTiffG4(ms).ReadBytesToEnd(); + // + // Get the Image View Detail record (type 50) + // + var detail = new ImageViewDetail + { + ImageIndicator = 1, + ImageCreatorRoutingNumber = destinationRoutingNumber, + ImageCreatorDate = options.ExportDateTime, + ImageViewFormatIndicator = 0, + DataSize = (int)tiffImageBytes.Length, + CompressionAlgorithmIdentifier = 0, + SideIndicator = i, + ViewDescriptor = 0, + DigitalSignatureIndicator = 0 + }; + + // + // Get the Image View Data record (type 52). + // + var data = new ImageViewData + { + InstitutionRoutingNumber = routingNumber, + CycleNumber = string.Empty, + BundleBusinessDate = options.BusinessDateTime, + ClientInstitutionItemSequenceNumber = creditDetail.InstitutionItemSequenceNumber, + ClippingOrigin = 0, + ImageData = ms.ReadBytesToEnd() + }; + + records.Add(detail); + records.Add(data); + } + } + + return records; + } + + /// + /// Gets the bundle control record (type 70). + /// + + protected override Records.X937.BundleControl GetBundleControl(ExportOptions options, List records) + { + var itemRecords = records.Where(r => r.RecordType == 25); + + // If we are including the credit items then we need to count those as well. + if (this.countDepositSlip) + { + itemRecords = records.Where(r => r.RecordType == 25 && r.RecordType == 61); // Just Overwrite + } + + var checkDetailRecords = records.Where(r => r.RecordType == 25).Cast(); // Only count checks and not the credit detail + var imageDetailRecords = records.Where(r => r.RecordType == 52); + + // Record Type 70 + var control = new Records.X937.BundleControl + { + ItemCount = itemRecords.Count(), + TotalAmount = checkDetailRecords.Sum(r => (decimal)r.ItemAmount), + MICRValidTotalAmount = 000000000000, //checkDetailRecords.Sum(r => (decimal)r.ItemAmount), + ImageCount = imageDetailRecords.Count() + }; + + return control; + } + + /// + /// Gets the cash letter control record (type 90). + /// + /// Export options to be used by the component. + /// Existing records in the cash letter. + /// A CashLetterControl record. + protected override Records.X937.CashLetterControl GetCashLetterControlRecord(ExportOptions options, List records) + { + var bundleHeaderRecords = records.Where(r => r.RecordType == 20); + var checkDetailRecords = records.Where(r => r.RecordType == 25).Cast(); + var itemRecords = records.Where(r => r.RecordType == 25); // Only count record types 25. + var imageDetailRecords = records.Where(r => r.RecordType == 52); + var organizationName = GetAttributeValue(options.FileFormat, "OriginName"); + + // Some banks *might* include the deposit slip + if (this.countDepositSlip) + { + itemRecords = records.Where(r => r.RecordType == 25 && r.RecordType == 61); // Just Overwrite. + } + + // Record Type 90 + var control = new Records.X937.CashLetterControl + { + BundleCount = bundleHeaderRecords.Count(), + ItemCount = itemRecords.Count(), + //TotalAmount = 000000000000,//checkDetailRecords.Sum(c => (decimal)c.ItemAmount), // Must be 0's According to Veronica, This should now be the total amount + TotalAmount = checkDetailRecords.Sum(c => (decimal)c.ItemAmount), // According to Veronica 09-28-2018 + ImageCount = imageDetailRecords.Count(), + ECEInstitutionName = organizationName + }; + + return control; + } + + /// + /// Gets the cash letter control record (type 99). + /// + protected override FileControl GetFileControlRecord(ExportOptions options, List records) + { + var fileControl = base.GetFileControlRecord(options, records); + var itemRecords = records.Where(r => r.RecordType == 25); // Only count record types 25. + // Some banks *might* include the deposit slip + if (this.countDepositSlip) + { + itemRecords = records.Where(r => r.RecordType == 25 && r.RecordType == 61); // Just Overwrite. + } + + fileControl.ImmediateOriginContactName = "".PadRight(14, ' '); + fileControl.ImmediateOriginContactPhoneNumber = "0".PadRight(10, ' '); + fileControl.TotalItemCount = itemRecords.Count(); + + return fileControl; + } + + #region Helper Methods + protected int GetNextItemSequenceNumber() + { + int lastSequence = GetSystemSetting(LastItemSequenceNumberKey).AsIntegerOrNull() ?? 0; + int nextSequence = lastSequence + 1; + + SetSystemSetting(LastItemSequenceNumberKey, nextSequence.ToString()); + + return nextSequence; + } + + /// + /// Gets the credit detail deposit record (type 61). + /// + /// A stream that contains the image data in TIFF 6.0 CCITT Group 4 format. + protected virtual Stream GetDepositSlipImage(ExportOptions options, CreditDetail creditDetail, bool isFrontSide) + { + var bitmap = new System.Drawing.Bitmap(1200, 550); + var g = System.Drawing.Graphics.FromImage(bitmap); + + var depositSlipTemplate = GetAttributeValue(options.FileFormat, "DepositSlipTemplate"); + var mergeFields = new Dictionary + { + { "FileFormat", options.FileFormat }, + { "Amount", creditDetail.Amount.ToString( "C" ) } + }; + var depositSlipText = depositSlipTemplate.ResolveMergeFields(mergeFields); + + // + // Ensure we are opague with white. + // + g.FillRectangle(System.Drawing.Brushes.White, new System.Drawing.Rectangle(0, 0, 1200, 550)); + + if (isFrontSide) + { + g.DrawString(depositSlipText, + new System.Drawing.Font("Tahoma", 30), + System.Drawing.Brushes.Black, + new System.Drawing.PointF(50, 50)); + } + + g.Flush(); + + // + // Ensure the DPI is correct. + // + bitmap.SetResolution(200, 200); + + // + // Compress using TIFF, CCITT Group 4 format. + // + var codecInfo = System.Drawing.Imaging.ImageCodecInfo.GetImageEncoders() + .Where(c => c.MimeType == "image/tiff") + .First(); + var parameters = new System.Drawing.Imaging.EncoderParameters(1); + parameters.Param[0] = new System.Drawing.Imaging.EncoderParameter(System.Drawing.Imaging.Encoder.Compression, (long)System.Drawing.Imaging.EncoderValue.CompressionCCITT4); + + var ms = new MemoryStream(); + bitmap.Save(ms, codecInfo, parameters); + ms.Position = 0; + + return ms; + } + #endregion + + + /// + /// Hashes the string with SHA256. + /// + protected string HashString(string contents) + { + byte[] byteContents = Encoding.Unicode.GetBytes(contents); + + var hash = new System.Security.Cryptography.SHA256CryptoServiceProvider().ComputeHash(byteContents); + + return string.Join("", hash.Select(b => b.ToString("x2")).ToArray()); + } + } +} diff --git a/com.bemaservices.RemoteCheckDeposit/FileFormatTypes/X937/FrostBank.cs b/com.bemaservices.RemoteCheckDeposit/FileFormatTypes/X937/FrostBank.cs index b4a139f..6dc8430 100644 --- a/com.bemaservices.RemoteCheckDeposit/FileFormatTypes/X937/FrostBank.cs +++ b/com.bemaservices.RemoteCheckDeposit/FileFormatTypes/X937/FrostBank.cs @@ -198,6 +198,7 @@ protected override List GetItemRecords( ExportOptions options, Financial //Modify Check Detail Adden A var checkDetailA = records.Where( r => r.RecordType == 26 ).Cast().FirstOrDefault(); + checkDetailA.BankOfFirstDepositItemSequenceNumber = sequenceNumber.ToString("000000000000000"); // DTS - added because Wells Fargo now requires this field checkDetailA.TruncationIndicator = "Y"; foreach ( var imageData in records.Where( r => r.RecordType == 52 ).Cast() ) diff --git a/com.bemaservices.RemoteCheckDeposit/FileFormatTypes/X937/JackHenry.cs b/com.bemaservices.RemoteCheckDeposit/FileFormatTypes/X937/JackHenry.cs new file mode 100644 index 0000000..a70df7f --- /dev/null +++ b/com.bemaservices.RemoteCheckDeposit/FileFormatTypes/X937/JackHenry.cs @@ -0,0 +1,410 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.Composition; +using System.IO; +using System.Linq; + +using Rock; +using Rock.Attribute; +using Rock.Model; + +using com.bemaservices.RemoteCheckDeposit.Model; +using com.bemaservices.RemoteCheckDeposit.Records.X937; +using com.bemaservices.RemoteCheckDeposit; +using System.Text; + +namespace com.bemaservices.RemoteCheckDeposit.FileFormatTypes +{ + + /// + /// Defines the x937 File export for Cass Commercial Bank + /// + [Description("Processes a batch export for Jack Henry & Associates, Inc.")] + [Export(typeof(FileFormatTypeComponent))] + [ExportMetadata("ComponentName", "Jack Henry & Associates")] + [EncryptedTextField("Destination Routing Number", "", true, "081000605", key: "DestinationRoutingNumber")] + [BooleanField(name: "Count the Deposit Ticket", + description: "Set this to true to include the deposit slip in the bundle count. Default is *usually* false.", + defaultValue: false, + key: "CountDepositSlip")] + //[EncryptedTextField("Origin Routing Number", "Used on Type 10 Record 3 for Account Routing", true, key: "OriginRoutingNumber")] + public class JackHenry : X937DSTU + { + + #region Private Members + + /// + /// Counts the deposit slip in the item counts + /// + private bool countDepositSlip = false; + #endregion + + + #region System Setting Keys + /// + /// The system setting for the next cash header identifier. These should never be + /// repeated. Ever. + /// + protected const string SystemSettingNextCashHeaderId = "JackHenry.NextCashHeaderId"; + + /// + /// The system setting that contains the last file modifier we used. + /// + protected const string SystemSettingLastFileModifier = "JackHenry.LastFileModifier"; + + /// + /// The last item sequence number used for items. + /// + protected const string LastItemSequenceNumberKey = "JackHenry.LastItemSequenceNumber"; + #endregion + + /// + /// Gets the File Header Record (type 01) + /// + protected override FileHeader GetFileHeaderRecord(ExportOptions options) + { + var header = base.GetFileHeaderRecord(options); + + var originRoutingNumber = Rock.Security.Encryption.DecryptString(GetAttributeValue(options.FileFormat, "InstitutionRoutingNumber")); + + // Override account number with institution routing number for Field 5 + header.ImmediateOriginRoutingNumber = originRoutingNumber; + + return header; + } + + /// + /// Gets the Cash Letter Header Record (type 10) + /// + protected override CashLetterHeader GetCashLetterHeaderRecord(ExportOptions options) + { + int cashHeaderId = GetSystemSetting(SystemSettingNextCashHeaderId).AsIntegerOrNull() ?? 0; + var originRoutingNumber = Rock.Security.Encryption.DecryptString(GetAttributeValue(options.FileFormat, "InstitutionRoutingNumber")); + + var header = base.GetCashLetterHeaderRecord(options); + header.ID = cashHeaderId.ToString("D8"); + SetSystemSetting(SystemSettingNextCashHeaderId, (cashHeaderId + 1).ToString()); + + // Override routing number with institution routing number for Field 4 + header.ClientInstitutionRoutingNumber = originRoutingNumber; + + return header; + } + + /// + /// Gets the Bundle Header Record (type 20) + /// + protected override BundleHeader GetBundleHeader(ExportOptions options, int bundleIndex) + { + var header = base.GetBundleHeader(options, bundleIndex); + var originRoutingNumber = Rock.Security.Encryption.DecryptString(GetAttributeValue(options.FileFormat, "InstitutionRoutingNumber")); + + // Override routing number with institution routing number for Field 4 + header.ClientInstitutionRoutingNumber = originRoutingNumber; + + // Set Bundle ID (should be same as Bundle Sequence Number ) + header.ID = (bundleIndex + 1).ToString(); + + return header; + } + + /// + /// Gets the item detail records (type 25) + /// + protected override List GetItemDetailRecords(ExportOptions options, FinancialTransaction transaction) + { + // Parse the MICR data from the transaction. + var micr = GetMicrInstance(transaction.CheckMicrEncrypted); + + var transactionRoutingNumber = micr.GetRoutingNumber(); + var transSubString = transactionRoutingNumber.Substring(0, 8); + var transSubStringCheckDigit = transactionRoutingNumber.Substring(8, 1); + + // Check Detail Record Type (25) + var detail = new Records.X937.CheckDetail + { + AuxiliaryOnUs = micr.GetAuxOnUs(), + ExternalProcessingCode = micr.GetExternalProcessingCode(), + PayorBankRoutingNumber = transSubString, + PayorBankRoutingNumberCheckDigit = transSubStringCheckDigit, + OnUs = string.Format("{0}/{1}", micr.GetAccountNumber(), micr.GetCheckNumber()).PadLeft(18, '0').PadLeft(20, ' '), + ItemAmount = transaction.TotalAmount, + ClientInstitutionItemSequenceNumber = transaction.Id.ToString(), + DocumentationTypeIndicator = "G", // Field Value must be "G" - Meaning there are 2 images present + ElectronicReturnAcceptanceIndicator = string.Empty, + MICRValidIndicator = null, + BankOfFirstDepositIndicator = "Y", + CheckDetailRecordAddendumCount = 00, // From Veronica + CorrectionIndicator = string.Empty, + ArchiveTypeIndicator = string.Empty + + }; + + return new List { detail }; + } + + + /// + /// Gets the image record for a specific transaction image (type 50 and 52). + /// + protected override List GetImageRecords(ExportOptions options, FinancialTransaction transaction, FinancialTransactionImage image, bool isFront) + { + var institutionRoutingNumber = Rock.Security.Encryption.DecryptString(GetAttributeValue(options.FileFormat, "InstitutionRoutingNumber")); + var records = base.GetImageRecords(options, transaction, image, isFront); + + foreach (var imageData in records.Where(r => r.RecordType == 52).Cast()) + { + imageData.InstitutionRoutingNumber = institutionRoutingNumber; + } + + foreach (var imageData in records.Where(r => r.RecordType == 50).Cast()) + { + imageData.ImageCreatorRoutingNumber = institutionRoutingNumber; + } + + return records; + } + + /// + /// Gets the credit detail deposit record (type 61). + /// + protected override List GetCreditDetailRecords(ExportOptions options, int bundleIndex, List transactions) + { + var accountNumber = Rock.Security.Encryption.DecryptString(GetAttributeValue(options.FileFormat, "AccountNumber")); + var routingNumber = Rock.Security.Encryption.DecryptString(GetAttributeValue(options.FileFormat, "RoutingNumber")); + var destinationRoutingNumber = Rock.Security.Encryption.DecryptString(GetAttributeValue(options.FileFormat, "DestinationRoutingNumber")); + var records = new List(); + + var creditDetail = new CreditDetail + { + PayorRoutingNumber = routingNumber, + CreditAccountNumber = accountNumber + "/", + Amount = transactions.Sum(t => t.TotalAmount), + InstitutionItemSequenceNumber = GetNextItemSequenceNumber().ToString("000000000000000"), + DocumentTypeIndicator = "G", + SourceOfWorkCode = "3", + DebitCreditIndicator = "2" + }; + records.Add(creditDetail); + + //var creditDetail = new CheckDetail //Type 25 + //{ + // PayorBankRoutingNumber = routingNumber.Substring(0, 8), + // PayorBankRoutingNumberCheckDigit = routingNumber.Substring(8, 1), + // OnUs = string.Format("{0}/", accountNumber).PadLeft(18, '0').PadLeft(20, ' '), + // ItemAmount = transactions.Sum(t => t.TotalAmount), + // ClientInstitutionItemSequenceNumber = GetNextItemSequenceNumber().ToString("000000000000000"), + // BankOfFirstDepositIndicator = "U", + // CheckDetailRecordAddendumCount = 00, + // DocumentationTypeIndicator = "G" + //}; + //records.Add(creditDetail); + + //for (int i = 0; i < 2; i++) + //{ + // using (var ms = GetDepositSlipImage(options, creditDetail, i == 0, transactions)) + // { + + // //Get the Image View Detail record (type 50). + + // var detail = new ImageViewDetail + // { + // ImageIndicator = 1, + // ImageCreatorRoutingNumber = routingNumber, + // ImageCreatorDate = options.ExportDateTime, + // ImageViewFormatIndicator = 0, + // CompressionAlgorithmIdentifier = 0, + // SideIndicator = i, + // ViewDescriptor = 0, + // DigitalSignatureIndicator = 0 + // }; + + + // //Get the Image View Data record (type 52). + + // var data = new ImageViewData + // { + // InstitutionRoutingNumber = routingNumber, + // BundleBusinessDate = options.BusinessDateTime, + // ClientInstitutionItemSequenceNumber = creditDetail.ClientInstitutionItemSequenceNumber, + // ClippingOrigin = 0, + // ImageData = ms.ReadBytesToEnd() + // }; + + // records.Add(detail); + // records.Add(data); + // } + //} + return records; + } + + /// + /// Gets the bundle control record (type 70). + /// + + protected override Records.X937.BundleControl GetBundleControl(ExportOptions options, List records) + { + var itemRecords = records.Where(r => r.RecordType == 25); + + // If we are including the credit items then we need to count those as well. + if (this.countDepositSlip) + { + itemRecords = records.Where(r => r.RecordType == 25 && r.RecordType == 61); // Just Overwrite + } + + var checkDetailRecords = records.Where(r => r.RecordType == 25).Cast(); // Only count checks and not the credit detail + var imageDetailRecords = records.Where(r => r.RecordType == 52); + + // Record Type 70 + var control = new Records.X937.BundleControl + { + ItemCount = itemRecords.Count(), + TotalAmount = checkDetailRecords.Sum(r => (decimal)r.ItemAmount), + MICRValidTotalAmount = 000000000000, //checkDetailRecords.Sum(r => (decimal)r.ItemAmount), + ImageCount = imageDetailRecords.Count() + }; + + return control; + } + + /// + /// Gets the cash letter control record (type 90). + /// + /// Export options to be used by the component. + /// Existing records in the cash letter. + /// A CashLetterControl record. + protected override Records.X937.CashLetterControl GetCashLetterControlRecord(ExportOptions options, List records) + { + var bundleHeaderRecords = records.Where(r => r.RecordType == 20); + var checkDetailRecords = records.Where(r => r.RecordType == 25).Cast(); + var itemRecords = records.Where(r => r.RecordType == 25); // Only count record types 25. + var imageDetailRecords = records.Where(r => r.RecordType == 52); + var organizationName = GetAttributeValue(options.FileFormat, "OriginName"); + + var controlRecord = base.GetCashLetterControlRecord(options, records); + + // Some banks *might* include the deposit slip + if (this.countDepositSlip) + { + itemRecords = records.Where(r => r.RecordType == 25 && r.RecordType == 61); // Just Overwrite. + } + + // Record Type 90 + var control = new Records.X937.CashLetterControl + { + BundleCount = bundleHeaderRecords.Count(), + ItemCount = itemRecords.Count(), + TotalAmount = checkDetailRecords.Sum(c => (decimal)c.ItemAmount), + ImageCount = imageDetailRecords.Count(), + ECEInstitutionName = organizationName, + SettlementDate = controlRecord.SettlementDate + }; + + return control; + } + + /// + /// Gets the cash letter control record (type 99). + /// + protected override FileControl GetFileControlRecord(ExportOptions options, List records) + { + var fileControl = base.GetFileControlRecord(options, records); + var itemRecords = records.Where(r => r.RecordType == 25); // Only count record types 25. + // Some banks *might* include the deposit slip + if (this.countDepositSlip) + { + itemRecords = records.Where(r => r.RecordType == 25 && r.RecordType == 61); // Just Overwrite. + } + + fileControl.ImmediateOriginContactName = "".PadRight(14, ' '); + fileControl.ImmediateOriginContactPhoneNumber = "0".PadRight(10, ' '); + fileControl.TotalItemCount = itemRecords.Count(); + + return fileControl; + } + + #region Helper Methods + protected int GetNextItemSequenceNumber() + { + int lastSequence = GetSystemSetting(LastItemSequenceNumberKey).AsIntegerOrNull() ?? 0; + int nextSequence = lastSequence + 1; + + SetSystemSetting(LastItemSequenceNumberKey, nextSequence.ToString()); + + return nextSequence; + } + + /// + /// Gets the credit detail deposit record (type 61). + /// + /// A stream that contains the image data in TIFF 6.0 CCITT Group 4 format. + protected virtual Stream GetDepositSlipImage(ExportOptions options, CreditDetail creditDetail, bool isFrontSide) + { + var bitmap = new System.Drawing.Bitmap(1200, 550); + var g = System.Drawing.Graphics.FromImage(bitmap); + + var depositSlipTemplate = GetAttributeValue(options.FileFormat, "DepositSlipTemplate"); + var mergeFields = new Dictionary + { + { "FileFormat", options.FileFormat }, + { "Amount", creditDetail.Amount.ToString( "C" ) } + }; + var depositSlipText = depositSlipTemplate.ResolveMergeFields(mergeFields); + + // + // Ensure we are opague with white. + // + g.FillRectangle(System.Drawing.Brushes.White, new System.Drawing.Rectangle(0, 0, 1200, 550)); + + if (isFrontSide) + { + g.DrawString(depositSlipText, + new System.Drawing.Font("Tahoma", 30), + System.Drawing.Brushes.Black, + new System.Drawing.PointF(50, 50)); + } + + g.Flush(); + + // + // Ensure the DPI is correct. + // + bitmap.SetResolution(200, 200); + + // + // Compress using TIFF, CCITT Group 4 format. + // + var codecInfo = System.Drawing.Imaging.ImageCodecInfo.GetImageEncoders() + .Where(c => c.MimeType == "image/tiff") + .First(); + var parameters = new System.Drawing.Imaging.EncoderParameters(1); + parameters.Param[0] = new System.Drawing.Imaging.EncoderParameter(System.Drawing.Imaging.Encoder.Compression, (long)System.Drawing.Imaging.EncoderValue.CompressionCCITT4); + + var ms = new MemoryStream(); + bitmap.Save(ms, codecInfo, parameters); + ms.Position = 0; + + return ms; + } + #endregion + + + /// + /// Hashes the string with SHA256. + /// + protected string HashString(string contents) + { + byte[] byteContents = Encoding.Unicode.GetBytes(contents); + + var hash = new System.Security.Cryptography.SHA256CryptoServiceProvider().ComputeHash(byteContents); + + return string.Join("", hash.Select(b => b.ToString("x2")).ToArray()); + } + } +} diff --git a/com.bemaservices.RemoteCheckDeposit/FileFormatTypes/X937/MontgomeryBank.cs b/com.bemaservices.RemoteCheckDeposit/FileFormatTypes/X937/MontgomeryBank.cs new file mode 100644 index 0000000..d28dd8a --- /dev/null +++ b/com.bemaservices.RemoteCheckDeposit/FileFormatTypes/X937/MontgomeryBank.cs @@ -0,0 +1,401 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.Composition; +using System.IO; +using System.Linq; + +using Rock; +using Rock.Attribute; +using Rock.Model; + +using com.bemaservices.RemoteCheckDeposit.Model; +using com.bemaservices.RemoteCheckDeposit.Records.X937; +using com.bemaservices.RemoteCheckDeposit; +using System.Text; + +namespace com.bemaservices.RemoteCheckDeposit.FileFormatTypes +{ + + /// + /// Defines the x937 File export for Montgomery Bank + /// + [Description("Processes a batch export for Montgomery Bank.")] + [Export(typeof(FileFormatTypeComponent))] + [ExportMetadata("ComponentName", "Montgomery Bank")] + [EncryptedTextField("Destination Routing Number", "", true, "081000605", key: "DestinationRoutingNumber")] + [BooleanField(name: "Count the Deposit Ticket", + description: "Set this to true to include the deposit slip in the bundle count. Default is *usually* false.", + defaultValue: false, + key: "CountDepositSlip")] + //[EncryptedTextField("Origin Routing Number", "Used on Type 10 Record 3 for Account Routing", true, key: "OriginRoutingNumber")] + public class MontgomeryBank : X937DSTU + { + + #region Private Members + + /// + /// Counts the deposit slip in the item counts + /// + private bool countDepositSlip = false; + #endregion + + + #region System Setting Keys + /// + /// The system setting for the next cash header identifier. These should never be + /// repeated. Ever. + /// + protected const string SystemSettingNextCashHeaderId = "Montgomery.NextCashHeaderId"; + + /// + /// The system setting that contains the last file modifier we used. + /// + protected const string SystemSettingLastFileModifier = "Montgomery.LastFileModifier"; + + /// + /// The last item sequence number used for items. + /// + protected const string LastItemSequenceNumberKey = "Montgomery.LastItemSequenceNumber"; + #endregion + + /// + /// Gets the File Header Record (type 01) + /// + protected override FileHeader GetFileHeaderRecord(ExportOptions options) + { + var header = base.GetFileHeaderRecord(options); + + var originRoutingNumber = Rock.Security.Encryption.DecryptString(GetAttributeValue(options.FileFormat, "InstitutionRoutingNumber")); + + // Override account number with institution routing number for Field 5 + header.ImmediateOriginRoutingNumber = originRoutingNumber; + + return header; + } + + /// + /// Gets the Cash Letter Header Record (type 10) + /// + protected override CashLetterHeader GetCashLetterHeaderRecord(ExportOptions options) + { + int cashHeaderId = GetSystemSetting(SystemSettingNextCashHeaderId).AsIntegerOrNull() ?? 0; + var originRoutingNumber = Rock.Security.Encryption.DecryptString(GetAttributeValue(options.FileFormat, "InstitutionRoutingNumber")); + + var header = base.GetCashLetterHeaderRecord(options); + header.ID = cashHeaderId.ToString("D8"); + SetSystemSetting(SystemSettingNextCashHeaderId, (cashHeaderId + 1).ToString()); + + // Override routing number with institution routing number for Field 4 + header.ClientInstitutionRoutingNumber = originRoutingNumber; + + return header; + } + + /// + /// Gets the Bundle Header Record (type 20) + /// + protected override BundleHeader GetBundleHeader(ExportOptions options, int bundleIndex) + { + var header = base.GetBundleHeader(options, bundleIndex); + var originRoutingNumber = Rock.Security.Encryption.DecryptString(GetAttributeValue(options.FileFormat, "InstitutionRoutingNumber")); + + // Override routing number with institution routing number for Field 4 + header.ClientInstitutionRoutingNumber = originRoutingNumber; + + // Set Bundle ID (should be same as Bundle Sequence Number ) + header.ID = (bundleIndex + 1).ToString(); + + return header; + } + + /// + /// Gets the item detail records (type 25) + /// + protected override List GetItemDetailRecords(ExportOptions options, FinancialTransaction transaction) + { + //var accountNumber = Rock.Security.Encryption.DecryptString(GetAttributeValue(options.FileFormat, "AccountNumber")); + //var routingNumber = Rock.Security.Encryption.DecryptString(GetAttributeValue(options.FileFormat, "RoutingNumber")); + + // + // Parse the MICR data from the transaction. + // + var micr = GetMicrInstance(transaction.CheckMicrEncrypted); + + var transactionRoutingNumber = micr.GetRoutingNumber(); + + // Check Detail Record Type (25) + var detail = new Records.X937.CheckDetail + { + AuxiliaryOnUs = micr.GetAuxOnUs(), + ExternalProcessingCode = micr.GetExternalProcessingCode(), + PayorBankRoutingNumber = transactionRoutingNumber.Substring(0, 8), + PayorBankRoutingNumberCheckDigit = transactionRoutingNumber.Substring(8, 1), + OnUs = string.Format("{0}/{1}", micr.GetAccountNumber(), micr.GetCheckNumber()), + ItemAmount = transaction.TotalAmount, + ClientInstitutionItemSequenceNumber = transaction.Id.ToString(), + DocumentationTypeIndicator = "G", // Field Value must be "G" - Meaning there are 2 images present + ElectronicReturnAcceptanceIndicator = string.Empty, + MICRValidIndicator = null, + BankOfFirstDepositIndicator = "Y", + CheckDetailRecordAddendumCount = 00, // From Veronica + CorrectionIndicator = string.Empty, + ArchiveTypeIndicator = string.Empty + + }; + + return new List { detail }; + } + + /// + /// Gets the image record for a specific transaction image (type 50 and 52). + /// + protected override List GetImageRecords(ExportOptions options, FinancialTransaction transaction, FinancialTransactionImage image, bool isFront) + { + var institutionRoutingNumber = Rock.Security.Encryption.DecryptString(GetAttributeValue(options.FileFormat, "InstitutionRoutingNumber")); + var records = base.GetImageRecords(options, transaction, image, isFront); + + foreach (var imageData in records.Where(r => r.RecordType == 52).Cast()) + { + imageData.InstitutionRoutingNumber = institutionRoutingNumber; + } + + foreach (var imageData in records.Where(r => r.RecordType == 50).Cast()) + { + imageData.ImageCreatorRoutingNumber = institutionRoutingNumber; + } + + return records; + } + + /// + /// Gets the credit detail deposit record (type 61). + /// + protected override List GetCreditDetailRecords(ExportOptions options, int bundleIndex, List transactions) + { + var accountNumber = Rock.Security.Encryption.DecryptString(GetAttributeValue(options.FileFormat, "AccountNumber")); + var routingNumber = Rock.Security.Encryption.DecryptString(GetAttributeValue(options.FileFormat, "RoutingNumber")); + var destinationRoutingNumber = Rock.Security.Encryption.DecryptString(GetAttributeValue(options.FileFormat, "DestinationRoutingNumber")); + var records = new List(); + + var creditDetail = new CreditDetail + { + PayorRoutingNumber = routingNumber, + CreditAccountNumber = accountNumber + "/", + Amount = transactions.Sum(t => t.TotalAmount), + InstitutionItemSequenceNumber = GetNextItemSequenceNumber().ToString("000000000000000"), + DocumentTypeIndicator = "G", + SourceOfWorkCode = "3", + DebitCreditIndicator = "2" + }; + records.Add(creditDetail); + + for (int i = 0; i < 2; i++) + { + using (var ms = GetDepositSlipImage(options, creditDetail, i == 0)) + { + var tiffImageBytes = ConvertImageToTiffG4(ms).ReadBytesToEnd(); + // + // Get the Image View Detail record (type 50) + // + var detail = new ImageViewDetail + { + ImageIndicator = 1, + ImageCreatorRoutingNumber = destinationRoutingNumber, + ImageCreatorDate = options.ExportDateTime, + ImageViewFormatIndicator = 0, + DataSize = (int)tiffImageBytes.Length, + CompressionAlgorithmIdentifier = 0, + SideIndicator = i, + ViewDescriptor = 0, + DigitalSignatureIndicator = 0 + }; + + // + // Get the Image View Data record (type 52). + // + var data = new ImageViewData + { + InstitutionRoutingNumber = routingNumber, + CycleNumber = string.Empty, + BundleBusinessDate = options.BusinessDateTime, + ClientInstitutionItemSequenceNumber = creditDetail.InstitutionItemSequenceNumber, + ClippingOrigin = 0, + ImageData = ms.ReadBytesToEnd() + }; + + records.Add(detail); + records.Add(data); + } + } + + return records; + } + + /// + /// Gets the bundle control record (type 70). + /// + + protected override Records.X937.BundleControl GetBundleControl(ExportOptions options, List records) + { + var itemRecords = records.Where(r => r.RecordType == 25); + + // If we are including the credit items then we need to count those as well. + if (this.countDepositSlip) + { + itemRecords = records.Where(r => r.RecordType == 25 && r.RecordType == 61); // Just Overwrite + } + + var checkDetailRecords = records.Where(r => r.RecordType == 25).Cast(); // Only count checks and not the credit detail + var imageDetailRecords = records.Where(r => r.RecordType == 52); + + // Record Type 70 + var control = new Records.X937.BundleControl + { + ItemCount = itemRecords.Count(), + TotalAmount = checkDetailRecords.Sum(r => (decimal)r.ItemAmount), + MICRValidTotalAmount = 000000000000, //checkDetailRecords.Sum(r => (decimal)r.ItemAmount), + ImageCount = imageDetailRecords.Count() + }; + + return control; + } + + /// + /// Gets the cash letter control record (type 90). + /// + /// Export options to be used by the component. + /// Existing records in the cash letter. + /// A CashLetterControl record. + protected override Records.X937.CashLetterControl GetCashLetterControlRecord(ExportOptions options, List records) + { + var bundleHeaderRecords = records.Where(r => r.RecordType == 20); + var checkDetailRecords = records.Where(r => r.RecordType == 25).Cast(); + var itemRecords = records.Where(r => r.RecordType == 25); // Only count record types 25. + var imageDetailRecords = records.Where(r => r.RecordType == 52); + var organizationName = GetAttributeValue(options.FileFormat, "OriginName"); + + // Some banks *might* include the deposit slip + if (this.countDepositSlip) + { + itemRecords = records.Where(r => r.RecordType == 25 && r.RecordType == 61); // Just Overwrite. + } + + // Record Type 90 + var control = new Records.X937.CashLetterControl + { + BundleCount = bundleHeaderRecords.Count(), + ItemCount = itemRecords.Count(), + //TotalAmount = 000000000000,//checkDetailRecords.Sum(c => (decimal)c.ItemAmount), // Must be 0's According to Veronica, This should now be the total amount + TotalAmount = checkDetailRecords.Sum(c => (decimal)c.ItemAmount), // According to Veronica 09-28-2018 + ImageCount = imageDetailRecords.Count(), + ECEInstitutionName = organizationName + }; + + return control; + } + + /// + /// Gets the cash letter control record (type 99). + /// + protected override FileControl GetFileControlRecord(ExportOptions options, List records) + { + var fileControl = base.GetFileControlRecord(options, records); + var itemRecords = records.Where(r => r.RecordType == 25); // Only count record types 25. + // Some banks *might* include the deposit slip + if (this.countDepositSlip) + { + itemRecords = records.Where(r => r.RecordType == 25 && r.RecordType == 61); // Just Overwrite. + } + + fileControl.ImmediateOriginContactName = "".PadRight(14, ' '); + fileControl.ImmediateOriginContactPhoneNumber = "0".PadRight(10, ' '); + fileControl.TotalItemCount = itemRecords.Count(); + + return fileControl; + } + + #region Helper Methods + protected int GetNextItemSequenceNumber() + { + int lastSequence = GetSystemSetting(LastItemSequenceNumberKey).AsIntegerOrNull() ?? 0; + int nextSequence = lastSequence + 1; + + SetSystemSetting(LastItemSequenceNumberKey, nextSequence.ToString()); + + return nextSequence; + } + + /// + /// Gets the credit detail deposit record (type 61). + /// + /// A stream that contains the image data in TIFF 6.0 CCITT Group 4 format. + protected virtual Stream GetDepositSlipImage(ExportOptions options, CreditDetail creditDetail, bool isFrontSide) + { + var bitmap = new System.Drawing.Bitmap(1200, 550); + var g = System.Drawing.Graphics.FromImage(bitmap); + + var depositSlipTemplate = GetAttributeValue(options.FileFormat, "DepositSlipTemplate"); + var mergeFields = new Dictionary + { + { "FileFormat", options.FileFormat }, + { "Amount", creditDetail.Amount.ToString( "C" ) } + }; + var depositSlipText = depositSlipTemplate.ResolveMergeFields(mergeFields); + + // + // Ensure we are opague with white. + // + g.FillRectangle(System.Drawing.Brushes.White, new System.Drawing.Rectangle(0, 0, 1200, 550)); + + if (isFrontSide) + { + g.DrawString(depositSlipText, + new System.Drawing.Font("Tahoma", 30), + System.Drawing.Brushes.Black, + new System.Drawing.PointF(50, 50)); + } + + g.Flush(); + + // + // Ensure the DPI is correct. + // + bitmap.SetResolution(200, 200); + + // + // Compress using TIFF, CCITT Group 4 format. + // + var codecInfo = System.Drawing.Imaging.ImageCodecInfo.GetImageEncoders() + .Where(c => c.MimeType == "image/tiff") + .First(); + var parameters = new System.Drawing.Imaging.EncoderParameters(1); + parameters.Param[0] = new System.Drawing.Imaging.EncoderParameter(System.Drawing.Imaging.Encoder.Compression, (long)System.Drawing.Imaging.EncoderValue.CompressionCCITT4); + + var ms = new MemoryStream(); + bitmap.Save(ms, codecInfo, parameters); + ms.Position = 0; + + return ms; + } + #endregion + + + /// + /// Hashes the string with SHA256. + /// + protected string HashString(string contents) + { + byte[] byteContents = Encoding.Unicode.GetBytes(contents); + + var hash = new System.Security.Cryptography.SHA256CryptoServiceProvider().ComputeHash(byteContents); + + return string.Join("", hash.Select(b => b.ToString("x2")).ToArray()); + } + } +} diff --git a/com.bemaservices.RemoteCheckDeposit/com.bemaservices.RemoteCheckDeposit.csproj b/com.bemaservices.RemoteCheckDeposit/com.bemaservices.RemoteCheckDeposit.csproj index e43f91d..fa47600 100644 --- a/com.bemaservices.RemoteCheckDeposit/com.bemaservices.RemoteCheckDeposit.csproj +++ b/com.bemaservices.RemoteCheckDeposit/com.bemaservices.RemoteCheckDeposit.csproj @@ -1,5 +1,6 @@  + @@ -14,6 +15,8 @@ 512 true + + true @@ -36,11 +39,25 @@ $(SolutionDir)RockWeb\Bin\DotLiquid.dll - - $(SolutionDir)packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll + + ..\packages\EntityFramework.6.5.1\lib\net45\EntityFramework.dll + True - - $(SolutionDir)packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll + + ..\packages\EntityFramework.6.5.1\lib\net45\EntityFramework.SqlServer.dll + True + + + ..\packages\Microsoft.Data.Edm.5.8.4\lib\net40\Microsoft.Data.Edm.dll + True + + + ..\packages\Microsoft.Data.OData.5.8.4\lib\net40\Microsoft.Data.OData.dll + True + + + ..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll + True @@ -60,6 +77,10 @@ + + ..\packages\System.Spatial.5.8.4\lib\net40\System.Spatial.dll + True + @@ -92,6 +113,7 @@ + @@ -230,6 +252,9 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + +