Courses

Build Laravel API for Car Parking App: Step-By-Step

Price Calculation

Of course, users need to know how much they would pay for parking. It should happen in three phases:

  • Before parking - when getting the list of zones (done)
  • During parking - when getting the parking by ID (not done yet)
  • After parking - as a result of the stopping function (not done yet)

As I mentioned in the very beginning, we won't cover the payments themselves in this tutorial, we only take care of the calculations.

So, we need to create some function to calculate the current price by zone and duration, and then save that price in the parkings.total_price when the parking is stopped.

For that, let's create a separate Service class with a method to calculate the price. In Laravel, there's no Artisan command make:service, so we just create this file manually in the IDE.

app/Services/ParkingPriceService.php:

namespace App\Services;
 
use App\Models\Zone;
use Carbon\Carbon;
 
class ParkingPriceService {
 
public static function calculatePrice(int $zone_id, string $startTime, string $stopTime = null): int
{
$start = new Carbon($startTime);
$stop = (!is_null($stopTime)) ? new Carbon($stopTime) : now();
 
$totalTimeByMinutes = $stop->diffInMinutes($start);
 
$priceByMinutes = Zone::find($zone_id)->price_per_hour / 60;
 
return ceil($totalTimeByMinutes * $priceByMinutes);
}
 
}

As you can see, we convert $startTime and $stopTime to Carbon objects, calculate the difference, and multiply that by price per minute, for better accuracy than calculating per hour.

Notice: alternatively, you can choose to convert the DB fields to Carbon objects automatically, by using Eloquent casting.

Now, where do we use that service?

First, in the stop() method of the Controller.

use App\Models\Parking;
use App\Services\ParkingPriceService;
 
class ParkingController extends Controller
{
public function stop(Parking $parking)
{
$parking->update([
'stop_time' => now(),
'total_price' => ParkingPriceService::calculatePrice($parking->zone_id, $parking->start_time),
]);
 
return ParkingResource::make($parking);
}
}

Note that this Service with a static method is only one way to do it. You could put this method in the Model itself, or a Service with a non-static regular method.

So, when the parking is stopped, calculations are performed automatically, and in the DB, we have the saved value:

Laravel API Prices

But what if the user wants to find the current price before the parking is stopped? Well, we can call the calculation directly on the API Resource file:

app/Http/Resources/ParkingResource.php:

use App\Services\ParkingPriceService;
 
class ParkingResource extends JsonResource
{
public function toArray($request)
{
$totalPrice = $this->total_price ?? ParkingPriceService::calculatePrice(
$this->zone_id,
$this->start_time,
$this->stop_time
);
 
return [
'id' => $this->id,
'zone' => [
'name' => $this->zone->name,
'price_per_hour' => $this->zone->price_per_hour,
],
'vehicle' => [
'plate_number' => $this->vehicle->plate_number
],
'start_time' => $this->start_time,
'stop_time' => $this->stop_time,
'total_price' => $totalPrice,
];
}
}

So, if we don't have a stop_time yet, the current price will be calculated in real-time mode:

Laravel API Prices

So, that's about it: we've created all the basic API functionality for the parking application.

But wait, there's more in this tutorial...

Previous: Start/Stop Parking
avatar

Povilas i notice the comment editor here is not very intuitive and its hard to paste/write a block of code. is there any way you can fix this?

avatar

Well, I use laravel-comments.com from Spatie, so not much I personally can fix. I do see you guys are pasting code with Markdown and it's shown quite well to me, or am I missing something?

avatar

I just use the three backticks method and that seems to work ok for me

avatar
You can use Markdown
avatar

Noticed an error with this if there isn't a vehicle already set on the $this->vehicle relation - would it be ideal to add $request validation in this function? Or was there a reason why you wouldn't add validation here?

    public function toArray($request)
    {
        $totalPrice = $this->total_price ?? ParkingPriceService::calculatePrice(
            $this->zone_id,
            $this->start_time,
            $this->stop_time
        );
 
        return [
            'id' => $this->id,
            'zone' => [
                'name' => $this->zone->name,
                'price_per_hour' => $this->zone->price_per_hour,
            ],
            'vehicle' => [
                'plate_number' => $this->vehicle->plate_number
            ],
            'start_time' => $this->start_time,
            'stop_time' => $this->stop_time,
            'total_price' => $totalPrice,
        ];
    }
avatar

I added the following code to the top of the toArray() above and it seems to work ok, or would you recommend a different approach?

        if (!$this->vehicle) {
            abort('400', 'Vehicle missing');
        }

        if (!$this->zone) {
            abort('400', 'Zone missing');
        }
avatar

I don't think we need to add validation in here because at the time the ParkingResource was used, it already has the vehicle and zone. Moreover, if the zone or vehicle which the parking belong to was deleted,I pretty am sure the laravel will throw exception regarding foreign key violation constraint.

avatar

Always before the code reaches 'ParkingResource' a validation like that already exists (in start and stop) or is unnecessary (show) - unnecessary because it gets a Parking record from the database, and we imply the data stored is valid for sure.

avatar
You can use Markdown
avatar

I found that in our ParkingController class's stop() method, we didn't check if the parking stopped before updating its stop_time property, which could result in an incorrect price calculation if the /stop endpoint was triggered multiple times within different intervals.

This situation might be avoided in a frontend client by not allowing the user to click the stop button twice, however it cannot be totally avoided if the user hits the Api endpoint directly.

To resolve this issue, I added the following if statement to the top of ParkingController's stop() method:

if($parking->stop_time) {
return response()->json(['errors' => ['general' => ['Parking already stopped.']], ]
, Response::HTTP_UNPROCESSABLE_ENTITY);
}

I'm not sure if I'm overthinking the situation, but better safe than sorry.

👍 7
avatar

Good catch, better safe than sorry indeed!

avatar

Nice catch!

avatar

Brilliant

avatar
You can use Markdown
avatar

In the variable $totalTimeByMinutes I had to add abs() so that it returns a positive value because otherwise it would return a negative $totalprice

public static function calculatePrice(int $zone_id, string $startTime, string $stopTime = null): int { $start = new Carbon($startTime); $stop = (!is_null($stopTime)) ? new Carbon($stopTime) : now();

    $totalTimeByMinutes = abs($stop->diffInMinutes($start));

    $priceByMinutes = Zone::find($zone_id)->price_per_hour / 60;

    $totalPrice = ceil($totalTimeByMinutes * $priceByMinutes);

    return $totalPrice;
}
👍 1
avatar
You can use Markdown
avatar
You can use Markdown