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:
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:
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...
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?
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?
I just use the three backticks method and that seems to work ok for me
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?
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?
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.
Always before the code reaches 'ParkingResource' a validation like that already exists (in
start
andstop
) or is unnecessary (show
) - unnecessary because it gets a Parking record from the database, and we imply the data stored is valid for sure.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:
I'm not sure if I'm overthinking the situation, but better safe than sorry.
Good catch, better safe than sorry indeed!
Nice catch!
Brilliant
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();