def create_new_payment(cancelled_payment=None): """ Creates and new payment and copies over the the payment profile from the cancelled payment.""" # TODO See if we can use something like Django-lazy-user so that the payment profile can always be set with date from the user model. payment = DocDataPaymentOrder() if cancelled_payment: payment.email = cancelled_payment.email payment.first_name = cancelled_payment.first_name payment.last_name = cancelled_payment.last_name payment.address = cancelled_payment.address payment.postal_code = cancelled_payment.postal_code payment.city = cancelled_payment.city payment.country = cancelled_payment.country payment.save() order.payments.add(payment)
def process_monthly_donations(recurring_payments_queryset, send_email): """ The starting point for creating DocData payments for the monthly donations. """ recurring_donation_errors = [] RecurringDonationError = namedtuple('RecurringDonationError', 'recurring_payment error_message') skipped_recurring_payments = [] SkippedRecurringPayment = namedtuple('SkippedRecurringPayment', 'recurring_payment orders') donation_count = 0 # The adapter is used after the recurring Order and donations have been adjusted. It's created here so that we can # reuse it to process all recurring donations. webdirect_payment_adapter = WebDirectDocDataDirectDebitPaymentAdapter() # Fixed lists of the popular projects. popular_projects_all = list(Project.objects.filter(phase=ProjectPhases.campaign).order_by('-popularity')) top_three_projects = popular_projects_all[:3] popular_projects_rest = popular_projects_all[3:] logger.info("Config: Using these projects as 'Top Three':") for project in top_three_projects: logger.info(" {0}".format(project.title)) # The main loop that processes each monthly donation. for recurring_payment in recurring_payments_queryset: top_three_donation = False user_selected_projects = [] # Skip payment if there has been a recurring Order recently. ten_days_ago = timezone.now() + timezone.timedelta(days=-10) recent_closed_recurring_orders = Order.objects.filter(user=recurring_payment.user, status=OrderStatuses.closed, recurring=True, updated__gt=ten_days_ago) if recent_closed_recurring_orders.count() > 0: skipped_recurring_payments.append(SkippedRecurringPayment(recurring_payment, list(recent_closed_recurring_orders))) logger.warn( "Skipping '{0}' because it looks like it has been processed recently with one of these Orders:".format( recurring_payment)) for closed_order in recent_closed_recurring_orders: logger.warn(" Order Number: {0}".format(closed_order.order_number)) continue # Check if there is a monthly shopping cart (Order status is 'recurring') for this recurring_payment user. try: recurring_order = Order.objects.get(user=recurring_payment.user, status=OrderStatuses.recurring) logger.debug("Using existing recurring Order for user: {0}.".format(recurring_payment.user)) except Order.DoesNotExist: # There is no monthly shopping cart. The user is supporting the top three projects so we need to create an # Order with Donations for the top three projects. logger.debug("Creating new 'Top Three' recurring Order for user {0}.".format(recurring_payment.user)) recurring_order = create_recurring_order(recurring_payment.user, top_three_projects) top_three_donation = True except Order.MultipleObjectsReturned: error_message = "Multiple Orders with status 'recurring' returned for '{0}'. Not processing this recurring donation.".format( recurring_payment) logger.error(error_message) recurring_donation_errors.append(RecurringDonationError(recurring_payment, error_message)) continue # Check if we're above the DocData minimum for direct debit. if recurring_payment.amount < 113: # Cleanup the Order if there's an error. if top_three_donation: remove_order(recurring_order) error_message = "Payment amount for '{0}' is less than the DocData minimum for direct debit (113). Skipping.".format( recurring_payment) logger.error(error_message) recurring_donation_errors.append(RecurringDonationError(recurring_payment, error_message)) continue # Remove donations to projects that are no longer in the campaign phase. for donation in recurring_order.donations.all(): project = Project.objects.get(id=donation.project.id) if project.phase != ProjectPhases.campaign: donation.delete() if recurring_order.donations.count() > 0: # There are donations in the recurring Order and we need to redistribute / correct the donation amounts. # Save a copy of the projects that have been selected by the user so that the monthly shopping cart can # recreated after the payment has been successfully started. for donation in recurring_order.donations.all(): user_selected_projects.append(donation.project) correct_donation_amounts(popular_projects_all, recurring_order, recurring_payment) else: # There are no donations in the recurring Order so we need to create a monthly shopping cart to support the # top three projects and redistribute / correct the donation amounts. create_recurring_order(recurring_payment.user, top_three_projects, recurring_order) if recurring_order.donations.count() == 0: logger.debug("The top three donations are full. Using next three projects for top three.") top_three_projects = popular_projects_rest[:3] popular_projects_rest = popular_projects_rest[3:] create_recurring_order(recurring_payment.user, top_three_projects, recurring_order) correct_donation_amounts(popular_projects_rest, recurring_order, recurring_payment) top_three_donation = True # At this point the order should be correctly setup and ready for the DocData payment. if top_three_donation: donation_type_message = "supporting the 'Top Three' projects" else: donation_type_message = "with {0} donations".format(recurring_order.donations.count()) logger.info("Starting payment for '{0}' {1}.".format(recurring_payment, donation_type_message)) # Safety check to ensure the modifications to the donations in the recurring result in an Order total that # matches the RecurringDirectDebitPayment. if recurring_payment.amount != recurring_order.total: # Cleanup the Order if there's an error. error_message = "RecurringDirectDebitPayment amount: {0} does not equal recurring Order amount: {1} for '{2}'. Not processing this recurring donation.".format( recurring_payment.amount, recurring_order.total, recurring_payment) if top_three_donation: remove_order(recurring_order) logger.error(error_message) recurring_donation_errors.append(RecurringDonationError(recurring_payment, error_message)) continue # Check if the IBAN / BIC is stored correctly on the RecurringDirectDebitPayment. if recurring_payment.iban == '' or recurring_payment.bic == '' or \ not recurring_payment.iban.endswith(recurring_payment.account) or \ recurring_payment.bic[:4] != recurring_payment.iban[4:8]: # Cleanup the Order if there's an error. if top_three_donation: remove_order(recurring_order) error_message = "Cannot create payment because the IBAN and/or BIC are not available." logger.error(error_message) recurring_donation_errors.append(RecurringDonationError(recurring_payment, error_message)) continue # Create and fill in the DocDataPaymentOrder. payment = DocDataPaymentOrder() payment.order = recurring_order payment.payment_method_id = 'dd-webdirect' payment.amount = recurring_payment.amount payment.currency = recurring_payment.currency payment.customer_id = recurring_payment.user.id payment.email = recurring_payment.user.email # Use the recurring payment name (bank account name) to set the first and last name if they're not set. if not recurring_payment.user.first_name: if ' ' in recurring_payment.name: payment.first_name = recurring_payment.name.split(' ')[0] else: payment.first_name = recurring_payment.name else: payment.first_name = recurring_payment.user.first_name if not recurring_payment.user.last_name: if ' ' in recurring_payment.name: payment.last_name = recurring_payment.name[recurring_payment.name.index(' ') + 1:] else: payment.last_name = recurring_payment.name else: payment.last_name = recurring_payment.user.last_name # Try to use the address from the profile if it's set. address = recurring_payment.user.address if not address: # Cleanup the Order if there's an error. if top_three_donation: remove_order(recurring_order) error_message = "Cannot create a payment for '{0}' because user does not have an address set.".format(recurring_payment) logger.error(error_message) recurring_donation_errors.append(RecurringDonationError(recurring_payment, error_message)) continue # Set a default value for the pieces of the address that we don't have. unknown_value = u'Unknown' if not address.line1: logger.warn("User '{0}' does not have their street and street number set. Using '{1}'.".format(recurring_payment.user, unknown_value)) payment.address = unknown_value else: payment.address = address.line1 if not address.city: logger.warn("User '{0}' does not have their city set. Using '{1}'.".format(recurring_payment.user, unknown_value)) payment.city = unknown_value else: payment.city = address.city if not address.postal_code: logger.warn("User '{0}' does not have their postal code set. Using '{1}'.".format(recurring_payment.user, unknown_value)) payment.postal_code = unknown_value else: payment.postal_code = address.postal_code # Assume the Netherlands when country not set. if address.country: payment.country = address.country.alpha2_code else: payment.country = 'NL' # Try to use the language from the User settings if it's set. if recurring_payment.user.primary_language: payment.language = recurring_payment.user.primary_language[:2] # Cut off locale. else: payment.language = 'nl' payment.save() # Start the WebDirect payment. try: webdirect_payment_adapter.create_remote_payment_order(payment) except DocDataPaymentException as e: # Cleanup the Order if there's an error. if top_three_donation: remove_order(recurring_order) error_message = "Problem creating remote payment order." logger.error(error_message) recurring_donation_errors.append( RecurringDonationError(recurring_payment, "{0} {1}".format(error_message, e.message))) continue else: recurring_order.status = OrderStatuses.closed recurring_order.save() try: webdirect_payment_adapter.start_payment(payment, recurring_payment) except DocDataPaymentException as e: # Cleanup the Order if there's an error. if top_three_donation: remove_order(recurring_order) else: recurring_order.status = OrderStatuses.recurring recurring_order.save() error_message = "Problem starting payment." logger.error(error_message) recurring_donation_errors.append( RecurringDonationError(recurring_payment, "{0} {1}".format(error_message, e.message))) continue logger.debug("Payment for '{0}' started.".format(recurring_payment)) donation_count += 1 # Send an email to the user. if send_email: mail_monthly_donation_processed_notification(recurring_payment, recurring_order) # Create a new recurring Order (monthly shopping cart) for donations that are not to the 'Top Three'. if not top_three_donation and len(user_selected_projects) > 0: new_recurring_order = create_recurring_order(recurring_payment.user, user_selected_projects) # Adjust donation amounts in a simple way for the recurring Order (the monthly donations shopping cart). num_donations = new_recurring_order.donations.count() amount_per_project = math.floor(recurring_payment.amount / num_donations) donations = new_recurring_order.donations.all() for i in range(0, num_donations - 1): donation = donations[i] donation.amount = amount_per_project donation.donation_type = Donation.DonationTypes.recurring donation.save() # Update the last donation with the remaining amount. donation = donations[num_donations - 1] donation.amount = recurring_payment.amount - (amount_per_project * (num_donations - 1)) donation.donation_type = Donation.DonationTypes.recurring donation.save() logger.info("") logger.info("Recurring Donation Processing Summary") logger.info("=====================================") logger.info("") logger.info("Total number of recurring donations: {0}".format(recurring_payments_queryset.count())) logger.info("Number of recurring Orders successfully processed: {0}".format(donation_count)) logger.info("Number of errors: {0}".format(len(recurring_donation_errors))) logger.info("Number of skipped payments: {0}".format(len(skipped_recurring_payments))) if len(recurring_donation_errors) > 0: logger.info("") logger.info("") logger.info("Detailed Error List") logger.info("===================") logger.info("") for error in recurring_donation_errors: logger.info("RecurringDirectDebitPayment: {0} {1}".format(error.recurring_payment.id, error.recurring_payment)) logger.info("Error: {0}".format(error.error_message)) logger.info("--") if len(skipped_recurring_payments) > 0: logger.info("") logger.info("") logger.info("Skipped Recurring Payments") logger.info("==========================") logger.info("") for skipped_payment in skipped_recurring_payments: logger.info("RecurringDirectDebitPayment: {0} {1}".format(skipped_payment.recurring_payment.id, skipped_payment.recurring_payment)) for closed_order in skipped_payment.orders: logger.info("Order Number: {0}".format(closed_order.order_number)) logger.info("--")