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, (double)amountCents / 100.0, description, NotificationResource.ExpenseClaim_Created); // Slightly misplaced logic, but failsafer here OutboundComm.CreateNotificationFinancialValidationNeeded(organization, (double)amountCents / 100.0, NotificationResource.Receipts_Filed); SwarmopsLogEntry.Create(claimer, new ExpenseClaimFiledLogEntry(claimer /*filing person*/, claimer /*beneficiary*/, (double)amountCents / 100.0, budget, description), newClaim); return(newClaim); }
public void CheckForexProfitLoss() { // Compare current balance in native currency with the current balance in foreign currency. If off by more than 100 cents, // log a corrective transaction to account for exchange rate fluctuations. DateTime compareDateTime = DateTime.UtcNow; Int64 nativeCents = BalanceTotalCents; Money foreignCents = new Money(ForeignBalanceTotalCents.Cents, ForeignCurrency, compareDateTime); Int64 currentNativeValueOfForeignCents = foreignCents.ToCurrency(Organization.Currency).Cents; 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); } }
public static InboundInvoice Create(Organization organization, DateTime dueDate, Int64 amountCents, 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 // Create a corresponding financial transaction with rows FinancialTransaction transaction = FinancialTransaction.Create(organization.Identity, DateTime.Now, "Invoice #" + newInvoice.Identity + " from " + supplier); transaction.AddRow(organization.FinancialAccounts.DebtsInboundInvoices, -amountCents, creatingPerson); 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.CreateNotificationAttestationNeeded(budget, creatingPerson, supplier, amountCents / 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); return(newInvoice); }
private void AddRow(int financialAccountId, Int64 amountCents, int personId) { if (this.DateTime.Year <= FinancialAccount.FromIdentity(financialAccountId).Organization.Parameters.FiscalBooksClosedUntilYear) { // Recurse down into continuation transactions to write row in first nonclosed year FinancialTransaction transactionContinued = this.ContinuedTransaction; if (transactionContinued == null) { // No continuation; create one transactionContinued = FinancialTransaction.Create(this.OrganizationId, DateTime.Now, "Continued Tx #" + this.Identity.ToString()); transactionContinued.AddRow(financialAccountId, amountCents, personId); transactionContinued.Dependency = this; } else { // Recurse transactionContinued.AddRow(financialAccountId, amountCents, personId); } return; } SwarmDb.GetDatabaseForWriting().CreateFinancialTransactionRow(Identity, financialAccountId, amountCents, personId); }
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); } }
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); } }
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); }
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); } } } } } } }
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); }
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); }
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); }
public static FinancialTransaction Create(Organization organization, DateTime dateTime, string description) { return(FinancialTransaction.Create(organization.Identity, dateTime, description)); }
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); }