private FinancialTransactionRow AddRow(int financialAccountId, Int64 amountCents, int personId)
        {
            // private function that actually executes the row adding

            FinancialAccount account = FinancialAccount.FromIdentity(financialAccountId);

            if (DateTime.Year <= account.Organization.Parameters.FiscalBooksClosedUntilYear)
            {
                // Recurse down into continuation transactions to write row in first nonclosed year

                FinancialTransactionRow newRow = null;

                FinancialTransaction transactionContinued = ContinuedTransaction;

                if (transactionContinued == null)
                {
                    // No continuation; create one

                    transactionContinued = Create(OrganizationId, DateTime.Now,
                                                  "Continued Tx #" + Identity);
                    newRow = transactionContinued.AddRow(financialAccountId, amountCents, personId);
                    transactionContinued.Dependency = this;
                }
                else
                {
                    // Recurse

                    newRow = transactionContinued.AddRow(financialAccountId, amountCents, personId);
                }

                return(newRow);
            }

            FinancialTransactionRow addedRow = FinancialTransactionRow.FromIdentityAggressive(SwarmDb.GetDatabaseForWriting()
                                                                                              .CreateFinancialTransactionRow(Identity, financialAccountId, amountCents, personId));

            // If we're running from web, and this was a P&L account, then also notify the server that the P&L has changed
            // (doing this here means that the server can get pinged multiple times, but that's more defensive coding than
            // having to remember doing it everywhere at the UI level)

            if (account.AccountType == FinancialAccountType.Income || account.AccountType == FinancialAccountType.Cost)
            {
                if (SupportFunctions.OperatingTopology == OperatingTopology.FrontendWeb)
                {
                    SocketMessage newMessage = new SocketMessage
                    {
                        MessageType            = "ProfitLossChanged",
                        OrganizationId         = account.Organization.Identity,
                        FinancialTransactionId = this.Identity
                    };

                    newMessage.SendUpstream();
                }
            }

            return(addedRow);
        }
        public bool RecalculateTransaction(Dictionary <int, Int64> nominalTransaction, Person loggingPerson)
        {
            bool changedTransaction = false;

            // We need to create a delta. This is... somewhat complicated.

            // 1) Iterate over the rows to build a "current" transaction record.
            // 2) Create a "should-look-like" transaction record. (done in calling routine, already).
            // 3) Apply the delta, in two steps.

            Dictionary <int, Int64> currentTransaction = GetRecalculationBase();

            FinancialTransaction continuedTransaction = ContinuedTransaction;

            if (continuedTransaction != null)
            {
                continuedTransaction.AddContinuedTransactionsToLookup(currentTransaction);
                // Recurses to all continued transactions
            }

            // Step 2: create an image of what the transaction SHOULD look like with changes.
            //         now done in calling routine.

            // Step 3a: For all accounts existing in Current but not in Nominal, set them to 0, and
            // vice versa.

            foreach (int accountId in currentTransaction.Keys)
            {
                if (!nominalTransaction.ContainsKey(accountId))
                {
                    nominalTransaction[accountId] = 0;
                }
            }

            foreach (int accountId in nominalTransaction.Keys)
            {
                if (!currentTransaction.ContainsKey(accountId))
                {
                    currentTransaction[accountId] = 0;
                }
            }

            // Step 3b: Iterate over all accounts in the two sets -- which now has the same keys --
            // and apply the delta to the transaction.

            foreach (int accountId in currentTransaction.Keys)
            {
                if (currentTransaction[accountId] != nominalTransaction[accountId])
                {
                    AddRow(accountId, nominalTransaction[accountId] - currentTransaction[accountId],
                           loggingPerson == null ? 0 : loggingPerson.Identity);
                    changedTransaction = true;
                }
            }

            return(changedTransaction);
        }
Example #3
0
        public void AddItem(FinancialTransaction transaction, Int64 turnoverCents,
                            Int64 vatInboundCents, Int64 vatOutboundCents)
        {
            if (!UnderConstruction)
            {
                throw new InvalidOperationException("Cannot add items once the report is released");
            }

            VatReportItem.Create(this, transaction, turnoverCents, vatInboundCents, vatOutboundCents);
        }
Example #4
0
        public void BindToTransactionAndClose(FinancialTransaction transaction, Person bindingPerson)
        {
            Dictionary <int, Int64> accountDebitLookup = new Dictionary <int, Int64>();
            Organization            organization       = Organization;

            accountDebitLookup[organization.FinancialAccounts.DebtsExpenseClaims.Identity]            = 0;
            accountDebitLookup[organization.FinancialAccounts.DebtsInboundInvoices.Identity]          = 0;
            accountDebitLookup[organization.FinancialAccounts.DebtsSalary.Identity]                   = 0;
            accountDebitLookup[organization.FinancialAccounts.DebtsTax.Identity]                      = 0;
            accountDebitLookup[organization.FinancialAccounts.AssetsOutstandingCashAdvances.Identity] = 0;

            if (this.DependentExpenseClaims.Count > 0)
            {
                accountDebitLookup[organization.FinancialAccounts.DebtsExpenseClaims.Identity] +=
                    this.DependentExpenseClaims.TotalAmountCents;
            }
            if (this.DependentInvoices.Count > 0)
            {
                accountDebitLookup[organization.FinancialAccounts.DebtsInboundInvoices.Identity] +=
                    this.DependentInvoices.TotalAmountCents;
            }
            if (this.DependentSalariesNet.Count > 0)
            {
                accountDebitLookup[organization.FinancialAccounts.DebtsSalary.Identity] +=
                    this.DependentSalariesNet.TotalAmountCentsNet;
            }
            if (this.DependentSalariesTax.Count > 0)
            {
                accountDebitLookup[organization.FinancialAccounts.DebtsTax.Identity] +=
                    this.DependentSalariesTax.TotalAmountCentsTax;
            }
            if (this.DependentCashAdvancesPayout.Count > 0)
            {
                accountDebitLookup[organization.FinancialAccounts.AssetsOutstandingCashAdvances.Identity] +=
                    this.DependentCashAdvancesPayout.TotalAmountCents;
            }
            if (this.DependentCashAdvancesPayback.Count > 0)
            {
                accountDebitLookup[organization.FinancialAccounts.AssetsOutstandingCashAdvances.Identity] -=
                    // observe the minus
                    this.DependentCashAdvancesPayback.TotalAmountCents;
            }

            foreach (int financialAccountId in accountDebitLookup.Keys)
            {
                if (accountDebitLookup[financialAccountId] != 0)
                {
                    transaction.AddRow(FinancialAccount.FromIdentity(financialAccountId),
                                       accountDebitLookup[financialAccountId], bindingPerson);
                }
            }

            transaction.Dependency = this;
            Open = false;
        }
        public static FinancialTransaction Create(int organizationId, DateTime dateTime, string description)
        {
            int transactionId = SwarmDb.GetDatabaseForWriting()
                                .CreateFinancialTransaction(organizationId, dateTime, description);

            FinancialTransaction newTx = FromIdentityAggressive(transactionId);

            newTx.SetOrganizationSequenceId();

            return(newTx);
        }
Example #6
0
        public static FinancialTransactionTagTypes ForTransaction(FinancialTransaction transaction)
        {
            int[] tagTypeIdentities = SwarmDb.GetDatabaseForReading().GetFinancialTransactionTagTypes(transaction.Identity);

            if (tagTypeIdentities.Length == 0)
            {
                return(new FinancialTransactionTagTypes()); // none, so return empty set
            }

            return(FromIdentities(tagTypeIdentities));
        }
Example #7
0
        public static InboundInvoice Create(Organization organization, DateTime dueDate, Int64 amountCents,
                                            Int64 vatCents, FinancialAccount budget, string supplier, string description, string payToAccount, string ocr,
                                            string invoiceReference, Person creatingPerson)
        {
            InboundInvoice newInvoice = FromIdentity(SwarmDb.GetDatabaseForWriting().
                                                     CreateInboundInvoice(organization.Identity, dueDate, budget.Identity,
                                                                          supplier, payToAccount, ocr,
                                                                          invoiceReference, amountCents, creatingPerson.Identity));

            newInvoice.Description = description; // Not in original schema; not cause for schema update

            if (vatCents > 0)
            {
                newInvoice.VatCents = vatCents;
            }

            // Create a corresponding financial transaction with rows

            FinancialTransaction transaction =
                FinancialTransaction.Create(organization.Identity, DateTime.UtcNow,
                                            "Invoice #" + newInvoice.OrganizationSequenceId + " from " + supplier);

            transaction.AddRow(organization.FinancialAccounts.DebtsInboundInvoices, -amountCents, creatingPerson);
            if (vatCents > 0)
            {
                transaction.AddRow(organization.FinancialAccounts.AssetsVatInboundUnreported, vatCents,
                                   creatingPerson);
                transaction.AddRow(budget, amountCents - vatCents, creatingPerson);
            }
            else
            {
                transaction.AddRow(budget, amountCents, creatingPerson);
            }


            // Make the transaction dependent on the inbound invoice

            transaction.Dependency = newInvoice;

            // Create notification (slightly misplaced logic, but this is failsafest place)

            OutboundComm.CreateNotificationApprovalNeeded(budget, creatingPerson, supplier, (amountCents - vatCents) / 100.0,
                                                          description, NotificationResource.InboundInvoice_Created);
            // Slightly misplaced logic, but failsafer here
            SwarmopsLogEntry.Create(creatingPerson,
                                    new InboundInvoiceCreatedLogEntry(creatingPerson, supplier, description, amountCents / 100.0, budget),
                                    newInvoice);

            // Clear a cache
            FinancialAccount.ClearApprovalAdjustmentsCache(organization);

            return(newInvoice);
        }
Example #8
0
        private void UpdateTransaction(Person updatingPerson)
        {
            Dictionary <int, Int64> nominalTransaction = new Dictionary <int, Int64>();

            // Create an image of what the transaction SHOULD look like with changes.

            if (Attested || Open)
            {
                // ...only holds values if not closed as invalid...

                nominalTransaction[Organization.FinancialAccounts.DebtsInboundInvoices.Identity] = -AmountCents;
                nominalTransaction[BudgetId] = AmountCents;
            }

            FinancialTransaction.RecalculateTransaction(nominalTransaction, updatingPerson);
        }
Example #9
0
        public void DenyAttestation(Person denyingPerson, string reason)
        {
            this.Attested = false;
            this.Open     = false;

            SwarmDb.GetDatabaseForWriting().CreateFinancialValidation(FinancialValidationType.Kill,
                                                                      FinancialDependencyType.ExpenseClaim, Identity,
                                                                      DateTime.UtcNow, denyingPerson.Identity, this.CostTotalCents);

            OutboundComm.CreateNotificationOfFinancialValidation(Budget, this.PayrollItem.Person, NetSalaryCents / 100.0, this.PayoutDate.ToString("MMMM yyyy"),
                                                                 NotificationResource.Salary_Denied, reason);

            FinancialTransaction transaction = FinancialTransaction.FromDependency(this);

            transaction.RecalculateTransaction(new Dictionary <int, long>(), denyingPerson); // zeroes out the tx
        }
        public static FinancialTransaction ImportWithStub(int organizationId, DateTime dateTime, int financialAccountId,
                                                          Int64 amountCents, string description, string importHash, int personId)
        {
            int transactionId = SwarmDb.GetDatabaseForWriting()
                                .CreateFinancialTransactionStub(organizationId, dateTime,
                                                                financialAccountId, amountCents,
                                                                description, importHash, personId);

            if (transactionId == 0)
            {
                return(null); // This was a dupe -- already imported, as determined by ImportHash
            }

            FinancialTransaction newTx = FromIdentityAggressive(transactionId);

            newTx.SetOrganizationSequenceId();

            return(newTx);
        }
        private void AddContinuedTransactionsToLookup(Dictionary <int, Int64> currentTransactionData)
        {
            FinancialTransaction continuedTransaction = ContinuedTransaction;

            if (continuedTransaction != null)
            {
                continuedTransaction.AddContinuedTransactionsToLookup(currentTransactionData);
            }

            foreach (FinancialTransactionRow row in Rows)
            {
                if (!currentTransactionData.ContainsKey(row.FinancialAccountId))
                {
                    currentTransactionData[row.FinancialAccountId] = 0;
                }

                currentTransactionData[row.FinancialAccountId] += row.AmountCents;
            }
        }
Example #12
0
        public static VatReportItem Create(VatReport report, FinancialTransaction transaction, Int64 turnoverCents,
                                           Int64 vatInboundCents, Int64 vatOutboundCents)
        {
            // Assumes there's a dependency of some sort

            IHasIdentity            foreignObject  = transaction.Dependency;
            FinancialDependencyType dependencyType =
                (foreignObject != null
                ? FinancialTransaction.GetFinancialDependencyType(transaction.Dependency)
                : FinancialDependencyType.Unknown);

            // The transaction dependency is stored for quick lookup; it duplicates information in the database
            // to save an expensive query as a mere optimization.

            int newVatReportItemId = SwarmDb.GetDatabaseForWriting()
                                     .CreateVatReportItem(report.Identity, transaction.Identity, foreignObject?.Identity ?? 0,
                                                          dependencyType, turnoverCents, vatInboundCents, vatOutboundCents);

            return(FromIdentityAggressive(newVatReportItemId));
        }
Example #13
0
        private void UpdateFinancialTransaction(Person updatingPerson)
        {
            Dictionary <int, Int64> nominalTransaction = new Dictionary <int, Int64>();

            int debtAccountId = Organization.FinancialAccounts.DebtsExpenseClaims.Identity;

            if (!Claimed)
            {
                debtAccountId = Organization.FinancialAccounts.CostsAllocatedFunds.Identity;
            }

            if (Validated || Open)
            {
                // ...only holds values if not closed as invalid...

                nominalTransaction[debtAccountId] = -AmountCents;
                nominalTransaction[BudgetId]      = AmountCents;
            }

            FinancialTransaction.RecalculateTransaction(nominalTransaction, updatingPerson);
        }
        public static FinancialTransaction FromBlockchainHash(Organization organization, string blockchainTransactionHash)
        {
            int[] transactionIds =
                SwarmDb.GetDatabaseForReading()
                .GetObjectsByOptionalData(ObjectType.FinancialTransaction,
                                          ObjectOptionalDataType.FinancialTransactionBlockchainHash, blockchainTransactionHash);

            // There may be multiple transactions in this Swarmops installation referring to this transaction on the blockchain, but only
            // one per organization. So find the transaction that matches the org we want.

            foreach (int transactionId in transactionIds)
            {
                FinancialTransaction potentialResult = FinancialTransaction.FromIdentity(transactionId);
                if (potentialResult.OrganizationId == organization.Identity)
                {
                    return(potentialResult);
                }
            }

            throw new ArgumentException("No match for supplied blockchain tx hash and organization");
        }
Example #15
0
        public static ExpenseClaim Create(Person claimer, Organization organization, FinancialAccount budget,
                                          DateTime expenseDate, string description, Int64 amountCents)
        {
            ExpenseClaim newClaim =
                FromIdentityAggressive(SwarmDb.GetDatabaseForWriting()
                                       .CreateExpenseClaim(claimer.Identity, organization.Identity,
                                                           budget.Identity, expenseDate, description, amountCents));
            // Create the financial transaction with rows

            string transactionDescription = "Expense #" + newClaim.Identity + ": " + description; // TODO: Localize

            if (transactionDescription.Length > 64)
            {
                transactionDescription = transactionDescription.Substring(0, 61) + "...";
            }

            FinancialTransaction transaction =
                FinancialTransaction.Create(organization.Identity, DateTime.Now,
                                            transactionDescription);

            transaction.AddRow(organization.FinancialAccounts.DebtsExpenseClaims, -amountCents, claimer);
            transaction.AddRow(budget, amountCents, claimer);

            // Make the transaction dependent on the expense claim

            transaction.Dependency = newClaim;

            // Create notifications

            OutboundComm.CreateNotificationAttestationNeeded(budget, claimer, string.Empty, amountCents / 100.0,
                                                             description, NotificationResource.ExpenseClaim_Created); // Slightly misplaced logic, but failsafer here
            OutboundComm.CreateNotificationFinancialValidationNeeded(organization, amountCents / 100.0,
                                                                     NotificationResource.Receipts_Filed);
            SwarmopsLogEntry.Create(claimer,
                                    new ExpenseClaimFiledLogEntry(claimer /*filing person*/, claimer /*beneficiary*/, amountCents / 100.0,
                                                                  budget, description), newClaim);

            return(newClaim);
        }
Example #16
0
        public void LogForexProfitLoss(Money accountBalanceInAnyCurrency)
        {
            Int64 currentNativeValueOfForeignCents = accountBalanceInAnyCurrency.ToCurrency(Organization.Currency).Cents;
            Int64 nativeCents = BalanceTotalCents;

            if (nativeCents - 100 > currentNativeValueOfForeignCents)
            {
                // log a loss

                Int64 lossCents             = nativeCents - currentNativeValueOfForeignCents;
                FinancialTransaction lossTx = FinancialTransaction.Create(Organization, DateTime.UtcNow, "Forex Loss");
                lossTx.AddRow(this, -lossCents, null);
                lossTx.AddRow(Organization.FinancialAccounts.CostsCurrencyFluctuations, lossCents, null);
            }
            else if (nativeCents + 100 < currentNativeValueOfForeignCents)
            {
                // log a profit

                Int64 profitCents             = currentNativeValueOfForeignCents - nativeCents;
                FinancialTransaction profitTx = FinancialTransaction.Create(Organization, DateTime.UtcNow, "Forex Gains");
                profitTx.AddRow(this, profitCents, null);
                profitTx.AddRow(Organization.FinancialAccounts.IncomeCurrencyFluctuations, -profitCents, null);
            }
        }
Example #17
0
        public void BindToTransactionAndClose (FinancialTransaction transaction, Person bindingPerson)
        {
            Dictionary<int, Int64> accountDebitLookup = new Dictionary<int, Int64>();
            Organization organization = this.Organization;

            accountDebitLookup[organization.FinancialAccounts.DebtsExpenseClaims.Identity] = 0;
            accountDebitLookup[organization.FinancialAccounts.DebtsInboundInvoices.Identity] = 0;
            accountDebitLookup[organization.FinancialAccounts.DebtsSalary.Identity] = 0;
            accountDebitLookup[organization.FinancialAccounts.DebtsTax.Identity] = 0;
            accountDebitLookup[organization.FinancialAccounts.AssetsOutstandingCashAdvances.Identity] = 0;

            if (this.DependentExpenseClaims.Count > 0)
            {
                accountDebitLookup[organization.FinancialAccounts.DebtsExpenseClaims.Identity] +=
                    this.DependentExpenseClaims.TotalAmountCents;
            }
            if (this.DependentInvoices.Count > 0)
            {
                accountDebitLookup[organization.FinancialAccounts.DebtsInboundInvoices.Identity] +=
                    this.DependentInvoices.TotalAmountCents;
            }
            if (this.DependentSalariesNet.Count > 0)
            {
                accountDebitLookup[organization.FinancialAccounts.DebtsSalary.Identity] +=
                    this.DependentSalariesNet.TotalAmountCentsNet;
            }
            if (this.DependentSalariesTax.Count > 0)
            {
                accountDebitLookup[organization.FinancialAccounts.DebtsTax.Identity] +=
                    this.DependentSalariesTax.TotalAmountCentsTax;
            }
            if (this.DependentCashAdvancesPayout.Count > 0)
            {
                accountDebitLookup[organization.FinancialAccounts.AssetsOutstandingCashAdvances.Identity] +=
                    this.DependentCashAdvancesPayout.TotalAmountCents;
            }
            if (this.DependentCashAdvancesPayback.Count > 0)
            {
                accountDebitLookup[organization.FinancialAccounts.AssetsOutstandingCashAdvances.Identity] -=  // observe the minus
                    this.DependentCashAdvancesPayback.TotalAmountCents;
            }

            foreach (int financialAccountId in accountDebitLookup.Keys)
            {
                if (accountDebitLookup[financialAccountId] != 0)
                {
                    transaction.AddRow(FinancialAccount.FromIdentity(financialAccountId),
                                       accountDebitLookup[financialAccountId], bindingPerson);
                }
            }

            transaction.Dependency = this;
            this.Open = false;
        }
Example #18
0
        public static void AutomatchAgainstUnbalancedTransactions(Organization organization)
        {
            // Matches unbalanced financial transactions against unclosed payouts

            // Should this be in bot?

            Payouts payouts = ForOrganization(organization);

            FinancialTransactions transactions = FinancialTransactions.GetUnbalanced(organization);

            foreach (FinancialTransaction transaction in transactions)
            {
                // Console.WriteLine("Looking at transaction #{0} ({1:yyyy-MM-dd}, {2:N2}).", transaction.Identity, transaction.DateTime, transaction.Rows.AmountTotal);

                // First, establish that there are no similar transactions within 7 days. N^2 search.

                DateTime timeLow  = transaction.DateTime.AddDays(-7);
                DateTime timeHigh = transaction.DateTime.AddDays(7);

                bool foundCompeting = false;

                foreach (FinancialTransaction possiblyCompetingTransaction in transactions)
                {
                    if (possiblyCompetingTransaction.Rows.AmountCentsTotal == transaction.Rows.AmountCentsTotal &&
                        possiblyCompetingTransaction.DateTime >= timeLow &&
                        possiblyCompetingTransaction.DateTime <= timeHigh &&
                        possiblyCompetingTransaction.Identity != transaction.Identity)
                    {
                        foundCompeting = true;
                        // Console.WriteLine(" - Transaction #{0} ({1:yyyy-MM-dd} is competing, aborting", possiblyCompetingTransaction.Identity, possiblyCompetingTransaction.DateTime);
                    }
                }

                if (foundCompeting)
                {
                    continue;
                }

                // Console.WriteLine(" - no competing transactions...\r\n - transaction description is \"{0}\".", transaction.Description);

                // Console.WriteLine(" - looking for matching payouts");

                int foundCount    = 0;
                int payoutIdFound = 0;

                // As the amount of payouts grow, this becomes less efficient exponentially.

                foreach (Payout payout in payouts)
                {
                    // Ugly hack to fix cash advance payouts

                    DateTime payoutLowerTimeLimit = timeLow;
                    DateTime payoutUpperTimeLimit = timeHigh;

                    if (payout.AmountCents == -transaction.Rows.AmountCentsTotal &&
                        (payout.DependentCashAdvancesPayout.Count > 0 || payout.DependentCashAdvancesPayback.Count > 0))
                    {
                        // HACK: While PW5 doesn't have a manual-debug interface, special case for cash advances

                        payoutLowerTimeLimit = transaction.DateTime.AddDays(-60);
                        payoutUpperTimeLimit = transaction.DateTime.AddDays(60);
                    }

                    // HACK: Allow for up to 20 days beyond scheduled payment to catch tax payments

                    if (payout.DependentSalariesTax.Count > 0)
                    {
                        payoutLowerTimeLimit = transaction.DateTime.AddDays(-25);
                        payoutUpperTimeLimit = transaction.DateTime.AddDays(3);  // nobody pays taxes early...
                    }

                    if (payout.ExpectedTransactionDate >= payoutLowerTimeLimit &&
                        payout.ExpectedTransactionDate <= payoutUpperTimeLimit &&
                        payout.AmountCents == -transaction.Rows.AmountCentsTotal)
                    {
                        // Console.WriteLine(" - - payout #{0} matches ({1}, {2:yyyy-MM-dd})", payout.Identity, payout.Recipient, payout.ExpectedTransactionDate);

                        try
                        {
                            // If this succeeds, there is a transaction already

                            FinancialTransaction.FromDependency(payout);

                            break;
                        }
                        catch (Exception)
                        {
                            // There isn't such a transaction, which is what we want
                        }

                        foundCount++;
                        payoutIdFound = payout.Identity;
                    }
                }

                if (foundCount == 0)
                {
                    // Console.WriteLine(" - none found");
                }
                else if (foundCount > 1)
                {
                    // Console.WriteLine(" - multiple found, not autoprocessing");
                }
                else
                {
                    Payout payout = Payout.FromIdentity(payoutIdFound);
                    payout.BindToTransactionAndClose(transaction, null);
                }
            }
        }
Example #19
0
        public static void PerformAutomated(BitcoinChain chain)
        {
            // Perform all waiting hot payouts for all orgs in the installation

            throw new NotImplementedException("Waiting for rewrite for Bitcoin Cash");

            // TODO

            DateTime utcNow = DateTime.UtcNow;

            foreach (Organization organization in Organizations.GetAll())
            {
                // If this org doesn't do hotwallet, continue
                if (organization.FinancialAccounts.AssetsBitcoinHot == null)
                {
                    continue;
                }

                Payouts orgPayouts     = Payouts.Construct(organization);
                Payouts bitcoinPayouts = new Payouts();
                Dictionary <string, Int64> satoshiPayoutLookup     = new Dictionary <string, long>();
                Dictionary <string, Int64> nativeCentsPayoutLookup = new Dictionary <string, long>();
                Dictionary <int, Int64>    satoshiPersonLookup     = new Dictionary <int, long>();
                Dictionary <int, Int64>    nativeCentsPersonLookup = new Dictionary <int, long>();
                Int64 satoshisTotal = 0;

                string currencyCode = organization.Currency.Code;

                // For each ready payout that can automate, add an output to a constructed transaction

                TransactionBuilder txBuilder = null; // TODO TODO TODO TODO new TransactionBuilder();
                foreach (Payout payout in orgPayouts)
                {
                    if (payout.ExpectedTransactionDate > utcNow)
                    {
                        continue; // payout is not due yet
                    }

                    if (payout.RecipientPerson != null && payout.RecipientPerson.BitcoinPayoutAddress.Length > 2 &&
                        payout.Account.Length < 4)
                    {
                        // If the payout address is still in quarantine, don't pay out yet

                        string addressSetTime = payout.RecipientPerson.BitcoinPayoutAddressTimeSet;
                        if (addressSetTime.Length > 4 && DateTime.Parse(addressSetTime, CultureInfo.InvariantCulture).AddHours(48) > utcNow)
                        {
                            continue; // still in quarantine
                        }

                        // Test the payout address - is it valid and can we handle it?

                        if (!BitcoinUtility.IsValidBitcoinAddress(payout.RecipientPerson.BitcoinPayoutAddress))
                        {
                            // Notify person that address is invalid, then clear it

                            NotificationStrings       primaryStrings   = new NotificationStrings();
                            NotificationCustomStrings secondaryStrings = new NotificationCustomStrings();
                            primaryStrings[NotificationString.OrganizationName] = organization.Name;
                            secondaryStrings["BitcoinAddress"] = payout.RecipientPerson.BitcoinPayoutAddress;

                            OutboundComm.CreateNotification(organization, NotificationResource.BitcoinPayoutAddress_Bad,
                                                            primaryStrings, secondaryStrings,
                                                            People.FromSingle(payout.RecipientPerson));

                            payout.RecipientPerson.BitcoinPayoutAddress = string.Empty;

                            continue; // do not add this payout
                        }

                        // Ok, so it seems we're making this payout at this time.

                        bitcoinPayouts.Add(payout);

                        int recipientPersonId = payout.RecipientPerson.Identity;

                        if (!satoshiPersonLookup.ContainsKey(recipientPersonId))
                        {
                            satoshiPersonLookup[recipientPersonId]     = 0;
                            nativeCentsPersonLookup[recipientPersonId] = 0;
                        }

                        nativeCentsPersonLookup[recipientPersonId] += payout.AmountCents;

                        // Find the amount of satoshis for this payout

                        if (organization.Currency.IsBitcoinCore)
                        {
                            satoshiPayoutLookup[payout.ProtoIdentity]     = payout.AmountCents;
                            nativeCentsPayoutLookup[payout.ProtoIdentity] = payout.AmountCents;
                            satoshisTotal += payout.AmountCents;
                            satoshiPersonLookup[recipientPersonId] += payout.AmountCents;
                        }
                        else
                        {
                            // Convert currency
                            Money payoutAmount = new Money(payout.AmountCents, organization.Currency);
                            Int64 satoshis     = payoutAmount.ToCurrency(Currency.BitcoinCore).Cents;
                            satoshiPayoutLookup[payout.ProtoIdentity]     = satoshis;
                            nativeCentsPayoutLookup[payout.ProtoIdentity] = payout.AmountCents;
                            satoshisTotal += satoshis;
                            satoshiPersonLookup[recipientPersonId] += satoshis;
                        }
                    }
                    else if (payout.RecipientPerson != null && payout.RecipientPerson.BitcoinPayoutAddress.Length < 3 && payout.Account.Length < 4)
                    {
                        // There is a payout for this person, but they don't have a bitcoin payout address set. Send notification to this effect once a day.

                        if (utcNow.Minute != 0)
                        {
                            continue;
                        }
                        if (utcNow.Hour != 12)
                        {
                            continue;
                        }

                        NotificationStrings primaryStrings = new NotificationStrings();
                        primaryStrings[NotificationString.OrganizationName] = organization.Name;
                        OutboundComm.CreateNotification(organization, NotificationResource.BitcoinPayoutAddress_PleaseSet, primaryStrings, People.FromSingle(payout.RecipientPerson));
                    }
                    else if (payout.Account.StartsWith("bitcoin:"))
                    {
                    }
                }

                if (bitcoinPayouts.Count == 0)
                {
                    // no automated payments pending for this organization, nothing more to do
                    continue;
                }

                // We now have our desired payments. The next step is to find enough inputs to reach the required amount (plus fees; we're working a little blind here still).

                BitcoinTransactionInputs inputs      = null;
                Int64 satoshisMaximumAnticipatedFees = BitcoinUtility.GetRecommendedFeePerThousandBytesSatoshis(chain) * 20; // assume max 20k transaction size

                try
                {
                    inputs = BitcoinUtility.GetInputsForAmount(organization, satoshisTotal + satoshisMaximumAnticipatedFees);
                }
                catch (NotEnoughFundsException)
                {
                    // If we're at the whole hour, send a notification to people responsible for refilling the hotwallet

                    if (utcNow.Minute != 0)
                    {
                        continue; // we're not at the whole hour, so continue with next org instead
                    }

                    // Send urgent notification to top up the damn wallet so we can make payments

                    NotificationStrings primaryStrings = new NotificationStrings();
                    primaryStrings[NotificationString.CurrencyCode]     = organization.Currency.Code;
                    primaryStrings[NotificationString.OrganizationName] = organization.Name;
                    NotificationCustomStrings secondaryStrings = new NotificationCustomStrings();
                    Int64 satoshisAvailable = HotBitcoinAddresses.ForOrganization(organization).BalanceSatoshisTotal;

                    secondaryStrings["AmountMissingMicrocoinsFloat"] =
                        ((satoshisTotal - satoshisAvailable + satoshisMaximumAnticipatedFees) / 100.0).ToString("N2");

                    if (organization.Currency.IsBitcoinCore)
                    {
                        secondaryStrings["AmountNeededFloat"] = ((satoshisTotal + satoshisMaximumAnticipatedFees) / 100.0).ToString("N2");
                        secondaryStrings["AmountWalletFloat"] = (satoshisAvailable / 100.0).ToString("N2");
                    }
                    else
                    {
                        // convert to org native currency

                        secondaryStrings["AmountNeededFloat"] =
                            (new Money(satoshisTotal, Currency.BitcoinCore).ToCurrency(organization.Currency).Cents / 100.0).ToString("N2");
                        secondaryStrings["AmountWalletFloat"] =
                            (new Money(satoshisAvailable, Currency.BitcoinCore).ToCurrency(organization.Currency).Cents / 100.0).ToString("N2");
                    }

                    OutboundComm.CreateNotification(organization,
                                                    NotificationResource.Bitcoin_Shortage_Critical, primaryStrings, secondaryStrings, People.FromSingle(Person.FromIdentity(1)));

                    continue; // with next organization
                }

                // If we arrive at this point, the previous function didn't throw, and we have enough money.
                // Ensure the existence of a cost account for bitcoin miner fees.

                organization.EnsureMinerFeeAccountExists();

                // Add the inputs to the transaction.

                txBuilder = txBuilder.AddCoins(inputs.Coins);
                txBuilder = txBuilder.AddKeys(inputs.PrivateKeys);
                Int64 satoshisInput = inputs.AmountSatoshisTotal;

                // Add outputs and prepare notifications

                Int64 satoshisUsed = 0;
                Dictionary <int, List <string> > notificationSpecLookup   = new Dictionary <int, List <string> >();
                Dictionary <int, List <Int64> >  notificationAmountLookup = new Dictionary <int, List <Int64> >();
                Payout            masterPayoutPrototype = Payout.Empty;
                HotBitcoinAddress changeAddress         = HotBitcoinAddress.OrganizationWalletZero(organization, BitcoinChain.Core); // TODO: CHAIN!

                foreach (Payout payout in bitcoinPayouts)
                {
                    int recipientPersonId = payout.RecipientPerson.Identity;
                    if (!notificationSpecLookup.ContainsKey(recipientPersonId))
                    {
                        notificationSpecLookup[recipientPersonId]   = new List <string>();
                        notificationAmountLookup[recipientPersonId] = new List <Int64>();
                    }
                    notificationSpecLookup[recipientPersonId].Add(payout.Specification);
                    notificationAmountLookup[recipientPersonId].Add(payout.AmountCents);

                    if (payout.RecipientPerson.BitcoinPayoutAddress.StartsWith("1"))  // regular address
                    {
                        txBuilder = txBuilder.Send(new BitcoinPubKeyAddress(payout.RecipientPerson.BitcoinPayoutAddress),
                                                   new Satoshis(satoshiPayoutLookup[payout.ProtoIdentity]));
                    }
                    else if (payout.RecipientPerson.BitcoinPayoutAddress.StartsWith("3"))  // multisig
                    {
                        txBuilder = txBuilder.Send(new BitcoinScriptAddress(payout.RecipientPerson.BitcoinPayoutAddress, Network.Main),
                                                   new Satoshis(satoshiPayoutLookup[payout.ProtoIdentity]));
                    }
                    else
                    {
                        throw new InvalidOperationException("Unhandled bitcoin address type in Payouts.PerformAutomated(): " + payout.RecipientPerson.BitcoinPayoutAddress);
                    }

                    satoshisUsed += satoshiPayoutLookup[payout.ProtoIdentity];

                    payout.MigrateDependenciesTo(masterPayoutPrototype);
                }

                // Set change address to wallet slush

                txBuilder.SetChange(new BitcoinPubKeyAddress(changeAddress.Address));

                // Add fee

                int transactionSizeBytes = txBuilder.EstimateSize(txBuilder.BuildTransaction(false)) + inputs.Count;
                // +inputs.Count for size variability

                Int64 feeSatoshis = (transactionSizeBytes / 1000 + 1) * BitcoinUtility.GetRecommendedFeePerThousandBytesSatoshis(chain);

                txBuilder     = txBuilder.SendFees(new Satoshis(feeSatoshis));
                satoshisUsed += feeSatoshis;

                // Sign transaction - ready to execute

                Transaction txReady = txBuilder.BuildTransaction(true);

                // Verify that transaction is ready

                if (!txBuilder.Verify(txReady))
                {
                    // Transaction was not signed with the correct keys. This is a serious condition.

                    NotificationStrings primaryStrings = new NotificationStrings();
                    primaryStrings[NotificationString.OrganizationName] = organization.Name;

                    OutboundComm.CreateNotification(organization, NotificationResource.Bitcoin_PrivateKeyError,
                                                    primaryStrings);

                    throw new InvalidOperationException("Transaction is not signed enough");
                }

                // Broadcast transaction

                BitcoinUtility.BroadcastTransaction(txReady, BitcoinChain.Cash);

                // Note the transaction hash

                string transactionHash = txReady.GetHash().ToString();

                // Delete all old inputs, adjust balance for addresses (re-register unused inputs)

                inputs.AsUnspents.DeleteAll();

                // Log the new unspent created by change (if there is any)

                if (satoshisInput - satoshisUsed > 0)
                {
                    SwarmDb.GetDatabaseForWriting()
                    .CreateHotBitcoinAddressUnspentConditional(changeAddress.Identity, transactionHash,
                                                               +/* the change address seems to always get index 0? is this a safe assumption? */ 0, satoshisInput - satoshisUsed, /* confirmation count*/ 0);
                }

                // Register new balance of change address, should have increased by (satoshisInput-satoshisUsed)

                // TODO

                // Send notifications

                foreach (int personId in notificationSpecLookup.Keys)
                {
                    Person person = Person.FromIdentity(personId);

                    string spec = string.Empty;
                    for (int index = 0; index < notificationSpecLookup[personId].Count; index++)
                    {
                        spec += String.Format(" * {0,-40} {1,14:N2} {2,-4}\r\n", notificationSpecLookup[personId][index], notificationAmountLookup[personId][index] / 100.0, currencyCode);
                    }

                    spec = spec.TrimEnd();

                    NotificationStrings       primaryStrings   = new NotificationStrings();
                    NotificationCustomStrings secondaryStrings = new NotificationCustomStrings();

                    primaryStrings[NotificationString.OrganizationName]         = organization.Name;
                    primaryStrings[NotificationString.CurrencyCode]             = organization.Currency.DisplayCode;
                    primaryStrings[NotificationString.EmbeddedPreformattedText] = spec;
                    secondaryStrings["AmountFloat"]        = (nativeCentsPersonLookup[personId] / 100.0).ToString("N2");
                    secondaryStrings["BitcoinAmountFloat"] = (satoshiPersonLookup[personId] / 100.0).ToString("N2");
                    secondaryStrings["BitcoinAddress"]     = person.BitcoinPayoutAddress; // warn: potential rare race condition here

                    OutboundComm.CreateNotification(organization, NotificationResource.Bitcoin_PaidOut, primaryStrings,
                                                    secondaryStrings, People.FromSingle(person));
                }

                // Create the master payout from its prototype

                Payout masterPayout = Payout.CreateBitcoinPayoutFromPrototype(organization, masterPayoutPrototype, txReady.GetHash().ToString());

                // Finally, create ledger entries and notify

                NotificationStrings       masterPrimaryStrings   = new NotificationStrings();
                NotificationCustomStrings masterSecondaryStrings = new NotificationCustomStrings();

                masterPrimaryStrings[NotificationString.OrganizationName] = organization.Name;
                masterPrimaryStrings[NotificationString.CurrencyCode]     = organization.Currency.DisplayCode;
                masterSecondaryStrings["AmountFloat"] =
                    (new Swarmops.Logic.Financial.Money(satoshisUsed, Currency.BitcoinCore).ToCurrency(
                         organization.Currency).Cents / 100.0).ToString("N2", CultureInfo.InvariantCulture);
                masterSecondaryStrings["BitcoinAmountFloat"] = (satoshisUsed / 100.0).ToString("N2", CultureInfo.InvariantCulture);
                masterSecondaryStrings["PaymentCount"]       = bitcoinPayouts.Count.ToString("N0", CultureInfo.InvariantCulture);

                OutboundComm.CreateNotification(organization, NotificationResource.Bitcoin_Hotwallet_Outflow,
                                                masterPrimaryStrings, masterSecondaryStrings);

                // TODO: special case for native-bitcoin organizations vs. fiat-currency organizations

                FinancialTransaction ledgerTransaction = FinancialTransaction.Create(organization, utcNow,
                                                                                     "Bitcoin automated payout");

                if (organization.Currency.IsBitcoinCore)
                {
                    ledgerTransaction.AddRow(organization.FinancialAccounts.AssetsBitcoinHot, -(masterPayoutPrototype.AmountCents + feeSatoshis), null);
                    ledgerTransaction.AddRow(organization.FinancialAccounts.CostsBitcoinFees, feeSatoshis, null);
                }
                else
                {
                    // If the ledger isn't using bitcoin natively, we need to translate the miner fee paid to ledger cents before entering it into ledger

                    Int64 feeCentsLedger = new Money(feeSatoshis, Currency.BitcoinCore).ToCurrency(organization.Currency).Cents;
                    ledgerTransaction.AddRow(organization.FinancialAccounts.AssetsBitcoinHot, -(masterPayoutPrototype.AmountCents + feeCentsLedger), null).AmountForeignCents = new Money(satoshisUsed, Currency.BitcoinCore);
                    ledgerTransaction.AddRow(organization.FinancialAccounts.CostsBitcoinFees, feeCentsLedger, null);
                }

                ledgerTransaction.BlockchainHash = transactionHash;

                masterPayout.BindToTransactionAndClose(ledgerTransaction, null);
            }
        }
Example #20
0
        public static Salary Create(PayrollItem payrollItem, DateTime payoutDate)
        {
            // Load the existing adjustments.

            PayrollAdjustments adjustments = PayrollAdjustments.ForPayrollItem(payrollItem);

            Int64 payCents = payrollItem.BaseSalaryCents;

            // Apply all before-tax adjustments

            foreach (PayrollAdjustment adjustment in adjustments)
            {
                if (adjustment.Type == PayrollAdjustmentType.GrossAdjustment)
                {
                    payCents += adjustment.AmountCents;
                }
            }

            Int64 subtractiveTaxCents = 0;
            Int64 additiveTaxCents    = 0;

            if (!payrollItem.IsContractor)
            {
                // calculate tax

                Money grossInOrgCurrency = new Money
                {
                    Cents             = payCents,
                    Currency          = payrollItem.Organization.Currency,
                    ValuationDateTime = DateTime.UtcNow
                };

                Money grossInTaxCurrency = grossInOrgCurrency.ToCurrency(payrollItem.Country.Currency);

                Money subtractiveTax = TaxLevels.GetTax(payrollItem.Country, payrollItem.SubtractiveTaxLevelId,
                                                        grossInTaxCurrency);

                Money subtractiveTaxInOrgCurrency = subtractiveTax.ToCurrency(payrollItem.Organization.Currency);

                subtractiveTaxCents = (Int64)(subtractiveTaxInOrgCurrency.Cents);

                additiveTaxCents = (Int64)(payCents * payrollItem.AdditiveTaxLevel);

                payCents -= subtractiveTaxCents;
            }

            // Apply all after-tax adjustments

            foreach (PayrollAdjustment adjustment in adjustments)
            {
                if (adjustment.Type == PayrollAdjustmentType.NetAdjustment)
                {
                    payCents += adjustment.AmountCents;
                }
            }

            // If net is negative, create rollover adjustment

            PayrollAdjustment rolloverAdjustment = null;

            if (payCents < 0)
            {
                rolloverAdjustment = PayrollAdjustment.Create(payrollItem, PayrollAdjustmentType.NetAdjustment,
                                                              -payCents,
                                                              "Deficit rolls over to next salary");

                PayrollAdjustment.Create(payrollItem, PayrollAdjustmentType.NetAdjustment,
                                         payCents, "Deficit rolled over from " +
                                         payoutDate.ToString("yyyy-MM-dd"));

                // keep second rollover open, so the deficit from this salary is carried to the next

                payCents = 0;
            }

            // Create salary, close adjustments

            Salary salary = Create(payrollItem, payoutDate, payCents, subtractiveTaxCents, additiveTaxCents);

            // For each adjustment, close and bind to salary

            foreach (PayrollAdjustment adjustment in adjustments)
            {
                adjustment.Close(salary);
            }

            if (rolloverAdjustment != null)
            {
                rolloverAdjustment.Close(salary);
            }

            // Add the financial transaction

            FinancialTransaction transaction =
                FinancialTransaction.Create(payrollItem.OrganizationId, DateTime.Now,
                                            "Salary #" + salary.Identity + ": " + payrollItem.PersonCanonical +
                                            " " +
                                            salary.PayoutDate.ToString("yyyy-MMM", CultureInfo.InvariantCulture));

            transaction.AddRow(payrollItem.Budget, salary.CostTotalCents, null);
            transaction.AddRow(payrollItem.Organization.FinancialAccounts.DebtsSalary, -salary.NetSalaryCents, null);
            if (salary.TaxTotalCents != 0)
            {
                transaction.AddRow(payrollItem.Organization.FinancialAccounts.DebtsTax, -salary.TaxTotalCents, null);
            }
            transaction.Dependency = salary;

            // Finally, check if net and/or tax are zero, and if so, mark them as already-paid (i.e. not due for payment)

            if (salary.NetSalaryCents == 0)
            {
                salary.NetPaid = true;
            }

            if (salary.TaxTotalCents == 0)
            {
                salary.TaxPaid = true;
            }

            // Clear a cache
            FinancialAccount.ClearAttestationAdjustmentsCache(payrollItem.Organization);



            return(salary);
        }
Example #21
0
        public static Salary Create(PayrollItem payrollItem, DateTime payoutDate)
        {
            // Load the existing adjustments.

            PayrollAdjustments adjustments = PayrollAdjustments.ForPayrollItem(payrollItem);

            Int64 payCents = payrollItem.BaseSalaryCents;

            // Apply all before-tax adjustments

            foreach (PayrollAdjustment adjustment in adjustments)
            {
                if (adjustment.Type == PayrollAdjustmentType.GrossAdjustment)
                {
                    payCents += adjustment.AmountCents;
                }
            }

            // calculate tax

            double subtractiveTax = TaxLevels.GetTax(payrollItem.Country, payrollItem.SubtractiveTaxLevelId,
                                                     payCents / 100.0);

            if (subtractiveTax < 1.0)
            {
                // this is a percentage and not an absolute number

                subtractiveTax = payCents * subtractiveTax;
            }
            Int64 subtractiveTaxCents = (Int64)(subtractiveTax * 100);

            Int64 additiveTaxCents = (Int64)(payCents * payrollItem.AdditiveTaxLevel);

            payCents -= subtractiveTaxCents;

            // Apply all after-tax adjustments

            foreach (PayrollAdjustment adjustment in adjustments)
            {
                if (adjustment.Type == PayrollAdjustmentType.NetAdjustment)
                {
                    payCents += adjustment.AmountCents;
                }
            }

            // Create salary, close adjustments

            Salary salary = Create(payrollItem, payoutDate, payCents, subtractiveTaxCents, additiveTaxCents);

            // For each adjustment, close and bind to salary

            foreach (PayrollAdjustment adjustment in adjustments)
            {
                adjustment.Close(salary);
            }

            // If net is negative, create rollover adjustment

            if (payCents < 0)
            {
                PayrollAdjustment rollover1 = PayrollAdjustment.Create(payrollItem, PayrollAdjustmentType.NetAdjustment,
                                                                       -payCents,
                                                                       "Deficit rolls over to next salary");

                rollover1.Close(salary);

                PayrollAdjustment rollover2 = PayrollAdjustment.Create(payrollItem, PayrollAdjustmentType.NetAdjustment,
                                                                       payCents, "Deficit rolled over from " +
                                                                       payoutDate.ToString("yyyy-MM-dd"));

                // keep rollover2 open, so the deficit from this salary is carried to the next

                salary.NetSalaryCents = 0;
            }

            // Add the financial transaction

            FinancialTransaction transaction =
                FinancialTransaction.Create(payrollItem.OrganizationId, DateTime.Now,
                                            "Salary #" + salary.Identity + ": " + payrollItem.PersonCanonical +
                                            " " +
                                            salary.PayoutDate.ToString("yyyy-MMM", CultureInfo.InvariantCulture));

            transaction.AddRow(payrollItem.Budget, salary.CostTotalCents, null);
            transaction.AddRow(payrollItem.Organization.FinancialAccounts.DebtsSalary, -salary.NetSalaryCents, null);
            transaction.AddRow(payrollItem.Organization.FinancialAccounts.DebtsTax, -salary.TaxTotalCents, null);
            transaction.Dependency = salary;

            // Finally, check if net and/or tax are zero, and if so, mark them as already-paid (i.e. not due for payment)

            if (salary.NetSalaryCents == 0)
            {
                salary.NetPaid = true;
            }

            if (salary.TaxTotalCents == 0)
            {
                salary.TaxPaid = true;
            }

            return(salary);
        }
 public static FinancialTransaction Create(Organization organization, DateTime dateTime, string description)
 {
     return(FinancialTransaction.Create(organization.Identity, dateTime, description));
 }
Example #23
0
        private static void CheckColdStorageRecurse(FinancialAccount parent, Dictionary <string, int> addressAccountLookup)
        {
            foreach (FinancialAccount child in parent.Children)
            {
                CheckColdStorageRecurse(child, addressAccountLookup);
            }

            // After recursing, get all transactions for this account and verify against our records

            // is the account name a valid bitcoin address on the main network?

            string address = parent.BitcoinAddress;

            if (string.IsNullOrEmpty(address))
            {
                if (BitcoinUtility.IsValidBitcoinAddress(parent.Name.Trim()))
                {
                    parent.BitcoinAddress = address = parent.Name.Trim();
                }
                else
                {
                    return; // not a bitcoin address but something else; do not process
                }
            }

            Organization organization = parent.Organization;
            bool         organizationLedgerUsesBitcoin = organization.Currency.IsBitcoinCore;

            JObject addressData = JObject.Parse(
                new WebClient().DownloadString("https://blockchain.info/address/" + address +
                                               "?format=json&api_key=" +
                                               SystemSettings.BlockchainSwarmopsApiKey));

            //int transactionCount = (int) (addressData["n_tx"]);

            foreach (JObject txJson in addressData["txs"])
            {
                FinancialTransaction      ourTx          = null;
                Dictionary <Int64, Int64> satoshisLookup = new Dictionary <long, long>(); // map from ledgercents to satoshis
                BlockchainTransaction     blockchainTx   = BlockchainTransaction.FromBlockchainInfoJson(txJson);

                try
                {
                    ourTx = FinancialTransaction.FromBlockchainHash(parent.Organization, blockchainTx.TransactionHash);
                    // If the transaction was fetched fine, we have already seen this transaction, but need to re-check it
                }
                catch (ArgumentException)
                {
                    // We didn't have this transaction, so we need to create it

                    ourTx = FinancialTransaction.Create(parent.Organization, blockchainTx.TransactionDateTimeUtc, "Blockchain tx");
                    ourTx.BlockchainHash = blockchainTx.TransactionHash;

                    // Did we lose or gain money?
                    // Find all in- and outpoints, determine which are ours (hot and cold wallet) and which aren't
                }

                Dictionary <int, long> transactionReconstructedRows = new Dictionary <int, long>();

                // Note the non-blockchain rows in this tx, keep them for reconstruction

                foreach (FinancialTransactionRow row in ourTx.Rows)
                {
                    if (!addressAccountLookup.ContainsValue(row.FinancialAccountId))  // not optimal but n is small
                    {
                        // This is not a bitcoin address account, so note it for reconstruction

                        if (!transactionReconstructedRows.ContainsKey(row.FinancialAccountId))
                        {
                            transactionReconstructedRows[row.FinancialAccountId] = 0; // init
                        }

                        transactionReconstructedRows[row.FinancialAccountId] += row.AmountCents;
                    }
                    else
                    {
                        // this is a known blockchain row, note its ledgered value in satoshis
                        if (!organizationLedgerUsesBitcoin)
                        {
                            Money nativeMoney = row.AmountForeignCents;
                            if (nativeMoney != null && nativeMoney.Currency.IsBitcoinCore) // it damn well should be, but just checking
                            {
                                satoshisLookup[row.AmountCents] = row.AmountForeignCents.Cents;
                            }
                        }
                    }
                }

                // Reconstruct the blockchain rows: input, output, fees, in that order

                // -- inputs

                foreach (BlockchainTransactionRow inputRow in blockchainTx.Inputs)
                {
                    if (addressAccountLookup.ContainsKey(inputRow.Address))
                    {
                        // this input is ours

                        int financialAccountId = addressAccountLookup[inputRow.Address];

                        if (!transactionReconstructedRows.ContainsKey(financialAccountId))
                        {
                            transactionReconstructedRows[financialAccountId] = 0; // initialize
                        }

                        if (organizationLedgerUsesBitcoin)
                        {
                            transactionReconstructedRows[financialAccountId] +=
                                -inputRow.ValueSatoshis; // note the negation!
                        }
                        else
                        {
                            Int64 ledgerCents =
                                new Money(inputRow.ValueSatoshis, Currency.BitcoinCore, ourTx.DateTime).ToCurrency(
                                    organization.Currency).Cents;
                            transactionReconstructedRows[financialAccountId] +=
                                -ledgerCents; // note the negation!
                            satoshisLookup[ledgerCents] = inputRow.ValueSatoshis;
                        }
                    }
                }

                // -- outputs

                foreach (BlockchainTransactionRow outputRow in blockchainTx.Outputs)
                {
                    if (addressAccountLookup.ContainsKey(outputRow.Address))
                    {
                        // this output is ours

                        int financialAccountId = addressAccountLookup[outputRow.Address];

                        if (!transactionReconstructedRows.ContainsKey(financialAccountId))
                        {
                            transactionReconstructedRows[financialAccountId] = 0; // initialize
                        }

                        if (organizationLedgerUsesBitcoin)
                        {
                            transactionReconstructedRows[financialAccountId] +=
                                outputRow.ValueSatoshis;
                        }
                        else
                        {
                            Int64 ledgerCents =
                                new Money(outputRow.ValueSatoshis, Currency.BitcoinCore, ourTx.DateTime).ToCurrency(
                                    organization.Currency).Cents;
                            transactionReconstructedRows[financialAccountId] +=
                                ledgerCents;
                            satoshisLookup[ledgerCents] = outputRow.ValueSatoshis;
                        }
                    }
                }

                // -- fees

                if (addressAccountLookup.ContainsKey(blockchainTx.Inputs[0].Address))
                {
                    // if the first input is ours, we're paying the fee (is there any case where this is not true?)

                    if (organizationLedgerUsesBitcoin)
                    {
                        transactionReconstructedRows[organization.FinancialAccounts.CostsBitcoinFees.Identity] =
                            blockchainTx.FeeSatoshis;
                    }
                    else
                    {
                        Int64 feeLedgerCents =
                            new Money(blockchainTx.FeeSatoshis, Currency.BitcoinCore,
                                      blockchainTx.TransactionDateTimeUtc).ToCurrency(organization.Currency).Cents;
                        transactionReconstructedRows[organization.FinancialAccounts.CostsBitcoinFees.Identity] =
                            feeLedgerCents;
                    }
                }


                // Rewrite the transaction (called always, but the function won't do anything if everything matches)

                ourTx.RecalculateTransaction(transactionReconstructedRows, /* loggingPerson*/ null);

                // Finally, add foreign cents, if any

                if (!organizationLedgerUsesBitcoin)
                {
                    foreach (FinancialTransactionRow row in ourTx.Rows)
                    {
                        if (addressAccountLookup.ContainsValue(row.Account.Identity))  // "ContainsValue" is bad, but n is low
                        {
                            // Do we have a foreign amount for this row already?

                            Money foreignMoney = row.AmountForeignCents;
                            if (foreignMoney == null || foreignMoney.Cents == 0)
                            {
                                // no we didn't; create one

                                if (satoshisLookup.ContainsKey(row.AmountCents))
                                {
                                    row.AmountForeignCents = new Money(satoshisLookup[row.AmountCents], Currency.BitcoinCore, ourTx.DateTime);
                                }
                                else if (satoshisLookup.ContainsKey(-row.AmountCents))  // the negative counterpart
                                {
                                    row.AmountForeignCents = new Money(-satoshisLookup[-row.AmountCents], Currency.BitcoinCore, ourTx.DateTime);
                                }
                                else
                                {
                                    // There's a last case which may happen if the row is an addition to a previous row; if so, calculate
                                    row.AmountForeignCents = new Money(row.AmountCents, organization.Currency, ourTx.DateTime).ToCurrency(Currency.BitcoinCore);
                                }
                            }
                        }
                    }
                }
            }
        }
Example #24
0
        public OutboundInvoice Credit(Person creditingPerson)
        {
            OutboundInvoice credit = Create(Organization, DateTime.Now,
                                            Budget, CustomerName,
                                            InvoiceAddressMail,
                                            InvoiceAddressPaper, Currency,
                                            Domestic, TheirReference, creditingPerson);

            if (Domestic) // TODO: LANGUAGE
            {
                credit.AddItem("Kredit för faktura " + Reference, -AmountCents);
                credit.AddItem("DETTA ÄR EN KREDITFAKTURA OCH SKA EJ BETALAS", 0.00);

                AddItem(
                    String.Format("KREDITERAD {0:yyyy-MM-dd} i kreditfaktura {1}", DateTime.Today, credit.Reference),
                    0.00);
            }
            else
            {
                credit.AddItem("Credit for invoice " + Reference, -AmountCents);
                credit.AddItem("THIS IS A CREDIT. DO NOT PAY.", 0.00);

                AddItem(
                    String.Format("CREDITED {0:yyyy-MM-dd} in credit invoice {1}", DateTime.Today, credit.Reference),
                    0.00);
            }

            CreditInvoice         = credit;
            credit.CreditsInvoice = this;


            credit.Open = false;

            // Create the financial transaction with rows

            FinancialTransaction transaction =
                FinancialTransaction.Create(credit.OrganizationId, DateTime.Now,
                                            "Credit Invoice #" + credit.Identity + " to " + credit.CustomerName);

            transaction.AddRow(
                Organization.FromIdentity(credit.OrganizationId).FinancialAccounts.AssetsOutboundInvoices,
                credit.AmountCents, creditingPerson);
            transaction.AddRow(credit.Budget, -credit.AmountCents, creditingPerson);

            // Make the transaction dependent on the credit

            transaction.Dependency = credit;

            // Create the event for PirateBot-Mono to send off mails

            PWEvents.CreateEvent(EventSource.PirateWeb, EventType.OutboundInvoiceCreated, creditingPerson.Identity,
                                 OrganizationId, Geography.RootIdentity, 0, credit.Identity, string.Empty);

            // If this invoice was already closed, issue a credit. If not closed, close it.

            if (Open)
            {
                Open = false;
            }
            else
            {
                Payment payment = Payment;

                if (payment != null)
                {
                    payment.Refund(creditingPerson);
                }
            }

            return(credit);
        }
Example #25
0
        public static ExpenseClaim Create(Person claimer, Organization organization, FinancialAccount budget,
                                          DateTime expenseDate, string description, Int64 amountCents, Int64 vatCents, ExpenseClaimGroup group = null)
        {
            ExpenseClaim newClaim =
                FromIdentityAggressive(SwarmDb.GetDatabaseForWriting()
                                       .CreateExpenseClaim(claimer.Identity, organization?.Identity ?? 0,
                                                           budget?.Identity ?? 0, expenseDate, description, amountCents)); // budget can be 0 initially if created with a group

            if (vatCents > 0)
            {
                newClaim.VatCents = vatCents;
            }

            if (group != null)
            {
                newClaim.Group = group;
            }

            if (budget != null && organization != null)
            {
                // Create the financial transaction with rows

                string transactionDescription = "Expense #" + newClaim.OrganizationSequenceId + ": " + description;
                // TODO: Localize

                if (transactionDescription.Length > 64)
                {
                    transactionDescription = transactionDescription.Substring(0, 61) + "...";
                }


                DateTime expenseTxDate      = expenseDate;
                int      ledgersClosedUntil = organization.Parameters.FiscalBooksClosedUntilYear;

                if (ledgersClosedUntil >= expenseDate.Year)
                {
                    expenseTxDate = DateTime.UtcNow; // If ledgers are closed for the actual expense time, account now
                }

                FinancialTransaction transaction =
                    FinancialTransaction.Create(organization.Identity, expenseTxDate,
                                                transactionDescription);

                transaction.AddRow(organization.FinancialAccounts.DebtsExpenseClaims, -amountCents, claimer);
                if (vatCents > 0)
                {
                    transaction.AddRow(budget, amountCents - vatCents, claimer);
                    transaction.AddRow(organization.FinancialAccounts.AssetsVatInboundUnreported, vatCents, claimer);
                }
                else
                {
                    transaction.AddRow(budget, amountCents, claimer);
                }

                // Make the transaction dependent on the expense claim

                transaction.Dependency = newClaim;

                // Create notifications

                OutboundComm.CreateNotificationAttestationNeeded(budget, claimer, string.Empty,
                                                                 newClaim.BudgetAmountCents / 100.0,
                                                                 description, NotificationResource.ExpenseClaim_Created);
                // Slightly misplaced logic, but failsafer here
                OutboundComm.CreateNotificationFinancialValidationNeeded(organization, newClaim.AmountCents / 100.0,
                                                                         NotificationResource.Receipts_Filed);
                SwarmopsLogEntry.Create(claimer,
                                        new ExpenseClaimFiledLogEntry(claimer /*filing person*/, claimer /*beneficiary*/,
                                                                      newClaim.BudgetAmountCents / 100.0,
                                                                      vatCents / 100.0, budget, description), newClaim);

                // Clear a cache
                FinancialAccount.ClearAttestationAdjustmentsCache(organization);
            }

            return(newClaim);
        }
Example #26
0
        public static VatReport Create(Organization organization, int year, int startMonth, int monthCount)
        {
            VatReport newReport = CreateDbRecord(organization, year, startMonth, monthCount);
            DateTime  endDate   = new DateTime(year, startMonth, 1).AddMonths(monthCount);

            FinancialAccount vatInbound  = organization.FinancialAccounts.AssetsVatInboundUnreported;
            FinancialAccount vatOutbound = organization.FinancialAccounts.DebtsVatOutboundUnreported;
            FinancialAccount sales       = organization.FinancialAccounts.IncomeSales;

            FinancialAccountRows inboundRows  = RowsNotInVatReport(vatInbound, endDate);
            FinancialAccountRows outboundRows = RowsNotInVatReport(vatOutbound, endDate);
            FinancialAccountRows turnoverRows = RowsNotInVatReport(sales, endDate);

            Dictionary <int, bool> transactionsIncludedLookup = new Dictionary <int, bool>();

            newReport.AddVatReportItemsFromAccountRows(inboundRows, transactionsIncludedLookup);
            newReport.AddVatReportItemsFromAccountRows(outboundRows, transactionsIncludedLookup);
            newReport.AddVatReportItemsFromAccountRows(turnoverRows, transactionsIncludedLookup);

            newReport.Release();

            // Create financial TX that moves this VAT from unreported to reported

            Int64 differenceCents = newReport.VatInboundCents - newReport.VatOutboundCents;

            if (differenceCents != 0 && newReport.VatInboundCents > 0)
            {
                // if there's anything to report

                FinancialTransaction vatReportTransaction = FinancialTransaction.Create(organization, endDate.AddDays(4).AddHours(9),
                                                                                        newReport.Description);

                if (newReport.VatInboundCents > 0)
                {
                    vatReportTransaction.AddRow(organization.FinancialAccounts.AssetsVatInboundUnreported,
                                                -newReport.VatInboundCents, null);
                }
                if (newReport.VatOutboundCents > 0)
                {
                    vatReportTransaction.AddRow(organization.FinancialAccounts.DebtsVatOutboundUnreported,
                                                newReport.VatOutboundCents, null);
                    // not negative, because our number is sign-different from the bookkeeping's
                }

                if (differenceCents < 0) // outbound > inbound
                {
                    vatReportTransaction.AddRow(organization.FinancialAccounts.DebtsVatOutboundReported,
                                                differenceCents, null); // debt, so negative as in our variable
                }
                else // inbound > outbound
                {
                    vatReportTransaction.AddRow(organization.FinancialAccounts.AssetsVatInboundReported,
                                                differenceCents, null); // asset, so positive as in our variable
                }

                vatReportTransaction.Dependency = newReport;
                newReport.OpenTransaction       = vatReportTransaction;
            }
            else
            {
                newReport.Open = false; // nothing to close, no tx created
            }

            return(newReport);
        }
 private static FinancialDependencyType GetDependencyType(IHasIdentity foreignObject)
 {
     return(FinancialTransaction.GetFinancialDependencyType(foreignObject));
 }
Example #28
0
        private void AddVatReportItemsFromAccountRows(FinancialAccountRows rows,
                                                      Dictionary <int, bool> transactionsIncludedLookup)
        {
            if (rows.Count == 0)
            {
                return;
            }

            Organization organization         = rows[0].Transaction.Organization; // there is always a rows[0] because check above
            int          vatInboundAccountId  = organization.FinancialAccounts.AssetsVatInboundUnreported.Identity;
            int          vatOutboundAccountId = organization.FinancialAccounts.DebtsVatOutboundUnreported.Identity;

            Dictionary <int, bool> turnoverAccountLookup = new Dictionary <int, bool>();

            FinancialAccounts turnoverAccounts = FinancialAccounts.ForOrganization(organization,
                                                                                   FinancialAccountType.Income);

            foreach (FinancialAccount turnoverAccount in turnoverAccounts)
            {
                turnoverAccountLookup[turnoverAccount.Identity] = true;
            }

            foreach (FinancialAccountRow accountRow in rows)
            {
                FinancialTransaction tx = accountRow.Transaction;

                if (tx.Dependency is VatReport)
                {
                    continue; // Never include previous VAT reports in new VAT reports
                }

                if (!transactionsIncludedLookup.ContainsKey(tx.Identity))
                {
                    Int64 vatInbound  = 0;
                    Int64 vatOutbound = 0;
                    Int64 turnOver    = 0;

                    transactionsIncludedLookup[accountRow.FinancialTransactionId] = true;

                    FinancialTransactionRows txRows = accountRow.Transaction.Rows;

                    foreach (FinancialTransactionRow txRow in txRows)
                    {
                        if (txRow.FinancialAccountId == vatInboundAccountId)
                        {
                            vatInbound += txRow.AmountCents;
                        }
                        else if (txRow.FinancialAccountId == vatOutboundAccountId)
                        {
                            vatOutbound += -txRow.AmountCents;  // this is a negative, so converting to positive
                        }
                        else if (turnoverAccountLookup.ContainsKey(txRow.FinancialAccountId))
                        {
                            turnOver -= txRow.AmountCents;  // turnover accounts are sign reversed, so convert to positive
                        }
                    }

                    // Add new row to the VAT report

                    AddItem(tx, turnOver, vatInbound, vatOutbound);
                }
            }
        }