Uses the available funds on the account (credits and payments) to pay the given invoice.
    :param invoice_id: The id of the invoice.
    :return: True if the invoice status is paid.

    A lot of side effects may occur in the database:
    - Funds (either payments or credits) may get assigned to the invoice.
    - The invoice status may change.
    - Credits entities may be created.

def assign_funds_to_invoice(invoice_id: str) -> bool:
    """
    Uses the available funds on the account (credits and payments) to pay the given invoice.
    :param invoice_id: The id of the invoice.
    :return: True if the invoice status is paid.

    A lot of side effects may occur in the database:
    - Funds (either payments or credits) may get assigned to the invoice.
    - The invoice status may change.
    - Credits entities may be created.
    """

    logger.info('assign-funds-to-invoice', invoice_id=invoice_id)
    invoice = Invoice.objects.get(pk=invoice_id)
    account_id = invoice.account_id

    #
    # Precondition. Don't touch invoices that are not PENDING
    #
    if invoice.status != Invoice.PENDING:
        logger.info('assign-funds-to-invoice.status-is-not-pending', invoice_id=invoice_id)
        return False

    #
    # Precondition: Only handle invoices in a single currency
    #
    invoice_due_monies = invoice.due().monies()
    if len(invoice_due_monies) != 1:
        logger.info('assign-funds-to-invoice.more-than-one-currency', invoice_id=invoice_id)
        return False
    invoice_due_amount = invoice_due_monies[0].amount
    invoice_due_currency = invoice_due_monies[0].currency

    #
    # 1. Collect funds as long as long as we need them
    #
    if invoice_due_amount > 0:

        payments = Transaction.successful \
            .payments() \
            .uninvoiced(account_id=account_id) \
            .in_currency(invoice_due_currency) \
            .order_by('created')

        credits = Charge.objects \
            .credits() \
            .uninvoiced(account_id=account_id) \
            .in_currency(invoice_due_currency) \
            .order_by('created')

        funds = list(credits) + list(payments)
        for fund in funds:
            contributed_amount = abs(fund.amount.amount)  # 'abs' because credits have a negative value
            logger.info('assign-funds-to-invoice.assigning-fund',
                        invoice_id=invoice_id,
                        fund_type=type(fund).__name__,
                        fund_id=str(fund.pk),
                        contributed_amount=contributed_amount)
            fund.invoice_id = invoice_id
            fund.save()
            invoice_due_amount -= contributed_amount
            if invoice_due_amount <= 0:
                break

    #
    # 2. Mark invoice paid if nothing is due.
    #
    if invoice_due_amount <= 0:
        logger.info('assign-funds-to-invoice.mark-paid', invoice_id=invoice_id, invoice_due_amount=invoice_due_amount)
        invoice.status = Invoice.PAID
        invoice.save()

    #
    # 3. Carry forward any overpaid money.
    #
    if invoice_due_amount < 0:
        overpayment = Money(abs(invoice_due_amount), invoice_due_currency)
        logger.info('assign-funds-to-invoice.handling-overpayment',
                    invoice_id=invoice_id,
                    overpayment=overpayment)
        with transaction.atomic():
            Charge.objects.create(account_id=account_id, amount=overpayment, product_code=CARRIED_FORWARD,
                                  invoice_id=invoice_id)
            Charge.objects.create(account_id=account_id, amount=-overpayment, product_code=CREDIT_REMAINING)

    return invoice.status == Invoice.PAID