فهرست منبع

working stuff

Fabien Chéret 5 سال پیش
والد
کامیت
94e48fc7bf
27فایلهای تغییر یافته به همراه1216 افزوده شده و 96 حذف شده
  1. 24 0
      README.md
  2. 60 0
      docs/delete.md
  3. 98 0
      docs/delpark.md
  4. 60 0
      docs/get.md
  5. 58 0
      docs/getid.md
  6. 117 0
      docs/post.md
  7. 93 0
      docs/postpark.md
  8. 128 0
      docs/putid.md
  9. 0 2
      src/main/java/eu/fibane/parkingtoll/ParkingTollApplication.java
  10. 0 9
      src/main/java/eu/fibane/parkingtoll/api/ApiException.java
  11. 0 9
      src/main/java/eu/fibane/parkingtoll/api/NotFoundException.java
  12. 1 2
      src/main/java/eu/fibane/parkingtoll/api/ParkingLotApi.java
  13. 10 10
      src/main/java/eu/fibane/parkingtoll/api/ParkingLotApiController.java
  14. 23 10
      src/main/java/eu/fibane/parkingtoll/core/InMemoryPersistenceManager.java
  15. 1 3
      src/main/java/eu/fibane/parkingtoll/core/PersistenceManager.java
  16. 11 0
      src/main/java/eu/fibane/parkingtoll/exceptions/DepartureIsBeforeArrivalException.java
  17. 11 0
      src/main/java/eu/fibane/parkingtoll/exceptions/ParkingNotFoundException.java
  18. 2 8
      src/main/java/eu/fibane/parkingtoll/model/CarSlot.java
  19. 10 0
      src/main/java/eu/fibane/parkingtoll/model/FareProcessor.java
  20. 9 7
      src/main/java/eu/fibane/parkingtoll/model/Layout.java
  21. 7 6
      src/main/java/eu/fibane/parkingtoll/model/ParkingLot.java
  22. 7 11
      src/main/java/eu/fibane/parkingtoll/model/PricingPolicy.java
  23. 165 4
      src/test/java/eu/fibane/parkingtoll/ParkingTollApplicationTests.java
  24. 19 15
      src/test/java/eu/fibane/parkingtoll/core/InMemoryPersistenceManagerTest.java
  25. 94 0
      src/test/java/eu/fibane/parkingtoll/model/LayoutTest.java
  26. 117 0
      src/test/java/eu/fibane/parkingtoll/model/ParkingLotTest.java
  27. 91 0
      src/test/java/eu/fibane/parkingtoll/model/PricingPolicyTest.java

+ 24 - 0
README.md

@@ -0,0 +1,24 @@
+# Toll Parking Library
+
+Server created with SpringBoot, Lombok, and maven
+
+## Build instructions  
+
+`mvn clean spring-boot:run`
+
+The server application will spawn on http://localhost:8080
+
+###Open Endpoints
+
+Endpoints for viewing and manipulating the Parking Lots and park cars.
+
+ - [Show Accessible Parking Lots](docs/get.md) : `GET /parking_lot`
+ - [Create Parking Lot](docs/get.md) : `POST /parking_lot`
+ - [Delete a Parking Lot](docs/delete.md) : `DELETE /parking_lot/{parkingLotId}`
+ - [Display a Parking Lot](docs/getid.md) : `GET /parking_lot/{parkingLotId}`
+ - [Update a Parking Lot](docs/putid.md) : `PUT /parking_lot/{parkingLotId}`
+ - [Park a car in a Parking Lot](docs/postpark.md) : `POST /parking_lot​/{parkingLotId}​/park`
+ - [Remove a car from a Parking Lot and returns the amount to pay](docs/delpark.md) : `DELETE /parking_lot​/{parkingLotId}​/park`
+
+
+

+ 60 - 0
docs/delete.md

@@ -0,0 +1,60 @@
+# Show Accessible Parking Lots
+
+Deletes a parking lot, given its id
+
+**URL** : `/parking_lot/{parkingLotId}`
+
+**Method** : `DELETE`
+
+## Parameters
+
+Name | Description 
+--- | --- 
+parkingLotId | Parking Lot id to delete 
+integer($int64) |
+
+
+## Success Responses
+
+**Code** : `200 OK`
+
+
+**Content** : 
+```json
+{
+     "id": 0,
+     "name": "parking Sophia 2",
+     "layout": [
+         {
+             "name": "standard",
+             "available": 10
+         },
+         {
+             "name": "25kW",
+             "available": 10
+         }
+     ],
+     "pricing_policy": {
+         "flat_fee": 1,
+         "per_hour_fare": 0.5
+     }
+ }
+```
+
+
+## Error Responses
+
+**Condition** : If parking lot id does not exist
+
+**Code** : `404 NOT FOUND`
+
+**Content example**
+```json
+{
+    "timestamp": "2020-01-01T10:00:00.542+0000",
+    "status": 404,
+    "error": "Not Found",
+    "message": "The given ID is not associated with a parking lot.",
+    "path": "/parking_lot/50"
+}
+```

+ 98 - 0
docs/delpark.md

@@ -0,0 +1,98 @@
+# Remove a car from a Parking Lot
+
+Remove a car from a Parking Lot, and returns the amount to pay.
+
+**URL** : `/parking_lot/{parkingLotId}/park`
+
+**Method** : `DELETE`
+
+**Parameters**
+
+Name | Description 
+--- | --- 
+parkingLotId | Parking Lot id where to park  
+integer($int64) |
+
+
+**Data constraints**
+
+`type` should be a non-null length character string
+
+`parking_lot_id` should be an integer
+
+**Data example** all fields must be sent
+
+```json
+{
+    "slot": 1,
+    "parking_lot_id": 5,
+    "arrival_time": "2020-03-08T19:12:31.042474300Z",
+    "type": "25kW"
+}
+```
+
+
+
+## Success Responses
+
+**Code** : `200 OK`
+
+**Content** : 
+```json
+{
+    "slot": 1,
+    "parking_lot_id": 5,
+    "arrival_time": "2020-01-01T02:12:31.042474300Z",
+    "departure_time": "2020-03-08T05:13:52.297646900Z",
+    "type": "25kW",
+    "price": 1.5
+}
+```
+
+
+## Error Responses
+
+**Condition** : If the type of car is not present in the parking lot  
+
+**Code** : `404 NOT FOUND`
+
+**Content example**
+```json
+{
+    "timestamp": "2020-03-08T05:13:52.297646900Z",
+    "status": 404,
+    "error": "Not Found",
+    "message": "This car is not at the specified location",
+    "path": "/parking_lot/0/park"
+}
+```
+
+**Condition** : If the given ID is not associated with a Parking Lot 
+
+**Code** : `404 NOT FOUND`
+
+**Content example**
+```json
+{
+    "timestamp": "2020-01-01T01:01:01.449192500Z",
+    "status": 404,
+    "error": "Not Found",
+    "message": "The given ID is not associated with a parking lot.",
+    "path": "/parking_lot/5/park"
+}
+```
+
+**Condition** : If the given ID is not associated with a parking lot 
+
+**Code** : `404 NOT FOUND`
+
+**Content example**
+```json
+{
+    "timestamp": "2020-01-01T01:01:01.449192500Z",
+    "status": 404,
+    "error": "Not Found",
+    "message": "The given ID is not associated with a parking lot.",
+    "path": "/parking_lot/8/park"
+}
+```

+ 60 - 0
docs/get.md

@@ -0,0 +1,60 @@
+# Show Accessible Parking Lots
+
+Show all Parking Lots
+
+**URL** : `/parking_lot`
+
+**Method** : `GET`
+
+## Optional Parameters
+
+Name | Description 
+--- | --- 
+searchString | pass an optional search string for looking up parking lots 
+string |
+
+## Success Responses
+
+**Code** : `200 OK`
+
+**Content** : 
+```json
+[
+    {
+        "id": 0,
+        "name": "parking victoria 1",
+        "layout": [
+            {
+                "name": "standard",
+                "available": 10
+            },
+            {
+                "name": "25kW",
+                "available": 10
+            }
+        ],
+        "pricing_policy": {
+            "flat_fee": 1,
+            "per_hour_fare": 0.5
+        }
+    },
+    {
+        "id": 1,
+        "name": "Sophia 2",
+        "layout": [
+            {
+                "name": "standard",
+                "available": 10
+            },
+            {
+                "name": "25kW",
+                "available": 10
+            }
+        ],
+        "pricing_policy": {
+            "flat_fee": 1,
+            "per_hour_fare": 0.5
+        }
+    }
+]
+```

+ 58 - 0
docs/getid.md

@@ -0,0 +1,58 @@
+# Display a Parking Lot
+
+Display a Parking Lot given its id
+
+**URL** : `/parking_lot/{parkingLotId}`
+
+**Method** : `GET`
+
+## Parameters
+
+Name | Description 
+--- | --- 
+parkingLotId | Parking Lot id to display 
+integer($int64) |
+
+## Success Responses
+
+**Code** : `200 OK`
+
+**Content** : 
+```json
+{
+    "id": 0,
+    "name": "parking victoria 1",
+    "layout": [
+        {
+            "name": "standard",
+            "available": 10
+        },
+        {
+            "name": "25kW",
+            "available": 10
+        }
+    ],
+    "pricing_policy": {
+        "flat_fee": 1,
+        "per_hour_fare": 0.5
+    }
+}
+```
+
+## Error Responses
+
+**Condition** : If parking lot id does not exist
+
+**Code** : `404 NOT FOUND`
+
+**Content example**
+```json
+{
+    "timestamp": "2020-01-01T10:00:00.542+0000",
+    "status": 404,
+    "error": "Not Found",
+    "message": "The given ID is not associated with a parking lot.",
+    "path": "/parking_lot/50"
+}
+```
+

+ 117 - 0
docs/post.md

@@ -0,0 +1,117 @@
+# Create a Parking Lot
+
+Create a Parking Lot
+
+**URL** : `/parking_lot`
+
+**Method** : `POST`
+
+**Data constraints**
+
+`name` should be a non-null length character string
+
+`layout` should not be null
+
+`layout.available` should be a positive integer
+
+`layout.name` should be a non-null length character string
+
+`pricing_policy` should not be null
+
+
+**Data example** all fields must be sent
+
+```json
+{
+  "name": "parking Sophia 2",
+  "layout": [
+    {
+      "name": "standard",
+      "available": 10
+    },
+    {
+      "name": "25kW",
+      "available": 10
+    }
+  ],
+  "pricing_policy": {
+    "flat_fee": 1,
+    "per_hour_fare": 0.5
+  }
+}
+```
+
+
+
+## Success Responses
+
+**Code** : `201 CREATED`
+
+**Headers** : `Location: /parking_lot/{id}`
+
+**Content** : 
+```json
+{
+     "id": 0,
+     "name": "parking Sophia 2",
+     "layout": [
+         {
+             "name": "standard",
+             "available": 10
+         },
+         {
+             "name": "25kW",
+             "available": 10
+         }
+     ],
+     "pricing_policy": {
+         "flat_fee": 1,
+         "per_hour_fare": 0.5
+     }
+ }
+```
+
+
+## Error Responses
+
+**Condition** : If fields are missing
+
+**Code** : `400 BAD REQUEST`
+
+**Content example**
+```json
+{
+    "timestamp": "2020-01-01T10:00:00.542+0000",
+    "status": 400,
+    "error": "Bad Request",
+    "errors": [
+        {
+            "codes": [
+                "NotNull.parkingLot.pricingPolicy",
+                "NotNull.pricingPolicy",
+                "NotNull.eu.fibane.parkingtoll.model.PricingPolicy",
+                "NotNull"
+            ],
+            "arguments": [
+                {
+                    "codes": [
+                        "parkingLot.pricingPolicy",
+                        "pricingPolicy"
+                    ],
+                    "arguments": null,
+                    "defaultMessage": "pricingPolicy",
+                    "code": "pricingPolicy"
+                }
+            ],
+            "defaultMessage": "ne peut pas être nul",
+            "objectName": "parkingLot",
+            "field": "pricingPolicy",
+            "rejectedValue": null,
+            "bindingFailure": false,
+            "code": "NotNull"
+        }
+    ],
+    "message": "Validation failed for object='parkingLot'. Error count: 1",
+    "path": "/parking_lot"
+}
+```

+ 93 - 0
docs/postpark.md

@@ -0,0 +1,93 @@
+# Park a car at a specified Parking Lot
+
+Park a car at a specified Parking Lot
+
+**URL** : `/parking_lot/{parkingLotId}/park`
+
+**Method** : `POST`
+
+**Parameters**
+
+Name | Description 
+--- | --- 
+parkingLotId | Parking Lot id where to park  
+integer($int64) |
+
+
+**Data constraints**
+
+`type` should be a non-null length character string
+
+**Data example** all fields must be sent
+
+```json
+{
+  "type": "25kW"
+}
+```
+
+
+
+## Success Responses
+
+**Code** : `200 OK`
+
+**Content** : 
+```json
+{
+    "slot": 0,
+    "parking_lot_id": 0,
+    "arrival_time": "2020-01-01T01:01:01.449192500Z",
+    "departure_time": null,
+    "type": "25kW",
+    "price": null
+}
+```
+
+
+## Error Responses
+
+**Condition** : If the type of slot does not exist in the parking lot  
+
+**Code** : `404 NOT FOUND`
+
+**Content example**
+```json
+{
+    "timestamp": "2020-01-01T01:01:01.449192500Z",
+    "status": 404,
+    "error": "Not Found",
+    "message": "This type of slot does not exist in this parking lot",
+    "path": "/parking_lot/0/park"
+}
+```
+
+**Condition** : If the given ID is not associated with a Parking Lot 
+
+**Code** : `404 NOT FOUND`
+
+**Content example**
+```json
+{
+    "timestamp": "2020-01-01T01:01:01.449192500Z",
+    "status": 404,
+    "error": "Not Found",
+    "message": "The given ID is not associated with a parking lot.",
+    "path": "/parking_lot/5/park"
+}
+```
+
+**Condition** : If the Parking Lot is full for this type of car 
+
+**Code** : `503 SERVICE UNAVAILABLE`
+
+**Content example**
+```json
+{
+    "timestamp": "2020-01-01T01:01:01.449192500Z",
+    "status": 503,
+    "error": "Service Unavailable",
+    "message": "Parking is full for this type of car",
+    "path": "/parking_lot/0/park"
+}
+```

+ 128 - 0
docs/putid.md

@@ -0,0 +1,128 @@
+# Update a Parking Lot
+
+Update a Parking Lot given its id
+
+**URL** : `/parking_lot/{parkingLotId}`
+
+**Method** : `PUT`
+
+## Parameters
+
+Name | Description 
+--- | --- 
+parkingLotId | Parking Lot id to update 
+integer($int64) |
+
+**Data constraints**
+
+`id` integer($int64)
+
+`name` should be a non-null length character string
+
+`layout` should not be null
+
+`layout.available` should be a positive integer
+
+`layout.name` should be a non-null length character string
+
+`pricing_policy` should not be null
+
+
+**Data example**
+
+```json
+{
+  "id": 0,
+  "name": "parking Sophia 2",
+  "layout": [
+    {
+      "name": "standard",
+      "available": 10
+    },
+    {
+      "name": "25kW",
+      "available": 10
+    }
+  ],
+  "pricing_policy": {
+    "flat_fee": 1,
+    "per_hour_fare": 0.5
+  }
+}
+```
+
+## Success Responses
+
+**Code** : `204 NO CONTENT` if the update was successful
+
+**Code** : `201 CREATED` if the given `parkingLotId` did not exist
+
+**Headers** : `Location: /parking_lot/{id}`
+
+**Content** : 
+```json
+{
+    "id": 0,
+    "name": "parking victoria 1",
+    "layout": [
+        {
+            "name": "standard",
+            "available": 10
+        },
+        {
+            "name": "25kW",
+            "available": 10
+        }
+    ],
+    "pricing_policy": {
+        "flat_fee": 1,
+        "per_hour_fare": 0.5
+    }
+}
+```
+
+
+
+## Error Responses
+
+**Condition** : If fields are missing
+
+**Code** : `400 BAD REQUEST`
+
+**Content example**
+```json
+{
+    "timestamp": "2020-01-01T10:00:00.542+0000",
+    "status": 400,
+    "error": "Bad Request",
+    "errors": [
+        {
+            "codes": [
+                "NotNull.parkingLot.pricingPolicy",
+                "NotNull.pricingPolicy",
+                "NotNull.eu.fibane.parkingtoll.model.PricingPolicy",
+                "NotNull"
+            ],
+            "arguments": [
+                {
+                    "codes": [
+                        "parkingLot.pricingPolicy",
+                        "pricingPolicy"
+                    ],
+                    "arguments": null,
+                    "defaultMessage": "pricingPolicy",
+                    "code": "pricingPolicy"
+                }
+            ],
+            "defaultMessage": "ne peut pas être nul",
+            "objectName": "parkingLot",
+            "field": "pricingPolicy",
+            "rejectedValue": null,
+            "bindingFailure": false,
+            "code": "NotNull"
+        }
+    ],
+    "message": "Validation failed for object='parkingLot'. Error count: 1",
+    "path": "/parking_lot"
+}
+```

+ 0 - 2
src/main/java/eu/fibane/parkingtoll/ParkingTollApplication.java

@@ -8,8 +8,6 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
 @SpringBootApplication
 public class ParkingTollApplication {
 
-	protected final Log logger = LogFactory.getLog(getClass());
-
 	public static void main(String[] args) {
 		SpringApplication.run(ParkingTollApplication.class, args);
 	}

+ 0 - 9
src/main/java/eu/fibane/parkingtoll/api/ApiException.java

@@ -1,9 +0,0 @@
-package eu.fibane.parkingtoll.api;
-
-public class ApiException extends Exception{
-    private int code;
-    public ApiException (int code, String msg) {
-        super(msg);
-        this.code = code;
-    }
-}

+ 0 - 9
src/main/java/eu/fibane/parkingtoll/api/NotFoundException.java

@@ -1,9 +0,0 @@
-package eu.fibane.parkingtoll.api;
-
-public class NotFoundException extends ApiException {
-    private int code;
-    public NotFoundException (int code, String msg) {
-        super(code, msg);
-        this.code = code;
-    }
-}

+ 1 - 2
src/main/java/eu/fibane/parkingtoll/api/ParkingLotApi.java

@@ -7,7 +7,6 @@ import org.springframework.web.bind.annotation.*;
 
 import javax.validation.Valid;
 import java.util.Collection;
-import java.util.List;
 
 public interface ParkingLotApi {
 
@@ -38,7 +37,7 @@ public interface ParkingLotApi {
     ResponseEntity<CarSlot> parkParkingLot(@PathVariable("parkingLotId") Long parkingLotId, @Valid @RequestBody CarSlot carSlotItem);
 
 
-    @PutMapping(value = "/parking_lot",
+    @PutMapping(value = "/parking_lot/{parkingLotId}",
             produces = { "application/json" },
             consumes = { "application/json" })
     ResponseEntity<ParkingLot> updateParkingLot(@PathVariable("parkingLotId") Long parkingLotId, @Valid @RequestBody ParkingLot parkingLotItem);

+ 10 - 10
src/main/java/eu/fibane/parkingtoll/api/ParkingLotApiController.java

@@ -7,7 +7,6 @@ import eu.fibane.parkingtoll.model.ParkingLot;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RequestBody;
@@ -39,23 +38,17 @@ public class ParkingLotApiController implements ParkingLotApi {
 
     public ResponseEntity<ParkingLot> addParkingLot(@Valid @RequestBody ParkingLot parkingLotItem) {
 
-        persistenceManager.addParkingLot(parkingLotItem);
+        parkingLotItem = persistenceManager.addParkingLot(parkingLotItem);
         return ResponseEntity.created(URI.create("/parking_lot/" + parkingLotItem.getId())).body(parkingLotItem);
     }
 
     public ResponseEntity<ParkingLot> parkingLotDeleteById(@PathVariable("parkingLotId") Long parkingLotId) {
         ParkingLot parkingLot = persistenceManager.deleteParkingLotById(parkingLotId);
-        if(parkingLot == null){
-            return ResponseEntity.notFound().build();
-        }
         return ResponseEntity.ok(parkingLot);
     }
 
     public ResponseEntity<ParkingLot> parkingLotGetById(@PathVariable("parkingLotId") Long parkingLotId) {
         ParkingLot parkingLot = persistenceManager.getParkingLotById(parkingLotId);
-        if(parkingLot == null){
-            return ResponseEntity.notFound().build();
-        }
         return ResponseEntity.ok(parkingLot);
     }
 
@@ -78,8 +71,15 @@ public class ParkingLotApiController implements ParkingLotApi {
     }
 
     public ResponseEntity<ParkingLot> updateParkingLot(@PathVariable("parkingLotId") Long parkingLotId, @Valid @RequestBody ParkingLot parkingLotItem) {
-        ParkingLot parkingLot = persistenceManager.updateParkingLot(parkingLotId, parkingLotItem);
-        return  ResponseEntity.ok(parkingLot);
+        Long oldID = parkingLotItem.getId();
+        parkingLotItem.setId(parkingLotId);
+        persistenceManager.updateParkingLot(parkingLotId, parkingLotItem);
+        if(!parkingLotItem.getId().equals(oldID)){
+            //it's a creation - 201
+            return ResponseEntity.created(URI.create("/parking_lot/" + parkingLotItem.getId())).build();
+        }
+        //204 no content
+        return ResponseEntity.noContent().build();
     }
 
     public ResponseEntity<Collection<ParkingLot>> searchParkingLot(@Valid @RequestParam(value = "searchString", required = false) String searchString) {

+ 23 - 10
src/main/java/eu/fibane/parkingtoll/core/InMemoryPersistenceManager.java

@@ -1,9 +1,13 @@
 package eu.fibane.parkingtoll.core;
 
+import eu.fibane.parkingtoll.exceptions.ParkingNotFoundException;
 import eu.fibane.parkingtoll.model.ParkingLot;
 import org.springframework.stereotype.Repository;
 
-import java.util.*;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.concurrent.atomic.AtomicLong;
 
 @Repository
@@ -27,26 +31,35 @@ public class InMemoryPersistenceManager implements PersistenceManager {
 
     @Override
     public ParkingLot getParkingLotById(Long id) {
-        return parkingLotMap.get(id);
+        ParkingLot result = parkingLotMap.get(id);
+        if(result == null){
+            throw new ParkingNotFoundException("The given ID is not associated with a parking lot.");
+        }
+        return result;
+    }
+
+    public boolean parkingLotExists(Long id){
+        return parkingLotMap.get(id) != null;
     }
 
     @Override
-    public ParkingLot updateParkingLot(Long id, ParkingLot parkingLot) {
-        //TODO concurrent updates
-        ParkingLot response;
-        if(parkingLot.getId() == null){
-            response = addParkingLot(parkingLot);
+    public void updateParkingLot(Long id, ParkingLot parkingLot) {
+        if(!parkingLotExists(id)){
+            addParkingLot(parkingLot);
         } else {
             //secure case where user puts wrong ID
             parkingLot.setId(id);
-            response = parkingLotMap.put(parkingLot.getId(), parkingLot);
+            parkingLotMap.put(parkingLot.getId(), parkingLot);
         }
-        return response;
     }
 
     @Override
     public ParkingLot deleteParkingLotById(Long id) {
-        return parkingLotMap.remove(id);
+        ParkingLot result = parkingLotMap.remove(id);
+        if(result == null){
+            throw new ParkingNotFoundException("The given ID is not associated with a parking lot.");
+        }
+        return result;
     }
 
 

+ 1 - 3
src/main/java/eu/fibane/parkingtoll/core/PersistenceManager.java

@@ -2,7 +2,6 @@ package eu.fibane.parkingtoll.core;
 
 import eu.fibane.parkingtoll.model.ParkingLot;
 
-import java.math.BigInteger;
 import java.util.Collection;
 
 public interface PersistenceManager {
@@ -30,9 +29,8 @@ public interface PersistenceManager {
     /**
      * Update the specified ParkingLot in database. If it was not added previously, add it.
      * @param parkingLot the new value of parkingLot
-     * @return the previous representation of parkingLot
      */
-    ParkingLot updateParkingLot(Long id, ParkingLot parkingLot);
+    void updateParkingLot(Long id, ParkingLot parkingLot);
 
     //public void parkCarAtParking(Long id);
 

+ 11 - 0
src/main/java/eu/fibane/parkingtoll/exceptions/DepartureIsBeforeArrivalException.java

@@ -0,0 +1,11 @@
+package eu.fibane.parkingtoll.exceptions;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE)
+public class DepartureIsBeforeArrivalException extends RuntimeException {
+    public DepartureIsBeforeArrivalException(String message){
+        super(message);
+    }
+}

+ 11 - 0
src/main/java/eu/fibane/parkingtoll/exceptions/ParkingNotFoundException.java

@@ -0,0 +1,11 @@
+package eu.fibane.parkingtoll.exceptions;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(HttpStatus.NOT_FOUND)
+public class ParkingNotFoundException extends RuntimeException {
+    public ParkingNotFoundException(String message){
+        super(message);
+    }
+}

+ 2 - 8
src/main/java/eu/fibane/parkingtoll/model/CarSlot.java

@@ -1,14 +1,12 @@
 package eu.fibane.parkingtoll.model;
 
-import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import lombok.Data;
 import org.springframework.validation.annotation.Validated;
 
-import javax.validation.Valid;
+import javax.validation.constraints.NotNull;
 import java.math.BigDecimal;
 import java.time.Instant;
-import java.util.Objects;
 
 
 @Validated @Data
@@ -27,16 +25,12 @@ public class CarSlot   {
   private Instant departureTime = null;
 
   @JsonProperty("type")
+  @NotNull
   private String type = null;
 
   @JsonProperty("price")
   private BigDecimal price = null;
 
-  public CarSlot slot(Long slot) {
-    this.slot = slot;
-    return this;
-  }
-
   public void updateDepartureTime(){
     this.departureTime = Instant.now();
   }

+ 10 - 0
src/main/java/eu/fibane/parkingtoll/model/FareProcessor.java

@@ -0,0 +1,10 @@
+package eu.fibane.parkingtoll.model;
+
+import java.math.BigDecimal;
+import java.time.Instant;
+
+public interface FareProcessor {
+
+    BigDecimal computeFare(Instant arrival, Instant departure);
+
+}

+ 9 - 7
src/main/java/eu/fibane/parkingtoll/model/Layout.java

@@ -8,11 +8,14 @@ import eu.fibane.parkingtoll.exceptions.ParkingIsFullException;
 import lombok.Data;
 import org.springframework.validation.annotation.Validated;
 
+import javax.validation.constraints.NotNull;
 import java.time.Instant;
-import java.util.*;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.NoSuchElementException;
 import java.util.concurrent.ConcurrentLinkedDeque;
 import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.atomic.AtomicLong;
 import java.util.stream.Collectors;
 import java.util.stream.LongStream;
 
@@ -24,9 +27,11 @@ import java.util.stream.LongStream;
 @JsonTypeName("layout")
 public class Layout {
   @JsonProperty("name")
-  private String name = null;
+  @NotNull
+  private String name;
 
   @JsonProperty("available")
+  @NotNull
   private AtomicInteger available = new AtomicInteger();
   @JsonIgnore
   Deque<Long> ids;
@@ -48,7 +53,7 @@ public class Layout {
     try {
       id = ids.pop();
     } catch (NoSuchElementException e){
-      return null; //parking is full
+      throw new ParkingIsFullException("Parking is full for this type of car");
     }
     this.available.decrementAndGet();
     return id;
@@ -62,9 +67,6 @@ public class Layout {
 
   public void parkCar(CarSlot carSlot) {
     Long id = decrementAndGetID();
-    if(id == null){
-      throw new ParkingIsFullException("Parking is full for this type of car");
-    }
     carSlot.setSlot(id);
     carSlot.setArrivalTime(Instant.now());
     carSlots.put(id,carSlot);

+ 7 - 6
src/main/java/eu/fibane/parkingtoll/model/ParkingLot.java

@@ -8,10 +8,9 @@ import org.springframework.validation.annotation.Validated;
 
 import javax.validation.Valid;
 import javax.validation.constraints.NotNull;
-import javax.validation.constraints.Null;
+import javax.validation.constraints.Size;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Objects;
 import java.util.Optional;
 import java.util.stream.Collectors;
 
@@ -25,25 +24,27 @@ public class ParkingLot   {
   private Long id = null;
 
   @JsonProperty("name")
+  @NotNull @Size(min = 1)
   private String name = null;
 
   @JsonProperty("layout")
-  @Valid
+  @Valid @NotNull
   private List<Layout> layoutList = new ArrayList<Layout>();
 
   @JsonProperty("pricing_policy")
-  private PricingPolicy pricingPolicy = null;
+  @Valid @NotNull
+  private PricingPolicy pricingPolicy;
 
   public CarSlot parkCar(CarSlot carSlot){
     Layout layout = getLayoutByName(carSlot.getType());
     layout.parkCar(carSlot);
+    carSlot.setParkingLotId(this.id);
     return carSlot;
   }
 
   public CarSlot removeCar(CarSlot carSlot){
     Layout layout = getLayoutByName(carSlot.getType());
-    CarSlot oldCarSlot = layout.removeCar(carSlot);
-    return oldCarSlot;
+    return layout.removeCar(carSlot);
   }
 
 

+ 7 - 11
src/main/java/eu/fibane/parkingtoll/model/PricingPolicy.java

@@ -1,34 +1,30 @@
 package eu.fibane.parkingtoll.model;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
+import eu.fibane.parkingtoll.exceptions.DepartureIsBeforeArrivalException;
 import lombok.Data;
 import org.springframework.validation.annotation.Validated;
 
-import javax.validation.Valid;
 import java.math.BigDecimal;
 import java.time.Duration;
 import java.time.Instant;
-import java.time.temporal.ChronoUnit;
-import java.util.Objects;
 
 /**
  * PricingPolicy
  */
 @Validated
 @Data
-public class PricingPolicy   {
+public class PricingPolicy implements FareProcessor {
   @JsonProperty("flat_fee")
   private BigDecimal flatFee = null;
 
   @JsonProperty("per_hour_fare")
   private BigDecimal perHourFare = null;
 
-  public PricingPolicy flatFee(BigDecimal flatFee) {
-    this.flatFee = flatFee;
-    return this;
-  }
-
   public BigDecimal computeFare(Instant arrival, Instant departure){
+    if(arrival.isAfter(departure)){
+      throw new DepartureIsBeforeArrivalException("Departure is before arrival, should not happen");
+    }
     BigDecimal result = BigDecimal.ZERO;
     if(flatFee != null && flatFee.compareTo(BigDecimal.ZERO) != 0){
       result = result.add(flatFee);
@@ -40,9 +36,9 @@ public class PricingPolicy   {
       if(duration.toMinutesPart() != 0){
         multiplier++;
       }
-      result = result.add(flatFee.multiply(BigDecimal.valueOf(multiplier)));
+      result = result.add(perHourFare.multiply(BigDecimal.valueOf(multiplier)));
     }
-    return result;
+    return result.stripTrailingZeros();
   }
 
 }

+ 165 - 4
src/test/java/eu/fibane/parkingtoll/ParkingTollApplicationTests.java

@@ -1,21 +1,182 @@
 package eu.fibane.parkingtoll;
 
 import eu.fibane.parkingtoll.api.ParkingLotApiController;
+import eu.fibane.parkingtoll.core.InMemoryPersistenceManager;
+import eu.fibane.parkingtoll.exceptions.ParkingNotFoundException;
+import eu.fibane.parkingtoll.model.CarSlot;
+import eu.fibane.parkingtoll.model.ParkingLot;
+import eu.fibane.parkingtoll.model.ParkingLotTest;
+import eu.fibane.parkingtoll.model.PricingPolicy;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
 
-import static org.junit.jupiter.api.Assertions.assertNotNull;
+import java.math.BigDecimal;
+import java.net.URI;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.*;
 
 @SpringBootTest
 class ParkingTollApplicationTests {
 
-	@Autowired
+	@Mock @Autowired
+	private InMemoryPersistenceManager persistenceManager;
+	@InjectMocks
 	private ParkingLotApiController parkingLotApiController;
 
+	private List<ParkingLot> parkingLots;
+
+	@BeforeEach
+	void init(){
+		MockitoAnnotations.initMocks(this);
+
+		//create data for our tests
+		parkingLots = new ArrayList<>();
+		parkingLots.add(ParkingLotTest.generateParking("Test parking 1",1L));
+		parkingLots.add(ParkingLotTest.generateParking("Test parking 2",2L));
+		parkingLots.add(ParkingLotTest.generateParking("Test parking 3",3L));
+		parkingLots.add(ParkingLotTest.generateParking("Test parking 3 second part",4L));
+	}
+
+	@Test
+	void searchEmptyParkingLotTest() {
+		when(persistenceManager.getAllParkingLots()).thenReturn(Collections.emptyList());
+
+		ResponseEntity<Collection<ParkingLot>> result = parkingLotApiController.searchParkingLot("");
+		assertNotNull(result.getBody());
+		assertEquals(0, result.getBody().size());
+	}
+
+	@Test
+	void searchParkingLotTestWithWrongName() {
+		when(persistenceManager.getAllParkingLots()).thenReturn(parkingLots);
+
+		ResponseEntity<Collection<ParkingLot>> result = parkingLotApiController.searchParkingLot("");
+		assertNotNull(result.getBody());
+		assertEquals(parkingLots.size(), result.getBody().size());
+
+		result = parkingLotApiController.searchParkingLot("String that is not in the names of the parking lots");
+		assertNotNull(result.getBody());
+		assertEquals(0, result.getBody().size());
+	}
+
+	@Test
+	void searchParkingLotTestWithCorrectName() {
+		when(persistenceManager.getAllParkingLots()).thenReturn(parkingLots);
+
+		ResponseEntity<Collection<ParkingLot>> result = parkingLotApiController.searchParkingLot("");
+		assertNotNull(result.getBody());
+		assertEquals(parkingLots.size(), result.getBody().size());
+
+		//entire name
+		result = parkingLotApiController.searchParkingLot("Test parking 1");
+		assertNotNull(result.getBody());
+		assertEquals(1, result.getBody().size());
+
+		result = parkingLotApiController.searchParkingLot("Test parking 3");
+		assertNotNull(result.getBody());
+		assertEquals(2, result.getBody().size());
+	}
+
+	@Test
+	void parkingLotDeleteByIdTest(){
+		when(persistenceManager.deleteParkingLotById(eq(-5L))).thenThrow(ParkingNotFoundException.class);
+		when(persistenceManager.deleteParkingLotById(eq(1L))).thenReturn(parkingLots.get(0));
+
+		//non existing id
+		assertThrows(ParkingNotFoundException.class, () -> parkingLotApiController.parkingLotDeleteById(-5L));
+
+		ResponseEntity<ParkingLot> result = parkingLotApiController.parkingLotDeleteById(1L);
+		assertEquals(parkingLots.get(0), result.getBody());
+		assertEquals(result.getStatusCode(), HttpStatus.OK);
+
+		verify(persistenceManager, times(2)).deleteParkingLotById(any());
+	}
+
 	@Test
-	void contextLoads() {
-		assertNotNull(parkingLotApiController);
+	void addParkingLotTest(){
+		ParkingLot storedParkingLot = ParkingLotTest.generateParking("created parking", 55L);
+
+		when(persistenceManager.addParkingLot(any())).thenReturn(storedParkingLot);
+
+		ResponseEntity<ParkingLot> response = parkingLotApiController.addParkingLot(parkingLots.get(0));
+
+		assertEquals(HttpStatus.CREATED, response.getStatusCode());
+		assertEquals(URI.create("/parking_lot/55") , response.getHeaders().getLocation());
+		assertNotNull(response.getBody());
+		assertEquals(55L, response.getBody().getId());
+	}
+
+	@Test
+	void parkingLotGetByIdTest(){
+		ParkingLot storedParkingLot = ParkingLotTest.generateParking("created parking", 5L);
+		when(persistenceManager.getParkingLotById(5L)).thenReturn(storedParkingLot);
+		ResponseEntity<ParkingLot> result = parkingLotApiController.parkingLotGetById(5L);
+
+		assertEquals(HttpStatus.OK, result.getStatusCode());
+
+	}
+
+	@Test
+	void leaveParkingLotTest(){
+		ParkingLot storedParkingLot = ParkingLotTest.generateParking("created parking", 5L);
+		PricingPolicy policy = new PricingPolicy();
+		policy.setFlatFee(BigDecimal.valueOf(1));
+		storedParkingLot.setPricingPolicy(policy);
+
+		when(persistenceManager.getParkingLotById(storedParkingLot.getId())).thenReturn(storedParkingLot);
+
+		CarSlot carSlot = new CarSlot();
+		carSlot.setArrivalTime(Instant.now().minus(Duration.ofHours(1)));
+		carSlot.setType(storedParkingLot.getSlotTypes().get(0));
+		storedParkingLot.parkCar(carSlot);
+
+		ResponseEntity<CarSlot> result = parkingLotApiController.leaveParkingLot(storedParkingLot.getId(), carSlot);
+		assertEquals(HttpStatus.OK, result.getStatusCode());
+	}
+
+	@Test
+	void parkParkingLotTest(){
+		ParkingLot storedParkingLot = ParkingLotTest.generateParking("created parking", 5L);
+		CarSlot carSlot = new CarSlot();
+		carSlot.setType(storedParkingLot.getSlotTypes().get(0));
+		when(persistenceManager.getParkingLotById(storedParkingLot.getId())).thenReturn(storedParkingLot);
+
+		ResponseEntity<CarSlot> result = parkingLotApiController.parkParkingLot(storedParkingLot.getId(), carSlot);
+		assertEquals(HttpStatus.OK, result.getStatusCode());
+	}
+
+	@Test
+	void updateParkingLotTest(){
+		//no existing parking lots
+		when(persistenceManager.getParkingLotById(any())).thenReturn(null);
+		doNothing().when(persistenceManager).updateParkingLot(anyLong(), any());
+
+		ParkingLot newParkingLot = ParkingLotTest.generateParking("created parking", 5L);
+		ResponseEntity<ParkingLot> result = parkingLotApiController.updateParkingLot(newParkingLot.getId(), newParkingLot);
+		assertEquals(HttpStatus.NO_CONTENT, result.getStatusCode());
+
+
+		when(persistenceManager.parkingLotExists(5L)).thenReturn(true);
+		newParkingLot = ParkingLotTest.generateParking("created parking", 5L);
+		result = parkingLotApiController.updateParkingLot(newParkingLot.getId(), newParkingLot);
+		assertEquals(HttpStatus.NO_CONTENT, result.getStatusCode());
+
 	}
 
 }

+ 19 - 15
src/test/java/eu/fibane/parkingtoll/core/InMemoryPersistenceManagerTest.java

@@ -1,7 +1,9 @@
 package eu.fibane.parkingtoll.core;
 
+import eu.fibane.parkingtoll.exceptions.ParkingNotFoundException;
 import eu.fibane.parkingtoll.model.Layout;
 import eu.fibane.parkingtoll.model.ParkingLot;
+import eu.fibane.parkingtoll.model.ParkingLotTest;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
@@ -20,19 +22,28 @@ import java.util.stream.Collectors;
 import static org.junit.jupiter.api.Assertions.*;
 
 class InMemoryPersistenceManagerTest {
-    private long ids = 0;
     private final PersistenceManager manager = new InMemoryPersistenceManager();
     private final SecureRandom random = new SecureRandom();
+
     @BeforeEach
     void clearDatabase() {
         manager.clearDatabase();
     }
 
     @Test
-    void testEmptyDatabase() {
+    void testClearDatabase() {
+        manager.clearDatabase();
         assertEquals(0, manager.getAllParkingLots().size());
-        assertNull(manager.getParkingLotById(0L));
-        assertNull(manager.deleteParkingLotById(getNewParkingLot().getId()));
+        assertThrows(ParkingNotFoundException.class, () -> manager.getParkingLotById(0L));
+        assertThrows(ParkingNotFoundException.class, () -> manager.deleteParkingLotById(getNewParkingLot().getId()));
+    }
+
+    @Test
+    void getParkingLotByIdTest(){
+        assertThrows(ParkingNotFoundException.class, () -> manager.getParkingLotById(5L));
+        ParkingLot parkingLot = manager.addParkingLot(ParkingLotTest.generateParking("test", 5L));
+        assertNotNull(parkingLot);
+        assertNotNull(parkingLot.getId());
     }
 
     @Test
@@ -111,17 +122,15 @@ class InMemoryPersistenceManagerTest {
         ParkingLot parkingLot = getNewParkingLot();
         assertEquals(0, manager.getAllParkingLots().size());
         assertNull(parkingLot.getId());
-        parkingLot = manager.updateParkingLot(parkingLot.getId(), parkingLot);
+        manager.updateParkingLot(parkingLot.getId(), parkingLot);
         assertEquals(1, manager.getAllParkingLots().size());
         assertNotNull(parkingLot.getId());
 
         ParkingLot parkingLot2 = getNewParkingLot();
         //set the id of parking 1
         parkingLot2.setId(parkingLot.getId());
-        ParkingLot parkingLotUpdated = manager.updateParkingLot(parkingLot2.getId(), parkingLot2);
+        manager.updateParkingLot(parkingLot2.getId(), parkingLot2);
         assertEquals(1, manager.getAllParkingLots().size());
-        assertEquals(parkingLotUpdated, parkingLot);
-
     }
 
     @Test
@@ -136,8 +145,7 @@ class InMemoryPersistenceManagerTest {
         assertEquals(result,parkingLot);
         assertEquals(0, manager.getAllParkingLots().size());
 
-        result = manager.deleteParkingLotById(parkingLot.getId());
-        assertNull(result);
+        assertThrows(ParkingNotFoundException.class, () -> manager.deleteParkingLotById(parkingLot.getId()));
         assertEquals(0, manager.getAllParkingLots().size());
 
         manager.addParkingLot(parkingLot);
@@ -145,9 +153,8 @@ class InMemoryPersistenceManagerTest {
         //now try to delete something else
         ParkingLot parkingLot1 = new ParkingLot();
         parkingLot1.setId(parkingLot.getId() + 1);
-        result = manager.deleteParkingLotById(parkingLot1.getId());
+        assertThrows(ParkingNotFoundException.class, () -> manager.deleteParkingLotById(parkingLot1.getId()));
         assertEquals(1, manager.getAllParkingLots().size());
-        assertNull(result);
     }
 
     @Test
@@ -159,9 +166,6 @@ class InMemoryPersistenceManagerTest {
 
         assertEquals(20, duration.toMinutesPart());
         assertEquals(27, duration.toHours());
-
-
     }
 
-
 }

+ 94 - 0
src/test/java/eu/fibane/parkingtoll/model/LayoutTest.java

@@ -0,0 +1,94 @@
+package eu.fibane.parkingtoll.model;
+
+import eu.fibane.parkingtoll.exceptions.NoSuchCarInParkingException;
+import eu.fibane.parkingtoll.exceptions.ParkingIsFullException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.time.Duration;
+import java.time.Instant;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class LayoutTest {
+
+    @Test
+    void decrementAndGetID() {
+        Layout layout = new Layout();
+        layout.setAvailable(2);
+        Long id1 = layout.decrementAndGetID();
+        assertEquals(1, layout.getAvailable().get());
+        Long id2 = layout.decrementAndGetID();
+        assertEquals(0, layout.getAvailable().get());
+        assertThrows(ParkingIsFullException.class, layout::decrementAndGetID);
+        assertEquals(0, layout.getAvailable().get());
+        assertNotEquals(id2,id1);
+    }
+
+    @Test
+    void incrementAndFree() {
+        Layout layout = new Layout();
+        layout.setAvailable(2);
+        Long id1 = layout.decrementAndGetID();
+        assertEquals(1, layout.getAvailable().get());
+        layout.incrementAndFree(id1);
+        assertEquals(2, layout.getAvailable().get());
+        Long id2 = layout.decrementAndGetID();
+        Long id3 = layout.decrementAndGetID();
+        //id1 should be re-used
+        assertTrue(id1.equals(id3) || id2.equals(id3));
+    }
+
+    @Test
+    void parkCar() {
+        Layout layout = new Layout();
+        layout.setAvailable(2);
+        CarSlot carSlot = new CarSlot();
+        Instant now = Instant.now();
+        layout.parkCar(carSlot);
+        assertEquals(1, layout.getAvailable().get());
+        assertNotNull(carSlot.getSlot());
+        Duration duration = Duration.between(now,carSlot.getArrivalTime());
+        assertTrue(duration.toSeconds() < 1); // not the exact same time because of GC or VM sleep
+        assertEquals(layout.getCarSlots().get(carSlot.getSlot()), carSlot);
+
+        CarSlot carSlot2 = new CarSlot();
+        layout.parkCar(carSlot2);
+        assertEquals(0, layout.getAvailable().get());
+        assertNotNull(carSlot2.getSlot());
+        assertEquals(layout.getCarSlots().get(carSlot2.getSlot()), carSlot2);
+
+        CarSlot carSlot3 = new CarSlot();
+        assertThrows(ParkingIsFullException.class, () -> layout.parkCar(carSlot3));
+    }
+
+    @Test
+    void parkCarOnEmptyParkingLot(){
+        Layout layout = new Layout();
+        layout.setAvailable(0);
+        CarSlot carSlot = new CarSlot();
+        assertThrows(ParkingIsFullException.class, () -> layout.parkCar(carSlot));
+        assertEquals(0, layout.getAvailable().get());
+    }
+
+    @Test
+    void removeCar() {
+        Layout layout = new Layout();
+        layout.setAvailable(2);
+        CarSlot carSlot = new CarSlot();
+        layout.parkCar(carSlot);
+        assertEquals(1, layout.getAvailable().get());
+        assertNotNull(carSlot.getSlot());
+
+        //call the service
+        CarSlot removedCar = layout.removeCar(carSlot);
+        Instant now = Instant.now();
+        assertEquals(2, layout.getAvailable().get());
+        assertTrue(layout.getCarSlots().isEmpty());
+        assertNotNull(removedCar.getDepartureTime());
+        Duration duration = Duration.between(now,carSlot.getDepartureTime());
+        assertTrue(duration.toSeconds() < 1); // not the exact same time because of GC or VM sleep
+
+        assertThrows(NoSuchCarInParkingException.class, () -> layout.removeCar(carSlot));
+    }
+}

+ 117 - 0
src/test/java/eu/fibane/parkingtoll/model/ParkingLotTest.java

@@ -0,0 +1,117 @@
+package eu.fibane.parkingtoll.model;
+
+import eu.fibane.parkingtoll.exceptions.ParkingIsFullException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class ParkingLotTest {
+
+    private static ParkingLot parkingLot;
+    private static final String[] TYPES = {"STANDARD",  "25kW", "50kW"};
+
+    @BeforeEach
+    void initParkingLot(){
+        parkingLot = generateParking("Test parking 1", 42L);
+    }
+
+     public static ParkingLot generateParking(String name, Long id){
+        parkingLot = new ParkingLot();
+        parkingLot.setName(name);
+        parkingLot.setId(id); //usually DAO assigns it an id
+        List<Layout> layouts = new ArrayList<>(TYPES.length);
+        Layout layout;
+        for (String type : TYPES) {
+            layout = new Layout();
+            layout.setAvailable(2);
+            layout.setName(type);
+            layouts.add(layout);
+        }
+        parkingLot.setLayoutList(layouts);
+        return parkingLot;
+    }
+
+    @Test
+    void parkCar() {
+        CarSlot carSlot = new CarSlot();
+        carSlot.setType(TYPES[0]);
+        CarSlot parkedCarSlot = parkingLot.parkCar(carSlot);
+        assertEquals(parkingLot.getId(), carSlot.getParkingLotId());
+        assertNotNull(carSlot.getArrivalTime());
+
+        //is the car parked at the right parking lot ?
+        Layout foundLayout = parkingLot.getLayoutList().stream().
+                filter(layout -> layout.getName().equals(parkedCarSlot.getType())).findFirst().orElse(null);
+        assertNotNull(foundLayout);
+        assertNotNull(foundLayout.getCarSlots());
+        assertTrue(foundLayout.getCarSlots().containsValue(parkedCarSlot));
+
+        CarSlot carSlot2 = new CarSlot();
+        carSlot2.setType(TYPES[1]);
+        CarSlot parkedCarSlot2 = parkingLot.parkCar(carSlot2);
+
+        //is the car parked at the right parking lot ?
+        Layout foundLayout2 = parkingLot.getLayoutList().stream().
+                filter(layout -> layout.getName().equals(parkedCarSlot2.getType())).findFirst().orElse(null);
+        assertNotNull(foundLayout2);
+        assertNotNull(foundLayout2.getCarSlots());
+        assertTrue(foundLayout2.getCarSlots().containsValue(parkedCarSlot2));
+
+    }
+
+    @Test
+    void testParkingLotCapacity(){
+        CarSlot carSlot2 = new CarSlot();
+        carSlot2.setType(TYPES[1]);
+        CarSlot parkedCarSlot2 = parkingLot.parkCar(carSlot2);
+
+        CarSlot carSlot3 = new CarSlot();
+        carSlot3.setType(TYPES[1]);
+        CarSlot parkedCarSlot3 = parkingLot.parkCar(carSlot3);
+
+        CarSlot carSlot5 = new CarSlot();
+        carSlot5.setType(TYPES[1]);
+        assertThrows(ParkingIsFullException.class, () -> parkingLot.parkCar(carSlot5));
+
+    }
+
+
+    @Test
+    void removeCar() {
+        CarSlot carSlot2 = new CarSlot();
+        carSlot2.setType(TYPES[1]);
+        CarSlot parkedCarSlot2 = parkingLot.parkCar(carSlot2);
+
+        CarSlot carSlot3 = new CarSlot();
+        carSlot3.setType(TYPES[1]);
+        CarSlot parkedCarSlot3 = parkingLot.parkCar(carSlot3);
+
+        //parking lot for type 1 is full
+        Layout foundLayout2 = parkingLot.getLayoutList().stream().
+                filter(layout -> layout.getName().equals(parkedCarSlot2.getType())).findFirst().orElse(null);
+        assertNotNull(foundLayout2);
+        assertNotNull(foundLayout2.getCarSlots());
+        assertEquals(0, foundLayout2.getAvailable().get());
+
+        parkingLot.removeCar(parkedCarSlot3);
+        assertNotNull(foundLayout2.getCarSlots());
+        assertEquals(1, foundLayout2.getAvailable().get());
+
+    }
+
+    @Test
+    void getSlotTypes() {
+
+        List<String> types = parkingLot.getSlotTypes();
+        assertNotNull(types);
+        assertEquals(TYPES.length,types.size());
+        assertTrue(types.contains(TYPES[0]));
+        assertTrue(types.contains(TYPES[1]));
+        assertTrue(types.contains(TYPES[2]));
+
+    }
+}

+ 91 - 0
src/test/java/eu/fibane/parkingtoll/model/PricingPolicyTest.java

@@ -0,0 +1,91 @@
+package eu.fibane.parkingtoll.model;
+
+import eu.fibane.parkingtoll.exceptions.DepartureIsBeforeArrivalException;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.time.Duration;
+import java.time.Instant;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class PricingPolicyTest {
+
+    @Test
+    void computeFareWithOnlyFlatFee() {
+        PricingPolicy policy = new PricingPolicy();
+
+        BigDecimal flatFee = BigDecimal.valueOf(12342.55);
+        policy.setFlatFee(flatFee);
+
+        Instant now = Instant.now();
+        Instant oneHour = now.plus(Duration.ofHours(1));
+
+        BigDecimal fare = policy.computeFare(now, oneHour);
+        assertEquals(flatFee, fare);
+        fare = policy.computeFare(now, now);
+        assertEquals(flatFee, fare);
+
+        flatFee = BigDecimal.valueOf(- 12_355.55);
+        policy.setFlatFee(flatFee);
+        fare = policy.computeFare(now, oneHour);
+        assertEquals(flatFee, fare);
+
+        Instant longAfter = now.plus(Duration.ofDays(500));
+        fare = policy.computeFare(now, longAfter);
+        assertEquals(flatFee, fare);
+    }
+
+    @Test
+    void computeFareWithOnlyHourRate() {
+        PricingPolicy policy = new PricingPolicy();
+        BigDecimal rate = BigDecimal.valueOf(12342.55);
+        BigDecimal flatFee = BigDecimal.valueOf(0);
+        policy.setPerHourFare(rate);
+        policy.setFlatFee(flatFee);
+
+        Instant now = Instant.now();
+        BigDecimal fare = policy.computeFare(now, now);
+        assertEquals(0, BigDecimal.ZERO.compareTo(fare));
+
+        Instant oneHour = now.plus(Duration.ofHours(1));
+        fare = policy.computeFare(now, oneHour);
+        assertEquals(rate, fare);
+
+        Instant twoHour = now.plus(Duration.ofHours(2));
+        fare = policy.computeFare(now, twoHour);
+        assertEquals(0, rate.multiply(BigDecimal.valueOf(2)).compareTo(fare));
+
+        Instant twoHourAndSomeMinutes = now.plus(Duration.ofHours(2)).plus(Duration.ofMinutes(15));
+        fare = policy.computeFare(now, twoHourAndSomeMinutes);
+        assertEquals(0, rate.multiply(BigDecimal.valueOf(3)).compareTo(fare));
+
+        //departure is before arrival - should not happen
+        assertThrows(DepartureIsBeforeArrivalException.class,() -> policy.computeFare(twoHourAndSomeMinutes, now));
+    }
+
+
+    @Test
+    void testMixOfFares(){
+        PricingPolicy policy = new PricingPolicy();
+        BigDecimal rate = BigDecimal.valueOf(12342.55);
+        BigDecimal flatFee = BigDecimal.valueOf(54321);
+        policy.setPerHourFare(rate);
+        policy.setFlatFee(flatFee);
+
+        //0s fare
+        Instant now = Instant.now();
+        BigDecimal fare = policy.computeFare(now, now);
+        assertEquals(flatFee, fare);
+
+        //1h fare
+        fare = policy.computeFare(now, now.plus(Duration.ofHours(1)));
+        assertEquals(0, flatFee.add(rate).compareTo(fare));
+
+        //1h20m fare
+        fare = policy.computeFare(now, now.plus(Duration.ofHours(1)).plus(Duration.ofMinutes(20)));
+        assertEquals(0, flatFee.add(rate.multiply(BigDecimal.valueOf(2))).compareTo(fare));
+    }
+
+}