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