2
\$\begingroup\$

I am currently working on integrating PayPal with my Laravel application, and it's my first time incorporating PayPal for production use. I have written the following code and would greatly appreciate your expertise in reviewing it for potential issues and ensuring its readiness for production.

Objective

I intend to use PayPal to purchase lead packages for my site.

Code

Below is a snippet of the relevant code

<?php

namespace App\Paypal;

use App\Models\Comptor;
use App\Models\Event;
use App\Models\Package;
use App\Models\Purchase;
use App\Models\User;
use PayPalCheckoutSdk\Core\PayPalHttpClient;
use PayPalCheckoutSdk\Core\ProductionEnvironment;
use PayPalCheckoutSdk\Core\SandboxEnvironment;
use PayPalCheckoutSdk\Orders\OrdersGetRequest;
use PayPalCheckoutSdk\Payments\AuthorizationsCaptureRequest;
use PayPalCheckoutSdk\Payments\AuthorizationsGetRequest;
use \Illuminate\Http\Request;
use PayPalHttp\HttpException;
use Illuminate\Http\Exceptions\HttpResponseException;

class PaypalPayment
{
    private $clientSecret;
    private $clientId;
    private $sandbox;


    /**
     * @param string $clientId
     * @param string $clientSecret
     * @param bool $sandbox
     */

    public function __construct(string $clientId, string $clientSecret, bool $sandbox){
        $this->clientId = $clientId;
        $this->clientSecret = $clientSecret;
        $this->sandbox = $sandbox;
    }
    /**
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function handle(Request $request){
        // Retrieve the authorization_id sent from the frontend
        $authorizationID = $request->input('authorization_id');
        // Create a PayPal client to capture funds
        $environment = $this->sandbox
            ? new SandboxEnvironment($this->clientId, $this->clientSecret)
            : new ProductionEnvironment($this->clientId, $this->clientSecret);

        $client = new PayPalHttpClient($environment);
        try {
            // Check authorization status
            $requestAuthorization = new AuthorizationsGetRequest($authorizationID);
            $responseAuthorization = $client->execute($requestAuthorization);
            if ($responseAuthorization->result->status !== 'CREATED') {
                throw new \Exception('Error: authorization not completed');
            }

            // Recover order ID from authorization
            $orderId = $responseAuthorization->result->supplementary_data->related_ids->order_id;

            // Check order status
            $requestOrder = new OrdersGetRequest($orderId);
            $responseOrder = $client->execute($requestOrder);

            if ($responseOrder->result->status !== "COMPLETED") {
                throw new \Exception('Error: order not completed');
            }

            $package = Package::find($request->input('package_id'));
            $user = User::find(session()->get("user"));

            // Check whether the amount captured corresponds to the expected amount
            $amountCaptured = intval($responseAuthorization->result->amount->value);

            if ($package->price !== $amountCaptured) {
                throw new HttpResponseException(response()->json(['success' => false, 'message' => 'Erreur lors de la capture des fonds.'], 500));
            }

            // Save the order ID in your database here

            $comptor = Comptor::where('user_id', '=', $user->id)->first();

            if($comptor){
                $purchase = Purchase::create([
                    "user_id" => $user->id,
                    "package_id" => $package->id
                ]);

                $comptor->increment("leads", $package->nb_lead);

                if($purchase){
                    return response()->json(['status' => true, 'message' => 'Package successfully purchased and the number of your leads to increase by: ' . $package->nb_lead . 'leads']);
                }
            }

            $purchase = Purchase::create([
                "user_id" => $user->id,
                "package_id" => $package->id
            ]);

            $comptor = Comptor::create([
                "user_id" => $user->id,
                "leads" => $package->nb_lead
            ]);

            if($purchase && $comptor){
                return response()->json(['status' => true, 'message' => 'Package successfully purchased and you are entitled to '. $package->nb_lead . 'leads']);
            }

        } catch (\PayPalHttp\HttpException $ex) {
            // Fund capture failed
            // You can manage errors here
            return response()->json(['status' => false, 'message' => 'Error during fund capture.']);
        }
    }
}

Specific Concerns: I want to know if this code will transfer to my Paypal account.

Additional Context: I use Laravel 8.83 and Paypal checkout SDK with JavaScript, but with server-side authorization (my Laravel server).

What I'm Looking For

  • General code review for potential bugs or security vulnerabilities.
  • Feedback on best practices for integrating PayPal into a Laravel application.
  • Advice on ensuring the code is production-ready.

Note:

I have already referred to the official PayPal documentation and followed the guidelines, but as a first-time user, I want to ensure I haven't missed anything crucial.

Can you please provide valuable insights? I'm eager to learn from the community's expertise and improve my implementation.

\$\endgroup\$
1
  • \$\begingroup\$ @Billal Begueradj, I didn't understand your message, tell me, with this code, is the deposit in my payPal account? \$\endgroup\$ Dec 26, 2023 at 19:22

1 Answer 1

1
\$\begingroup\$

Foreward

I have not used the Paypal API though I have used PHP and Laravel for many years. I don't spot any bugs or security vulnerabilities, though that does not mean there is no possibility of them being present. The advice below should help simplify the code.

Code Review

Variable $purchase is created inside conditional block as well as outside block

There is a check to see if $comptor is not false-y and if so $purchase is created. Outside of that block is an identical creation of $purchase.

screenshot of duplicate code

The creation of that variable could happen before the conditional line if($comptor){ - i.e. unconditionally. As was mentioned in this review this would make it comply with the Don't repeat yourself (DRY) principle.

Spacing is inconsistent

There are some if statements followed by a space - e.g.

        if ($responseOrder->result->status !== "COMPLETED") {

and

        if ($package->price !== $amountCaptured) {

Yet others are not followed by a space - e.g.

        if($comptor){

For the sake of readability it would be wise to keep spacing consistent. Many PHP developers follow a style guide - e.g. PSR-12: Extended Coding Style.

Constructor and properties can be simplified

As of the time of writing PHP 8.1 and 8.2 are the currently supported versions with Active support. With PHP 8 Contructor Promotion can be used to greatly simplify the code in the constructor.

The following lines:

private $clientSecret;
private $clientId;
private $sandbox;

/**
 * @param string $clientId
 * @param string $clientSecret
 * @param bool $sandbox
 */

public function __construct(string $clientId, string $clientSecret, bool $sandbox){
    $this->clientId = $clientId;
    $this->clientSecret = $clientSecret;
    $this->sandbox = $sandbox;
}

Can be simplified to this:

/**
 * @param string $clientId
 * @param string $clientSecret
 * @param bool $sandbox
 */
public function __construct(
    private string $clientId, 
    private string $clientSecret, 
    private bool $sandbox
){
}

Eloquent shortcut method can be used for simplification

Instead of calling the Eloquent where() method and then calling first():

            $comptor = Comptor::where('user_id', '=', $user->id)->first();

One could simply call the firstWhere() method:

$comptor = Comptor::firstWhere('user_id', '=', $user->id);

Additionally the '=' can typically be omitted:

$comptor = Comptor::firstWhere('user_id', $user->id);

See the Laravel Database: Query Builder section Where Clauses:

For convenience, if you want to verify that a column is = to a given value, you may pass the value as the second argument to the where method.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.