static private ImportResults ProcessImportedData(ExternalBankData import, ProcessThreadArguments args) { FinancialAccount assetAccount = args.Account; FinancialAccount autoDepositAccount = args.Organization.FinancialAccounts.IncomeDonations; int autoDepositLimit = 1000; // TODO: this.CurrentOrganization.Parameters.AutoDonationLimit; ImportResults result = new ImportResults(); int count = 0; int progressUpdateInterval = import.Records.Length/40; if (progressUpdateInterval > 100) { progressUpdateInterval = 100; } foreach (ExternalBankDataRecord row in import.Records) { // Update progress. count++; if (progressUpdateInterval < 2 || count % progressUpdateInterval == 0) { int percent = (count*99)/import.Records.Length; GuidCache.Set(args.Guid + "-Progress", percent); } // Update high- and low-water marks. if (row.DateTime < result.EarliestTransaction) { result.EarliestTransaction = row.DateTime; } if (row.DateTime > result.LatestTransaction) { result.LatestTransaction = row.DateTime; } string importKey = row.ImportHash; Int64 amountCents = row.TransactionNetCents; if (amountCents == 0) // defensive programming - these _should_ be duplicated in the interpreter if no "fee" field { amountCents = row.TransactionGrossCents; } if (args.Organization.Identity == 1 && assetAccount.Identity == 1 && PilotInstallationIds.IsPilot(PilotInstallationIds.PiratePartySE)) { // This is an ugly-as-f**k hack that sorts under the category "just bring our pilots the f**k back to operational // status right f*****g now". // This code can and should be safely removed once the pilot's books are closed for 2014, which should be some time mid-2015. if (row.DateTime < new DateTime(2014,03,22)) { result.DuplicateTransactions++; continue; } } FinancialTransaction transaction = FinancialTransaction.ImportWithStub(args.Organization.Identity, row.DateTime, assetAccount.Identity, amountCents, row.Description, importKey, args.CurrentUser.Identity); if (transaction != null) { // The transaction was created. Examine if the autobook criteria are true. result.TransactionsImported++; FinancialAccounts accounts = FinancialAccounts.FromBankTransactionTag(row.Description); if (accounts.Count == 1) { // This is a labelled local donation. Geography geography = accounts[0].AssignedGeography; FinancialAccount localAccount = accounts[0]; transaction.AddRow(args.Organization.FinancialAccounts.IncomeDonations, -amountCents, args.CurrentUser); transaction.AddRow(args.Organization.FinancialAccounts.CostsLocalDonationTransfers, amountCents, args.CurrentUser); transaction.AddRow(localAccount, -amountCents, args.CurrentUser); PWEvents.CreateEvent(EventSource.PirateWeb, EventType.LocalDonationReceived, args.CurrentUser.Identity, args.Organization.Identity, geography.Identity, 0, transaction.Identity, localAccount.Identity.ToString()); } else if (row.Description.ToLowerInvariant().StartsWith(args.Organization.IncomingPaymentTag)) { // Check for previously imported payment group // TODO: MAKE FLEXIBLE - CALL PAYMENTREADERINTERFACE! // HACK HACK HACK HACK PaymentGroup group = PaymentGroup.FromTag(args.Organization, "SEBGM" + DateTime.Today.Year.ToString() + // TODO: Get tags from org row.Description.Substring(args.Organization.IncomingPaymentTag.Length).Trim()); if (group != null && group.Open) { // There was a previously imported and not yet closed payment group matching this transaction // Close the payment group and match the transaction against accounts receivable transaction.Dependency = group; group.Open = false; transaction.AddRow(args.Organization.FinancialAccounts.AssetsOutboundInvoices, -amountCents, args.CurrentUser); } } else if (amountCents < 0) { // Autowithdrawal mechanisms removed, condition kept because of downstream else-if conditions } else if (amountCents > 0) { if (row.FeeCents < 0) { // This is always an autodeposit, if there is a fee (which is never > 0.0) transaction.AddRow(args.Organization.FinancialAccounts.CostsBankFees, -row.FeeCents, args.CurrentUser); transaction.AddRow(autoDepositAccount, -row.TransactionGrossCents, args.CurrentUser); } else if (amountCents < autoDepositLimit * 100) { // Book against autoDeposit account. transaction.AddRow(autoDepositAccount, -amountCents, args.CurrentUser); } } } else { // Transaction was not imported; assume duplicate result.DuplicateTransactions++; } } // Import complete. Return true if the bookkeeping account matches the bank data. Int64 databaseAccountBalanceCents = assetAccount.BalanceTotalCents; // Subtract any transactions made after the most recent imported transaction. // This is necessary in case of Paypal and others which continuously feed the // bookkeeping account with new transactions; it will already have fed transactions // beyond the end-of-file. Int64 beyondEofCents = assetAccount.GetDeltaCents(result.LatestTransaction.AddSeconds(1), DateTime.Now.AddDays(2)); // Caution: the "AddSeconds(1)" is not foolproof, there may be other new txs on the same second. if (databaseAccountBalanceCents - beyondEofCents == import.LatestAccountBalanceCents) { Payouts.AutomatchAgainstUnbalancedTransactions(args.Organization); result.AccountBalanceMatchesBank = true; result.BalanceMismatchCents = 0; } else { result.AccountBalanceMatchesBank = false; result.BalanceMismatchCents = (databaseAccountBalanceCents - beyondEofCents) - import.LatestAccountBalanceCents; } result.CurrencyCode = args.Organization.Currency.Code; return result; }
private static void ProcessUploadThread(object args) { string guid = ((ProcessThreadArguments) args).Guid; Documents documents = Documents.RecentFromDescription(guid); if (documents.Count != 1) { return; // abort } Document uploadedDoc = documents[0]; FinancialAccount account = ((ProcessThreadArguments)args).Account; ExternalBankData externalData = new ExternalBankData(); externalData.Profile = ExternalBankDataProfile.FromIdentity(ExternalBankDataProfile.SESebId); // TODO: HACK HACK HACK HACK LOAD using (StreamReader reader = new StreamReader(StorageRoot + uploadedDoc.ServerFileName, Encoding.GetEncoding(1252))) { externalData.LoadData(reader, ((ProcessThreadArguments) args).Organization); } _staticDataLookup[guid + "FirstTx"] = externalData.Records[0].DateTime.ToLongDateString(); _staticDataLookup[guid + "LastTx"] = externalData.Records[externalData.Records.Length - 1].DateTime.ToLongDateString(); _staticDataLookup[guid + "TxCount"] = externalData.Records.Length.ToString("N0"); _staticDataLookup[guid + "Profile"] = externalData.Profile; _staticDataLookup[guid + "PercentRead"] = 1; DateTime timeWalker = externalData.Records[0].DateTime; int currentRecordIndex = 0; // Walk past the first identical time records, and verify that we have at least one balance record that matches // our own for this timestamp. There may be several transactions in the master file, but at least one should have // a post-transaction balance that matches our records, or something is much more broken. long swarmopsCentsStart = account.GetDeltaCents(DateTime.MinValue, timeWalker.AddSeconds(1)); // At least one of the transactions for this timestamp should match. bool foundMatch = false; while (externalData.Records [currentRecordIndex].DateTime == timeWalker) { if (externalData.Records[currentRecordIndex].AccountBalanceCents == swarmopsCentsStart) { foundMatch = true; // continue loop until on first record past initial conditions } currentRecordIndex++; // no test for array overrun in while } if (!foundMatch) { throw new InvalidOperationException("Unable to locate stable initial conditions for resynchronization"); } // From here on out, every new timestamp should have the exact same delta in Swarmops as it has in the master file. List<ExternalBankMismatchingDateTime> mismatchList = new List<ExternalBankMismatchingDateTime>(); while (currentRecordIndex < externalData.Records.Length) { DateTime lastTimestamp = timeWalker; timeWalker = externalData.Records[currentRecordIndex].DateTime; long swarmopsDeltaCents = account.GetDeltaCents(lastTimestamp.AddSeconds(1), timeWalker.AddSeconds(1)); // "AddSeconds" because DeltaCents operates on ">= lowbound, < highbound" int timestampStartIndex = currentRecordIndex; long masterDeltaCents = externalData.Records[currentRecordIndex++].TransactionNetCents; int masterTransactionCount = 1; while (currentRecordIndex < externalData.Records.Length && externalData.Records[currentRecordIndex].DateTime == timeWalker) { masterDeltaCents += externalData.Records[currentRecordIndex++].TransactionNetCents; masterTransactionCount++; } if (masterDeltaCents != swarmopsDeltaCents) { // We have a mismatch. Add it to the list. ExternalBankMismatchingDateTime newMismatch = new ExternalBankMismatchingDateTime(); newMismatch.DateTime = timeWalker; newMismatch.MasterDeltaCents = masterDeltaCents; newMismatch.MasterTransactionCount = masterTransactionCount; newMismatch.SwarmopsDeltaCents = swarmopsDeltaCents; newMismatch.SwarmopsTransactionCount = 0; // TODO // Load transactions from both sources. First, create the interim construction object. ExternalBankMismatchConstruction mismatchConstruction = new ExternalBankMismatchConstruction(); // Load from Master for (int innerRecordIndex = timestampStartIndex; innerRecordIndex < currentRecordIndex; innerRecordIndex++) { string description = externalData.Records[innerRecordIndex].Description.Replace(" ", " "); if (!mismatchConstruction.Master.ContainsKey(description)) { mismatchConstruction.Master[description] = new ExternalBankMismatchingRecordConstruction(); } mismatchConstruction.Master[description].Cents.Add(externalData.Records[innerRecordIndex].TransactionNetCents); mismatchConstruction.Master[description].Transactions.Add(null); // no dependencies on the master side, only on swarmops side } // Load from Swarmops FinancialAccountRows swarmopsTransactionRows = account.GetRowsFar(lastTimestamp, timeWalker); // the "select far" is a boundary < x <= boundary selector. Default is boundary <= x < boundary. Dictionary<int, FinancialTransaction> lookupTransactions = new Dictionary<int, FinancialTransaction>(); // note all transaction IDs, then sum up per transaction foreach (FinancialAccountRow swarmopsTransactionRow in swarmopsTransactionRows) { lookupTransactions[swarmopsTransactionRow.FinancialTransactionId] = swarmopsTransactionRow.Transaction; } foreach (FinancialTransaction transaction in lookupTransactions.Values) { string description = transaction.Description.Replace(" ", " "); // for legacy compatibility with new importer if (!mismatchConstruction.Swarmops.ContainsKey(description)) { mismatchConstruction.Swarmops[description] = new ExternalBankMismatchingRecordConstruction(); } long cents = transaction[account]; if (cents != 0) // only add nonzero records { mismatchConstruction.Swarmops[description].Cents.Add(transaction[account]); mismatchConstruction.Swarmops[description].Transactions.Add(transaction); } } // Then, parse the intermediate construction object to the presentation-and-action object. Dictionary <string,ExternalBankMismatchingRecordDescription> mismatchingRecordList = new Dictionary<string, ExternalBankMismatchingRecordDescription>(); foreach (string masterKey in mismatchConstruction.Master.Keys) { Dictionary<int, bool> checkMasterIndex = new Dictionary<int, bool>(); Dictionary<int, bool> checkSwarmopsIndex = new Dictionary<int, bool>(); // For each key and entry for each key; // 1) locate an exact corresponding amount in swarmops records and log; failing that, // 2) if exactly one record left in master and swarmops records, log; failing that, // 3) log the rest of the master OR rest of swarmops records with no corresponding // equivalent with counterpart. (May produce bad results if 2 consistent mismatches // for every description.) ExternalBankMismatchingRecordDescription newRecord = new ExternalBankMismatchingRecordDescription(); newRecord.Description = masterKey; List<long> masterCentsList = new List<long>(); List<long> swarmopsCentsList = new List<long>(); List<object> dependenciesList = new List<object>(); List<FinancialTransaction> transactionsList = new List<FinancialTransaction>(); List<ExternalBankMismatchResyncAction> actionsList = new List<ExternalBankMismatchResyncAction>(); // STEP 1 - locate all identical matches if (mismatchConstruction.Swarmops.ContainsKey(masterKey)) { for (int masterIndex = 0; masterIndex < mismatchConstruction.Master[masterKey].Cents.Count; masterIndex++) { // no "continue" necessary on first run-through; nothing has been checked off yet long findMasterCents = mismatchConstruction.Master[masterKey].Cents[masterIndex]; for (int swarmopsIndex = 0; swarmopsIndex < mismatchConstruction.Swarmops[masterKey].Cents.Count; swarmopsIndex++) { if (checkSwarmopsIndex.ContainsKey(swarmopsIndex)) { continue; // may have been checked off already in the rare case of twin identical amounts } if (findMasterCents == mismatchConstruction.Swarmops[masterKey].Cents[swarmopsIndex]) { // There is a match as per case 1. Record both, mark both as used, continue. masterCentsList.Add(findMasterCents); swarmopsCentsList.Add( mismatchConstruction.Swarmops[masterKey].Cents[swarmopsIndex]); // should be equal, we're defensive here transactionsList.Add( mismatchConstruction.Swarmops[masterKey].Transactions[swarmopsIndex]); dependenciesList.Add( mismatchConstruction.Swarmops[masterKey].Transactions[swarmopsIndex].Dependency); actionsList.Add(ExternalBankMismatchResyncAction.NoAction); checkMasterIndex[masterIndex] = true; checkSwarmopsIndex[swarmopsIndex] = true; break; } } } } // STEP 2 - if exactly one record left on both sides, connect and log as mismatching record // TODO: improve logic to handle same number of records left on both sides if (mismatchConstruction.Swarmops.ContainsKey(masterKey) && mismatchConstruction.Master[masterKey].Cents.Count - checkMasterIndex.Keys.Count == 1 && mismatchConstruction.Swarmops [masterKey].Cents.Count - checkSwarmopsIndex.Keys.Count == 1) { for (int masterIndex = 0; masterIndex < mismatchConstruction.Master[masterKey].Cents.Count; masterIndex++) { if (checkMasterIndex.ContainsKey(masterIndex)) { continue; // This will fire for all but one indexes } long findMasterCents = mismatchConstruction.Master[masterKey].Cents[masterIndex]; for (int swarmopsIndex = 0; swarmopsIndex < mismatchConstruction.Swarmops[masterKey].Cents.Count; swarmopsIndex++) { if (checkSwarmopsIndex.ContainsKey(swarmopsIndex)) { continue; } masterCentsList.Add(findMasterCents); swarmopsCentsList.Add(mismatchConstruction.Swarmops[masterKey].Cents[swarmopsIndex]); dependenciesList.Add(mismatchConstruction.Swarmops[masterKey].Transactions[swarmopsIndex].Dependency); transactionsList.Add(mismatchConstruction.Swarmops[masterKey].Transactions[swarmopsIndex]); actionsList.Add(ExternalBankMismatchResyncAction.RewriteSwarmops); checkMasterIndex[masterIndex] = true; checkSwarmopsIndex[swarmopsIndex] = true; } } } // STEP 3 - log remaining records on both sides as missing counterparts. Only one of these should fire. // STEP 3a - log remaining on Master side if (mismatchConstruction.Master[masterKey].Cents.Count > checkMasterIndex.Keys.Count) { for (int masterIndex = 0; masterIndex < mismatchConstruction.Master[masterKey].Cents.Count; masterIndex++) { if (checkMasterIndex.ContainsKey(masterIndex)) { continue; } masterCentsList.Add(mismatchConstruction.Master[masterKey].Cents[masterIndex]); swarmopsCentsList.Add(0); // null equivalent; invalid value dependenciesList.Add(null); transactionsList.Add(null); actionsList.Add(ExternalBankMismatchResyncAction.CreateSwarmops); checkMasterIndex[masterIndex] = true; } } // STEP 3b - log remaining on Swarmops side if (mismatchConstruction.Swarmops.ContainsKey(masterKey) && mismatchConstruction.Swarmops[masterKey].Cents.Count > checkSwarmopsIndex.Keys.Count) { for (int swarmopsIndex = 0; swarmopsIndex < mismatchConstruction.Swarmops[masterKey].Cents.Count; swarmopsIndex++) { if (checkSwarmopsIndex.ContainsKey(swarmopsIndex)) { continue; } masterCentsList.Add(0); // null equivalent; invalid value swarmopsCentsList.Add(mismatchConstruction.Swarmops[masterKey].Cents[swarmopsIndex]); transactionsList.Add(mismatchConstruction.Swarmops[masterKey].Transactions[swarmopsIndex]); if (mismatchConstruction.Swarmops[masterKey].Transactions[swarmopsIndex].Dependency != null) { dependenciesList.Add( mismatchConstruction.Swarmops[masterKey].Transactions[swarmopsIndex].Dependency); actionsList.Add(ExternalBankMismatchResyncAction.ManualAction); // can't auto } else { dependenciesList.Add(null); actionsList.Add(ExternalBankMismatchResyncAction.DeleteSwarmops); } checkMasterIndex[swarmopsIndex] = true; } } newRecord.MasterCents = masterCentsList.ToArray(); newRecord.SwarmopsCents = swarmopsCentsList.ToArray(); newRecord.ResyncActions = actionsList.ToArray(); // newRecord.TransactionDependencies = dependenciesList.ToArray(); newRecord.Transactions = transactionsList.ToArray(); mismatchingRecordList[masterKey] = newRecord; } // Finally, add the transactions that were (described) in Swarmops but not in Master foreach (string swarmopsKey in mismatchConstruction.Swarmops.Keys) { if (!mismatchingRecordList.ContainsKey(swarmopsKey)) { mismatchingRecordList[swarmopsKey] = new ExternalBankMismatchingRecordDescription(); mismatchingRecordList[swarmopsKey].Description = swarmopsKey; mismatchingRecordList[swarmopsKey].SwarmopsCents = mismatchConstruction.Swarmops[swarmopsKey].Cents.ToArray(); mismatchingRecordList[swarmopsKey].Transactions = mismatchConstruction.Swarmops[swarmopsKey].Transactions.ToArray(); mismatchingRecordList[swarmopsKey].MasterCents = new long[mismatchConstruction.Swarmops[swarmopsKey].Cents.Count]; // inits to zero mismatchingRecordList[swarmopsKey].ResyncActions = new ExternalBankMismatchResyncAction[mismatchConstruction.Swarmops[swarmopsKey].Cents.Count]; for (int index = 0; index < mismatchingRecordList[swarmopsKey].ResyncActions.Length; index++) { mismatchingRecordList[swarmopsKey].ResyncActions[index] = ExternalBankMismatchResyncAction.DeleteSwarmops; } } } newMismatch.MismatchingRecords = mismatchingRecordList.Values.ToArray(); mismatchList.Add(newMismatch); } int percentProcessed = (int) (currentRecordIndex*100L/externalData.Records.Length); lock (_staticDataLookup) { if (percentProcessed > 1) { _staticDataLookup[guid + "PercentRead"] = percentProcessed; // for the progress bar to update async } if (percentProcessed > 99) { // Placed inside loop to have a contiguous lock block, even though it decreases performance. // Should normally be placed just outside. _staticDataLookup[guid + "MismatchArray"] = mismatchList.ToArray(); _staticDataLookup[guid + "Account"] = account; } } } }
private static void ProcessUploadThread(object args) { string guid = ((ProcessThreadArguments) args).Guid; BankFileType fileType = ((ProcessThreadArguments) args).FileType; Documents documents = Documents.RecentFromDescription(guid); GuidCache.Set(guid + "-Result", ImportResultsCategory.Bad); // default - this is what happens if exception if (documents.Count != 1) { return; // abort } Document uploadedDoc = documents[0]; try { FinancialAccount account = ((ProcessThreadArguments) args).Account; ExternalBankData externalData = new ExternalBankData(); externalData.Profile = account.ExternalBankDataProfile; if (fileType == BankFileType.AccountStatement) { using (StreamReader reader = uploadedDoc.GetReader(1252)) { externalData.LoadData(reader, ((ProcessThreadArguments) args).Organization); // catch here and set result to BAD ImportResults results = ProcessImportedData(externalData, (ProcessThreadArguments) args); GuidCache.Set(guid + "-ResultDetails", results); if (results.AccountBalanceMatchesBank) { GuidCache.Set(guid + "-Result", ImportResultsCategory.Good); } else { GuidCache.Set(guid + "-Result", ImportResultsCategory.Questionable); } } } else if (fileType == BankFileType.PaymentDetails) { // Get reader factory from ExternalBankData throw new NotImplementedException("Need to implement new flexible payment reader structure"); // IBankDataPaymentsReader paymentsReader = externalData.GetPaymentsReader(); // then read } } catch (Exception e) { GuidCache.Set(guid + "-Exception", e.ToString()); } finally { GuidCache.Set(guid + "-Progress", 100); // only here may the caller fetch the results uploadedDoc.Delete(); // document no longer needed after processing, no matter the result } }