diff --git a/data/DoctrineORMModule/Proxy/.gitignore b/data/DoctrineORMModule/Proxy/.gitignore new file mode 100755 index 0000000..d6b7ef3 --- /dev/null +++ b/data/DoctrineORMModule/Proxy/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/module/Application/config/module.config.php b/module/Application/config/module.config.php index e28bf54..1166999 100644 --- a/module/Application/config/module.config.php +++ b/module/Application/config/module.config.php @@ -77,10 +77,14 @@ ), ), 'service_manager' => array( + 'invokables' => array( + 'overbooking_policy' => 'Application\Service\Policy\TenPercentOverbookingPolicy', + ), 'factories' => array( 'main_navigation' => 'Zend\Navigation\Service\DefaultNavigationFactory', 'cargo_form' => 'Application\Form\Service\CargoFormFactory', 'voyage_form' => 'Application\Form\Service\VoyageFormFactory', + 'booking_service' => 'Application\Service\Factory\BookingServiceFactory', 'cargo_repository' => function($sl) { $em = $sl->get('doctrine.entitymanager.orm_default'); return $em->getRepository('Application\Domain\Model\Cargo\Cargo'); @@ -120,6 +124,7 @@ $cargoController = new Application\Controller\CargoController(); $cargoController->setCargoRepository($cargoRepository); + $cargoController->setVoyageRepository($serviceManager->get('voyage_repository')); $cargoController->setCargoForm($serviceManager->get('cargo_form')); return $cargoController; }, @@ -130,6 +135,15 @@ $voyageController->setVoyageForm($serviceManager->get('voyage_form')); $voyageController->setVoyageRepository($serviceManager->get('voyage_repository')); return $voyageController; + }, + 'Application\Controller\Booking' => function($controllerLoader) { + $serviceManager = $controllerLoader->getServiceLocator(); + + $bookingController = new Application\Controller\BookingController(); + $bookingController->setCargoRepository($serviceManager->get('cargo_repository')); + $bookingController->setVoyageRepository($serviceManager->get('voyage_repository')); + $bookingController->setBookingService($serviceManager->get('booking_service')); + return $bookingController; } ) ), @@ -154,7 +168,6 @@ 'orm_default' => array( //Define custom doctrine types to map the ddd value objects 'types' => array( - 'uid' => 'Application\Infrastructure\Persistence\Doctrine\Type\UID', 'trackingid' => 'Application\Infrastructure\Persistence\Doctrine\Type\TrackingId', 'voyagenumber' => 'Application\Infrastructure\Persistence\Doctrine\Type\VoyageNumber', ), diff --git a/module/Application/src/Application/Controller/BookingController.php b/module/Application/src/Application/Controller/BookingController.php new file mode 100644 index 0000000..01f1d99 --- /dev/null +++ b/module/Application/src/Application/Controller/BookingController.php @@ -0,0 +1,141 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Application\Controller; + +use Zend\Mvc\Controller\AbstractActionController; +use Zend\View\Model\ViewModel; +use Zend\Validator\StringLength; +use Zend\Validator\Regex; +use Application\Domain\Model\Cargo; +use Application\Domain\Model\Voyage; +use Application\Service\BookingService; +use Application\Service\Exception\ServiceException; +use Application\Service\Exception\RuntimeException; +/** + * MVC Controller that manages the booking process of Voyage. + * + * @author Alexander Miertsch + */ +class BookingController extends AbstractActionController +{ + /** + * The CargoRepository + * + * @var Cargo\CargoRepositoryInterface + */ + protected $cargoRepository; + + /** + * + * @var Voyage\VoyageRepositoryInterface + */ + protected $voyageRepository; + + /** + * + * @var BookingService + */ + protected $bookingService; + + /** + * Book a Cargo on a Voyage + */ + public function bookingAction() + { + //we use the post redirect get pattern and redirect to same location + $prg = $this->prg(); + + if ($prg instanceof \Zend\Http\PhpEnvironment\Response) { + // returned a response to redirect us + return $prg; + } elseif ($prg === false) { + throw new RuntimeException('Request is out of date.'); + } + + try { + $trackingId = $prg['tracking_id']; + + if (empty($trackingId)) { + throw new RuntimeException('TrackingId must not be empty'); + } + + $strLengthVal = new StringLength(1, 13); + $regexVal = new Regex('/^[a-zA-Z0-9_-]+$/'); + + if (!$strLengthVal->isValid($trackingId) || !$regexVal->isValid($trackingId)) { + throw new RuntimeException('TrackingId is invalid'); + } + + $voyageNumber = $prg['voyage_number']; + + if (empty($voyageNumber)) { + throw new RuntimeException('VoyageNumber must not be empty'); + } + + $strLengthVal = new StringLength(3, 30); + $regexVal = new Regex('/^[a-zA-Z0-9_-]+$/'); + + if (!$strLengthVal->isValid($voyageNumber) || !$regexVal->isValid($voyageNumber)) { + throw new RuntimeException('VoyageNumber is invalid'); + } + + $cargo = $this->cargoRepository->findCargo(new Cargo\TrackingId($trackingId)); + + if (is_null($cargo)) { + throw new RuntimeException('Cargo can not be found'); + } + + $voyage = $this->voyageRepository->findVoyage(new Voyage\VoyageNumber($voyageNumber)); + + if (is_null($voyage)) { + throw new RuntimeException('Voyage can not be found'); + } + + $this->bookingService->bookNewCargo($cargo, $voyage); + + return array('msg' => 'Cargo was successfully booked', 'success' => true); + + } catch (ServiceException $ex) { + return array('msg' => $ex->getMessage(), 'success' => false); + } + } + + /** + * Set the CargoRepository + * + * @param Cargo\CargoRepositoryInterface $cargoRepository + * @return void + */ + public function setCargoRepository(Cargo\CargoRepositoryInterface $cargoRepository) + { + $this->cargoRepository = $cargoRepository; + } + + /** + * Set the voyage repository + * + * @param Voyage\VoyageRepositoryInterface $voyageRepository + * @return void + */ + public function setVoyageRepository(Voyage\VoyageRepositoryInterface $voyageRepository) + { + $this->voyageRepository = $voyageRepository; + } + + /** + * Set BookingService + * + * @param BookingService $bookingService + * @return void + */ + public function setBookingService(BookingService $bookingService) + { + $this->bookingService = $bookingService; + } +} diff --git a/module/Application/src/Application/Controller/CargoController.php b/module/Application/src/Application/Controller/CargoController.php index cc3d67e..dbc3ddc 100644 --- a/module/Application/src/Application/Controller/CargoController.php +++ b/module/Application/src/Application/Controller/CargoController.php @@ -11,6 +11,7 @@ use Zend\Mvc\Controller\AbstractActionController; use Zend\View\Model\ViewModel; use Application\Domain\Model\Cargo; +use Application\Domain\Model\Voyage\VoyageRepositoryInterface; use Application\Form\CargoForm; /** * MVC Controller for Cargo Management @@ -26,6 +27,13 @@ class CargoController extends AbstractActionController */ protected $cargoRepository; + /** + * + * @var VoyageRepositoryInterface + */ + protected $voyageRepository; + + /** * * @var CargoForm @@ -56,7 +64,13 @@ public function showAction() throw new \RuntimeException('Cargo can not be found. Please check the trackingId!'); } - return array('cargo' => $cargo); + if ($cargo->isBooked()) { + $voyages = array(); + } else { + $voyages = $this->voyageRepository->findAll(); + } + + return array('cargo' => $cargo, 'voyages' => $voyages); } public function addAction() @@ -103,6 +117,17 @@ public function setCargoRepository(Cargo\CargoRepositoryInterface $cargoReposito $this->cargoRepository = $cargoRepository; } + /** + * Set the voyage repository + * + * @param VoyageRepositoryInterface $voyageRepository + * @return void + */ + public function setVoyageRepository(VoyageRepositoryInterface $voyageRepository) + { + $this->voyageRepository = $voyageRepository; + } + /** * Set a cargo form. * diff --git a/module/Application/src/Application/Controller/VoyageController.php b/module/Application/src/Application/Controller/VoyageController.php index 926e069..29f93c4 100644 --- a/module/Application/src/Application/Controller/VoyageController.php +++ b/module/Application/src/Application/Controller/VoyageController.php @@ -73,6 +73,19 @@ public function addAction() } } + public function showAction() + { + $voyageNumber = $this->getEvent()->getRouteMatch()->getParam('voyagenumber', ''); + + $voyage = $this->voyageRepository->findVoyage(new Voyage\VoyageNumber($voyageNumber)); + + if(is_null($voyage)) { + throw new \Exception('Voyage could not be found'); + } + + return array('voyage' => $voyage); + } + public function setVoyageRepository(Voyage\VoyageRepositoryInterface $voyageRepository) { $this->voyageRepository = $voyageRepository; diff --git a/module/Application/src/Application/Domain/Model/Cargo/Cargo.php b/module/Application/src/Application/Domain/Model/Cargo/Cargo.php index f11b82a..c9fc90c 100644 --- a/module/Application/src/Application/Domain/Model/Cargo/Cargo.php +++ b/module/Application/src/Application/Domain/Model/Cargo/Cargo.php @@ -9,10 +9,13 @@ namespace Application\Domain\Model\Cargo; use Application\Domain\Shared\EntityInterface; +use Application\Domain\Model\Voyage\Voyage; use Doctrine\ORM\Mapping\Entity; use Doctrine\ORM\Mapping\Table; use Doctrine\ORM\Mapping\Column; use Doctrine\ORM\Mapping\Id; +use Doctrine\ORM\Mapping\ManyToOne; +use Doctrine\ORM\Mapping\JoinColumn; /** * A Cargo. This is the central class in the domain model. * @@ -49,6 +52,18 @@ class Cargo implements EntityInterface * @var integer */ protected $size; + + /** + * The booked Voyage + * + * --Annotations required by Doctrine---- + * @ManyToOne(targetEntity="Application\Domain\Model\Voyage\Voyage", inversedBy="bookedCargos", fetch="LAZY") + * @JoinColumn(name="voyage_number", referencedColumnName="voyage_number") + * -------------------------------------- + * + * @var Voyage + */ + protected $voyage; /** * Construct @@ -90,7 +105,36 @@ public function setSize($size) $this->size = $size; } + /** + * + * @return Voyage + */ + public function getVoyage() + { + return $this->voyage; + } + + /** + * + * @param Voyage $voyage + * @return void + */ + public function setVoyage(Voyage $voyage) + { + $this->voyage = $voyage; + } + /** + * Check if Cargo is already booked + * + * @return boolean + */ + public function isBooked() + { + return !is_null($this->getVoyage()); + } + + /** * {@inheritDoc} */ diff --git a/module/Application/src/Application/Domain/Model/Voyage/Voyage.php b/module/Application/src/Application/Domain/Model/Voyage/Voyage.php index 1f39d81..7cd24f1 100644 --- a/module/Application/src/Application/Domain/Model/Voyage/Voyage.php +++ b/module/Application/src/Application/Domain/Model/Voyage/Voyage.php @@ -9,10 +9,14 @@ namespace Application\Domain\Model\Voyage; use Application\Domain\Shared\EntityInterface; +use Application\Domain\Model\Cargo\Cargo; use Doctrine\ORM\Mapping\Entity; use Doctrine\ORM\Mapping\Table; use Doctrine\ORM\Mapping\Column; use Doctrine\ORM\Mapping\Id; +use Doctrine\ORM\Mapping\OneToMany; +use Doctrine\ORM\Mapping\JoinColumn; +use Doctrine\Common\Collections\ArrayCollection; /** * A Voyage. Cargos can be booked on a Voyage. * @@ -61,6 +65,17 @@ class Voyage implements EntityInterface */ protected $capacity; + /** + * Collection of booked Cargos + * + * --Annotations required by Doctrine---- + * @OneToMany(targetEntity="Application\Domain\Model\Cargo\Cargo", mappedBy="voyage", cascade={"persist"}, fetch="LAZY") + * -------------------------------------- + * + * @var ArrayCollection + */ + protected $bookedCargos; + /** * Construct * @@ -69,6 +84,7 @@ class Voyage implements EntityInterface public function __construct(VoyageNumber $voyageNumber) { $this->voyageNumber = $voyageNumber; + $this->bookedCargos = new ArrayCollection(); } /** @@ -100,6 +116,25 @@ public function getCapacity() { return $this->capacity; } + + /** + * Get the free capacity of Voyage. + * + * @return integer + */ + public function getFreeCapacity() + { + $cargos = $this->getBookedCargos(); + + $bookedCapacity = 0; + + $cargos->forAll(function($key, $cargo) use (&$bookedCapacity) { + $bookedCapacity += $cargo->getSize(); + return true; + }); + + return $this->getCapacity() - $bookedCapacity; + } /** * Set name of Voyage @@ -122,8 +157,27 @@ public function setCapacity($capacity) { $this->capacity = $capacity; } + + /** + * Get the booked Cargos + * + * @return ArrayCollection + */ + public function getBookedCargos() + { + return $this->bookedCargos; + } - + /** + * Book a cargo + * + * @param Cargo $cargo + */ + public function bookCargo(Cargo $cargo) + { + $cargo->setVoyage($this); + $this->bookedCargos->add($cargo); + } /** * {@inheritDoc} */ diff --git a/module/Application/src/Application/Form/CargoForm.php b/module/Application/src/Application/Form/CargoForm.php index 62a7db7..0429047 100644 --- a/module/Application/src/Application/Form/CargoForm.php +++ b/module/Application/src/Application/Form/CargoForm.php @@ -12,6 +12,7 @@ use Zend\InputFilter\Input; use Zend\InputFilter\InputFilter; use Zend\Validator\StringLength; +use Zend\Validator\Regex; use Zend\Validator\Digits; /** * Form class to manage add and update of a Cargo. @@ -61,11 +62,13 @@ public function getInputFilter() ->addValidator($sizeValidator); $trackingIdValidator = new StringLength(13); + $regexValidator = new Regex('/^[a-zA-Z0-9_-]+$/'); $trackingIdInput = new Input('trackingId'); $trackingIdInput->allowEmpty(); $trackingIdInput->setRequired(false); $trackingIdInput->getValidatorChain() - ->addValidator($trackingIdValidator); + ->addValidator($trackingIdValidator) + ->addValidator($regexValidator); $filter = new InputFilter(); $filter->add($sizeInput)->add($trackingIdInput); diff --git a/module/Application/src/Application/Form/VoyageForm.php b/module/Application/src/Application/Form/VoyageForm.php index 70d5009..9d5b6f0 100644 --- a/module/Application/src/Application/Form/VoyageForm.php +++ b/module/Application/src/Application/Form/VoyageForm.php @@ -13,6 +13,7 @@ use Zend\InputFilter\InputFilter; use Zend\Validator\StringLength; use Zend\Validator\Digits; +use Zend\Validator\Regex; /** * VoyageForm * @@ -74,10 +75,12 @@ public function getInputFilter() { if (is_null($this->filter)) { $voyageNumberLengthValidator = new StringLength(3, 30); + $regexValidator = new Regex('/^[a-zA-Z0-9_-]+$/'); $voyageNumberInput = new Input('voyage_number'); $voyageNumberInput->setRequired(true) ->getValidatorChain() - ->addValidator($voyageNumberLengthValidator); + ->addValidator($voyageNumberLengthValidator) + ->addValidator($regexValidator); $nameLengthValidator = new StringLength(3, 100); $nameInput = new Input('name'); diff --git a/module/Application/src/Application/Infrastructure/Persistence/Doctrine/Type/TrackingId.php b/module/Application/src/Application/Infrastructure/Persistence/Doctrine/Type/TrackingId.php index 120d63e..f2a7c32 100644 --- a/module/Application/src/Application/Infrastructure/Persistence/Doctrine/Type/TrackingId.php +++ b/module/Application/src/Application/Infrastructure/Persistence/Doctrine/Type/TrackingId.php @@ -24,6 +24,10 @@ class TrackingId extends TextType */ public function convertToPHPValue($value, AbstractPlatform $platform) { + if (empty($value)) { + return null; + } + return new DomainTrackingId($value); } @@ -32,6 +36,14 @@ public function convertToPHPValue($value, AbstractPlatform $platform) */ public function convertToDatabaseValue($value, AbstractPlatform $platform) { + if (empty($value)) { + return null; + } + + if (is_string($value)) { + return $value; + } + if (!$value instanceof DomainTrackingId) { throw ConversionException::conversionFailed($value, $this->getName()); } diff --git a/module/Application/src/Application/Infrastructure/Persistence/Doctrine/Type/UID.php b/module/Application/src/Application/Infrastructure/Persistence/Doctrine/Type/UID.php deleted file mode 100644 index 0b1b014..0000000 --- a/module/Application/src/Application/Infrastructure/Persistence/Doctrine/Type/UID.php +++ /dev/null @@ -1,41 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace Application\Infrastructure\Persistence\Doctrine\Type; - -use Doctrine\DBAL\Types\TextType; -use Doctrine\DBAL\Platforms\AbstractPlatform; -use Doctrine\DBAL\Types\ConversionException; -use Application\Domain\Shared\UID as DomainUID; -/** - * Custom Doctrine Type UID - * - * @author Alexander Miertsch - */ -class UID extends TextType -{ - /** - * {@inheritDoc} - */ - public function convertToPHPValue($value, AbstractPlatform $platform) - { - return new DomainUID($value); - } - - /** - * {@inheritDoc} - */ - public function convertToDatabaseValue($value, AbstractPlatform $platform) - { - if (!$value instanceof DomainUID) { - throw ConversionException::conversionFailed($value, $this->getName()); - } - - return $value->toString(); - } -} diff --git a/module/Application/src/Application/Infrastructure/Persistence/Doctrine/Type/VoyageNumber.php b/module/Application/src/Application/Infrastructure/Persistence/Doctrine/Type/VoyageNumber.php index 77d5c32..9b1ef8e 100644 --- a/module/Application/src/Application/Infrastructure/Persistence/Doctrine/Type/VoyageNumber.php +++ b/module/Application/src/Application/Infrastructure/Persistence/Doctrine/Type/VoyageNumber.php @@ -24,6 +24,10 @@ class VoyageNumber extends TextType */ public function convertToPHPValue($value, AbstractPlatform $platform) { + if (empty($value)) { + return null; + } + return new DomainVoyageNumber($value); } @@ -32,6 +36,14 @@ public function convertToPHPValue($value, AbstractPlatform $platform) */ public function convertToDatabaseValue($value, AbstractPlatform $platform) { + if (empty($value)) { + return null; + } + + if (is_string($value)) { + return $value; + } + if (!$value instanceof DomainVoyageNumber) { throw ConversionException::conversionFailed($value, $this->getName()); } diff --git a/module/Application/src/Application/Service/BookingService.php b/module/Application/src/Application/Service/BookingService.php new file mode 100644 index 0000000..645c264 --- /dev/null +++ b/module/Application/src/Application/Service/BookingService.php @@ -0,0 +1,90 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Application\Service; + +use Application\Domain\Model\Cargo; +use Application\Domain\Model\Voyage; +/** + * BookingService + * + * @author Alexander Miertsch + */ +class BookingService +{ + /** + * The CargoRepository + * + * @var Cargo\CargoRepositoryInterface + */ + protected $cargoRepository; + + /** + * + * @var Voyage\VoyageRepositoryInterface + */ + protected $voyageRepository; + + /** + * + * @var Policy\OverbookingPolicyInterface + */ + protected $overbookingPolicy; + + public function bookNewCargo(Cargo\Cargo $cargo, Voyage\Voyage $voyage) + { + if (!$this->overbookingPolicy->isAllowed($cargo, $voyage)) { + throw new Exception\RuntimeException( + sprintf( + 'Cargo <%s> can not be booked. Voyage <%s> has not enough capacity.', + $cargo->getTrackingId()->toString(), + $voyage->getVoyageNumber()->toString() + ) + ); + } + + $voyage->bookCargo($cargo); + + $this->voyageRepository->store($voyage); + $this->cargoRepository->store($cargo); + } + + + /** + * Set the CargoRepository + * + * @param Cargo\CargoRepositoryInterface $cargoRepository + * @return void + */ + public function setCargoRepository(Cargo\CargoRepositoryInterface $cargoRepository) + { + $this->cargoRepository = $cargoRepository; + } + + /** + * Set the voyage repository + * + * @param Voyage\VoyageRepositoryInterface $voyageRepository + * @return void + */ + public function setVoyageRepository(Voyage\VoyageRepositoryInterface $voyageRepository) + { + $this->voyageRepository = $voyageRepository; + } + + /** + * Set OverbookingPolicy + * + * @param Policy\OverbookingPolicyInterface $overbookingPolicy + * @return void + */ + public function setOverbookingPolicy(Policy\OverbookingPolicyInterface $overbookingPolicy) + { + $this->overbookingPolicy = $overbookingPolicy; + } +} diff --git a/module/Application/src/Application/Service/Exception/RuntimeException.php b/module/Application/src/Application/Service/Exception/RuntimeException.php new file mode 100644 index 0000000..de1dd65 --- /dev/null +++ b/module/Application/src/Application/Service/Exception/RuntimeException.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Application\Service\Exception; + +/** + * Service RuntimeException + * + * @author Alexander Miertsch + */ +class RuntimeException extends \RuntimeException implements ServiceException +{ +} diff --git a/module/Application/src/Application/Service/Exception/ServiceException.php b/module/Application/src/Application/Service/Exception/ServiceException.php new file mode 100644 index 0000000..ba536e4 --- /dev/null +++ b/module/Application/src/Application/Service/Exception/ServiceException.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Application\Service\Exception; + +/** + * Exception marker interface + * + * @author Alexander Miertsch + */ +interface ServiceException +{ +} diff --git a/module/Application/src/Application/Service/Factory/BookingServiceFactory.php b/module/Application/src/Application/Service/Factory/BookingServiceFactory.php new file mode 100644 index 0000000..866b6f8 --- /dev/null +++ b/module/Application/src/Application/Service/Factory/BookingServiceFactory.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Application\Service\Factory; + +use Application\Service\BookingService; +use Zend\ServiceManager\FactoryInterface; +use Zend\ServiceManager\ServiceLocatorInterface; +/** + * ServiceFactory of BookingService + * + * @author Alexander Miertsch + */ +class BookingServiceFactory implements FactoryInterface +{ + public function createService(ServiceLocatorInterface $serviceLocator) + { + $bookingService = new BookingService(); + $bookingService->setCargoRepository($serviceLocator->get('cargo_repository')); + $bookingService->setVoyageRepository($serviceLocator->get('voyage_repository')); + $bookingService->setOverbookingPolicy($serviceLocator->get('overbooking_policy')); + return $bookingService; + } +} diff --git a/module/Application/src/Application/Service/Policy/OverbookingPolicyInterface.php b/module/Application/src/Application/Service/Policy/OverbookingPolicyInterface.php new file mode 100644 index 0000000..09ddd33 --- /dev/null +++ b/module/Application/src/Application/Service/Policy/OverbookingPolicyInterface.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Application\Service\Policy; + +use Application\Domain\Model\Cargo\Cargo; +use Application\Domain\Model\Voyage\Voyage; +/** + * Interface of an OverbookingPolicy + * + * @author Alexander Miertsch + */ +interface OverbookingPolicyInterface +{ + /** + * Check if Voyage has enough capacity (including overbooking capacity) for given Cargo + */ + public function isAllowed(Cargo $cargo, Voyage $voyage); +} diff --git a/module/Application/src/Application/Service/Policy/TenPercentOverbookingPolicy.php b/module/Application/src/Application/Service/Policy/TenPercentOverbookingPolicy.php new file mode 100644 index 0000000..2c0eb8d --- /dev/null +++ b/module/Application/src/Application/Service/Policy/TenPercentOverbookingPolicy.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Application\Service\Policy; + +use Application\Domain\Model\Cargo\Cargo; +use Application\Domain\Model\Voyage\Voyage; +/** + * TenPercentOverbookingPolicy + * + * @author Alexander Miertsch + */ +class TenPercentOverbookingPolicy implements OverbookingPolicyInterface +{ + public function isAllowed(Cargo $cargo, Voyage $voyage) + { + $overbookingCapacity = $voyage->getCapacity() * 1.1; + $bookedCapacity = $voyage->getCapacity() - $voyage->getFreeCapacity(); + $freeWithOverbookingCapacity = $overbookingCapacity - $bookedCapacity; + + if ($cargo->getSize() > $freeWithOverbookingCapacity) { + return false; + } + + return true; + } + +} diff --git a/module/Application/tests/PHPUnit/ApplicationTest/Domain/Model/Cargo/CargoTest.php b/module/Application/tests/PHPUnit/ApplicationTest/Domain/Model/Cargo/CargoTest.php index 10913ec..0b0787c 100644 --- a/module/Application/tests/PHPUnit/ApplicationTest/Domain/Model/Cargo/CargoTest.php +++ b/module/Application/tests/PHPUnit/ApplicationTest/Domain/Model/Cargo/CargoTest.php @@ -11,6 +11,7 @@ use ApplicationTest\TestCase; use Application\Domain\Model\Cargo\Cargo; use Application\Domain\Model\Cargo\TrackingId; +use Application\Domain\Model\Voyage; use Application\Domain\Shared\UID; /** * CargoTest @@ -33,4 +34,17 @@ public function testSameIdentityAs() $this->assertFalse($cargo1->sameIdentityAs($cargo3)); } + + public function testIsBooked() + { + $cargo = new Cargo(new TrackingId('123')); + + $this->assertFalse($cargo->isBooked()); + + $voyage = new Voyage\Voyage(new Voyage\VoyageNumber('SHIP030')); + + $cargo->setVoyage($voyage); + + $this->assertTrue($cargo->isBooked()); + } } diff --git a/module/Application/tests/PHPUnit/ApplicationTest/Domain/Model/Voyage/VoyageTest.php b/module/Application/tests/PHPUnit/ApplicationTest/Domain/Model/Voyage/VoyageTest.php index 953d805..bbde132 100644 --- a/module/Application/tests/PHPUnit/ApplicationTest/Domain/Model/Voyage/VoyageTest.php +++ b/module/Application/tests/PHPUnit/ApplicationTest/Domain/Model/Voyage/VoyageTest.php @@ -10,6 +10,7 @@ use ApplicationTest\TestCase; use Application\Domain\Model\Voyage; +use Application\Domain\Model\Cargo; /** * VoyageTest * @@ -46,6 +47,27 @@ public function testGetCapacity() $this->assertEquals(100, $this->voyage->getCapacity()); } + public function testBookCargo() + { + $cargo = new Cargo\Cargo(new Cargo\TrackingId('1234')); + $this->voyage->bookCargo($cargo); + + $cargos = $this->voyage->getBookedCargos(); + + $this->assertTrue($cargo->sameIdentityAs($cargos[0])); + } + + public function testGetFreeCapacity() + { + $this->assertEquals($this->voyage->getCapacity(), $this->voyage->getFreeCapacity()); + + $cargo = new Cargo\Cargo(new Cargo\TrackingId('1234')); + $cargo->setSize(10); + $this->voyage->bookCargo($cargo); + + $this->assertEquals(90, $this->voyage->getFreeCapacity()); + } + public function testSameIdentityAs() { $voyageNumber = new Voyage\VoyageNumber('SHIP123'); diff --git a/module/Application/tests/PHPUnit/ApplicationTest/Infrastructure/Persistence/Doctrine/CargoRepositoryDoctrineTest.php b/module/Application/tests/PHPUnit/ApplicationTest/Infrastructure/Persistence/Doctrine/CargoRepositoryDoctrineTest.php index 6eb1642..fe878cf 100644 --- a/module/Application/tests/PHPUnit/ApplicationTest/Infrastructure/Persistence/Doctrine/CargoRepositoryDoctrineTest.php +++ b/module/Application/tests/PHPUnit/ApplicationTest/Infrastructure/Persistence/Doctrine/CargoRepositoryDoctrineTest.php @@ -42,6 +42,7 @@ public function testStoreAndFindCargo() { $trackingId = $this->cargoRepository->getNextTrackingId(); $cargo = new Cargo\Cargo($trackingId); + $cargo->setSize(12); $this->cargoRepository->store($cargo); diff --git a/module/Application/tests/PHPUnit/ApplicationTest/Infrastructure/Persistence/Doctrine/VoyageRepositoryDoctrineTest.php b/module/Application/tests/PHPUnit/ApplicationTest/Infrastructure/Persistence/Doctrine/VoyageRepositoryDoctrineTest.php index 8177815..13e6d20 100644 --- a/module/Application/tests/PHPUnit/ApplicationTest/Infrastructure/Persistence/Doctrine/VoyageRepositoryDoctrineTest.php +++ b/module/Application/tests/PHPUnit/ApplicationTest/Infrastructure/Persistence/Doctrine/VoyageRepositoryDoctrineTest.php @@ -10,6 +10,7 @@ use ApplicationTest\TestCase; use Application\Domain\Model\Voyage; +use Application\Domain\Model\Cargo; use Application\Infrastrucure\Persistence\Doctrine\CargoRepositoryDoctrine; /** * VoyageRepositoryDoctrineTest @@ -23,11 +24,19 @@ class VoyageRepositoryDoctrineTest extends TestCase */ protected $voyageRepository; + /** + * @var Cargo\CargoRepositoryInterface + */ + protected $cargoRepository; + + protected function setUp() { + $this->createEntitySchema('Application\Domain\Model\Cargo\Cargo'); $this->createEntitySchema('Application\Domain\Model\Voyage\Voyage'); $this->voyageRepository = $this->getTestEntityManager()->getRepository('Application\Domain\Model\Voyage\Voyage'); + $this->cargoRepository = $this->getTestEntityManager()->getRepository('Application\Domain\Model\Cargo\Cargo'); } public function testStoreAndFindVoyage() @@ -38,11 +47,22 @@ public function testStoreAndFindVoyage() $voyage->setName('MyVoyage'); $voyage->setCapacity(1); + $cargo = new Cargo\Cargo(new Cargo\TrackingId('1234')); + $cargo->setSize(12); + + $this->getTestEntityManager()->persist($cargo); + + $voyage->bookCargo($cargo); + $this->voyageRepository->store($voyage); + $this->cargoRepository->store($cargo); $checkVoyage = $this->voyageRepository->findVoyage($voyageNumber); + $bookedCargos = $checkVoyage->getBookedCargos(); + $this->assertTrue($voyage->sameIdentityAs($checkVoyage)); $this->assertEquals('MyVoyage', $checkVoyage->getName()); + $this->assertTrue($cargo->sameIdentityAs($bookedCargos[0])); } } diff --git a/module/Application/tests/PHPUnit/ApplicationTest/Service/BookingServiceTest.php b/module/Application/tests/PHPUnit/ApplicationTest/Service/BookingServiceTest.php new file mode 100644 index 0000000..d89b502 --- /dev/null +++ b/module/Application/tests/PHPUnit/ApplicationTest/Service/BookingServiceTest.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace ApplicationTest\Service; + +use Application\Service\BookingService; +use Application\Service\Policy\TenPercentOverbookingPolicy; +use Application\Domain\Model\Voyage; +use Application\Domain\Model\Cargo; +use ApplicationTest\TestCase; +/** + * BookingServiceTest + * + * @author Alexander Miertsch + */ +class BookingServiceTest extends TestCase +{ + /** + * + * @var BookingService + */ + protected $bookingService; + + protected function setUp() + { + $this->createEntitySchema('Application\Domain\Model\Cargo\Cargo'); + $this->createEntitySchema('Application\Domain\Model\Voyage\Voyage'); + + $this->bookingService = new BookingService(); + + $this->bookingService->setCargoRepository( + $this->getTestEntityManager()->getRepository('Application\Domain\Model\Cargo\Cargo') + ); + + $this->bookingService->setVoyageRepository( + $this->getTestEntityManager()->getRepository('Application\Domain\Model\Voyage\Voyage') + ); + + $this->bookingService->setOverbookingPolicy(new TenPercentOverbookingPolicy()); + } + + public function testBookNewCargo() + { + $voyage = new Voyage\Voyage(new Voyage\VoyageNumber('123')); + $voyage->setCapacity(100); + $voyage->setName('Shipping'); + + $cargo1 = new Cargo\Cargo(new Cargo\TrackingId('333')); + $cargo1->setSize(50); + + $this->bookingService->bookNewCargo($cargo1, $voyage); + + $cargos = $voyage->getBookedCargos(); + + $this->assertTrue(isset($cargos[0])); + + $this->assertTrue($cargo1->sameIdentityAs($cargos[0])); + } + + /** + * @expectedException Application\Service\Exception\ServiceException + */ + public function testBookNewCargoFailing() + { + $voyage = new Voyage\Voyage(new Voyage\VoyageNumber('123')); + $voyage->setCapacity(50); + $voyage->setName('Shipping'); + + $cargo1 = new Cargo\Cargo(new Cargo\TrackingId('333')); + $cargo1->setSize(60); + + $this->bookingService->bookNewCargo($cargo1, $voyage); + } +} diff --git a/module/Application/tests/PHPUnit/ApplicationTest/Service/Policy/TenPercentOverbookingPolicyTest.php b/module/Application/tests/PHPUnit/ApplicationTest/Service/Policy/TenPercentOverbookingPolicyTest.php new file mode 100644 index 0000000..97ae42b --- /dev/null +++ b/module/Application/tests/PHPUnit/ApplicationTest/Service/Policy/TenPercentOverbookingPolicyTest.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace ApplicationTest\Service\Policy; + +use Application\Service\Policy\TenPercentOverbookingPolicy; +use Application\Domain\Model\Cargo; +use Application\Domain\Model\Voyage; +use ApplicationTest\TestCase; +/** + * TenPercentOverbookingPolicyTest + * + * @author Alexander Miertsch + */ +class TenPercentOverbookingPolicyTest extends TestCase +{ + public function testIsAllowed() + { + $overbookingPolicy = new TenPercentOverbookingPolicy(); + + $voyage = new Voyage\Voyage(new Voyage\VoyageNumber('123')); + $voyage->setCapacity(100); + + $cargo1 = new Cargo\Cargo(new Cargo\TrackingId('333')); + $cargo1->setSize(50); + + $this->assertTrue($overbookingPolicy->isAllowed($cargo1, $voyage)); + + $voyage->bookCargo($cargo1); + + $cargo2 = new Cargo\Cargo(new Cargo\TrackingId('334')); + //overbooking limit: 50 + 60 = 100 * 1.1 + $cargo2->setSize(60); + + $this->assertTrue($overbookingPolicy->isAllowed($cargo2, $voyage)); + + $voyage->bookCargo($cargo2); + + $cargo3 = new Cargo\Cargo(new Cargo\TrackingId('334')); + //Voyage is charged off, booking of anathoer Cargo is not possible + $cargo3->setSize(1); + + $this->assertFalse($overbookingPolicy->isAllowed($cargo3, $voyage)); + } +} diff --git a/module/Application/tests/PHPUnit/ApplicationTest/TestCase.php b/module/Application/tests/PHPUnit/ApplicationTest/TestCase.php index 83d5cf4..4f353ab 100644 --- a/module/Application/tests/PHPUnit/ApplicationTest/TestCase.php +++ b/module/Application/tests/PHPUnit/ApplicationTest/TestCase.php @@ -45,6 +45,8 @@ public function getTestEntityManager() ) ) ); + + $config->setNamingStrategy(new \Doctrine\ORM\Mapping\UnderscoreNamingStrategy()); $config->setQueryCacheImpl(new \Doctrine\Common\Cache\ArrayCache()); $config->setMetadataCacheImpl(new \Doctrine\Common\Cache\ArrayCache()); @@ -59,10 +61,6 @@ public function getTestEntityManager() Type::addType('trackingid', 'Application\Infrastructure\Persistence\Doctrine\Type\TrackingId'); } - if (!Type::hasType('uid')) { - Type::addType('uid', 'Application\Infrastructure\Persistence\Doctrine\Type\UID'); - } - if (!Type::hasType('voyagenumber')) { Type::addType('voyagenumber', 'Application\Infrastructure\Persistence\Doctrine\Type\VoyageNumber'); } diff --git a/module/Application/view/application/booking/booking.phtml b/module/Application/view/application/booking/booking.phtml new file mode 100644 index 0000000..b3edd02 --- /dev/null +++ b/module/Application/view/application/booking/booking.phtml @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +?> +
+

translate('Cargo Booking') ?>

+
+
+
+

translate($this->msg) ?>

+
+
diff --git a/module/Application/view/application/cargo/add.phtml b/module/Application/view/application/cargo/add.phtml index fd258d2..b0090a1 100644 --- a/module/Application/view/application/cargo/add.phtml +++ b/module/Application/view/application/cargo/add.phtml @@ -9,7 +9,7 @@ ?>

translate('Add new Cargo') ?>

-

translate('In the Chapter One Version of the Shipping System you can only set the size of a Cargo. More attributes will follow in next chapters.') ?>

+

translate('In the Chapter One version of the Shipping System you can only set the size of a Cargo. More attributes will follow in next chapters.') ?>

diff --git a/module/Application/view/application/cargo/show.phtml b/module/Application/view/application/cargo/show.phtml index b33bd9f..206b163 100644 --- a/module/Application/view/application/cargo/show.phtml +++ b/module/Application/view/application/cargo/show.phtml @@ -11,11 +11,38 @@

cargo->getTrackingId()->toString() ?>

translate('You can book a voyage for the cargo as long as the voyage has enough capacity.') ?>

-
-
-

translate('Size') ?>: cargo->getSize() ?>

+
+
+
+

translate('Size') ?>: cargo->getSize() ?>

+
+
+ voyages)) : ?> +
+ +
+ +
+
+ cargo->isBooked()): ?> +

translate('The Cargo is already booked.') ?>

+ +

translate('No free voyages found.') ?>

+ +
+
+ cargo->isBooked()): ?> +
+
+ +
+
+ +
-
-

translate('Select a Voyage') ?>:

-
-
\ No newline at end of file + + \ No newline at end of file diff --git a/module/Application/view/application/voyage/add.phtml b/module/Application/view/application/voyage/add.phtml index 55d2d1c..e6910c3 100644 --- a/module/Application/view/application/voyage/add.phtml +++ b/module/Application/view/application/voyage/add.phtml @@ -9,7 +9,7 @@ ?>

translate('Add new Voyage') ?>

-

translate('The capacity of a Voyage controls how many Cargos can be booked. The size amount of all assigned Cargos must be less than or equal the capacity. Only the Overbooking-Policy can temporary increase the capacity.') ?>

+

translate('The capacity of a Voyage controls how many Cargos can be booked. The size amount of all booked Cargos must be less than or equal the capacity. Only the Overbooking-Policy can temporary increase the capacity.') ?>

diff --git a/module/Application/view/application/voyage/index.phtml b/module/Application/view/application/voyage/index.phtml index 98ce53b..85021cd 100644 --- a/module/Application/view/application/voyage/index.phtml +++ b/module/Application/view/application/voyage/index.phtml @@ -9,7 +9,7 @@ ?>

translate('Voyage Overview') ?>

-

translate('Choose a Voyage from the list to view all assigned Cargos.') ?>

+

translate('Choose a Voyage from the list to view all booked Cargos.') ?>

diff --git a/module/Application/view/application/voyage/show.phtml b/module/Application/view/application/voyage/show.phtml new file mode 100644 index 0000000..a9ad9ad --- /dev/null +++ b/module/Application/view/application/voyage/show.phtml @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +?> + +
+

translate('Voyage %s'), $this->voyage->getVoyageNumber()->toString()) ?>

+

voyage->getName() ?> -

+
+
+
+
+
+

Overview

+
+
+
+
+ Full Capacity: getCapacity() ?> +
+
+
+
+ Available Capacity: getFreeCapacity() ?> +
+
+
+
+
+
+

Booked Cargos

+
+
+
+
+ +
+
+
+
\ No newline at end of file diff --git a/scripts/cargo_sample.sql b/scripts/cargo_sample.sql index b0d88d1..534d3f9 100644 --- a/scripts/cargo_sample.sql +++ b/scripts/cargo_sample.sql @@ -11,6 +11,7 @@ CREATE TABLE IF NOT EXISTS `cargo` ( `tracking_id` varchar(13) NOT NULL, `size` int(11) NOT NULL, + `voyage_number` varchar(30) DEFAULT NULL, PRIMARY KEY (`tracking_id`) );