예제 #1
0
    def test_only_generates_outgoing_payments_if_incoming_payments_received(
            self):
        self.setup_one_incoming_payment({test_data.u1: 250})
        self.setup_one_missing_payment({test_data.u2: 75})

        self.add_donation_proportions([
            DonationProportion.new(user=test_data.u1,
                                   charity=test_data.c1,
                                   amount=100),
            DonationProportion.new(user=test_data.u1,
                                   charity=test_data.c2,
                                   amount=150),
            DonationProportion.new(user=test_data.u2,
                                   charity=test_data.c1,
                                   amount=75),
        ])

        expected = set([
            OutgoingPayment(charity=test_data.c1,
                            amount_GBPennies=100,
                            status=OutgoingPaymentState.DISPLAYED),
            OutgoingPayment(charity=test_data.c2,
                            amount_GBPennies=150,
                            status=OutgoingPaymentState.DISPLAYED),
        ])

        self.assertEquals(self.payment_repo.get_pending_outgoing_payments(),
                          expected)
예제 #2
0
    def test_aggregates_charity_totals_across_users(self):
        self.setup_one_incoming_payment({test_data.u1: 250, test_data.u2: 75})

        self.add_donation_proportions([
            DonationProportion.new(user=test_data.u1,
                                   charity=test_data.c1,
                                   amount=100),
            DonationProportion.new(user=test_data.u1,
                                   charity=test_data.c2,
                                   amount=150),
            DonationProportion.new(user=test_data.u2,
                                   charity=test_data.c1,
                                   amount=75),
        ])

        expected = set([
            OutgoingPayment(charity=test_data.c1,
                            amount_GBPennies=175,
                            status=OutgoingPaymentState.DISPLAYED),
            OutgoingPayment(charity=test_data.c2,
                            amount_GBPennies=150,
                            status=OutgoingPaymentState.DISPLAYED),
        ])

        self.assertEquals(self.payment_repo.get_pending_outgoing_payments(),
                          expected)
예제 #3
0
    def test_generates_outgoing_payments_despite_mismatches(self):
        self.setup_one_incoming_payment({
            test_data.u1: 100,
            test_data.u2: 75,
        })

        self.add_donation_proportions([
            DonationProportion.new(user=test_data.u1,
                                   charity=test_data.c1,
                                   amount=100),
        ])

        expected = set([
            OutgoingPayment(charity=test_data.c1,
                            amount_GBPennies=100,
                            status=OutgoingPaymentState.DISPLAYED),
        ])
        expected_notifications = set([
            AmountMismatch(user=test_data.u2,
                           incoming_GBPennies=75,
                           outgoing=[]),
        ])

        amount_mismatch_notifier = AccumulatingMismatchNotifier()
        self.assertEquals(
            self.payment_repo.get_pending_outgoing_payments(
                amount_mismatch_notifiers=[amount_mismatch_notifier]),
            expected)
        self.assertEquals(amount_mismatch_notifier.accumulated,
                          expected_notifications)
예제 #4
0
    def get_pending_outgoing_payments(self, amount_mismatch_notifiers=None):
        """Gets outgoing payments for which incoming payments have been received (i.e. which are ready to be sent to charities).

        As a side effect, notifies any passed amount_mismatch_notifiers of any incoming/outgoing payment mismatches, i.e. incoming payments with insufficient outgoing payments pending, and outgoing payments with insufficient incoming payments pending.
        """
        if amount_mismatch_notifiers is None:
            amount_mismatch_notifiers = []

        all_donation_proportions = self.donation_proportion_repository.get_donation_proportions()

        donation_proportions_by_user = self._index(lambda dp: dp.user, all_donation_proportions)

        incoming_payments = self.get_incoming_payments()
        incoming_payments_by_user = self._index(lambda ip: ip.user, incoming_payments)

        users_to_payment_amounts = {}
        for user in set(donation_proportions_by_user.keys()).union(set(incoming_payments_by_user.keys())):
            one_user_incoming_payments = incoming_payments_by_user[user] if user in incoming_payments_by_user else []
            amount_paid_in = reduce(lambda total, ip: total + ip.amount_GBPennies,
                                    one_user_incoming_payments,
                                    0)
            amount_to_pay_out = user.donation_amount
            one_user_donation_proportions = donation_proportions_by_user[user] if user in donation_proportions_by_user else []
            total_proportions = reduce(lambda total, dp: total + dp.amount,
                                       one_user_donation_proportions,
                                       0)
            users_to_payment_amounts[user] = (amount_paid_in, amount_to_pay_out, total_proportions)

        problem_users = [user for user in users_to_payment_amounts.keys() if users_to_payment_amounts[user][0] != users_to_payment_amounts[user][1] or users_to_payment_amounts[user][2] <= 0]

        charity_amounts = {}
        for (user, donation_proportions) in donation_proportions_by_user.items():
            if user in problem_users:
                continue
            for dp in donation_proportions:
                (_, amount_to_pay_out, total_proportions) = users_to_payment_amounts[user]
                amount = amount_to_pay_out * dp.amount / total_proportions
                charity_amounts[dp.charity] = charity_amounts.setdefault(dp.charity, 0) + amount

        outgoing_payments = set()
        for (charity, amount_GBPennies) in charity_amounts.items():
            outgoing_payment = OutgoingPayment.new(charity=charity, amount_GBPennies=amount_GBPennies, status=OutgoingPaymentState.DISPLAYED)
            outgoing_payment.put()
            outgoing_payments.add(outgoing_payment)

        self.notify_mismatches(amount_mismatch_notifiers, problem_users, incoming_payments_by_user, donation_proportions_by_user, users_to_payment_amounts)

        return outgoing_payments
예제 #5
0
    def notify_mismatches(self, amount_mismatch_notifiers, users, incoming_payments_by_user, donation_proportions_by_user, users_to_payment_amounts):
        """Notifies all amount_mismatch_notifiers of all amount mismatches.

        Args:
        amount_mismatch_notifiers: [amount_mismatch_notifiers]
        invalid_payments_by_user: {user: [OutgoingPayment]}
        not_paid_up_users: {user: incoming_amount}
        """
        # TODO: This logic really shouldn't live here
        mismatches = []
        for user in users:
            (amount_paid_in, amount_to_pay_out, total_proportions) = users_to_payment_amounts[user]
            incoming_payments_by_user.setdefault(user, [])
            donation_proportions = donation_proportions_by_user[user] if user in donation_proportions_by_user else []
            outgoing_payments = map(lambda dp: OutgoingPayment.new(charity=dp.charity, amount_GBPennies=amount_to_pay_out * dp.amount / total_proportions, status=OutgoingPaymentState.VALUE_MISMATCH),
                                    donation_proportions)
            mismatches.append(AmountMismatch(user=user, incoming_GBPennies=amount_paid_in, outgoing=outgoing_payments))


        for notifier in amount_mismatch_notifiers:
            for mismatch in mismatches:
                notifier.notify(mismatch)
예제 #6
0
    def test_logs_and_notifies_incoming_and_outgoing_mismatches(self):
        self.setup_one_mismatched_incoming_payment({
            test_data.u1: (250, 249),
            test_data.u2: (250, 251),
            test_data.u4: (200, 200),
            test_data.u5: (75, 75),
        })
        self.setup_one_missing_payment({test_data.u3: 10})

        self.add_donation_proportions([
            DonationProportion.new(user=test_data.u1,
                                   charity=test_data.c1,
                                   amount=100),
            DonationProportion.new(user=test_data.u1,
                                   charity=test_data.c2,
                                   amount=150),
            DonationProportion.new(user=test_data.u2,
                                   charity=test_data.c1,
                                   amount=250),
            DonationProportion.new(user=test_data.u3,
                                   charity=test_data.c1,
                                   amount=10),
            DonationProportion.new(user=test_data.u5,
                                   charity=test_data.c1,
                                   amount=75),
        ])

        amount_mismatch_notifier = AccumulatingMismatchNotifier()
        self.payment_repo.get_pending_outgoing_payments(
            amount_mismatch_notifiers=[amount_mismatch_notifier])

        expected = set([
            AmountMismatch(user=test_data.u1,
                           incoming_GBPennies=249,
                           outgoing=[
                               OutgoingPayment(
                                   charity=test_data.c1,
                                   amount_GBPennies=100,
                                   status=OutgoingPaymentState.VALUE_MISMATCH),
                               OutgoingPayment(
                                   charity=test_data.c2,
                                   amount_GBPennies=150,
                                   status=OutgoingPaymentState.VALUE_MISMATCH),
                           ]),
            AmountMismatch(user=test_data.u2,
                           incoming_GBPennies=251,
                           outgoing=[
                               OutgoingPayment(
                                   charity=test_data.c1,
                                   amount_GBPennies=250,
                                   status=OutgoingPaymentState.VALUE_MISMATCH)
                           ]),
            AmountMismatch(user=test_data.u3,
                           incoming_GBPennies=0,
                           outgoing=[
                               OutgoingPayment(
                                   charity=test_data.c1,
                                   amount_GBPennies=10,
                                   status=OutgoingPaymentState.VALUE_MISMATCH)
                           ]),
            AmountMismatch(user=test_data.u4,
                           incoming_GBPennies=200,
                           outgoing=[]),
        ])

        self.assertEquals(amount_mismatch_notifier.accumulated, expected)
예제 #7
0
    def get_pending_outgoing_payments(self, amount_mismatch_notifiers=None):
        """Gets outgoing payments for which incoming payments have been received (i.e. which are ready to be sent to charities).

        As a side effect, notifies any passed amount_mismatch_notifiers of any incoming/outgoing payment mismatches, i.e. incoming payments with insufficient outgoing payments pending, and outgoing payments with insufficient incoming payments pending.
        """
        if amount_mismatch_notifiers is None:
            amount_mismatch_notifiers = []

        all_donation_proportions = self.donation_proportion_repository.get_donation_proportions(
        )

        donation_proportions_by_user = self._index(lambda dp: dp.user,
                                                   all_donation_proportions)

        incoming_payments = self.get_incoming_payments()
        incoming_payments_by_user = self._index(lambda ip: ip.user,
                                                incoming_payments)

        users_to_payment_amounts = {}
        for user in set(donation_proportions_by_user.keys()).union(
                set(incoming_payments_by_user.keys())):
            one_user_incoming_payments = incoming_payments_by_user[
                user] if user in incoming_payments_by_user else []
            amount_paid_in = reduce(
                lambda total, ip: total + ip.amount_GBPennies,
                one_user_incoming_payments, 0)
            amount_to_pay_out = user.donation_amount
            one_user_donation_proportions = donation_proportions_by_user[
                user] if user in donation_proportions_by_user else []
            total_proportions = reduce(lambda total, dp: total + dp.amount,
                                       one_user_donation_proportions, 0)
            users_to_payment_amounts[user] = (amount_paid_in,
                                              amount_to_pay_out,
                                              total_proportions)

        problem_users = [
            user for user in users_to_payment_amounts.keys() if
            users_to_payment_amounts[user][0] != users_to_payment_amounts[user]
            [1] or users_to_payment_amounts[user][2] <= 0
        ]

        charity_amounts = {}
        for (user,
             donation_proportions) in donation_proportions_by_user.items():
            if user in problem_users:
                continue
            for dp in donation_proportions:
                (_, amount_to_pay_out,
                 total_proportions) = users_to_payment_amounts[user]
                amount = amount_to_pay_out * dp.amount / total_proportions
                charity_amounts[dp.charity] = charity_amounts.setdefault(
                    dp.charity, 0) + amount

        outgoing_payments = set()
        for (charity, amount_GBPennies) in charity_amounts.items():
            outgoing_payment = OutgoingPayment.new(
                charity=charity,
                amount_GBPennies=amount_GBPennies,
                status=OutgoingPaymentState.DISPLAYED)
            outgoing_payment.put()
            outgoing_payments.add(outgoing_payment)

        self.notify_mismatches(amount_mismatch_notifiers, problem_users,
                               incoming_payments_by_user,
                               donation_proportions_by_user,
                               users_to_payment_amounts)

        return outgoing_payments