pythondjangobraintree-sandbox

Braintree Django Exception "can only concatenate str (not "tuple") to str"


I'm using Braintree Drop-in with my Django Project. It's working completely fine when I use it in development environment (manage.py runserver). But when I'm accessing the same on elastic beanstalk im getting the error "can only concatenate str (not "tuple") to str". The following is my code.

extras.py

from django.conf import settings
import braintree

gateway = braintree.BraintreeGateway(
    braintree.Configuration(
        braintree.Environment.Sandbox,
        merchant_id=settings.BT_MERCHANT_ID,
        public_key=settings.BT_PUBLIC_KEY,
        private_key=settings.BT_PRIVATE_KEY,
    )
)

def generate_client_token():
    return gateway.client_token.generate()

def transact(options):
    return gateway.transaction.sale(options)

def find_transaction(id):
    return gateway.transaction.find(id)

signals.py

from django.db.models.signals import post_save
from django.dispatch import receiver
#from django.conf import settings

from invoices.extras import gateway


@receiver(post_save, sender=User)
def create_phone(sender, instance, created, **kwargs):
    if created:
        gateway.customer.create({
            "first_name": instance.first_name,
            "last_name": instance.last_name,
            "email": instance.email,
            "phone": instance.phone_number,
            "id": instance.id,
        })

views.py

@login_required()
  def customer_invoice_view(request, ticket_id):
      ticket = get_object_or_404(Ticket, pk=ticket_id)
      customer = ticket.customer
      client_token = gateway.client_token.generate({"customer_id": str(customer.id)})
      invoice = ticket.invoice
      if request.method == 'POST':
          result = transact({
              'amount': invoice.amount_payable,
              'payment_method_nonce': request.POST['payment_method_nonce'],
              'options': {
                  "submit_for_settlement": True
              }
          })

          if result.is_success or result.transaction:
              invoice.is_paid = True
              invoice.save()
              ticket = invoice.ticket
              ticket.status ='Payment Made'
              ticket.paid = True
              ticket.save()
              return redirect(customer_invoice_view, ticket_id=ticket.id)
          else:
              for x in result.errors.deep_errors:
                  messages.info(request, x)
              return redirect(customer_invoice_view, ticket_id=ticket.id)
      context = {
      'ticket' : ticket,
      'invoice' : invoice,
      'client_token' : client_token,
      }
      return render(request, 'invoice/ticket_invoice_view.html', context)

ticket_invoice_view.html

<div class="p-3"> 
    {% if not invoice.is_paid%}
    <div class="row no-gutters">
        <div class="mb-2">
        <ul>
            {% for message in messages %}
            <li class="text-dark">{{message}}</li>
            {% endfor %}
        </ul>
        </div>
    </div>
    <div class="row no-gutters my-4">
        <form id="payment-form" method="post" action="" autocomplete="off">
            {% csrf_token %}
          <section>
            <div class="bt-drop-in-wrapper">
              <div id="bt-dropin"></div>
            </div>
          </section>
    
          <input type="hidden" id="nonce" name="payment_method_nonce" />
          <button class="btn-success" type="submit" id="submit-button"><span>Make Payment</span></button>
        </form>
    </div>
    {% endif %}
</div>

<script src="https://js.braintreegateway.com/web/dropin/1.25.0/js/dropin.min.js"></script>
<script>
  var form = document.querySelector('#payment-form');
  var client_token = '{{ client_token }}';

  braintree.dropin.create({
    authorization: client_token,
    container: '#bt-dropin',
    paypal: {
      flow: 'vault'
    }
  }, function (createErr, instance) {
    form.addEventListener('submit', function (event) {
      event.preventDefault();

      instance.requestPaymentMethod(function (err, payload) {
        if (err) {
          console.log('Error', err);
          return;
        }

        // Add the nonce to the form and submit
        document.querySelector('#nonce').value = payload.nonce;
        form.submit();
      });
    });
  });
</script>

When I'm running the project locally (manage.py runserver) it's working fine, when user is created, a customer is created in Braintree with user-Id as Customer-ID. And the payments are also getting processed without any issues. But when the same is done through elastic beanstalk I'm getting the error "can only concatenate str (not "tuple") to str".

The following is the traceback.

TypeError: can only concatenate str (not "tuple") to str
  File "django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "django/core/handlers/base.py", line 179, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "django/contrib/auth/decorators.py", line 21, in _wrapped_view
    return view_func(request, *args, **kwargs)
  File "accounts/decorators.py", line 20, in wrapper_func
    return view_func(request, *args, **kwargs)
  File "invoices/views.py", line 33, in customer_invoice_view
    client_token = gateway.client_token.generate({"customer_id": str(customer.id),},)
  File "braintree/client_token_gateway.py", line 26, in generate
    response = self.config.http().post(self.config.base_merchant_path() + "/client_token", params)
  File "braintree/configuration.py", line 113, in base_merchant_path
    return "/merchants/" + self.merchant_id

This is the traceback of the Post Save Signal Error.

TypeError: can only concatenate str (not "tuple") to str
  File "django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "django/core/handlers/base.py", line 179, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "django/views/generic/base.py", line 70, in view
    return self.dispatch(request, *args, **kwargs)
  File "django/utils/decorators.py", line 43, in _wrapper
    return bound_method(*args, **kwargs)
  File "django/views/decorators/debug.py", line 89, in sensitive_post_parameters_wrapper
    return view(request, *args, **kwargs)
  File "allauth/account/views.py", line 230, in dispatch
    return super(SignupView, self).dispatch(request, *args, **kwargs)
  File "allauth/account/views.py", line 75, in dispatch
    request, *args, **kwargs
  File "allauth/account/views.py", line 204, in dispatch
    return super(CloseableSignupMixin, self).dispatch(request, *args, **kwargs)
  File "django/views/generic/base.py", line 98, in dispatch
    return handler(request, *args, **kwargs)
  File "allauth/account/views.py", line 102, in post
    response = self.form_valid(form)
  File "allauth/account/views.py", line 246, in form_valid
    self.user = form.save(self.request)
  File "allauth/account/forms.py", line 419, in save
    adapter.save_user(request, user, self)
  File "allauth/account/adapter.py", line 246, in save_user
    user.save()
  File "django/contrib/auth/base_user.py", line 67, in save
    super().save(*args, **kwargs)
  File "django/db/models/base.py", line 754, in save
    force_update=force_update, update_fields=update_fields)
  File "django/db/models/base.py", line 803, in save_base
    update_fields=update_fields, raw=raw, using=using,
  File "django/dispatch/dispatcher.py", line 179, in send
    for receiver in self._live_receivers(sender)
  File "django/dispatch/dispatcher.py", line 179, in <listcomp>
    for receiver in self._live_receivers(sender)
  File "accounts/signals.py", line 30, in create_phone
    "id": instance.id,
  File "braintree/customer_gateway.py", line 24, in create
    return self._post("/customers", {"customer": params})
  File "braintree/customer_gateway.py", line 79, in _post
    response = self.config.http().post(self.config.base_merchant_path() + url, params)
  File "braintree/configuration.py", line 113, in base_merchant_path
    return "/merchants/" + self.merchant_id

I will be thankful if someone can help me to resolve it.


Solution

  • Thank you for the well detailed question!

    The error is the clue. It says you're trying to concatenate a tuple and a string which isn't allowed. Since the line is return "/merchants/" + self.merchant_id and I don't see a comma indicating an extranous tuple, it means the problem is with self.merchant_id. That appears to be set from settings.BT_MERCHANT_ID. My hunch is that settings.BT_MERCHANT_ID has a comma at the end of it when it shouldn't causing it to be a tuple.

    For example:

    x = 1
    

    results in x being set to the integer 1. But

    x = 1,
    

    Results in x being set to the tuple (1, ). It's a painful gotcha that bites every python developer eventually.