Example #1
0
        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);
        }
        public static AjaxCallResult ExecuteSend(int recipientTypeId, int geographyId, string mode, string subject,
                                                 string body, string dummyMail, bool live)
        {
            AuthenticationData authData = GetAuthenticationDataAndCulture();

            if (PilotInstallationIds.IsPilot(PilotInstallationIds.DevelopmentSandbox) && authData.CurrentUser.Identity == 1 && !live)
            {
                OutboundComm.CreateSandboxMail(subject, body, dummyMail);
                return(new AjaxCallResult {
                    Success = true
                });
            }
            else if (!live)
            {
                // Test mail

                OutboundComm.CreateParticipantMail(subject, body,
                                                   authData.CurrentUser.ParticipationOf(authData.CurrentOrganization), authData.CurrentUser);

                return(new AjaxCallResult {
                    Success = true
                });
            }
            else // Send live
            {
                // TODO: change resolver to match selected group

                OutboundComm.CreateParticipantMail(subject, body, authData.CurrentUser, authData.CurrentUser, authData.CurrentOrganization, Geography.FromIdentity(geographyId));
                return(new AjaxCallResult {
                    Success = true
                });
            }
        }
Example #3
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, (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);
        }
Example #4
0
        public void Deattest(Person deattester)
        {
            SwarmDb.GetDatabaseForWriting().CreateFinancialValidation(FinancialValidationType.Deattestation,
                                                                      FinancialDependencyType.CashAdvance, this.Identity, DateTime.Now, deattester.Identity, this.AmountCents / 100.0);
            SwarmDb.GetDatabaseForWriting().SetCashAdvanceAttested(this.Identity, false, Person.NobodyId);

            OutboundComm.CreateNotificationOfFinancialValidation(this.Budget, this.Person, (double)this.AmountCents / 100.0, this.Description, NotificationResource.CashAdvance_Deattested);
        }
Example #5
0
        protected void ButtonSubmit_Click(object sender, EventArgs e)
        {
            DateTime dateOfBirth = new DateTime(1800, 1, 1);  // null equivalent

            if (this.TextDateOfBirth.Text.Length > 0)
            {
                dateOfBirth = DateTime.Parse(this.TextDateOfBirth.Text);
            }

            string street = this.TextStreet1.Text;

            if (!string.IsNullOrEmpty(this.TextStreet2.Text))
            {
                street += "|" + this.TextStreet2.Text;
            }

            Person newPerson = Person.Create(this.TextName.Text, this.TextMail.Text, string.Empty, this.TextPhone.Text,
                                             street, this.TextPostal.Text, this.TextCity.Text, this.DropCountries.SelectedValue, dateOfBirth,
                                             (PersonGender)Enum.Parse(typeof(PersonGender), this.DropGenders.SelectedValue));

            DateTime            participationExpiry = Constants.DateTimeHigh;
            ParticipantMailType welcomeMailType     = ParticipantMailType.ParticipantAddedWelcome_NoExpiry;

            int participationDurationMonths = Int32.Parse(CurrentOrganization.Parameters.ParticipationDuration);

            if (participationDurationMonths < 1000)
            {
                participationExpiry = DateTime.Today.AddMonths(participationDurationMonths);
                welcomeMailType     = ParticipantMailType.ParticipantAddedWelcome;
            }

            Participation newParticipation = Participation.Create(newPerson, CurrentOrganization, participationExpiry);

            OutboundComm.CreateParticipantMail(welcomeMailType, newParticipation, CurrentUser);

            newPerson.LastLogonOrganizationId = CurrentOrganization.Identity;

            SwarmopsLogEntry logEntry = SwarmopsLog.CreateEntry(newPerson,
                                                                new Swarmops.Logic.Support.LogEntries.PersonAddedLogEntry(newParticipation, CurrentUser));

            logEntry.CreateAffectedObject(newParticipation);
            logEntry.CreateAffectedObject(CurrentUser);

            // Clear form and make way for next person

            this.TextName.Text             = string.Empty;
            this.TextStreet1.Text          = string.Empty;
            this.TextStreet2.Text          = string.Empty;
            this.TextMail.Text             = string.Empty;
            this.TextPhone.Text            = string.Empty;
            this.TextPostal.Text           = string.Empty;
            this.TextCity.Text             = string.Empty;
            this.TextDateOfBirth.Text      = string.Empty;
            this.DropGenders.SelectedValue = "Unknown";

            this.TextName.Focus();
            this.LiteralLoadAlert.Text = Resources.Pages.Swarm.AddPerson_PersonSuccessfullyRegistered;
        }
        public static AjaxCallResult SetBitcoinPayoutAddress(string bitcoinAddress)
        {
            AuthenticationData authData = GetAuthenticationDataAndCulture();

            if (authData == null)
            {
                throw new UnauthorizedAccessException();
            }

            // Remove whitespace from submitted address (whitespace will probably be entered in some cases)

            bitcoinAddress = bitcoinAddress.Replace(" ", string.Empty);

            // Remove a possible start of "bitcoincash:"

            if (bitcoinAddress.StartsWith("bitcoincash:"))
            {
                bitcoinAddress = bitcoinAddress.Substring("bitcoincash:".Length);
            }

            if (string.IsNullOrEmpty(authData.CurrentUser.BitcoinPayoutAddress))
            {
                if (!BitcoinUtility.IsValidBitcoinAddress(bitcoinAddress))
                {
                    return(new AjaxCallResult {
                        Success = false, DisplayMessage = "Invalid address"
                    });
                }

                authData.CurrentUser.BitcoinPayoutAddress        = bitcoinAddress;
                authData.CurrentUser.BitcoinPayoutAddressTimeSet = DateTime.UtcNow.ToString(CultureInfo.InvariantCulture);

                // TODO: Create notifications for CEO and for user

                NotificationCustomStrings strings = new NotificationCustomStrings();
                strings["BitcoinAddress"] = bitcoinAddress;

                OutboundComm userNotify = OutboundComm.CreateNotification(authData.CurrentOrganization,
                                                                          "BitcoinPayoutAddress_Set", strings, People.FromSingle(authData.CurrentUser));

                strings["ConcernedPersonName"] = authData.CurrentUser.Canonical;

                OutboundComm adminNotify = OutboundComm.CreateNotification(authData.CurrentOrganization,
                                                                           "BitcoinPayoutAddress_Set_OfficerNotify", strings); // will send to admins of org as no people specified

                return(new AjaxCallResult {
                    Success = true
                });
            }
            else
            {
                // If the address is already set

                return(new AjaxCallResult {
                    Success = false, DisplayMessage = "Address already set"
                });
            }
        }
Example #7
0
        public void Approve(Person approvingPerson)
        {
            SwarmDb.GetDatabaseForWriting().CreateFinancialValidation(FinancialValidationType.Approval,
                                                                      FinancialDependencyType.CashAdvance, Identity, DateTime.UtcNow, approvingPerson.Identity, AmountCents / 100.0);
            SwarmDb.GetDatabaseForWriting().SetCashAdvanceAttested(Identity, true, approvingPerson.Identity);

            OutboundComm.CreateNotificationOfFinancialValidation(Budget, Person, AmountCents / 100.0, Description,
                                                                 NotificationResource.CashAdvance_Approved);
        }
Example #8
0
        public void RetractApproval(Person retractingPerson)
        {
            SwarmDb.GetDatabaseForWriting().CreateFinancialValidation(FinancialValidationType.UndoApproval,
                                                                      FinancialDependencyType.CashAdvance, Identity, DateTime.UtcNow, retractingPerson.Identity, AmountCents / 100.0);
            SwarmDb.GetDatabaseForWriting().SetCashAdvanceAttested(Identity, false, Person.NobodyId);

            OutboundComm.CreateNotificationOfFinancialValidation(Budget, Person, AmountCents / 100.0, Description,
                                                                 NotificationResource.CashAdvance_ApprovalRetracted);
        }
Example #9
0
        public static void VerifyBitcoinHotWallet()
        {
            // This must only be run from the backend

            if (HttpContext.Current != null)
            {
                throw new InvalidOperationException("Checking root keys cannot be done from the frontend");
            }

            // Make sure there's always a private hotwallet root, regardless of whether it's used or not

            if (!File.Exists(SystemSettings.EtcFolder + Path.DirectorySeparatorChar + "hotwallet"))
            {
                ExtKey privateRoot = new ExtKey();
                File.WriteAllText(SystemSettings.EtcFolder + Path.DirectorySeparatorChar + "hotwallet",
                                  privateRoot.GetWif(Network.Main).ToWif(), Encoding.ASCII);
                File.WriteAllText(
                    SystemSettings.EtcFolder + Path.DirectorySeparatorChar + "hotwallet-created-" +
                    DateTime.UtcNow.ToString("yyyy-MM-dd--HH-mm-ss--fff.backup"),
                    privateRoot.GetWif(Network.Main).ToWif(), Encoding.ASCII);  // an extra backup

                if (String.IsNullOrEmpty(Persistence.Key["BitcoinHotPublicRoot"]))
                {
                    Persistence.Key["BitcoinHotPublicRoot"] = privateRoot.Neuter().GetWif(Network.Main).ToWif();
                }
            }
            else
            {
                // The file exists. Does the database have the hotwallet public root?

                if (Persistence.Key["BitcoinHotPublicRoot"].Length < 3)
                {
                    // No, it has disappeared, which can happen for a few bad reasons

                    Persistence.Key["BitcoinHotPublicRoot"] =
                        BitcoinHotPrivateRoot.Neuter().GetWif(Network.Main).ToWif();
                    if (!PilotInstallationIds.IsPilot(PilotInstallationIds.DevelopmentSandbox))
                    {
                        // TODO: Log some sort of exception (the sandbox db is reset every night, so it's ok to lose the public key from there)
                    }
                }

                // Is the hotwallet public root equal to the private root, while in production environment?

                // ReSharper disable once RedundantCheckBeforeAssignment
                if (Persistence.Key["BitcoinHotPublicRoot"] !=
                    BitcoinHotPrivateRoot.Neuter().GetWif(Network.Main).ToWif() && !Debugger.IsAttached)
                {
                    // SERIOUS CONDITION - the public root key did not match the private root key. This needs to be logged somewhere.
                    OutboundComm.CreateNotification(NotificationResource.System_PublicRootReset);

                    // Reset it
                    Persistence.Key["BitcoinHotPublicRoot"] =
                        BitcoinHotPrivateRoot.Neuter().GetWif(Network.Main).ToWif();
                }
            }
        }
Example #10
0
        public static void UpgradeSchemata()
        {
            int    currentDbVersion  = SwarmDb.DbVersion;
            int    expectedDbVersion = SwarmDb.DbVersionExpected;
            string sql;
            bool   upgraded = false;

            while (currentDbVersion < expectedDbVersion)
            {
                currentDbVersion++;

                string fileName = String.Format("http://packages.swarmops.com/schemata/upgrade-{0:D4}.sql",
                                                currentDbVersion);

                using (WebClient client = new WebClient())
                {
                    sql = client.DownloadString(fileName);
                }

                string[] sqlCommands = sql.Split('#');
                // in the file, the commands are split by a single # sign. (Semicolons are an integral part of storedprocs, so they can't be used.)

                foreach (string sqlCommand in sqlCommands)
                {
                    try
                    {
                        SwarmDb.GetDatabaseForAdmin().ExecuteAdminCommand(sqlCommand.Trim());
                    }
                    catch (MySqlException exception)
                    {
                        SwarmDb.GetDatabaseForWriting()
                        .CreateExceptionLogEntry(DateTime.UtcNow, "DatabaseUpgrade", exception);

                        // Continue processing after logging error.
                        // TODO: Throw and abort? Tricky decision
                    }
                }

                upgraded = true;
                SwarmDb.GetDatabaseForWriting()
                .SetKeyValue("DbVersion", currentDbVersion.ToString(CultureInfo.InvariantCulture));
                // Increment after each successful run
            }

            if (upgraded)
            {
                try
                {
                    OutboundComm.CreateNotification(null, NotificationResource.System_DatabaseSchemaUpgraded);
                }
                catch (ArgumentException)
                {
                    // this is ok - if we're in the install process, person 1 or other notification targets won't exist yet
                }
            }
        }
Example #11
0
        public void Deattest(Person deattester)
        {
            Attested = false;
            SwarmDb.GetDatabaseForWriting().CreateFinancialValidation(FinancialValidationType.Deattestation,
                                                                      FinancialDependencyType.ExpenseClaim, Identity,
                                                                      DateTime.UtcNow, deattester.Identity, (double)Amount);

            OutboundComm.CreateNotificationOfFinancialValidation(Budget, Claimer, AmountCents / 100.0, Description,
                                                                 NotificationResource.ExpenseClaim_Deattested);
        }
Example #12
0
        public void RetractApproval(Person retractingPerson)
        {
            Attested = false;
            SwarmDb.GetDatabaseForWriting().CreateFinancialValidation(FinancialValidationType.UndoApproval,
                                                                      FinancialDependencyType.ExpenseClaim, Identity,
                                                                      DateTime.UtcNow, retractingPerson.Identity, (double)Amount);

            OutboundComm.CreateNotificationOfFinancialValidation(Budget, Claimer, AmountCents / 100.0, Description,
                                                                 NotificationResource.ExpenseClaim_ApprovalRetracted);
        }
Example #13
0
        public void Deattest(Person deattester)
        {
            SwarmDb.GetDatabaseForWriting().SetExpenseClaimAttested(this.Identity, false);
            SwarmDb.GetDatabaseForWriting().CreateFinancialValidation(FinancialValidationType.Deattestation,
                                                                      FinancialDependencyType.ExpenseClaim, this.Identity,
                                                                      DateTime.Now, deattester.Identity, (double)this.Amount);
            base.Attested = false;

            OutboundComm.CreateNotificationOfFinancialValidation(this.Budget, this.Claimer, (double)this.AmountCents / 100.0, this.Description, NotificationResource.ExpenseClaim_Deattested);
        }
Example #14
0
        public void DenyApproval(Person denyingPerson, string reason)
        {
            SwarmDb.GetDatabaseForWriting().CreateFinancialValidation(FinancialValidationType.Kill,
                                                                      FinancialDependencyType.CashAdvance, Identity, DateTime.UtcNow, denyingPerson.Identity, AmountCents / 100.0);

            OutboundComm.CreateNotificationOfFinancialValidation(Budget, Person, AmountCents / 100.0, Description,
                                                                 NotificationResource.CashAdvance_Denied, reason);
            Attested = false;
            Open     = false;
        }
Example #15
0
        public void Validate(Person validator)
        {
            SwarmDb.GetDatabaseForWriting().SetExpenseClaimValidated(this.Identity, true);
            SwarmDb.GetDatabaseForWriting().CreateFinancialValidation(FinancialValidationType.Validation,
                                                                      FinancialDependencyType.ExpenseClaim, this.Identity,
                                                                      DateTime.Now, validator.Identity, (double)this.Amount);
            base.Validated = true;

            OutboundComm.CreateNotificationOfFinancialValidation(this.Budget, this.Claimer, (double)this.AmountCents / 100.0, this.Description, NotificationResource.ExpenseClaim_Validated);
        }
Example #16
0
        public void Devalidate(Person devalidator)
        {
            SwarmDb.GetDatabaseForWriting().SetExpenseClaimValidated(Identity, false);
            SwarmDb.GetDatabaseForWriting().CreateFinancialValidation(FinancialValidationType.Devalidation,
                                                                      FinancialDependencyType.ExpenseClaim, Identity,
                                                                      DateTime.Now, devalidator.Identity, (double)Amount);
            base.Validated = false;

            OutboundComm.CreateNotificationOfFinancialValidation(Budget, Claimer, AmountCents / 100.0, Description,
                                                                 NotificationResource.ExpenseClaim_Devalidated);
        }
Example #17
0
        public void Attest(Person attester)
        {
            SwarmDb.GetDatabaseForWriting().SetExpenseClaimAttested(Identity, true);
            SwarmDb.GetDatabaseForWriting().CreateFinancialValidation(FinancialValidationType.Attestation,
                                                                      FinancialDependencyType.ExpenseClaim, Identity,
                                                                      DateTime.Now, attester.Identity, (double)Amount);
            base.Attested = true;

            OutboundComm.CreateNotificationOfFinancialValidation(Budget, Claimer, AmountCents / 100.0, Description,
                                                                 NotificationResource.ExpenseClaim_Attested);
        }
Example #18
0
        public void Attest(Person attester)
        {
            Attested = true;
            SwarmDb.GetDatabaseForWriting().CreateFinancialValidation(FinancialValidationType.Attestation,
                                                                      FinancialDependencyType.ExpenseClaim, Identity,
                                                                      DateTime.UtcNow, attester.Identity, (double)Amount);

            OutboundComm.CreateNotificationOfFinancialValidation(Budget, Claimer, AmountCents / 100.0, Description,
                                                                 NotificationResource.ExpenseClaim_Attested);

            UpdateFinancialTransaction(attester); // will re-enable tx if it was zeroed out earlier
        }
Example #19
0
        public void Resolve(OutboundComm comm)
        {
            Organization organization = Organization.FromIdentity(OrganizationId);
            Geography    geography    = Geography.FromIdentity(GeographyId);

            People allParticipants = People.FromOrganizationAndGeography(organization, geography);

            foreach (Person person in allParticipants)
            {
                // Todo: Check if still participant? Check if declined correspondence?

                comm.AddRecipient(person);
            }
        }
Example #20
0
        public void DenyAttestation(Person denyingPerson, string reason)
        {
            Attested = false;
            Open     = false;

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

            OutboundComm.CreateNotificationOfFinancialValidation(Budget, Claimer, AmountCents / 100.0, Description,
                                                                 NotificationResource.ExpenseClaim_Denied, reason);

            UpdateFinancialTransaction(denyingPerson); // will zero out transaction
        }
Example #21
0
        public static CashAdvance Create(Organization organization, Person forPerson, Person createdByPerson, Int64 amountCents, FinancialAccount budget, string description)
        {
            CashAdvance newAdvance = FromIdentityAggressive(SwarmDb.GetDatabaseForWriting().CreateCashAdvance(forPerson.Identity,
                                                                                                              createdByPerson.Identity,
                                                                                                              organization.Identity,
                                                                                                              budget.Identity, amountCents,
                                                                                                              description));

            OutboundComm.CreateNotificationAttestationNeeded(budget, forPerson, string.Empty, (double)amountCents / 100.0, description, NotificationResource.CashAdvance_Requested); // Slightly misplaced logic, but failsafer here
            SwarmopsLogEntry.Create(forPerson,
                                    new CashAdvanceRequestedLogEntry(createdByPerson, forPerson, (double)amountCents / 100.0, budget, description),
                                    newAdvance);

            return(newAdvance);
        }
Example #22
0
        internal static ICommsResolver FindResolver(OutboundComm comm)
        {
            // Resolve recipients

            ResolverEnvelope resolverEnvelope = ResolverEnvelope.FromXml(comm.ResolverDataXml);

            // Create the resolver via reflection of the static FromXml method

            Assembly assembly = typeof(ResolverEnvelope).Assembly;

            Type payloadType = assembly.GetType(resolverEnvelope.ResolverClass);
            var  methodInfo  = payloadType.GetMethod("FromXml", BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy);

            return((ICommsResolver)(methodInfo.Invoke(null, new object[] { resolverEnvelope.ResolverDataXml })));
        }
Example #23
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
        }
Example #24
0
        protected void ButtonSubmit_Click(object sender, EventArgs e)
        {
            DateTime dateOfBirth = new DateTime(1800, 1, 1);  // null equivalent

            if (this.TextDateOfBirth.Text.Length > 0)
            {
                dateOfBirth = DateTime.Parse(this.TextDateOfBirth.Text);
            }

            string street = this.TextStreet1.Text;

            if (!string.IsNullOrEmpty(this.TextStreet2.Text))
            {
                street += "|" + this.TextStreet2.Text;
            }

            Person newPerson = Person.Create(this.TextName.Text, this.TextMail.Text, string.Empty, this.TextPhone.Text,
                                             street, this.TextPostal.Text, this.TextCity.Text, this.DropCountries.SelectedValue, dateOfBirth,
                                             (PersonGender)Enum.Parse(typeof(PersonGender), this.DropGenders.SelectedValue));

            Membership newMembership = Membership.Create(newPerson, CurrentOrganization, DateTime.Today.AddYears(1));

            OutboundComm.CreateMembershipLetter(ParticipantMailType.MemberAddedWelcome, newMembership, CurrentUser);

            SwarmopsLogEntry logEntry = SwarmopsLog.CreateEntry(newPerson,
                                                                new Swarmops.Logic.Support.LogEntries.PersonAddedLogEntry(newMembership, CurrentUser));

            logEntry.CreateAffectedObject(newMembership);
            logEntry.CreateAffectedObject(CurrentUser);

            // Clear form and make way for next person

            this.TextName.Text             = string.Empty;
            this.TextStreet1.Text          = string.Empty;
            this.TextStreet2.Text          = string.Empty;
            this.TextMail.Text             = string.Empty;
            this.TextPhone.Text            = string.Empty;
            this.TextPostal.Text           = string.Empty;
            this.TextCity.Text             = string.Empty;
            this.TextDateOfBirth.Text      = string.Empty;
            this.DropGenders.SelectedValue = "Unknown";

            this.TextName.Focus();
            this.LiteralLoadAlert.Text = Resources.Pages.Swarm.AddPerson_PersonSuccessfullyRegistered;
        }
        public static bool RequestTicket(string mailAddress)
        {
            mailAddress = mailAddress.Trim();

            if (string.IsNullOrEmpty(mailAddress))
            {
                return(false); // this is the only case when we return false: a _syntactically_invalid_ address
            }

            People concernedPeople = People.FromMail(mailAddress);  // Should result in exactly 1

            if (concernedPeople.Count != 1)
            {
                return(true); // TODO: Prevent registration with duplicate mail addy, or this will cause problems down the road
            }

            Person concernedPerson = concernedPeople[0];

            if (!string.IsNullOrEmpty(concernedPerson.BitIdAddress))
            {
                // Cannot reset password - two factor auth is enabled. Manual intervention required.

                OutboundComm.CreateSecurityNotification(concernedPerson, null, null, string.Empty,
                                                        NotificationResource.Password_CannotReset2FA);
                return(true); // still returning true - the fail info is in mail only
            }


            string resetTicket = SupportFunctions.GenerateSecureRandomKey(16);

            resetTicket = resetTicket.Substring(0, 21);                                                                                   // We're using a 21-character (84-bit) key mostly for UI consistency with the ticket sent in mail, and it's secure enough

            concernedPerson.ResetPasswordTicket = DateTime.UtcNow.AddHours(1).ToString(CultureInfo.InvariantCulture) + "," + resetTicket; // Adds expiry - one hour

            OutboundComm.CreateSecurityNotification(concernedPerson, null, null, resetTicket,
                                                    NotificationResource.Password_ResetOnRequest);

            SwarmopsLog.CreateEntry(null,
                                    new PasswordResetRequestLogEntry(concernedPerson, SupportFunctions.GetRemoteIPAddressChain()));

            return(true);
        }
Example #26
0
        internal static void Run()
        {
            OutboundComms comms = OutboundComms.GetOpen();

            foreach (OutboundComm comm in comms)
            {
                BotLog.Write(1, "CommsTx", "OutboundComm #" + comm.Identity.ToString("N0"));

                if (!comm.Resolved)
                {
                    BotLog.Write(2, "CommsTx", "--resolving");

                    ICommsResolver resolver = FindResolver(comm);
                    resolver.Resolve(comm);
                    comm.Resolved = true;

                    int recipientCount = comm.Recipients.Count;
                    BotLog.Write(2, "CommsTx", "--resolved to " + recipientCount.ToString("N0") + " recipients");

                    if (recipientCount > 1 && comm.SenderPersonId != 0)
                    {
                        // "Your message has been queued for delivery and the recipients have been resolved.
                        // Your mail will be sent to, or be attempted to sent to, [RecipientCount] people in [Geography] in [OrganizationName]."

                        NotificationStrings       notifyStrings = new NotificationStrings();
                        NotificationCustomStrings customStrings = new NotificationCustomStrings();
                        notifyStrings[NotificationString.OrganizationName] = Organization.FromIdentity(comm.OrganizationId).Name;
                        customStrings["RecipientCount"] = comm.Recipients.Count.ToString("N0");
                        if (resolver is IHasGeography)
                        {
                            customStrings["GeographyName"] = ((IHasGeography)resolver).Geography.Localized;
                        }
                        OutboundComm.CreateNotification(Organization.FromIdentity(comm.OrganizationId),
                                                        NotificationResource.OutboundComm_Resolved, notifyStrings, customStrings,
                                                        People.FromSingle(Person.FromIdentity(comm.SenderPersonId)));
                    }

                    comm.StartTransmission();

                    continue; // continue is not strictly necessary; could continue processing the same OutboundComm after resolution
                }

                if (comm.TransmitterClass != "Swarmops.Utility.Communications.CommsTransmitterMail")
                {
                    throw new NotImplementedException();
                }

                ICommsTransmitter transmitter = new CommsTransmitterMail();

                const int batchSize = 1000;

                OutboundCommRecipients recipients = comm.GetRecipientBatch(batchSize);
                PayloadEnvelope        envelope   = PayloadEnvelope.FromXml(comm.PayloadXml);

                BotLog.Write(2, "CommsTx", "--transmitting to " + recipients.Count.ToString("N0") + " recipients");

                foreach (OutboundCommRecipient recipient in recipients)
                {
                    try
                    {
                        transmitter.Transmit(envelope, recipient.Person);
                        recipient.CloseSuccess();
                    }
                    catch (OutboundCommTransmitException e)
                    {
                        recipient.CloseFailed(e.Description);
                    }
                }

                if (recipients.Count < batchSize) // Was this the last batch?
                {
                    comm.Open = false;

                    BotLog.Write(2, "CommsTx", "--closing");

                    OutboundComm reloadedComm = OutboundComm.FromIdentity(comm.Identity);
                    // active object doesn't update as we get results, so need to load
                    // from database to get final counts of successes and fails

                    if (comm.RecipientCount > 1 && comm.SenderPersonId != 0)
                    {
                        BotLog.Write(2, "CommsTx", "--notifying");

                        ICommsResolver resolver = FindResolver(comm);

                        // "Your message to [GeographyName] has been sent to all scheduled recipients. Of the [RecipientCount] planned recipients,
                        // [RecipientsSuccess] succeeded from Swarmops' horizon. (These can fail later for a number of reasons, from broken
                        // computers to hospitalized recipients.) Time spent transmitting: [TransmissionTime]."

                        NotificationStrings       notifyStrings = new NotificationStrings();
                        NotificationCustomStrings customStrings = new NotificationCustomStrings();
                        notifyStrings[NotificationString.OrganizationName] =
                            Organization.FromIdentity(comm.OrganizationId).Name;
                        customStrings["RecipientCount"]    = reloadedComm.RecipientCount.ToString("N0");
                        customStrings["RecipientsSuccess"] = reloadedComm.RecipientsSuccess.ToString("N0");

                        TimeSpan resolveTime  = comm.StartTransmitDateTime - comm.CreatedDateTime;
                        TimeSpan transmitTime = comm.ClosedDateTime - comm.StartTransmitDateTime;
                        TimeSpan totalTime    = resolveTime + transmitTime;

                        customStrings["TransmissionTime"] = FormatTimespan(transmitTime);
                        customStrings["ResolvingTime"]    = FormatTimespan(resolveTime);
                        customStrings["TotalTime"]        = FormatTimespan(totalTime);
                        if (resolver is IHasGeography)
                        {
                            customStrings["GeographyName"] = ((IHasGeography)resolver).Geography.Localized;
                        }
                        OutboundComm.CreateNotification(Organization.FromIdentity(comm.OrganizationId),
                                                        NotificationResource.OutboundComm_Sent, notifyStrings, customStrings,
                                                        People.FromSingle(Person.FromIdentity(comm.SenderPersonId)));
                    }
                }
            }
        }
Example #27
0
        public static void TestMultisigPayout()
        {
            throw new InvalidOperationException("This function is only for testing purposes. It pays real money. Don't use except for dev/test.");

            // disable "code unreachable" warning for this code
            // ReSharper disable once CSharpWarnings::CS0162
            #pragma warning disable 162,219
            Organization organization = Organization.Sandbox; // a few testing cents here in test environment

            string bitcoinTestAddress = "3KS6AuQbZARSvqnaHoHfL1Xhm3bTLFAzoK";

            // Make a small test payment to a multisig address

            TransactionBuilder txBuilder = null; // TODO TODO TODO TODO  new TransactionBuilder();
            Int64 satoshis = new Money(100, Currency.FromCode("SEK")).ToCurrency(Currency.BitcoinCore).Cents;
            BitcoinTransactionInputs inputs      = null;
            Int64 satoshisMaximumAnticipatedFees = BitcoinUtility.GetRecommendedFeePerThousandBytesSatoshis(BitcoinChain.Cash) * 20; // assume max 20k transaction size

            try
            {
                inputs = BitcoinUtility.GetInputsForAmount(organization, satoshis + satoshisMaximumAnticipatedFees);
            }
            catch (NotEnoughFundsException)
            {
                Debugger.Break();
            }

            // If we arrive at this point, the previous function didn't throw, and we have enough money. 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);

            // Add the test payment

            if (bitcoinTestAddress.StartsWith("1")) // regular address
            {
                txBuilder = txBuilder.Send(new BitcoinPubKeyAddress(bitcoinTestAddress),
                                           new Satoshis(satoshis));
            }
            else if (bitcoinTestAddress.StartsWith("3")) // multisig
            {
                txBuilder = txBuilder.Send(new BitcoinScriptAddress(bitcoinTestAddress, Network.Main),
                                           new Satoshis(satoshis));
            }
            else
            {
                throw new InvalidOperationException("Unhandled address case");
            }
            satoshisUsed += satoshis;

            // 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(BitcoinChain.Cash);

            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);
            }

            // Restore "code unreachable", "unsued var" warnings
            #pragma warning restore 162,219

            // This puts the ledger out of sync, so only do this on Sandbox for various small-change (cents) testing
        }
Example #28
0
        public static AjaxCallResult SignupParticipant(string name, int organizationId, string mail, string password, string phone,
                                                       string street1, string street2, string postalCode, string city, string countryCode, string dateOfBirth,
                                                       int geographyId, bool activist, PersonGender gender, int[] positionIdsVolunteer)
        {
            CommonV5.CulturePreInit(HttpContext.Current.Request);  // Set culture, for date parsing

            if (geographyId == 0)
            {
                geographyId = Geography.RootIdentity; // if geo was undetermined, set it to "Global"
            }

            Organization organization      = Organization.FromIdentity(organizationId);
            DateTime     parsedDateOfBirth = new DateTime(1800, 1, 1); // Default if unspecified

            if (dateOfBirth.Length > 0)
            {
                parsedDateOfBirth = DateTime.Parse(dateOfBirth);
            }

            Person newPerson = Person.Create(name, mail, password, phone, street1 + "\n" + street2.Trim(), postalCode,
                                             city, countryCode, parsedDateOfBirth, gender);
            Participation participation = newPerson.AddParticipation(organization, DateTime.UtcNow.AddYears(1));    // TODO: set duration from organization settings of Participantship

            // TODO: SEND NOTIFICATIONS

            // Log the signup

            SwarmopsLog.CreateEntry(newPerson, new PersonAddedLogEntry(participation, newPerson));

            // Create notification

            OutboundComm.CreateParticipantNotification(newPerson, newPerson, organization,
                                                       NotificationResource.Participant_Signup);

            // Add the bells and whistles

            if (activist)
            {
                newPerson.CreateActivist(false, false);
            }

            if (positionIdsVolunteer.Length > 0)
            {
                Volunteer volunteer = newPerson.CreateVolunteer();
                foreach (int positionId in positionIdsVolunteer)
                {
                    Position position = Position.FromIdentity(positionId);
                    volunteer.AddPosition(position);
                    SwarmopsLog.CreateEntry(newPerson, new VolunteerForPositionLogEntry(newPerson, position));
                }
            }

            newPerson.LastLogonOrganizationId = organizationId;

            // Create a welcome message to the Dashboard

            HttpContext.Current.Response.AppendCookie(new HttpCookie("DashboardMessage", CommonV5.JavascriptEscape(String.Format(Resources.Pages.Public.Signup_DashboardMessage, organization.Name))));

            // Set authentication cookie, which will log the new person in using the credentials just given

            FormsAuthentication.SetAuthCookie(Authority.FromLogin(newPerson).ToEncryptedXml(), true);

            AjaxCallResult result = new AjaxCallResult {
                Success = true
            };

            return(result);
        }
Example #29
0
        public static void UpgradeSchemata()
        {
            int    currentDbVersion  = SwarmDb.DbVersion;
            int    expectedDbVersion = SwarmDb.DbVersionExpected;
            string sql;
            bool   upgraded = false;

            if (currentDbVersion < expectedDbVersion)
            {
                Console.WriteLine("Swarmops: Current DB version is {0}, but expected is {1}. A schema upgrade will take place.", currentDbVersion, expectedDbVersion);
            }

            while (currentDbVersion < expectedDbVersion)
            {
                currentDbVersion++;

                Console.Write("Schema {0} diff: Fetching...", currentDbVersion);

                string fileName = String.Format("http://packages.swarmops.com/schemata/upgrade-{0:D4}.sql",
                                                currentDbVersion);

                try
                {
                    using (WebClient client = new WebClient())
                    {
                        sql = client.DownloadString(fileName);
                    }
                }
                catch (Exception outerException)
                {
                    Console.WriteLine(" trying fallback...");

                    // Because Mono installs with an insufficient certificate store, we must disable certificate checking before accessing github

                    SupportFunctions.DisableSslCertificateChecks();

                    fileName = String.Format("https://raw.githubusercontent.com/Swarmops/Swarmops/master/Database/Schemata/upgrade-{0:D4}.sql",
                                             currentDbVersion);

                    try
                    {
                        using (WebClient client = new WebClient())
                        {
                            sql = client.DownloadString(fileName);
                        }
                    }
                    catch (Exception middleException)
                    {
                        try
                        {
                            OutboundComm.CreateNotification(null, NotificationResource.System_DatabaseUpgradeFailed);
                        }
                        catch (ArgumentException)
                        {
                            // if this happens during setup:

                            throw new Exception("Failed fetching upgrade packages:\r\n" + middleException.ToString() +
                                                "\r\n" + outerException.ToString());
                        }
                        Console.WriteLine(" FAILED! Aborting.");

                        return;
                    }
                }

                string[] sqlCommands = sql.Split('#');
                // in the file, the commands are split by a single # sign. (Semicolons are an integral part of storedprocs, so they can't be used.)

                Console.Write(" applying...");

                foreach (string sqlCommand in sqlCommands)
                {
                    try
                    {
                        string trimmedCommand = sqlCommand.Trim().TrimEnd(';').Trim(); // removes whitespace first, then any ; at the end (if left in by mistake)

                        if (!String.IsNullOrWhiteSpace(trimmedCommand))
                        {
                            SwarmDb.GetDatabaseForAdmin().ExecuteAdminCommand(trimmedCommand);
                        }
                    }
                    catch (MySqlException exception)
                    {
                        SwarmDb.GetDatabaseForWriting()
                        .CreateExceptionLogEntry(DateTime.UtcNow, "DatabaseUpgrade",
                                                 new Exception(string.Format("Exception upgrading to Db{0:D4}", currentDbVersion),
                                                               exception));

                        Console.Write(" EXCEPTION (see log)!");
                        // Continue processing after logging error.
                        // TODO: Throw and abort? Tricky decision
                    }
                }

                upgraded = true;
                SwarmDb.GetDatabaseForWriting()
                .SetKeyValue("DbVersion", currentDbVersion.ToString(CultureInfo.InvariantCulture));
                // Increment after each successful run

                Console.WriteLine(" done.");
            }

            if (upgraded)
            {
                Console.WriteLine("Swarmops database schema upgrade completed.\r\n");
                try
                {
                    OutboundComm.CreateNotification(null, NotificationResource.System_DatabaseSchemaUpgraded);
                }
                catch (ArgumentException)
                {
                    // this is ok - if we're in the install process, person 1 or other notification targets won't exist yet
                }
            }
        }
Example #30
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);
            }
        }