I’m trying to get PayPal’s transaction ID after the payment was approved on the client-side. I’m doing client-side integration of PayPal and Django. I can totally get the Payment ID and order ID and so forth, but these will be discarded by PayPal after the payment got approved. PayPal only recorded Transaction ID which can be used to track the payment with PayPal. When I’m trying to serialize the return actions which capture the Transaction ID – somehow I got a status code of 500 – Internal Server Error. The funny thing is that I can totally do console.log(transaction.id) and get the transaction ID in the console. Anyway, my error prone code is below:
In payment.html I got huge portion of html stuff, but I don’t post it here. I only post where the JavaScript begins:
<script>
// Generating csrf_token on the fly
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
let amount = "{{ grand_total }}"
const url = "{% url 'payment' %}"
let csrftoken = getCookie('csrftoken');
let orderID = "{{ order.order_number }}"
const payment_method = 'PayPal'
const redirect_url = "{% url 'order_complete' %}"
// Render the PayPal button into #paypal-button-container
const paypalButtonsComponent = paypal.Buttons({
// optional styling for buttons
// https://developer.paypal.com/docs/checkout/standard/customize/buttons-style-guide/
style: {
color: "gold",
shape: "pill",
layout: "vertical"
},
// set up the transaction
createOrder: (data, actions) => {
// pass in any options from the v2 orders create call:
// https://developer.paypal.com/api/orders/v2/#orders-create-request-body
const createOrderPayload = {
purchase_units: [
{
amount: {
value: amount
}
}
]
};
return actions.order.create(createOrderPayload);
},
// finalize the transaction
onApprove: (data, actions) => {
const captureOrderHandler = (details) => {
const payerName = details.payer.name.given_name;
console.log(details);
console.log('Transaction completed');
sendData();
function sendData() {
fetch(url, {
method: "POST",
headers: {
"Content-type": "application/json",
"X-CSRFToken": csrftoken,
},
body: JSON.stringify({
orderID: orderID,
transID: details.id,
payment_method: payment_method,
status: details.status,
}),
})
.then((response) => response.json())
.then((data) => {
window.location.href = redirect_url + '?order_number=' + data.order_number + '&payment_id=' + data.transID;
});
}
};
//return actions.order.capture().then(captureOrderHandler);
return actions.order.capture().then(function(orderData) {
// Successful capture! For dev/demo purposes:
const transaction = orderData.purchase_units[0].payments.captures[0];
sendTransactionID();
function sendTransactionID() {
fetch(url, {
method: "POST",
headers: {
"Content-type": "application/json",
"X-CSRFToken": csrftoken,
},
body: JSON.stringify({
actualTransID: transaction.id,
}),
})
}
});
},
// handle unrecoverable errors
onError: (err) => {
console.error('An error prevented the buyer from checking out with PayPal');
}
});
paypalButtonsComponent
.render("#paypal-button-container")
.catch((err) => {
console.error('PayPal Buttons failed to render');
});
</script>
In my order’s view I got this:
def payment(request):
body = json.loads(request.body)
order = Order.objects.get(user=request.user, is_ordered=False, order_number=body['orderID'])
# Store transaction details inside Payment model
processed_payment = Payment(
user=request.user,
payment_id=body['transID'],
payment_method=body['payment_method'],
amount_paid=order.order_total,
status=body['status'],
)
processed_payment.save()
order.payment = processed_payment
order.is_ordered = True
order.save()
# Move the cart items to Ordered Product table
cart_items = CartItem.objects.filter(user=request.user)
for item in cart_items:
ordered_product = OrderProduct()
ordered_product.order_id = order.id
ordered_product.payment = processed_payment
ordered_product.user_id = request.user.id
ordered_product.product_id = item.product_id
ordered_product.quantity = item.quantity
ordered_product.product_price = item.product.price
ordered_product.ordered = True
ordered_product.save()
cart_item = CartItem.objects.get(id=item.id)
product_variation = cart_item.variations.all()
ordered_product = OrderProduct.objects.get(id=ordered_product.id)
ordered_product.variation.set(product_variation)
ordered_product.save()
# Reduce the quantity of the sold products
product = Product.objects.get(id=item.product_id)
product.stock -= item.quantity
product.save()
# Clear the cart of cart items
CartItem.objects.filter(user=request.user).delete()
# Send order received email to customer
mail_subject = 'Thank you for your order!'
message = render_to_string('order_received_email.html', {
'user': request.user,
'order': order,
})
to_email = order.email
send_email = EmailMessage(mail_subject, message, to=[to_email])
send_email.send()
# Send order number and transaction id back to sendData method via JsonResponse
data = {
'order_number': order.order_number,
'transID': processed_payment.payment_id,
}
return JsonResponse(data)
If I take out this in payment.html:
return actions.order.capture().then(function(orderData) {
// Successful capture! For dev/demo purposes:
const transaction = orderData.purchase_units[0].payments.captures[0];
sendTransactionID();
function sendTransactionID() {
fetch(url, {
method: "POST",
headers: {
"Content-type": "application/json",
"X-CSRFToken": csrftoken,
},
body: JSON.stringify({
actualTransID: transaction.id,
}),
})
}
});
Which I will be left with:
<script>
// Generating csrf_token on the fly
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
let amount = "{{ grand_total }}"
const url = "{% url 'payment' %}"
let csrftoken = getCookie('csrftoken');
let orderID = "{{ order.order_number }}"
const payment_method = 'PayPal'
const redirect_url = "{% url 'order_complete' %}"
// Render the PayPal button into #paypal-button-container
const paypalButtonsComponent = paypal.Buttons({
// optional styling for buttons
// https://developer.paypal.com/docs/checkout/standard/customize/buttons-style-guide/
style: {
color: "gold",
shape: "pill",
layout: "vertical"
},
// set up the transaction
createOrder: (data, actions) => {
// pass in any options from the v2 orders create call:
// https://developer.paypal.com/api/orders/v2/#orders-create-request-body
const createOrderPayload = {
purchase_units: [
{
amount: {
value: amount
}
}
]
};
return actions.order.create(createOrderPayload);
},
// finalize the transaction
onApprove: (data, actions) => {
const captureOrderHandler = (details) => {
const payerName = details.payer.name.given_name;
console.log(details);
console.log('Transaction completed');
sendData();
function sendData() {
fetch(url, {
method: "POST",
headers: {
"Content-type": "application/json",
"X-CSRFToken": csrftoken,
},
body: JSON.stringify({
orderID: orderID,
transID: details.id,
payment_method: payment_method,
status: details.status,
}),
})
.then((response) => response.json())
.then((data) => {
window.location.href = redirect_url + '?order_number=' + data.order_number + '&payment_id=' + data.transID;
});
}
};
return actions.order.capture().then(captureOrderHandler);
},
// handle unrecoverable errors
onError: (err) => {
console.error('An error prevented the buyer from checking out with PayPal');
}
});
paypalButtonsComponent
.render("#paypal-button-container")
.catch((err) => {
console.error('PayPal Buttons failed to render');
});
</script>
This would totally work – and in my Payment model I could only record Payment ID and Order ID and so forth – but these are useless after payment went through with PayPal – because PayPal only retains the Transaction ID – and I can’t get Transaction ID to be sent to the backend – but I can only print to the console using console.log – and this is frustrating.
If I can get transaction ID to be sent to the backend using fetch – then I can do something like this:
completed_payment = Payment(
paypal_transaction_id=body['actualTransID']
)
completed_payment.save()
But can this be done even though the first redirection already happened with this code:
.then((data) => {
window.location.href = redirect_url + '?order_number=' + data.order_number + '&payment_id=' + data.transID;
So, is it then I need to fetch the redirect_url (such as payment_complete view) and not the previous url (such as payment view)? Basically, the JavaScript stuff really got me confused. Something is wrong with my code? Any help? Thanks…
2
Answers
An instructor of mine on Udemy solved this issue. The answer is to do this in the onApprove function using the code below:
Here is the completed working code for PayPal Client Side Integration with the ability to record PayPal Transaction ID into the database.
IN the payment view, you can always do something like this:
Don’t capture things client-side and then send the information to a server after the fact. That’s a fundamentally bad design for ecommerce payments.
Instead, implement server-side API calls to create and capture a PayPal order. You can use the Checkout-Python-SDK for this, or direct HTTPS API calls after first obtaining an access_token with your client_id and secret. You’ll need two django routes that only output JSON data, one for the create order API operation and one for the capture that takes an id as input (path parameter, query string, or w/e) and captures it. The capture route can then record the successful transaction ID in your database, as well as trigger any other logic you need it to do (such as sending a confirmation email or reserving product) — immediately before forwarding the JSON result to the frontend caller (regardless of success or failure, always forward the result in a format the frontend recognizes to handle any relevant errors on the client side)
Once you have your two routes, use this for the frontend approval flow: https://developer.paypal.com/demo/checkout/#/pattern/server