Paypal REST API - Payment capture response missing exchange rate - php

We have a Sandbox store account using Euro currency and configured to convert payments in the store currency.
If I make a purchase in US dollars, I would expect at least the exchange rate and the converted amount in the API capture response, but they are missing.
I'm calling the following API using PHP Curl with a fresh access token:
https://api-m.sandbox.paypal.com/v2/payments/captures/
and I get this response:
{
"id":"63F99991HX835842N",
"amount":{
"currency_code":"USD",
"value":"600.00"
},
"final_capture":true,
"seller_protection":{
"status":"ELIGIBLE",
"dispute_categories":[
"ITEM_NOT_RECEIVED",
"UNAUTHORIZED_TRANSACTION"
]
},
"disbursement_mode":"INSTANT",
"seller_receivable_breakdown":{
"gross_amount":{
"currency_code":"USD",
"value":"600.00"
},
"paypal_fee":{
"currency_code":"USD",
"value":"23.70"
},
"net_amount":{
"currency_code":"USD",
"value":"576.30"
}
},
"invoice_id":"10000000006",
"status":"COMPLETED",
"supplementary_data":{
"related_ids":{
"order_id":"EC-9F92771459179154X",
"authorization_id":"8T758094XY4322813"
}
},
"create_time":"2022-12-12T11:29:38Z",
"update_time":"2022-12-12T11:29:38Z",
"links":[
{
"href":"https://api.sandbox.paypal.com/v2/payments/captures/63F99991HX835842N",
"rel":"self",
"method":"GET"
},
{
"href":"https://api.sandbox.paypal.com/v2/payments/captures/63F99991HX835842N/refund",
"rel":"refund",
"method":"POST"
},
{
"href":"https://api.sandbox.paypal.com/v2/payments/authorizations/8T758094XY4322813",
"rel":"up",
"method":"GET"
}
]
}
As stated in the Paypal API reference, I should see the following which instead are missing:
"seller_receivable_breakdown": {
"gross_amount": {
"total": "10.99",
"currency": "USD"
},
"paypal_fee": {
"value": "0.33",
"currency": "USD"
},
"net_amount": {
"value": "10.66",
"currency": "USD"
},
"receivable_amount": {
"currency_code": "CNY",
"value": "59.26"
},
"paypal_fee_in_receivable_currency": {
"currency_code": "CNY",
"value": "1.13"
},
"exchange_rate": {
"source_currency": "USD",
"target_currency": "CNY",
"value": "5.9483297432325"
}
},
What am I doing wrong?
Thanks!
We tried to purchase in US Dollars on a Sandbox store with Euro currency, and we expect the currency conversion and exchange rate in the Capture API response, which are missing.

Ok I found the problem, we had configured the Sandbox store to accept USD and GBP currencies too, that's why Paypal did not do the conversion:
Money, Banks and Cards -> Currency Management menu must accept only the main currency

Related

PHP Paypal MALFORMED_REQUEST_JSON although JSON seems to be well formatted

I try to implement a paypal express checkout flow to a website.
The user must be able to adjust the order after they have logged in with paypal and choosed their payment option.
To do so, I create the order with paypal with the intent "AUTHORIZE" and user action "CONTINUE". It is created, the user is sent back to our website and I can fetch order and payer information with the (order) id created.
But then, when the payer is done checking and adjusting some final parameters and clicks the "buy now" button, I want to send the update order call as referenced here:
https://developer.paypal.com/api/orders/v2/#orders_patch
I create the payload in exactly the same way as for order creation, but this time with intent "CAPTURE" and user_action="PAY_NOW". I PATCH it to the correct path, but I keep getting the failure message "MALFORMED_REQUEST_JSON" .
Hoewever, the JSON itself is a valid JSON, no errors thrown in creation. Here is a sample:
{
"intent": "CAPTURE",
"application_context": {
"landing_page": "NO_PREFERENCE",
"shipping_preference": "SET_PROVIDED_ADDRESS",
"user_action": "PAY_NOW"
},
"purchase_units": [{
"reference_id": "2289256",
"description": "Your order at site",
"custom_id": "order id 2289256",
"soft_descriptor": "site name",
"invoice_id": "2289256",
"amount": {
"currency_code": "EUR",
"value": 59.98,
"breakdown": {
"item_total": {
"currency_code": "EUR",
"value": 50.41
},
"shipping": {
"currency_code": "EUR",
"value": 0
},
"discount": {
"currency_code": "EUR",
"value": 0
},
"tax_total": {
"currency_code": "EUR",
"value": 9.57
}
}
},
"items": [{
"name": "Product 1",
"description": "Product 1 Description",
"sku": "1019879",
"unit_amount": {
"currency_code": "EUR",
"value": 16.8
},
"tax": {
"currency_code": "EUR",
"value": 3.19
},
"quantity": "1",
"category": "PHYSICAL_GOODS"
}, {
"name": "Product 2",
"description": "Product 2 Description",
"sku": "1024593",
"unit_amount": {
"currency_code": "EUR",
"value": 33.61
},
"tax": {
"currency_code": "EUR",
"value": 6.38
},
"quantity": "1",
"category": "PHYSICAL_GOODS"
}],
"shipping": {
"name": {
"full_name": "John Doe"
},
"address": {
"address_line_1": "Badensche Str. 24",
"address_line_2": "",
"admin_area_2": "Berlin(Berlin)",
"postal_code": "10715",
"country_code": "DE"
}
}
}]
}
Here is paypal´s response
{
name: "INVALID_REQUEST",
message: "Request is not well-formed, syntactically incorrect, or violates schema.",
debug_id: "c315ce9eb90b4",
details: [{
field: "/",
location: "body",
issue: "MALFORMED_REQUEST_JSON",
description: "The request JSON is not well formed.",
}],
links: [{
href: "https://developer.paypal.com/docs/api/orders/v2/#error-MALFORMED_REQUEST_JSON",
rel: "information_link",
encType: "application/json",
}],
}
I just cannot figure out the problem. I tried to remove the whole application_context, purchase_units, intent and see if there is a problem within any of these parameters. Nothing changed. What exactly is wrong with this call?
That's not how a patch operation works. A patch operation's JSON payload must look something like the example in the API reference:
'[
{
"op": "replace",
"path": "/purchase_units/#reference_id==\'default\'/shipping/address",
"value": {
"address_line_1": "123 Townsend St",
"address_line_2": "Floor 6",
"admin_area_2": "San Francisco",
"admin_area_1": "CA",
"postal_code": "94107",
"country_code": "US"
}
}
]'
However, based on the information provided it's unclear why you are attempting to use a PATCH, since you do not list any relevant fields for a patch.
Intent authorize and intent capture are for creating an order. Use one or the other, never both for the same transaction. To capture an order that was created with intent capture, use a capture API call. To authorize an order that was created with intent authorize, use an authorize API call. The relevant API endpoints for either will be in the API response when you create the order, or you can read the documentation.
Since it appears your intended result is a captured payment (rather than an authorization), intent authorize is not relevant to your use case.

PayPal Smart buttons patch order via PHP API

I'm implementing Smart Buttons with Express checkout, so customers can select the delivery address on Paypal's popup. As agreed with Paypal support, I'm doing the communication with Paypal servers via PHP instead of Javascript. So in the onShippingChange event, I'm calling my server to calculate the delivery price and patch the order so Paypal updates the delivery price. The success path works fine, I need your help on error case(s).
How the patch request shall look like to instrument Paypal to display the warning that my webshop is not delivering to the selected location? (When implementing on Javascript, this was the return actions.reject in the onShippingChange.)
Thanks!
This is the patch request of a successful patch:
{
"path": "/v2/checkout/orders/xxxxxxxxxxxxxxxxxxxxx?",
"body": [
{
"op": "replace",
"path": "/intent",
"value": "CAPTURE"
},
{
"op": "replace",
"path": "/purchase_units/#reference_id=='default'/amount",
"value": {
"currency_code": "GBP",
"value": 265.95,
"breakdown": {
"item_total": {
"currency_code": "GBP",
"value": 236
},
"shipping": {
"currency_code": "GBP",
"value": 29.95
}
}
}
}
],
"verb": "PATCH",
"headers": {
"Content-Type": "application/json"
}
}
in the onShippingChange event, I'm calling my server to calculate the delivery price and patch the order
Great. But if the address is unsupported, your server must return the failed status in its response to that call. Based on that response, onShippingChange must return actions.reject() to the calling PayPal JS.

PHP Quickbooks SDK - Batch requests and handling failures

I've building out a small app that connects to a Quickbooks API via an SDK. The SDK provides batch operations to help reduce the number of API requests needed.
However, I'm hoping to make a large amount of requests (ie: bulk deletes, uploads in the 100s/1000s). I've gotten the deletes to work, however, now I'm hoping to integrate Laravel's Queue system so that any items in the $batch that fail (due to these business-rules or other reasons) are sent to a worker who will reattempt them after waiting a minute .
Below is an example of a delete request.
class QuickBooksAPIController extends Controller
{
public function batchDelete(Request $request, $category)
{
$chunks = array_chunk($request->data, 30);
foreach ($chunks as $key => $value) {
$batch[$key] = $this->dataService()->CreateNewBatch();
foreach ($value as $id) {
$item = $this->dataService()->FindById($category, $id);
$batch[$key]->AddEntity($item, $id, "delete");
}
$batch[$key]->Execute();
}
return response()->json(['message' => 'Items Deleted'], 200);
}
}
The documentations are a bit sparse for my scenario though. How can I get the failed batch items on order to try again?
Is using batches even the right choice here? Because I have to hit the API anyway to get the $item... which doesn't make sense to me (I think I'm doing something wrong there).
EDIT:
I intentionally sent out a request with more then 30 items and this is the failure message. Which doesn't have the values that didn't make the cut.
EDIT#2:
Ended up using array_chunk to separate the payload into 30 items (which is the limit of the API). Doing so helps process many requests. I've adjusted my code above to represent my current code.
How can I get the failed batch items on order to try again?
If you look at Intuit's documentation, you can see that the HTTP response the API returns contains this information. Here's the example request they show:
{
"BatchItemRequest": [
{
"bId": "bid1",
"Vendor": {
"DisplayName": "Smith Family Store"
},
"operation": "create"
},
{
"bId": "bid2",
"operation": "delete",
"Invoice": {
"SyncToken": "0",
"Id": "129"
}
},
{
"SalesReceipt": {
"PrivateNote": "A private note.",
"SyncToken": "0",
"domain": "QBO",
"Id": "11",
"sparse": true
},
"bId": "bid3",
"operation": "update"
},
{
"Query": "select * from SalesReceipt where TotalAmt > '300.00'",
"bId": "bid4"
}
]
}
And the corresponding response:
{
"BatchItemResponse": [
{
"Fault": {
"type": "ValidationFault",
"Error": [
{
"Message": "Duplicate Name Exists Error",
"code": "6240",
"Detail": "The name supplied already exists. : Another customer, vendor or employee is already using this \nname. Please use a different name.",
"element": ""
}
]
},
"bId": "bid1"
},
{
"Fault": {
"type": "ValidationFault",
"Error": [
{
"Message": "Object Not Found",
"code": "610",
"Detail": "Object Not Found : Something you're trying to use has been made inactive. Check the fields with accounts, customers, items, vendors or employees.",
"element": ""
}
]
},
"bId": "bid2"
},
{
"Fault": {
"type": "ValidationFault",
"Error": [
{
"Message": "Stale Object Error",
"code": "5010",
"Detail": "Stale Object Error : You and root were working on this at the same time. root finished before you did, so your work was not saved.",
"element": ""
}
]
},
"bId": "bid3"
},
{
"bId": "bid4",
"QueryResponse": {
"SalesReceipt": [
{
"TxnDate": "2015-08-25",
"domain": "QBO",
"CurrencyRef": {
"name": "United States Dollar",
"value": "USD"
},
"PrintStatus": "NotSet",
"PaymentRefNum": "10264",
"TotalAmt": 337.5,
"Line": [
{
"Description": "Custom Design",
"DetailType": "SalesItemLineDetail",
"SalesItemLineDetail": {
"TaxCodeRef": {
"value": "NON"
},
"Qty": 4.5,
"UnitPrice": 75,
"ItemRef": {
"name": "Design",
"value": "4"
}
},
"LineNum": 1,
"Amount": 337.5,
"Id": "1"
},
{
"DetailType": "SubTotalLineDetail",
"Amount": 337.5,
"SubTotalLineDetail": {}
}
],
"ApplyTaxAfterDiscount": false,
"DocNumber": "1003",
"PrivateNote": "A private note.",
"sparse": false,
"DepositToAccountRef": {
"name": "Checking",
"value": "35"
},
"CustomerMemo": {
"value": "Thank you for your business and have a great day!"
},
"Balance": 0,
"CustomerRef": {
"name": "Dylan Sollfrank",
"value": "6"
},
"TxnTaxDetail": {
"TotalTax": 0
},
"SyncToken": "1",
"PaymentMethodRef": {
"name": "Check",
"value": "2"
},
"EmailStatus": "NotSet",
"BillAddr": {
"Lat": "INVALID",
"Long": "INVALID",
"Id": "49",
"Line1": "Dylan Sollfrank"
},
"MetaData": {
"CreateTime": "2015-08-27T14:59:48-07:00",
"LastUpdatedTime": "2016-04-15T09:01:10-07:00"
},
"CustomField": [
{
"DefinitionId": "1",
"Type": "StringType",
"Name": "Crew #"
}
],
"Id": "11"
}
],
"startPosition": 1,
"maxResults": 1
}
}
],
"time": "2016-04-15T09:01:18.141-07:00"
}
Notice the separate response object for each request.
The bId value is a unique value you send in the request, which is then echo'd back to you in the response, so you can match up the requests you send with the responses you get back.
Here's the docs:
https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/batch#sample-batch-request
Is using batches even the right choice here?
Batches make a lot of sense when you are doing a lot of things all at once.
The way you're trying to use them is... weird. What you should probably be doing is:
Batch 1
- go find all your items
Batch 2
- delete all the items
Your existing code doesn't make sense because you're trying to both find the item and delete the item in the exact same batch HTTP request, which isn't possible via the API.
I intentionally sent out a request with more then 30 items and this is the failure message.
No, it's not. That's a PHP error message - you have an error in your code.
You need to fix the PHP error, and then look at the actual response you're getting back from the API.

Neteller paysafe api: Can I create a payment handle without specifying the consumer?

SO, i'm creating a payment handler and I redirect to the given payment_url.
In order to create the payment handler (and get the payment_url), I must specify the consumerId in the request body,
ex createPaymentHandler request:
{
"merchantRefNum": "merchantRefNum-201",
"transactionType": "PAYMENT",
"neteller": {
"consumerId": "netellertest_eur#neteller.com",
},
"paymentType": "NETELLER",
"amount": 500,
"currencyCode": "EUR",
"returnLinks": [{
"rel": "default",
"href": "https://example.com/payment/",
}
]
}
ex createPaymentHandleResponse
{
"id": "82d57742-e2db-48ea-a726-a60e6f8265a3",
"paymentType": "NETELLER",
"paymentHandleToken": "PHQhlWpTRKzBXubN",
"merchantRefNum": "5493aaf1a1d6dd13d2b53412f0ec",
"currencyCode": "USD",
"links": [
{
"rel": "redirect_payment",
"href": "https://customer.at.neteller.com/rest/payment/panel?mid=1090001806&mtid=pay_1090001806_00LwEhZ8WtIy8PmIhRCUi3JScUti6jKi_EUR&amount=0.01&currency=EUR&customerHash=741e624cf6ae4fbca4338cb5597fe531"
}
]
"dupCheck": true,
"status": "INITIATED",
"liveMode": true,
"usage": "SINGLE_USE",
"action": "REDIRECT",
"executionMode": "SYNCHRONOUS",
"amount": 500,
"billingDetails": {
"street": "George Street",
"street2": "3 Edgar Buildings",
"city": "Bath",
"zip": "BA1 2FJ",
"country": "GB"
},
"customerIp": "172.0.0.1",
"timeToLiveSeconds": 899,
"gatewayResponse": {
"orderId": "ORD_0d676b4b-0eb8-4d78-af25-e41ab431e325",
"totalAmount": 3599,
"currency": "EUR",
"status": "pending",
"lang": "en_US",
"processor": "NETELLER"
},
"neteller": {
"consumerId": "johndoe#email.com",
"detail1Description": "description",
"detail1Text": "Details 1 text"
},
"returnLinks": [
{
"rel": "default",
"href": "https://usgaminggambling.com/payment/"
},
{
"rel": "on_completed",
"href": "https://usgaminggambling.com/payment/return/success"
},
{
"rel": "on_failed",
"href": "https://usgaminggambling.com/payment/return/failed"
}
],
"txnTime": "2019-01-24T10:39:50Z",
"updatedTime": "2019-01-24T10:39:50Z",
"statusTime": "2019-01-24T10:39:50Z"
}
When I redirect to the payment_url the email field is already filled with the consumerId ("netellertest_eur#neteller.com") and it's readonly.
The thing is, I should not parse an email, because anyone could login from a different one to their Neteller account.
Is there any other way to get the payment url without parsing the consumerId?
Thanks in advance!
There is a new feature recently added where you can pass consumerIdLocked=false as part of the neteller request object. This will allow edits to the email field once the checkout page renders.
Documentation can be found here: https://developer.paysafe.com/en/additional-documentation/neteller-migration-guide/api/#/introduction/common-objects/meta
{
"merchantRefNum": "merchantRefNum-201",
"transactionType": "PAYMENT",
"neteller": {
"consumerId": "netellertest_eur#neteller.com",
"consumerIdLocked": false
},
"paymentType": "NETELLER",
"amount": 500,
"currencyCode": "EUR",
"returnLinks": [
{
"rel": "default",
"href": "https://example.com/payment/",
}
]
}
consumerId is required and the request will fail, if you don't pass it.
Users usually use the same email as the one in your system or you can create a field for the consumer to enter their email on the deposit page, before the the redirect, in case they would like to use a different one.
I found what I was looking for in the old Neteller REST API.
They have an endpoint for "orders" where you create the order without the consumerId and there is no readonly email in the checkout!

Paypal Billing agreement, weird response

I have an issue dealing with the PayPal-PHP-SDK,
My problem is, i'm creating the plan and the agreement, everything is working fine,
I'm putting 30€ as a fee in the agreement and the plan :
$amont = 30 ; // Prix
$merchantPreferences->setReturnUrl("$baseUrl/paiement-accept.php")
->setCancelUrl("$baseUrl/paiement-cancel.php")
->setAutoBillAmount("yes")
->setInitialFailAmountAction("CONTINUE")
->setMaxFailAttempts("0")
->setSetupFee(new Currency(array('value' => $amont, 'currency' => $currency)));
The date i'm using to create it it's :
$date = date('Y-m-d\TH:i:s', time()+210) ."Z";`
$agreement->setName($name)
->setDescription($description)
->setStartDate($date);
I tried with time()+210 and using only now's timestamp.
I execute the agreement, and there is the answer :
{
"id": "I-VD24XJ8MG14N",
"state": "Pending",
"description": "Abonnement Mensuel - 30\u20ac/mois",
"plan": {
"payment_definitions": [
{
"type": "REGULAR",
"frequency": "Month",
"amount": {
"currency": "EUR",
"value": "30.00"
},
"cycles": "0",
"charge_models": [
{
"type": "TAX",
"amount": {
"currency": "EUR",
"value": "0.00"
}
},
{
"type": "SHIPPING",
"amount": {
"currency": "EUR",
"value": "0.00"
}
}
],
"frequency_interval": "1"
}
],
"merchant_preferences": {
"setup_fee": {
"currency": "EUR",
"value": "0.00"
},
"max_fail_attempts": "0",
"auto_bill_amount": "YES"
}
},
"links": [
{
"href": "https://api.sandbox.paypal.com/v1/payments/billing-agreements/I-VD24XJ8MG14N/suspend",
"rel": "suspend",
"method": "POST"
},
{
"href": "https://api.sandbox.paypal.com/v1/payments/billing-agreements/I-VD24XJ8MG14N/re-activate",
"rel": "re_activate",
"method": "POST"
},
{
"href": "https://api.sandbox.paypal.com/v1/payments/billing-agreements/I-VD24XJ8MG14N/cancel",
"rel": "cancel",
"method": "POST"
},
{
"href": "https://api.sandbox.paypal.com/v1/payments/billing-agreements/I-VD24XJ8MG14N/bill-balance",
"rel": "self",
"method": "POST"
},
{
"href": "https://api.sandbox.paypal.com/v1/payments/billing-agreements/I-VD24XJ8MG14N/set-balance",
"rel": "self",
"method": "POST"
}
],
"start_date": "2014-11-21T23:00:00Z",
"agreement-details": {
"outstanding_balance": {
"currency": "EUR",
"value": "0.00"
},
"cycles_remaining": "0",
"cycles_completed": "0",
"final_payment_date": "1970-01-01T00:00:00Z",
"failed_payment_count": "0"
}
}
I don't know why the setup_fees are at 00
Why is start date at "2014-11-21T23:00:00Z"
And why is it the state Pending.
Can you help me how to make it work ? :(
If you need more informations (more code) to see where the problem is, ask me and i'll answer with more informations
Thanks a lot !
Xusifob
/***************************************************************************************************
EDIT :
****************************************************************************************************/
Thank you for taking the time of answering me.
For the date i changed it,
The payment method to create the billing agrement is a paypal account :
// Add Payer
$payer = new Payer();
$payer->setPaymentMethod('paypal');
$agreement->setPayer($payer);
I've already done all theses steps, click on the approval url, connect to my paypal test account and accept the transaction.
On my return page, I am on this page :
mywebsite/paiement-accept.php?token=EC-4VW6406102605853H
I've seen in the Paypal api that the ExecuteAgreement page is expecting a success value in the url ($_GET['success'] = true)
But i never got this value in my url, and the answer tell me that the User cancelled the Approval.
I changed the code not to have this error anymore and it gives me the response i put above.
This is my code :
$createdAgreement = require '../vendor/paypal/rest-api-sdk-php/sample/billing/ExecuteAgreement.php';
require '../vendor/paypal/rest-api-sdk-php/sample/billing/GetBillingAgreement.php';
And for the ExecuteAgreement :
if (isset($_GET['token'])) {
$token = $_GET['token'];
$agreement = new \PayPal\Api\Agreement();
try {
$agreement->execute($token, $apiContext);
} catch (Exception $ex) {
ResultPrinter::printError("Executed an Agreement", "Agreement", $agreement->getId(), $_GET['token'], $ex);
exit(1);
}
ResultPrinter::printResult("Executed an Agreement", "Agreement", $agreement->getId(), $_GET['token'], $agreement);
} else {
ResultPrinter::printResult("User Cancelled the Approval", null);
}
return $agreement;
Changing the date did not solve the problem with the start_date response issue
Thank you again for your help :)
For the date, you could use date('c', time()+210) as per http://php.net/manual/en/function.date.php
Would you mind answering the information as, what was the payment method while creating Billing Agrement. If you used PayPal, you need to click on the approval_url, and complete the flow.
Once done, you need to execute the payment, and then, when you do a get on the Agreement, you would be able to see the state as Active.
Let me know if that fixes this.

Categories