I'm trying to implement PayPal payments in our shopping cart using a combination of the JavaScript SDK and server side REST calls. I render the buttons using the JavaScript SDK and have the createOrder
and onApprove
methods call endpoints on my server, as the examples given by PayPal do (JavaScript SDK example). These, in turn, call to the PayPal REST API.
All of this works great IF I'm doing a capture. However, our requirements are to do an authorization. This doesn't appear to work. The createOrder
call successfully completes and the onApprove
method is hit. However it fails when the server side REST call is made with an error:
issue":"AMOUNT_CANNOT_BE_SPECIFIED","description":"An authorization amount can only be specified if an Order has been saved by calling /v2/checkout/orders/{order_id}/save. Please save the order and try again."}],"
There is no mention that I can find of having to call this save
method and indeed, if I try to call that method, I get an error indicating the transaction was not yet approved by the customer.
This also works fine when using the pure client side method of doing this, but we would like to avoid that for a number of reasons. The PayPal documentation seems to indicate that this should be possible.
A simplified/sanitized version of my code is below:
<script src="https://www.paypal.com/sdk/js?client-id=KEY¤cy=USD&disable-funding=card&components=funding-eligibility,buttons&intent=authorize&commit=true"></script>
<script>
paypal.Buttons({
createOrder: function (data, actions) {
return fetch("/checkout/paypal/order", {
method: "post",
body: $("#checkoutForm").serialize(),
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
}
}).then((order) => {
return order.json();
})
.then((orderData) => {
return orderData.OrderId;
});
},
onApprove: function (data, actions) {
return fetch("/checkout/paypal/authorize", {
method: "post",
body: $('#checkoutForm').serialize(),
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
}
})
.then((result) => {
console.log(result.json());
});
}
}).render('#paypal-button-container');
</script>
The backend goes through a controller and a number of services, but ultimately the resulting requests (using the C# SDK package: PayPalCheckoutSdk) looks like this:
Create Order:
public async Task<PayPalOrderResponseDTO> CreateOrder(PayPalOrderRequestDTO orderRequest)
{
OrderRequest ppor = new OrderRequest()
{
CheckoutPaymentIntent = "AUTHORIZE",
PurchaseUnits = new List<PurchaseUnitRequest>()
{
new PurchaseUnitRequest()
{
AmountWithBreakdown = new AmountWithBreakdown()
{
CurrencyCode = "USD",
Value = orderRequest.Total.ToString()
},
ShippingDetail = new ShippingDetail()
{
Name = new Name()
{
FullName = $"{orderRequest.ShippingAddress.FirstName} {orderRequest.ShippingAddress.LastName}"
},
AddressPortable = new AddressPortable()
{
AddressLine1 = orderRequest.ShippingAddress.Address1,
AddressLine2 = orderRequest.ShippingAddress.Address2,
AddressLine3 = orderRequest.ShippingAddress.Address3,
AdminArea2 = orderRequest.ShippingAddress.City,
AdminArea1 = orderRequest.ShippingAddress.State,
PostalCode = orderRequest.ShippingAddress.ZipCode,
CountryCode = orderRequest.ShippingAddress.CountryID
}
}
}
},
ApplicationContext = new ApplicationContext()
{
ShippingPreference = "SET_PROVIDED_ADDRESS",
LandingPage = "LOGIN",
UserAction = "PAY_NOW"
}
};
OrdersCreateRequest request = new OrdersCreateRequest();
request.Prefer("return=minimal");
request.RequestBody(ppor);
PayPalHttp.HttpResponse response = await _ppClient.Execute(request).ConfigureAwait(false);
System.Net.HttpStatusCode statusCode = response.StatusCode;
if (statusCode != System.Net.HttpStatusCode.Created)
{
// HANDLE ERROR
}
Order order = response.Result<Order>();
return new PayPalOrderResponseDTO()
{
Status = response.StatusCode.ToString(),
OrderID = order.Id
};
}
Authorize order:
public async Task<PayPalPaymentResponseDTO> Authorize(PayPalPaymentRequestDTO request)
{
OrdersAuthorizeRequest oar = new OrdersAuthorizeRequest(request.OrderID);
oar.RequestBody(new AuthorizeRequest()
{
Amount = new Money() { CurrencyCode = "USD", Value = request.Total },
ReferenceId = request.refId
});
PayPalHttp.HttpResponse response = await _ppClient.Execute(oar).ConfigureAwait(false);
Order order = response.Result<Order>();
return new PayPalPaymentResponseDTO()
{
StatusCode = (int)response.StatusCode,
ID = order.Id
};
}
As I said, this all works perfectly if I change it to use a "CAPTURE" intent. It's only when I attempt a "AUTHORIZE" that I get this error. I tried doing the final authorization call without the amount, just in case, but got an error indicating that the required payment amount field was missing.
Does any one have any ideas, or is this just not possible without doing an older-style redirect? I'd like to avoid both that and using the purely client side method of handling this using something like, e.g.:
paypal.Buttons({
// Sets up the transaction when a payment button is clicked
createOrder: (data, actions) => {
return actions.order.create({
purchase_units: [{
amount: {
value: '77.44' // Can also reference a variable or function
}
}]
});
},
// Finalize the transaction after payer approval
onApprove: (data, actions) => {
return actions.order.capture().then(function(orderData) {
// Successful capture! For dev/demo purposes:
console.log('Capture result', orderData, JSON.stringify(orderData, null, 2));
actions.redirect('thank_you.html');
});
}
}).render('#paypal-button-container');
CodePudding user response:
It is possible. The REST API call to authorize the order should be a POST with no payload body.
Here is an example log, triggered by createOrder on the server:
POST to v2/checkout/orders
{
"intent": "AUTHORIZE",
"purchase_units": [
{
"amount": {
"currency_code": "USD",
"value": "500",
"breakdown": {
"item_total": {
"currency_code": "USD",
"value": "500"
}
}
},
"items": [
{
"name": "Name of Item #1 (can be viewed in the upper-right dropdown during payment approval)",
"description": "Optional description; item details will also be in the completed paypal.com transaction view",
"unit_amount": {
"currency_code": "USD",
"value": "500"
},
"quantity": "1"
}
]
}
]
}
Response data
{
"id": "3J6935353G362625A",
"status": "CREATED",
"links": [
{
"href": "https://api.sandbox.paypal.com/v2/checkout/orders/3J6935353G362625A",
"rel": "self",
"method": "GET"
},
{
"href": "https://www.sandbox.paypal.com/checkoutnow?token=3J6935353G362625A",
"rel": "approve",
"method": "GET"
},
{
"href": "https://api.sandbox.paypal.com/v2/checkout/orders/3J6935353G362625A",
"rel": "update",
"method": "PATCH"
},
{
"href": "https://api.sandbox.paypal.com/v2/checkout/orders/3J6935353G362625A/authorize",
"rel": "authorize",
"method": "POST"
}
]
}
Followed by this on the server, triggered after onApprove:
Empty POST to v2/checkout/orders/3J6935353G362625A/authorize
Response data
{
"id": "3J6935353G362625A",
"status": "COMPLETED",
"purchase_units": [
{
"reference_id": "default",
"shipping": {
"name": {
"full_name": "Sandbox Buyer"
},
"address": {
"address_line_1": "123 street name",
"admin_area_2": "Phoenix",
"admin_area_1": "AZ",
"postal_code": "85001",
"country_code": "US"
}
},
"payments": {
"authorizations": [
{
"status": "CREATED",
"id": "9V1555595X9968645",
"amount": {
"currency_code": "USD",
"value": "500.00"
},
"seller_protection": {
"status": "ELIGIBLE",
"dispute_categories": [
"ITEM_NOT_RECEIVED",
"UNAUTHORIZED_TRANSACTION"
]
},
"expiration_time": "2022-05-13T20:14:50Z",
"links": [
{
"href": "https://api.sandbox.paypal.com/v2/payments/authorizations/9V1555595X9968645",
"rel": "self",
"method": "GET"
},
{
"href": "https://api.sandbox.paypal.com/v2/payments/authorizations/9V1555595X9968645/capture",
"rel": "capture",
"method": "POST"
},
{
"href": "https://api.sandbox.paypal.com/v2/payments/authorizations/9V1555595X9968645/void",
"rel": "void",
"method": "POST"
},
{
"href": "https://api.sandbox.paypal.com/v2/payments/authorizations/9V1555595X9968645/reauthorize",
"rel": "reauthorize",
"method": "POST"
},
{
"href": "https://api.sandbox.paypal.com/v2/checkout/orders/3J6935353G362625A",
"rel": "up",
"method": "GET"
}
],
"create_time": "2022-04-14T20:14:50Z",
"update_time": "2022-04-14T20:14:50Z"
}
]
}
}
],
"payer": {
"name": {
"given_name": "Sandbox",
"surname": "Buyer"
},
"email_address": "[email protected]",
"payer_id": "THLKV8VCTCSKL",
"address": {
"country_code": "US"
}
},
"links": [
{
"href": "https://api.sandbox.paypal.com/v2/checkout/orders/3J6935353G362625A",
"rel": "self",
"method": "GET"
}
]
}
A sample to do this with the Checkout-NET-SDK is here.