diff --git a/features/bootstrap/ApplicationContext.php b/features/bootstrap/ApplicationContext.php index cf24db9..fe1bc34 100644 --- a/features/bootstrap/ApplicationContext.php +++ b/features/bootstrap/ApplicationContext.php @@ -7,17 +7,38 @@ use Behat\Behat\Tester\Exception\PendingException; use Behat\Gherkin\Node\PyStringNode; use Behat\Gherkin\Node\TableNode; +use Behat\Transliterator\Transliterator; use Example\Domain\Administration\Application\AdministrationService; use Example\Domain\Administration\Application\HireCandidateCommand; +use Example\Domain\Administration\Application\MenuService; +use Example\Domain\Administration\Application\RegisterNewRecipe; +use Example\Domain\Administration\Application\ReleaseRecipe; +use Example\Domain\Administration\DomainModel\RecipeId; +use Example\Domain\Administration\DomainModel\RecipeName; use Example\Domain\Common\DomainModel\FullName; use Example\Domain\Administration\DomainModel\Identity\OwnerId; use Example\Domain\Administration\DomainModel\Owner; use Example\Domain\Common\DomainModel\JobTitle; +use Example\Domain\Common\DomainModel\Money; +use Example\Domain\Sale\Application\BuyerService; use Example\Domain\Sale\Application\CashierService; +use Example\Domain\Sale\Application\CreateMealOnRecipeCreated; +use Example\Domain\Sale\Application\OrderService; use Example\Domain\Sale\Application\WaitressService; +use Example\Domain\Sale\DomainModel\BuyerId; +use Example\Domain\Sale\DomainModel\BuyerRepository; +use Example\Domain\Sale\DomainModel\CustomerType; +use Example\Domain\Sale\DomainModel\Identity\EmployeeId; +use Example\Domain\Sale\DomainModel\Identity\MealId; +use Example\Domain\Sale\DomainModel\Identity\OrderId; +use Example\Domain\Sale\DomainModel\PhoneNumber; use Example\Domain\Shipping\Application\CreateDeliveryBoyHandler; +use Example\Infrastructure\ForgedIdGenerator; use Example\Infrastructure\InMemory\EmployeeCollection; use Example\Infrastructure\InMemory\OwnerCollection; +use Example\Infrastructure\InMemory\Sale\BuyerCollection; +use Example\Infrastructure\InMemory\Sale\MealCollection; +use Example\Infrastructure\InMemory\Sale\OrderCollection; use Example\Infrastructure\Symfony\SymfonyPublisher; use Example\Domain\Sale\Application\CookService; use PHPUnit_Framework_Assert as Assert; @@ -37,11 +58,51 @@ class ApplicationContext implements Context, SnippetAcceptingContext */ private $owners; + /** + * @var BuyerRepository + */ + private $buyers; + + /** + * @var PhoneNumber[] + */ + private $phones = []; + /** * @var AdministrationService */ private $administrationService; + /** + * @var MenuService + */ + private $menu; + + /** + * @var BuyerService + */ + private $buyerService; + + /** + * @var OrderService + */ + private $orderService; + + /** + * @var OrderCollection + */ + private $orders; + + /** + * @var MealCollection + */ + private $meals; + + /** + * @var ForgedIdGenerator + */ + private $fakeIdGenerator; + /** * Initializes context. * @@ -51,17 +112,28 @@ class ApplicationContext implements Context, SnippetAcceptingContext */ public function __construct() { + // Infrastructure $publisher = new SymfonyPublisher(); + $this->buyers = new BuyerCollection(); + $this->owners = new OwnerCollection(); + $this->employees = new EmployeeCollection(); + $this->fakeIdGenerator = new ForgedIdGenerator(); + $this->orders = new OrderCollection(); + $this->meals = new MealCollection(); // Administration context - $this->owners = new OwnerCollection(); $this->administrationService = new AdministrationService($this->owners, $publisher); + $this->menu = new MenuService($this->owners, $publisher); // Sale context - $this->employees = new EmployeeCollection(); new CookService($this->employees, $publisher); new CashierService($this->employees, $publisher); + $this->buyerService = new BuyerService($this->buyers, 'CA'); // default country new WaitressService($this->employees, $publisher); + $this->orderService = new OrderService( + $this->orders, $publisher, $this->buyers, $this->meals, $this->fakeIdGenerator + ); + new CreateMealOnRecipeCreated($this->meals, $publisher); // Shipping context new CreateDeliveryBoyHandler($this->employees, $publisher); @@ -82,8 +154,40 @@ public function isTheStoreOwner($name) */ public function alreadyEmploysAs($ownerName, $employeeName, $role) { - $this->owners->ownerWithId(new OwnerId(FullName::fromSingleString($ownerName))) - ->hire(FullName::fromSingleString($employeeName), JobTitle::fromString($role)); + $this->administrationService->hireCandidate( + new HireCandidateCommand( + new OwnerId(FullName::fromSingleString($ownerName)), + FullName::fromSingleString($employeeName), + JobTitle::fromString($role) + ) + ); + } + + /** + * @Given :owner has created the recipe :recipeName with price of :recipePrice each + */ + public function hasCreatedTheRecipeWithPriceOfEach($owner, $recipeName, $recipePrice) + { + $this->menu->registerRecipe( + new RegisterNewRecipe( + new OwnerId(FullName::fromSingleString($owner)), + RecipeName::fromString($recipeName), + Money::fromInt($recipePrice) + ) + ); + } + + /** + * @Given :owner has released the recipe :recipeName + */ + public function hasReleasedTheRecipe($owner, $recipeName) + { + $this->menu->releaseRecipe( + new ReleaseRecipe( + new OwnerId(FullName::fromSingleString($owner)), + new RecipeId(RecipeName::fromString($recipeName)) + ) + ); } /** @@ -93,6 +197,22 @@ public function postulateOnAJobOffering($applicantName) { } + /** + * @Given :customerName never ordered meals to the shop + */ + public function neverOrderedMealsToTheShop($customerName) + { + } + + /** + * @Given :customerName gives his phone number :phoneNumber and home address :address + */ + public function givesHisPhoneNumberAndHomeAddress($customerName, $phoneNumber, $address) + { + $this->phones[$customerName] = PhoneNumber::fromString($phoneNumber, 'CA'); + $this->buyerService->registerPhoneBuyer($phoneNumber, $address); + } + /** * @When :ownerName hires :applicantName as the new :role */ @@ -107,6 +227,66 @@ public function hiresAsTheNew($ownerName, $applicantName, $role) ); } + /** + * @When :waitressName starts the order :orderId of :customerName + */ + public function startsTheOrderOf($waitressName, $orderId, $customerName) + { + $this->fakeIdGenerator->returnsOrderIdOnNextCall(new OrderId($orderId)); + $this->orderService->startOrder( + EmployeeId::fromName(FullName::fromSingleString($waitressName)), + CustomerType::PhoneCustomer(), + BuyerId::phoneBuyer($this->getPhoneNumberOfCustomer($customerName)) + ); + } + + /** + * @When :customerName order :quantity meal :mealName on order :orderId + */ + public function orderMealOnOrder($customerName, $quantity, $mealName, $orderId) + { + $this->orderService->orderMeal( + new OrderId($orderId), + $quantity, + new MealId(Transliterator::transliterate($mealName)) + ); + } + + /** + * @When :waitressName confirms that :customerName order confirmation number is :orderId at :time + */ + public function confirmsThatOrderConfirmationNumberIsAt($waitressName, $customerName, $orderId, $time) + { + $this->orderService->confirmOrder( + new OrderId($orderId), + new \DateTime($time) + ); + } + + /** + * @When :cookName finishes to cook the order :orderId at :time + */ + public function finishesToCookTheOrderAt($cookName, $orderId, $time) + { + throw new PendingException(); + } + + /** + * @When :deliveryBoyName delivers the order :orderId at :time + */ + public function deliversTheOrderAt($deliveryBoyName, $orderId, $time) + { + throw new PendingException(); + } + + /** + * @When :customerName pays :price to :deliveryBoyName + */ + public function paysTo($customerName, $price, $deliveryBoyName) + { + throw new PendingException(); + } + /** * @Then :ownerName should have :employeeCount employees */ @@ -126,4 +306,46 @@ public function thereShouldBeEmployeesWithTitle($employeeCount, $role) (int) $employeeCount, $this->employees->employeesWithTitle(JobTitle::fromString($role)) ); } + + /** + * @Then Order :orderId should be closed at :time + */ + public function orderShouldBeClosedAt($orderId, $time) + { + throw new PendingException(); + } + + /** + * @Then The order :orderId should have an income of :money + */ + public function theOrderShouldHaveAnIncomeOf($orderId, $money) + { + throw new PendingException(); + } + + /** + * @Then The order :orderId should registers a tip of :money + */ + public function theOrderShouldRegistersATipOf($orderId, $money) + { + throw new PendingException(); + } + + /** + * @Then The order :orderId should have taken :time to complete + */ + public function theOrderShouldHaveTakenToComplete($orderId, $time) + { + throw new PendingException(); + } + + /** + * @param string $customerName + * + * @return PhoneNumber + */ + private function getPhoneNumberOfCustomer($customerName) + { + return $this->phones[$customerName]; + } } diff --git a/features/manage-store.feature b/features/manage-store.feature index 0f947ce..dbc376a 100644 --- a/features/manage-store.feature +++ b/features/manage-store.feature @@ -8,35 +8,60 @@ Feature: And 'John' already employs 'Jake' as 'delivery boy' And 'John' already employs 'Judy' as 'waitress' And 'John' already employs 'Mark' as 'cashier' + And 'John' already employs 'Esther' as 'cook' + And 'John' has created the recipe 'All dressed pizza' with price of '10.00$' each + And 'John' has released the recipe 'All dressed pizza' + And 'John' has created the recipe 'Small fries' with price of '5.00$' each + And 'John' has released the recipe 'Small fries' - Scenario: Hiring new cashier to help customer pay for meals + Scenario: Hiring new cashier to help customer pay for recipes Given 'Jane' postulate on a job offering When 'John' hires 'Jane' as the new 'cashier' - Then 'John' should have 4 employees + Then 'John' should have 5 employees And There should be 2 employees with 'cashier' title # todo cashier receives payment from customer for order # todo Should be in sale context - Scenario: Hiring new cook to cook meals + Scenario: Hiring new cook to cook recipes Given 'Luke' postulate on a job offering When 'John' hires 'Luke' as the new 'cook' - Then 'John' should have 4 employees - And There should be 1 employees with 'cook' title + Then 'John' should have 5 employees + And There should be 2 employees with 'cook' title # todo cook prepare order of customer # todo Should be in sale context Scenario: Hiring new waitress to take order from customer Given 'Leia' postulate on a job offering When 'John' hires 'Leia' as the new 'waitress' - Then 'John' should have 4 employees + Then 'John' should have 5 employees And There should be 2 employees with 'waitress' title # todo waitress take order from customer (create order) # todo Should be in sale context - Scenario: Hiring new delivery boy to deliver meals + Scenario: Hiring new delivery boy to deliver recipes Given 'Han' postulate on a job offering When 'John' hires 'Han' as the new 'delivery boy' - Then 'John' should have 4 employees + Then 'John' should have 5 employees And There should be 2 employees with 'delivery boy' title - # todo delivery boy deliver meal to customer + # todo delivery boy deliver recipe to customer # todo Should be in shipping context + + Scenario: Cashier serves a phone customer who wishes its order to be delivered + # todo address must be in range of delivery + Given 'Billy' never ordered meals to the shop + And 'Billy' gives his phone number '555-5555' and home address '1 main street' + When 'Mark' starts the order '333' of 'Billy' + And 'Billy' order 2 meal 'All dressed pizza' on order '333' + And 'Billy' order 1 meal 'Small fries' on order '333' + And 'Mark' confirms that 'Billy' order confirmation number is '333' at '18:00:00' + And 'Esther' finishes to cook the order '333' at '18:10:00' + And 'Jake' delivers the order '333' at '18:30:00' + And 'Billy' pays '12.00$' to 'Jake' + Then Order '333' should be closed at '18:30:00' + And The order '333' should have an income of '25.00$' + And The order '333' should registers a tip of '3.00$' + And The order '333' should have taken '0:30:00' to complete + +# Scenario: Cashier serves a front desk customer + +# Scenario: Cashier serves a drive-in customer diff --git a/src/Domain/Administration/Application/AdministrationService.php b/src/Domain/Administration/Application/AdministrationService.php index f7aa8b2..915df48 100644 --- a/src/Domain/Administration/Application/AdministrationService.php +++ b/src/Domain/Administration/Application/AdministrationService.php @@ -7,7 +7,6 @@ namespace Example\Domain\Administration\Application; -use Example\Domain\Administration\DomainModel\CandidateRepository; use Example\Domain\Administration\DomainModel\OwnerRepository; use Example\Domain\Common\Application\EventPublisher; diff --git a/src/Domain/Administration/Application/MenuService.php b/src/Domain/Administration/Application/MenuService.php new file mode 100644 index 0000000..2a1e447 --- /dev/null +++ b/src/Domain/Administration/Application/MenuService.php @@ -0,0 +1,63 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Administration\Application; + +use Example\Domain\Administration\DomainModel\OwnerRepository; +use Example\Domain\Common\Application\EventPublisher; + +/** + * All in one class service + */ +final class MenuService +{ + /** + * @var OwnerRepository + */ + private $owners; + + /** + * @var EventPublisher + */ + private $publisher; + + /** + * @param OwnerRepository $owners + * @param EventPublisher $publisher + */ + public function __construct(OwnerRepository $owners, EventPublisher $publisher) + { + $this->owners = $owners; + $this->publisher = $publisher; + } + + /** + * @param RegisterNewRecipe $command + */ + public function registerRecipe(RegisterNewRecipe $command) + { + $owner = $this->owners->ownerWithId($command->creator()); + $owner->newRecipe($command->name(), $command->price()); + + $this->owners->saveOwner($owner); + + $this->publisher->publish($owner->uncommitedEvents()); + } + + /** + * @param ReleaseRecipe $command + */ + public function releaseRecipe(ReleaseRecipe $command) + { + $owner = $this->owners->ownerWithId($command->creator()); + $owner->releaseRecipe($command->recipeId()); + + $this->owners->saveOwner($owner); + + $this->publisher->publish($owner->uncommitedEvents()); + } +} diff --git a/src/Domain/Administration/Application/RegisterNewRecipe.php b/src/Domain/Administration/Application/RegisterNewRecipe.php new file mode 100644 index 0000000..d3d45e8 --- /dev/null +++ b/src/Domain/Administration/Application/RegisterNewRecipe.php @@ -0,0 +1,66 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Administration\Application; + +use Example\Domain\Administration\DomainModel\Identity\OwnerId; +use Example\Domain\Administration\DomainModel\RecipeName; +use Example\Domain\Common\DomainModel\Money; + +final class RegisterNewRecipe +{ + /** + * @var OwnerId + */ + private $creator; + + /** + * @var RecipeName + */ + private $name; + + /** + * @var Money + */ + private $price; + + /** + * @param OwnerId $creator + * @param RecipeName $name + * @param Money $price + */ + public function __construct(OwnerId $creator, RecipeName $name, Money $price) + { + $this->creator = $creator; + $this->name = $name; + $this->price = $price; + } + + /** + * @return \Example\Domain\Administration\DomainModel\Identity\OwnerId + */ + public function creator() + { + return $this->creator; + } + + /** + * @return \Example\Domain\Common\DomainModel\RecipeName + */ + public function name() + { + return $this->name; + } + + /** + * @return \Example\Domain\Common\DomainModel\Money + */ + public function price() + { + return $this->price; + } +} diff --git a/src/Domain/Administration/Application/ReleaseRecipe.php b/src/Domain/Administration/Application/ReleaseRecipe.php new file mode 100644 index 0000000..2e945f0 --- /dev/null +++ b/src/Domain/Administration/Application/ReleaseRecipe.php @@ -0,0 +1,50 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Administration\Application; + +use Example\Domain\Administration\DomainModel\Identity\OwnerId; +use Example\Domain\Administration\DomainModel\RecipeId; + +final class ReleaseRecipe +{ + /** + * @var OwnerId + */ + private $creator; + + /** + * @var RecipeId + */ + private $recipeId; + + /** + * @param OwnerId $creator + * @param RecipeId $recipeId + */ + public function __construct(OwnerId $creator, RecipeId $recipeId) + { + $this->creator = $creator; + $this->recipeId = $recipeId; + } + + /** + * @return \Example\Domain\Administration\DomainModel\Identity\OwnerId + */ + public function creator() + { + return $this->creator; + } + + /** + * @return \Example\Domain\Administration\DomainModel\RecipeId + */ + public function recipeId() + { + return $this->recipeId; + } +} diff --git a/src/Domain/Administration/DomainModel/Candidate.php b/src/Domain/Administration/DomainModel/Candidate.php index faeb130..17db3ea 100644 --- a/src/Domain/Administration/DomainModel/Candidate.php +++ b/src/Domain/Administration/DomainModel/Candidate.php @@ -11,6 +11,9 @@ use Example\Domain\Common\DomainModel\FullName; use Example\Domain\Common\DomainModel\JobTitle; +/** + * Entity part of the Owner aggregate root + */ final class Candidate { /** diff --git a/src/Domain/Administration/DomainModel/Event/RecipeWasCreated.php b/src/Domain/Administration/DomainModel/Event/RecipeWasCreated.php new file mode 100644 index 0000000..97eff9b --- /dev/null +++ b/src/Domain/Administration/DomainModel/Event/RecipeWasCreated.php @@ -0,0 +1,77 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Administration\DomainModel\Event; + +use Example\Domain\Administration\DomainModel\Identity\OwnerId; +use Example\Domain\Administration\DomainModel\RecipeId; +use Example\Domain\Administration\DomainModel\RecipeName; +use Example\Domain\Common\DomainModel\Event\DomainEvent; +use Example\Domain\Common\DomainModel\Money; + +final class RecipeWasCreated implements DomainEvent +{ + /** + * @var OwnerId + */ + private $creator; + + /** + * @var RecipeId + */ + private $recipeId; + + /** + * @var RecipeName + */ + private $name; + + /** + * @var Money + */ + private $price; + + public function __construct(OwnerId $creator, RecipeId $id, RecipeName $name, Money $price) + { + $this->creator = $creator; + $this->recipeId = $id; + $this->name = $name; + $this->price = $price; + } + + /** + * @return \Example\Domain\Administration\DomainModel\RecipeId + */ + public function recipeId() + { + return $this->recipeId; + } + + /** + * @return \Example\Domain\Administration\DomainModel\Identity\OwnerId + */ + public function creator() + { + return $this->creator; + } + + /** + * @return \Example\Domain\Common\DomainModel\RecipeName + */ + public function name() + { + return $this->name; + } + + /** + * @return \Example\Domain\Common\DomainModel\Money + */ + public function price() + { + return $this->price; + } +} diff --git a/src/Domain/Administration/DomainModel/Event/RecipeWasReleased.php b/src/Domain/Administration/DomainModel/Event/RecipeWasReleased.php new file mode 100644 index 0000000..00908b4 --- /dev/null +++ b/src/Domain/Administration/DomainModel/Event/RecipeWasReleased.php @@ -0,0 +1,50 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Administration\DomainModel\Event; + +use Example\Domain\Administration\DomainModel\RecipeId; +use Example\Domain\Common\DomainModel\Event\DomainEvent; + +final class RecipeWasReleased implements DomainEvent +{ + /** + * @var RecipeId + */ + private $recipeId; + + /** + * @var string + */ + private $recipeName; + + /** + * @param RecipeId $recipeId + * @param string $recipeName + */ + public function __construct(RecipeId $recipeId, $recipeName) + { + $this->recipeId = $recipeId; + $this->recipeName = $recipeName; + } + + /** + * @return RecipeId + */ + public function recipeId() + { + return $this->recipeId; + } + + /** + * @return string + */ + public function recipeName() + { + return $this->recipeName; + } +} diff --git a/src/Domain/Administration/DomainModel/Event/RecipeWasRetired.php b/src/Domain/Administration/DomainModel/Event/RecipeWasRetired.php new file mode 100644 index 0000000..c6910a8 --- /dev/null +++ b/src/Domain/Administration/DomainModel/Event/RecipeWasRetired.php @@ -0,0 +1,32 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Administration\DomainModel\Event; + +use Example\Domain\Administration\DomainModel\RecipeId; +use Example\Domain\Common\DomainModel\Event\DomainEvent; + +final class RecipeWasRetired implements DomainEvent +{ + /** + * @var RecipeId + */ + private $recipeId; + + public function __construct(RecipeId $recipeId) + { + $this->recipeId = $recipeId; + } + + /** + * @return RecipeId + */ + public function recipeId() + { + return $this->recipeId; + } +} diff --git a/src/Domain/Administration/DomainModel/Owner.php b/src/Domain/Administration/DomainModel/Owner.php index 814ef0a..f22bfd5 100644 --- a/src/Domain/Administration/DomainModel/Owner.php +++ b/src/Domain/Administration/DomainModel/Owner.php @@ -2,12 +2,24 @@ namespace Example\Domain\Administration\DomainModel; +use Example\Domain\Administration\DomainModel\Event\RecipeWasCreated; +use Example\Domain\Administration\DomainModel\Event\RecipeWasReleased; +use Example\Domain\Administration\DomainModel\Event\RecipeWasRetired; use Example\Domain\Administration\DomainModel\Identity\OwnerId; use Example\Domain\Administration\DomainModel\Event\CandidateWasHired; use Example\Domain\Common\DomainModel\AggregateRoot; use Example\Domain\Common\DomainModel\FullName; use Example\Domain\Common\DomainModel\JobTitle; +use Example\Domain\Common\DomainModel\Money; +use Example\Domain\Common\Exception\EntityNotFoundException; +/** + * Aggregate root of the admin context + * This class manages: + * + * - Recipes + * - Candidates + */ final class Owner extends AggregateRoot { /** @@ -20,6 +32,11 @@ final class Owner extends AggregateRoot */ private $candidates = []; + /** + * @var Recipe[] + */ + private $recipes = []; + /** * @param OwnerId $id */ @@ -45,6 +62,52 @@ public function hire(FullName $name, JobTitle $title) $this->mutate(new CandidateWasHired($this->identity, $name, $title)); } + /** + * @param RecipeName $name + * @param Money $price + */ + public function newRecipe(RecipeName $name, Money $price) + { + $this->mutate( + new RecipeWasCreated( + $this->getIdentity(), new RecipeId($name), $name, $price + ) + ); + } + + /** + * @param RecipeId $id + */ + public function releaseRecipe(RecipeId $id) + { + $this->mutate(new RecipeWasReleased($id, $this->recipeWithId($id)->name()->toString())); + } + + /** + * @param RecipeId $id + */ + public function retireRecipe(RecipeId $id) + { + $this->mutate(new RecipeWasRetired($id)); + } + + /** + * @param RecipeId $id + * + * @throws \Example\Domain\Common\Exception\EntityNotFoundException + * @return Recipe + */ + public function recipeWithId(RecipeId $id) + { + foreach ($this->recipes as $recipe) { + if ($recipe->matchIdentity($id)) { + return $recipe; + } + } + + throw EntityNotFoundException::entityWithIdentity($id); + } + /** * @param CandidateWasHired $event */ @@ -54,12 +117,28 @@ protected function onCandidateWasHired(CandidateWasHired $event) } /** - * @param string $id - * - * @return Owner + * @param RecipeWasCreated $event + */ + protected function onRecipeWasCreated(RecipeWasCreated $event) + { + $this->recipes[] = new Recipe($this, $event->name(), $event->price()); + } + + /** + * @param RecipeWasReleased $event + */ + protected function onRecipeWasReleased(RecipeWasReleased $event) + { + $recipe = $this->recipeWithId($event->recipeId()); + $recipe->release(); + } + + /** + * @param RecipeWasRetired $event */ - public static function fakeWithId($id) + protected function onRecipeWasRetired(RecipeWasRetired $event) { - return new self(new OwnerId(FullName::fromSingleString($id))); + $recipe = $this->recipeWithId($event->recipeId()); + $recipe->retire(); } } diff --git a/src/Domain/Administration/DomainModel/PendingStatus.php b/src/Domain/Administration/DomainModel/PendingStatus.php new file mode 100644 index 0000000..007e700 --- /dev/null +++ b/src/Domain/Administration/DomainModel/PendingStatus.php @@ -0,0 +1,21 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Administration\DomainModel; + +final class PendingStatus extends RecipeStatus +{ + public function release(RecipeContext $recipe) + { + $recipe->setState(new ReleasedStatus()); + } + + public function retire(RecipeContext $recipe) + { + $recipe->setState(new RetiredStatus()); + } +} diff --git a/src/Domain/Administration/DomainModel/Recipe.php b/src/Domain/Administration/DomainModel/Recipe.php new file mode 100644 index 0000000..c384b81 --- /dev/null +++ b/src/Domain/Administration/DomainModel/Recipe.php @@ -0,0 +1,127 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Administration\DomainModel; + +use Example\Domain\Common\DomainModel\Money; + +/** + * Entity part of the Owner aggregate root + */ +final class Recipe implements RecipeContext +{ + /** + * @var Owner + */ + private $creator; + + /** + * @var string + */ + private $name; + + /** + * @var int + */ + private $price; + + /** + * @var string + */ + private $status; + + /** + * @param Owner $creator + * @param RecipeName $name + * @param Money $price + */ + public function __construct(Owner $creator, RecipeName $name, Money $price) + { + $this->creator = $creator; + $this->name = $name->toString(); + $this->price = $price->amount(); + $this->setState(new PendingStatus()); + } + + /** + * @return RecipeId + */ + public function getIdentity() + { + return new RecipeId($this->name()); + } + + /** + * @return bool + */ + public function isReleased() + { + return RecipeStatus::fromString($this->status)->isReleased(); + } + + /** + * @return bool + */ + public function isRetired() + { + return RecipeStatus::fromString($this->status)->isRetired(); + } + + /** + * @return bool + */ + public function isPending() + { + return RecipeStatus::fromString($this->status)->isPending(); + } + + /** + * @return Money + */ + public function price() + { + return Money::fromInt($this->price); + } + + /** + * @return RecipeName + */ + public function name() + { + return RecipeName::fromString($this->name); + } + + public function release() + { + RecipeStatus::fromString($this->status)->release($this); + } + + public function retire() + { + RecipeStatus::fromString($this->status)->retire($this); + } + + /** + * @param RecipeId $id + * + * @return bool + */ + public function matchIdentity(RecipeId $id) + { + return $this->getIdentity() == $id; + } + + /** + * @param RecipeStatus $status + * + * @internal Used by state machine only + */ + public function setState(RecipeStatus $status) + { + $this->status = $status->toString(); + } +} diff --git a/src/Domain/Administration/DomainModel/RecipeContext.php b/src/Domain/Administration/DomainModel/RecipeContext.php new file mode 100644 index 0000000..9c2fb35 --- /dev/null +++ b/src/Domain/Administration/DomainModel/RecipeContext.php @@ -0,0 +1,18 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Administration\DomainModel; + +interface RecipeContext +{ + /** + * @param RecipeStatus $status + * + * @internal Used by state machine only + */ + public function setState(RecipeStatus $status); +} diff --git a/src/Domain/Administration/DomainModel/RecipeId.php b/src/Domain/Administration/DomainModel/RecipeId.php new file mode 100644 index 0000000..4efd274 --- /dev/null +++ b/src/Domain/Administration/DomainModel/RecipeId.php @@ -0,0 +1,43 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Administration\DomainModel; + +use Behat\Transliterator\Transliterator; +use Example\Domain\Common\DomainModel\Identity\Identity; + +final class RecipeId implements Identity +{ + /** + * @var RecipeName + */ + private $name; + + /** + * @param RecipeName $name + */ + public function __construct(RecipeName $name) + { + $this->name = $name; + } + + /** + * @return string + */ + public function getEntityClass() + { + return Recipe::class; + } + + /** + * @return mixed + */ + public function id() + { + return Transliterator::transliterate($this->name->toString()); + } +} diff --git a/src/Domain/Administration/DomainModel/RecipeName.php b/src/Domain/Administration/DomainModel/RecipeName.php new file mode 100644 index 0000000..082a8c6 --- /dev/null +++ b/src/Domain/Administration/DomainModel/RecipeName.php @@ -0,0 +1,49 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Administration\DomainModel; + +use Example\Domain\Common\Exception\InvalidArgumentException; + +final class RecipeName +{ + /** + * @var string + */ + private $name; + + /** + * @param string $name + * @throws \Example\Domain\Common\Exception\InvalidArgumentException + */ + private function __construct($name) + { + if (empty($name)) { + throw new InvalidArgumentException('The recipe name cannot be empty.'); + } + + $this->name = $name; + } + + /** + * @return string + */ + public function toString() + { + return $this->name; + } + + /** + * @param string $name + * + * @return RecipeName + */ + public static function fromString($name) + { + return new self($name); + } +} diff --git a/src/Domain/Administration/DomainModel/RecipeStatus.php b/src/Domain/Administration/DomainModel/RecipeStatus.php new file mode 100644 index 0000000..1bbc798 --- /dev/null +++ b/src/Domain/Administration/DomainModel/RecipeStatus.php @@ -0,0 +1,83 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Administration\DomainModel; + +use Example\Domain\Administration\Exception\RecipeTransitionException; + +/** + * State workflow for recipes. + */ +abstract class RecipeStatus +{ + /** + * @return bool + */ + public function isReleased() + { + return false; + } + + /** + * @return bool + */ + public function isRetired() + { + return false; + } + + /** + * @return bool + */ + public function isPending() + { + return false; + } + + public function release(RecipeContext $recipe) + { + throw RecipeTransitionException::invalidRecipeTransition($this, new ReleasedStatus()); + } + + public function retire(RecipeContext $recipe) + { + throw RecipeTransitionException::invalidRecipeTransition($this, new RetiredStatus()); + } + + /** + * @return string + */ + public function toString() + { + return str_ireplace([__NAMESPACE__, '\\', 'Status'], '', get_class($this)); + } + + /** + * @param string $string + * + * @return RecipeStatus + */ + public static function fromString($string) + { + return self::$string(); + } + + private static function Pending() + { + return new PendingStatus(); + } + + private static function Released() + { + return new ReleasedStatus(); + } + + private static function Retired() + { + return new RetiredStatus(); + } +} diff --git a/src/Domain/Administration/DomainModel/ReleasedStatus.php b/src/Domain/Administration/DomainModel/ReleasedStatus.php new file mode 100644 index 0000000..0bcf804 --- /dev/null +++ b/src/Domain/Administration/DomainModel/ReleasedStatus.php @@ -0,0 +1,27 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Administration\DomainModel; + +final class ReleasedStatus extends RecipeStatus +{ + /** + * @return bool + */ + public function isReleased() + { + return true; + } + + /** + * @param RecipeContext $recipe + */ + public function retire(RecipeContext $recipe) + { + $recipe->setState(new RetiredStatus()); + } +} diff --git a/src/Domain/Administration/DomainModel/RetiredStatus.php b/src/Domain/Administration/DomainModel/RetiredStatus.php new file mode 100644 index 0000000..245c68c --- /dev/null +++ b/src/Domain/Administration/DomainModel/RetiredStatus.php @@ -0,0 +1,19 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Administration\DomainModel; + +final class RetiredStatus extends RecipeStatus +{ + /** + * @return bool + */ + public function isRetired() + { + return true; + } +} diff --git a/src/Domain/Administration/Exception/RecipeTransitionException.php b/src/Domain/Administration/Exception/RecipeTransitionException.php new file mode 100644 index 0000000..09f20ce --- /dev/null +++ b/src/Domain/Administration/Exception/RecipeTransitionException.php @@ -0,0 +1,24 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Administration\Exception; + +use Example\Domain\Administration\DomainModel\RecipeStatus; + +final class RecipeTransitionException extends \Exception +{ + /** + * @param RecipeStatus $from + * @param RecipeStatus $to + * + * @return RecipeTransitionException + */ + public static function invalidRecipeTransition(RecipeStatus $from, RecipeStatus $to) + { + return new self("The recipe transition from {$from->toString()} to {$to->toString()} is invalid."); + } +} diff --git a/src/Domain/Common/DomainModel/Money.php b/src/Domain/Common/DomainModel/Money.php new file mode 100644 index 0000000..68cc891 --- /dev/null +++ b/src/Domain/Common/DomainModel/Money.php @@ -0,0 +1,42 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Common\DomainModel; + +final class Money +{ + /** + * @var int + */ + private $amount; + + /** + * @param int $amount + */ + private function __construct($amount) + { + $this->amount = $amount; + } + + /** + * @return int + */ + public function amount() + { + return $this->amount; + } + + /** + * @param int $int + * + * @return Money + */ + public static function fromInt($int) + { + return new self($int); + } +} diff --git a/src/Domain/Sale/Application/BuyerService.php b/src/Domain/Sale/Application/BuyerService.php new file mode 100644 index 0000000..8aa9216 --- /dev/null +++ b/src/Domain/Sale/Application/BuyerService.php @@ -0,0 +1,51 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Sale\Application; + +use Example\Domain\Sale\DomainModel\Address; +use Example\Domain\Sale\DomainModel\Buyer; +use Example\Domain\Sale\DomainModel\BuyerRepository; +use Example\Domain\Sale\DomainModel\PhoneBuyer; +use Example\Domain\Sale\DomainModel\PhoneNumber; + +final class BuyerService +{ + /** + * @var BuyerRepository + */ + private $buyers; + + /** + * @var string + */ + private $countryCode; + + /** + * @param BuyerRepository $buyers + * @param string $countryCode + */ + public function __construct(BuyerRepository $buyers, $countryCode) + { + $this->buyers = $buyers; + $this->countryCode = $countryCode; + } + + /** + * @param string $phoneNumber + * @param string $address + */ + public function registerPhoneBuyer($phoneNumber, $address) + { + $buyer = new PhoneBuyer( + PhoneNumber::fromString($phoneNumber, $this->countryCode), + Address::fromString($address) + ); + + $this->buyers->saveBuyer($buyer); + } +} diff --git a/src/Domain/Sale/Application/CreateMealOnRecipeCreated.php b/src/Domain/Sale/Application/CreateMealOnRecipeCreated.php new file mode 100644 index 0000000..c5e80b4 --- /dev/null +++ b/src/Domain/Sale/Application/CreateMealOnRecipeCreated.php @@ -0,0 +1,49 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Sale\Application; + +use Example\Domain\Administration\DomainModel\Event\RecipeWasReleased; +use Example\Domain\Common\Application\EventPublisher; +use Example\Domain\Sale\DomainModel\Identity\MealId; +use Example\Domain\Sale\DomainModel\Meal; +use Example\Domain\Sale\DomainModel\MealRepository; + +final class CreateMealOnRecipeCreated +{ + /** + * @var MealRepository + */ + private $meals; + + /** + * @var EventPublisher + */ + private $publisher; + + /** + * @param MealRepository $meals + * @param EventPublisher $publisher + */ + public function __construct(MealRepository $meals, EventPublisher $publisher) + { + $this->meals = $meals; + $publisher->addListener(RecipeWasReleased::class, $this, 'onRecipeWasReleased'); + $this->publisher = $publisher; + } + + /** + * @param RecipeWasReleased $event + */ + public function onRecipeWasReleased(RecipeWasReleased $event) + { + $meal = new Meal(new MealId($event->recipeId()->id()), $event->recipeName()); + $this->meals->saveMeal($meal); + + $this->publisher->publish($meal->uncommitedEvents()); + } +} diff --git a/src/Domain/Sale/Application/OrderService.php b/src/Domain/Sale/Application/OrderService.php new file mode 100644 index 0000000..124d02b --- /dev/null +++ b/src/Domain/Sale/Application/OrderService.php @@ -0,0 +1,118 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Sale\Application; + +use Example\Domain\Common\Application\EventPublisher; +use Example\Domain\Sale\DomainModel\BuyerId; +use Example\Domain\Sale\DomainModel\BuyerRepository; +use Example\Domain\Sale\DomainModel\ConfirmationNumberGenerator; +use Example\Domain\Sale\DomainModel\CustomerType; +use Example\Domain\Sale\DomainModel\Identity\EmployeeId; +use Example\Domain\Sale\DomainModel\Identity\MealId; +use Example\Domain\Sale\DomainModel\Identity\OrderId; +use Example\Domain\Sale\DomainModel\MealRepository; +use Example\Domain\Sale\DomainModel\OrderRepository; + +final class OrderService +{ + /** + * @var OrderRepository + */ + private $orders; + + /** + * @var EventPublisher + */ + private $publisher; + + /** + * @var BuyerRepository + */ + private $buyers; + + /** + * @var MealRepository + */ + private $meals; + + /** + * @var ConfirmationNumberGenerator + */ + private $generator; + + /** + * @param OrderRepository $orders + * @param EventPublisher $publisher + * @param BuyerRepository $buyers + * @param MealRepository $meals + * @param ConfirmationNumberGenerator $generator + */ + public function __construct( + OrderRepository $orders, + EventPublisher $publisher, + BuyerRepository $buyers, + MealRepository $meals, + ConfirmationNumberGenerator $generator + ) { + $this->orders = $orders; + $this->publisher = $publisher; + $this->buyers = $buyers; + $this->meals = $meals; + $this->generator = $generator; + } + + /** + * @param EmployeeId $waitress + * @param CustomerType $type + * @param BuyerId $buyerId + */ + public function startOrder(EmployeeId $waitress, CustomerType $type, BuyerId $buyerId) + { + $order = $type->startOrder( + $this->generator->generateOrderId(), + $waitress, + $this->buyers->buyerWithId($buyerId) + ); + $this->orders->saveOrder($order); + + $this->publisher->publish($order->uncommitedEvents()); + } + + /** + * @param OrderId $orderId + * @param int $quantity + * @param MealId $mealId + */ + public function orderMeal(OrderId $orderId, $quantity, MealId $mealId) + { + $order = $this->orders->orderWithId($orderId); + $meal = $this->meals->activeMeal($mealId); + for ($i = 0; $i < $quantity; $i ++) { + $order->orderMeal($meal); + } + + $this->orders->saveOrder($order); + $this->publisher->publish($order->uncommitedEvents()); + } + + /** + * @param OrderId $orderId + * @param \DateTime $time + * @throws \RuntimeException + * + * @return ScheduledForPreparationOrder + */ + public function confirmOrder(OrderId $orderId, \DateTime $time) + { + throw new \RuntimeException(__METHOD__ . ' is not implemented yet.'); + $order = $this->orders->orderWithId($orderId); + $order->confirm($time); // todo should it return ConfirmedOrder class or only with flag true??? + $this->orders->saveOrder($order); + $this->publisher->publish($order->uncommitedEvents()); + } +} diff --git a/src/Domain/Sale/DomainModel/Address.php b/src/Domain/Sale/DomainModel/Address.php new file mode 100644 index 0000000..eee9a11 --- /dev/null +++ b/src/Domain/Sale/DomainModel/Address.php @@ -0,0 +1,21 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Sale\DomainModel; + +final class Address +{ + /** + * @param string $address + * + * @return Address + */ + public static function fromString($address) + { + return new self(); + } +} diff --git a/src/Domain/Sale/DomainModel/Buyer.php b/src/Domain/Sale/DomainModel/Buyer.php new file mode 100644 index 0000000..1fbf088 --- /dev/null +++ b/src/Domain/Sale/DomainModel/Buyer.php @@ -0,0 +1,16 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Sale\DomainModel; + +interface Buyer +{ + /** + * @return BuyerId + */ + public function getIdentity(); +} diff --git a/src/Domain/Sale/DomainModel/BuyerId.php b/src/Domain/Sale/DomainModel/BuyerId.php new file mode 100644 index 0000000..db6e8e8 --- /dev/null +++ b/src/Domain/Sale/DomainModel/BuyerId.php @@ -0,0 +1,62 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Sale\DomainModel; + +use Example\Domain\Common\DomainModel\Identity\Identity; + +final class BuyerId implements Identity +{ + /** + * @var string + */ + private $id; + + /** + * @param string $number + */ + private function __construct($number) + { + $this->id = $number; + } + + /** + * @return string + */ + public function getEntityClass() + { + return Buyer::class; + } + + /** + * @return mixed + */ + public function id() + { + return $this->id; + } + + /** + * @param string $string + * + * @return BuyerId + */ + public static function fromString($string) + { + return new self($string); + } + + /** + * @param PhoneNumber $number + * + * @return BuyerId + */ + public static function phoneBuyer(PhoneNumber $number) + { + return new self($number->toString()); + } +} diff --git a/src/Domain/Sale/DomainModel/BuyerRepository.php b/src/Domain/Sale/DomainModel/BuyerRepository.php new file mode 100644 index 0000000..317b664 --- /dev/null +++ b/src/Domain/Sale/DomainModel/BuyerRepository.php @@ -0,0 +1,26 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Sale\DomainModel; + +use Example\Domain\Common\Exception\EntityNotFoundException; + +interface BuyerRepository +{ + /** + * @param Buyer $buyer + */ + public function saveBuyer(Buyer $buyer); + + /** + * @param BuyerId $id + * + * @return Buyer + * @throws EntityNotFoundException + */ + public function buyerWithId(BuyerId $id); +} diff --git a/src/Domain/Sale/DomainModel/Cashier.php b/src/Domain/Sale/DomainModel/Cashier.php index 8682ec5..aa66d14 100644 --- a/src/Domain/Sale/DomainModel/Cashier.php +++ b/src/Domain/Sale/DomainModel/Cashier.php @@ -57,6 +57,11 @@ public function matchTitle(JobTitle $title) return $title->equal(JobTitle::Cashier()); } + public function takePhoneOrder() + { + + } + /** * @param CashierWasCreated $event */ diff --git a/src/Domain/Sale/DomainModel/ConfirmationNumberGenerator.php b/src/Domain/Sale/DomainModel/ConfirmationNumberGenerator.php new file mode 100644 index 0000000..23b5a04 --- /dev/null +++ b/src/Domain/Sale/DomainModel/ConfirmationNumberGenerator.php @@ -0,0 +1,18 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Sale\DomainModel; + +use Example\Domain\Sale\DomainModel\Identity\OrderId; + +interface ConfirmationNumberGenerator +{ + /** + * @return OrderId + */ + public function generateOrderId(); +} diff --git a/src/Domain/Sale/DomainModel/CustomerName.php b/src/Domain/Sale/DomainModel/CustomerName.php new file mode 100644 index 0000000..92deaa2 --- /dev/null +++ b/src/Domain/Sale/DomainModel/CustomerName.php @@ -0,0 +1,21 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Sale\DomainModel; + +final class CustomerName +{ + /** + * @param string $string + * + * @return CustomerName + */ + public static function fromString($string) + { + return new self(); + } +} diff --git a/src/Domain/Sale/DomainModel/CustomerType.php b/src/Domain/Sale/DomainModel/CustomerType.php new file mode 100644 index 0000000..39a9727 --- /dev/null +++ b/src/Domain/Sale/DomainModel/CustomerType.php @@ -0,0 +1,31 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Sale\DomainModel; + +use Example\Domain\Sale\DomainModel\Identity\EmployeeId; +use Example\Domain\Sale\DomainModel\Identity\OrderId; + +abstract class CustomerType +{ + /** + * @param OrderId $orderId + * @param EmployeeId $employeeId + * @param Buyer $buyer + * + * @return Order + */ + public abstract function startOrder(OrderId $orderId, EmployeeId $employeeId, Buyer $buyer); + + /** + * @return CustomerType + */ + public static function PhoneCustomer() + { + return new PhoneCustomer(); + } +} diff --git a/src/Domain/Sale/DomainModel/Event/MealWasOrdered.php b/src/Domain/Sale/DomainModel/Event/MealWasOrdered.php new file mode 100644 index 0000000..94784c9 --- /dev/null +++ b/src/Domain/Sale/DomainModel/Event/MealWasOrdered.php @@ -0,0 +1,60 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Sale\DomainModel\Event; + +use Example\Domain\Common\DomainModel\Event\DomainEvent; +use Example\Domain\Sale\DomainModel\Identity\MealId; +use Example\Domain\Sale\DomainModel\Identity\OrderId; +use Example\Domain\Sale\DomainModel\Meal; + +final class MealWasOrdered implements DomainEvent +{ + /** + * @var OrderId + */ + private $orderId; + + /** + * @var Meal + */ + private $meal; + + /** + * @param OrderId $orderId + * @param Meal $meal + */ + public function __construct(OrderId $orderId, Meal $meal) + { + $this->orderId = $orderId; + $this->meal = $meal; + } + + /** + * @return \Example\Domain\Sale\DomainModel\Identity\MealId + */ + public function mealId() + { + return $this->meal->getIdentity(); + } + + /** + * @return \Example\Domain\Sale\DomainModel\Identity\OrderId + */ + public function orderId() + { + return $this->orderId; + } + + /** + * @internal Should not be used to perform operation + */ + public function _meal() + { + return $this->meal; + } +} diff --git a/src/Domain/Sale/DomainModel/Event/OrderWasConfirmed.php b/src/Domain/Sale/DomainModel/Event/OrderWasConfirmed.php new file mode 100644 index 0000000..96be481 --- /dev/null +++ b/src/Domain/Sale/DomainModel/Event/OrderWasConfirmed.php @@ -0,0 +1,35 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Sale\DomainModel\Event; + +use Example\Domain\Common\DomainModel\Event\DomainEvent; +use Example\Domain\Sale\DomainModel\Identity\OrderId; + +final class OrderWasConfirmed implements DomainEvent +{ + /** + * @var OrderId + */ + private $orderId; + + /** + * @param OrderId $orderId + */ + public function __construct(OrderId $orderId) + { + $this->orderId = $orderId; + } + + /** + * @return OrderId + */ + public function orderId() + { + return $this->orderId; + } +} diff --git a/src/Domain/Sale/DomainModel/Event/OrderWasCreated.php b/src/Domain/Sale/DomainModel/Event/OrderWasCreated.php new file mode 100644 index 0000000..d638665 --- /dev/null +++ b/src/Domain/Sale/DomainModel/Event/OrderWasCreated.php @@ -0,0 +1,78 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Sale\DomainModel\Event; + +use Example\Domain\Common\DomainModel\Event\DomainEvent; +use Example\Domain\Sale\DomainModel\Buyer; +use Example\Domain\Sale\DomainModel\BuyerId; +use Example\Domain\Sale\DomainModel\Identity\EmployeeId; +use Example\Domain\Sale\DomainModel\Identity\OrderId; + +final class OrderWasCreated implements DomainEvent +{ + /** + * @var OrderId + */ + private $orderId; + + /** + * @var EmployeeId + */ + private $employeeId; + + /** + * @var Buyer + */ + private $buyer; + + /** + * @param OrderId $id + * @param EmployeeId $employeeId + * @param Buyer $buyer + */ + public function __construct(OrderId $id, EmployeeId $employeeId, Buyer $buyer) + { + $this->orderId = $id; + $this->employeeId = $employeeId; + $this->buyer = $buyer; + } + + /** + * @return OrderId + */ + public function orderId() + { + return $this->orderId; + } + + /** + * @return \Example\Domain\Sale\DomainModel\Identity\EmployeeId + */ + public function employeeId() + { + return $this->employeeId; + } + + /** + * @return \Example\Domain\Sale\DomainModel\BuyerId + */ + public function buyerId() + { + return $this->buyer->getIdentity(); + } + + /** + * @return Buyer + * + * @internal Should not be used for operation. + */ + public function _buyer() + { + return $this->buyer; + } +} diff --git a/src/Domain/Sale/DomainModel/Identity/EmployeeId.php b/src/Domain/Sale/DomainModel/Identity/EmployeeId.php index a251aaf..e31fe54 100644 --- a/src/Domain/Sale/DomainModel/Identity/EmployeeId.php +++ b/src/Domain/Sale/DomainModel/Identity/EmployeeId.php @@ -51,4 +51,14 @@ public static function fromName(FullName $name) { return new self($name->toString()); } + + /** + * @param string $string + * + * @return EmployeeId + */ + public static function fromString($string) + { + return new self($string); + } } diff --git a/src/Domain/Sale/DomainModel/Identity/MealId.php b/src/Domain/Sale/DomainModel/Identity/MealId.php new file mode 100644 index 0000000..9827eb5 --- /dev/null +++ b/src/Domain/Sale/DomainModel/Identity/MealId.php @@ -0,0 +1,43 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Sale\DomainModel\Identity; + +use Example\Domain\Common\DomainModel\Identity\Identity; +use Example\Domain\Sale\DomainModel\Meal; + +final class MealId implements Identity +{ + /** + * @var mixed + */ + private $id; + + /** + * @param mixed $id + */ + public function __construct($id) + { + $this->id = $id; + } + + /** + * @return string + */ + public function getEntityClass() + { + return Meal::class; + } + + /** + * @return mixed + */ + public function id() + { + return $this->id; + } +} diff --git a/src/Domain/Sale/DomainModel/Identity/OrderId.php b/src/Domain/Sale/DomainModel/Identity/OrderId.php new file mode 100644 index 0000000..afb0d0c --- /dev/null +++ b/src/Domain/Sale/DomainModel/Identity/OrderId.php @@ -0,0 +1,43 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Sale\DomainModel\Identity; + +use Example\Domain\Common\DomainModel\Identity\Identity; +use Example\Domain\Sale\DomainModel\Order; + +final class OrderId implements Identity +{ + /** + * @var mixed + */ + private $id; + + /** + * @param mixed $id + */ + public function __construct($id) + { + $this->id = $id; + } + + /** + * @return string + */ + public function getEntityClass() + { + return Order::Class; + } + + /** + * @return mixed + */ + public function id() + { + return $this->id; + } +} diff --git a/src/Domain/Sale/DomainModel/Meal.php b/src/Domain/Sale/DomainModel/Meal.php new file mode 100644 index 0000000..0c38fd6 --- /dev/null +++ b/src/Domain/Sale/DomainModel/Meal.php @@ -0,0 +1,42 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Sale\DomainModel; + +use Example\Domain\Common\DomainModel\AggregateRoot; +use Example\Domain\Sale\DomainModel\Identity\MealId; + +final class Meal extends AggregateRoot +{ + /** + * @var mixed + */ + private $id; + + /** + * @var string + */ + private $name; + + /** + * @param MealId $id + * @param string $name + */ + public function __construct(MealId $id, $name) + { + $this->id = $id->id(); + $this->name = $name; + } + + /** + * @return MealId + */ + public function getIdentity() + { + return new MealId($this->id); + } +} diff --git a/src/Domain/Sale/DomainModel/MealRepository.php b/src/Domain/Sale/DomainModel/MealRepository.php new file mode 100644 index 0000000..05a23b6 --- /dev/null +++ b/src/Domain/Sale/DomainModel/MealRepository.php @@ -0,0 +1,27 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Sale\DomainModel; + +use Example\Domain\Common\Exception\EntityNotFoundException; +use Example\Domain\Sale\DomainModel\Identity\MealId; + +interface MealRepository +{ + /** + * @param Meal $meal + */ + public function saveMeal(Meal $meal); + + /** + * @param MealId $id + * + * @return Meal + * @throws EntityNotFoundException + */ + public function activeMeal(MealId $id); +} diff --git a/src/Domain/Sale/DomainModel/Order.php b/src/Domain/Sale/DomainModel/Order.php new file mode 100644 index 0000000..0c5acc1 --- /dev/null +++ b/src/Domain/Sale/DomainModel/Order.php @@ -0,0 +1,180 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Sale\DomainModel; + +use Example\Domain\Common\DomainModel\AggregateRoot; +use Example\Domain\Common\DomainModel\Event\DomainEvent; +use Example\Domain\Sale\DomainModel\Event\MealWasOrdered; +use Example\Domain\Sale\DomainModel\Event\OrderWasConfirmed; +use Example\Domain\Sale\DomainModel\Event\OrderWasCreated; +use Example\Domain\Sale\DomainModel\Identity\EmployeeId; +use Example\Domain\Sale\DomainModel\Identity\MealId; +use Example\Domain\Sale\DomainModel\Identity\OrderId; + +final class Order extends AggregateRoot +{ + /** + * @var mixed + */ + private $id; + + /** + * @var mixed + */ + private $employeeId; + + /** + * @var Buyer + */ + private $buyer; + + /** + * @var Meal[] + */ + private $meals = []; + + /** + * @var bool + */ + private $isConfirmed = false; + + /** + * @param DomainEvent[] $events + */ + private function __construct(array $events = []) + { + foreach ($events as $event) { + $this->mutate($event); + } + } + + /** + * @return OrderId + */ + public function getIdentity() + { + return new OrderId($this->id); + } + + /** + * @return BuyerId + */ + public function buyerId() + { + return $this->buyer->getIdentity(); + } + + /** + * @return EmployeeId + */ + public function takenBy() + { + return EmployeeId::fromString($this->employeeId); + } + + /** + * @return bool + */ + public function isConfirmed() + { + return $this->isConfirmed; + } + + /** + * @param Meal $meal + */ + public function orderMeal(Meal $meal) + { + $this->mutate(new MealWasOrdered($this->getIdentity(), $meal)); + } + + /** + * @return MealId[] + */ + public function orderedMeals() + { + return array_map( + function (Meal $meal) { + return $meal->getIdentity(); + }, + $this->meals + ); + } + + /** + * @param MealWasOrdered $event + */ + protected function onMealWasOrdered(MealWasOrdered $event) + { + $this->meals[] = $event->_meal(); + } + + /** + * @param OrderWasCreated $event + */ + protected function onOrderWasCreated(OrderWasCreated $event) + { + $this->id = $event->orderId()->id(); + $this->employeeId = $event->employeeId()->id(); + $this->buyer = $event->_buyer(); + } + + /** + * @param OrderWasConfirmed $event + * @throws OrderException + */ + protected function onOrderWasConfirmed(OrderWasConfirmed $event) + { + if ($this->isConfirmed()) { + throw new OrderException('Cannot confirm an order twice.'); + } + + $this->isConfirmed = true; + } + + /** + * @param OrderId $id + * @param EmployeeId $takenBy + * @param Buyer $buyer + * + * @return Order + */ + public static function PendingOrder(OrderId $id, EmployeeId $takenBy, Buyer $buyer) + { + return new self( + [ + new OrderWasCreated($id, $takenBy, $buyer), + ] + ); + } + + /** + * @param OrderId $id + * @param EmployeeId $takenBy + * @param Buyer $buyer + * + * @return Order + */ + public static function ConfirmedOrder(OrderId $id, EmployeeId $takenBy, Buyer $buyer) + { + $order = self::PendingOrder($id, $takenBy, $buyer); + $order->mutate(new OrderWasConfirmed($id)); + + return $order; + } + + /** + * @param DomainEvent[] $events + * + * @return Order + */ + public static function fromStream(array $events) + { + return new self($events); + } +} diff --git a/src/Domain/Sale/DomainModel/OrderException.php b/src/Domain/Sale/DomainModel/OrderException.php new file mode 100644 index 0000000..1976cbd --- /dev/null +++ b/src/Domain/Sale/DomainModel/OrderException.php @@ -0,0 +1,12 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Sale\DomainModel; + +final class OrderException extends \Exception +{ +} diff --git a/src/Domain/Sale/DomainModel/OrderRepository.php b/src/Domain/Sale/DomainModel/OrderRepository.php new file mode 100644 index 0000000..648e9a1 --- /dev/null +++ b/src/Domain/Sale/DomainModel/OrderRepository.php @@ -0,0 +1,27 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Sale\DomainModel; + +use Example\Domain\Common\Exception\EntityNotFoundException; +use Example\Domain\Sale\DomainModel\Identity\OrderId; + +interface OrderRepository +{ + /** + * @param Order $order + */ + public function saveOrder(Order $order); + + /** + * @param OrderId $id + * + * @return Order + * @throws EntityNotFoundException + */ + public function orderWithId(OrderId $id); +} diff --git a/src/Domain/Sale/DomainModel/PhoneBuyer.php b/src/Domain/Sale/DomainModel/PhoneBuyer.php new file mode 100644 index 0000000..e64381d --- /dev/null +++ b/src/Domain/Sale/DomainModel/PhoneBuyer.php @@ -0,0 +1,45 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Sale\DomainModel; + +final class PhoneBuyer implements Buyer +{ + /** + * @var string + */ + private $phone_number; + + /** + * @var string + */ + private $phone_country; + + /** + * @var string + */ + private $address; + + /** + * @param PhoneNumber $number + * @param Address $address + */ + public function __construct(PhoneNumber $number, Address $address) + { + $this->phone_number = $number->toString(); + $this->phone_country = $number->countryCode(); + $this->address = $address; + } + + /** + * @return BuyerId + */ + public function getIdentity() + { + return BuyerId::phoneBuyer(PhoneNumber::fromString($this->phone_number, $this->phone_country)); + } +} diff --git a/src/Domain/Sale/DomainModel/PhoneCustomer.php b/src/Domain/Sale/DomainModel/PhoneCustomer.php new file mode 100644 index 0000000..e7371ac --- /dev/null +++ b/src/Domain/Sale/DomainModel/PhoneCustomer.php @@ -0,0 +1,26 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Sale\DomainModel; + +use Example\Domain\Sale\DomainModel\Identity\EmployeeId; +use Example\Domain\Sale\DomainModel\Identity\OrderId; + +final class PhoneCustomer extends CustomerType +{ + /** + * @param OrderId $orderId + * @param EmployeeId $employeeId + * @param Buyer $buyer + * + * @return Order + */ + public function startOrder(OrderId $orderId, EmployeeId $employeeId, Buyer $buyer) + { + return Order::PendingOrder($orderId, $employeeId, $buyer); + } +} diff --git a/src/Domain/Sale/DomainModel/PhoneNumber.php b/src/Domain/Sale/DomainModel/PhoneNumber.php new file mode 100644 index 0000000..641c4c8 --- /dev/null +++ b/src/Domain/Sale/DomainModel/PhoneNumber.php @@ -0,0 +1,66 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Sale\DomainModel; + +use Example\Domain\Sale\Exception\PhoneNumberException; + +final class PhoneNumber +{ + /** + * @var string + */ + private $number; + + /** + * @var PhoneNumberFormat + */ + private $format; + + /** + * @param string $number + * @param PhoneNumberFormat $format + */ + private function __construct($number, PhoneNumberFormat $format) + { + $this->number = $number; + $this->format = $format; + } + + /** + * @return string + */ + public function toString() + { + return $this->format->convert($this->number); + } + + /** + * @return string + */ + public function countryCode() + { + return $this->format->countryCode(); + } + + /** + * @param string $number + * @param string $countryCode + * + * @throws \Example\Domain\Sale\Exception\PhoneNumberException + * @return PhoneNumber + */ + public static function fromString($number, $countryCode) + { + $format = PhoneNumberFormat::fromString($countryCode); + if (! $format->validate($number)) { + throw PhoneNumberException::invalidNumberFormat($number, $countryCode); + } + + return new self($number, $format); + } +} diff --git a/src/Domain/Sale/DomainModel/PhoneNumberFormat.php b/src/Domain/Sale/DomainModel/PhoneNumberFormat.php new file mode 100644 index 0000000..38ee80f --- /dev/null +++ b/src/Domain/Sale/DomainModel/PhoneNumberFormat.php @@ -0,0 +1,90 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Sale\DomainModel; + +use Example\Domain\Sale\Exception\PhoneNumberException; + +final class PhoneNumberFormat +{ + private static $mappings = [ + 'CA' => 'REGEX', // todo add regex + ]; + + /** + * @var string + */ + private $countryCode; + + /** + * @param string $countryCode + */ + private function __construct($countryCode) + { + $this->countryCode = $countryCode; + } + + /** + * @return string + */ + public function countryCode() + { + return $this->countryCode; + } + + /** + * @param string $number + * + * @return string + */ + public function convert($number) + { + $number = str_ireplace('-', '', $number); + + // todo implement country base conversion per class + return substr_replace($number, '-', 3, 0); + } + + /** + * @param string $number + * + * @return bool + */ + public function validate($number) + { + $length = 7; + if (strpos($number, '-')) { + $length = 8; + } + + return strlen($number) === $length; + } + + /** + * @param string $string + * + * @throws \Example\Domain\Sale\Exception\PhoneNumberException + * @return PhoneNumberFormat + */ + public static function fromString($string) + { + if (is_object($string)) { + $string = 'Object'; + } + + if (is_array($string)) { + $string = 'Array'; + } + + $countryCode = strtoupper($string); + if (! isset(self::$mappings[$countryCode])) { + throw PhoneNumberException::incompatibleFormat($string); + } + + return new self($countryCode); + } +} diff --git a/src/Domain/Sale/Exception/PhoneNumberException.php b/src/Domain/Sale/Exception/PhoneNumberException.php new file mode 100644 index 0000000..8f83be6 --- /dev/null +++ b/src/Domain/Sale/Exception/PhoneNumberException.php @@ -0,0 +1,32 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Sale\Exception; + +final class PhoneNumberException extends \Exception +{ + /** + * @param string $string + * @param string $countryCode + * + * @return PhoneNumberException + */ + public static function invalidNumberFormat($string, $countryCode) + { + return new self("The phone number '{$string}' is not a valid format for country '{$countryCode}'."); + } + + /** + * @param string $string + * + * @return PhoneNumberException + */ + public static function incompatibleFormat($string) + { + return new self("The phone number format '{$string}' is not a recognized format."); + } +} diff --git a/src/Infrastructure/InMemory/Sale/BuyerCollection.php b/src/Infrastructure/InMemory/Sale/BuyerCollection.php new file mode 100644 index 0000000..dfce8f4 --- /dev/null +++ b/src/Infrastructure/InMemory/Sale/BuyerCollection.php @@ -0,0 +1,44 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Infrastructure\InMemory\Sale; + +use Example\Domain\Common\Exception\EntityNotFoundException; +use Example\Domain\Sale\DomainModel\Buyer; +use Example\Domain\Sale\DomainModel\BuyerId; +use Example\Domain\Sale\DomainModel\BuyerRepository; + +final class BuyerCollection implements BuyerRepository +{ + /** + * @var Buyer[] + */ + private $buyers = []; + + /** + * @param Buyer $buyer + */ + public function saveBuyer(Buyer $buyer) + { + $this->buyers[$buyer->getIdentity()->id()] = $buyer; + } + + /** + * @param BuyerId $id + * + * @return Buyer + * @throws EntityNotFoundException + */ + public function buyerWithId(BuyerId $id) + { + if (! isset($this->buyers[$id->id()])) { + + } + + return $this->buyers[$id->id()]; + } +} diff --git a/src/Infrastructure/InMemory/Sale/MealCollection.php b/src/Infrastructure/InMemory/Sale/MealCollection.php new file mode 100644 index 0000000..fae8b26 --- /dev/null +++ b/src/Infrastructure/InMemory/Sale/MealCollection.php @@ -0,0 +1,44 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Infrastructure\InMemory\Sale; + +use Example\Domain\Common\Exception\EntityNotFoundException; +use Example\Domain\Sale\DomainModel\Identity\MealId; +use Example\Domain\Sale\DomainModel\Meal; +use Example\Domain\Sale\DomainModel\MealRepository; + +final class MealCollection implements MealRepository +{ + /** + * @var Meal[] + */ + private $meals = []; + + /** + * @param Meal $meal + */ + public function saveMeal(Meal $meal) + { + $this->meals[$meal->getIdentity()->id()] = $meal; + } + + /** + * @param MealId $id + * + * @return Meal + * @throws EntityNotFoundException + */ + public function activeMeal(MealId $id) + { + if (! isset($this->meals[$id->id()])) { + throw EntityNotFoundException::entityWithIdentity($id); + } + + return $this->meals[$id->id()]; + } +} diff --git a/src/Infrastructure/InMemory/Sale/OrderCollection.php b/src/Infrastructure/InMemory/Sale/OrderCollection.php new file mode 100644 index 0000000..1b391f8 --- /dev/null +++ b/src/Infrastructure/InMemory/Sale/OrderCollection.php @@ -0,0 +1,44 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Infrastructure\InMemory\Sale; + +use Example\Domain\Common\Exception\EntityNotFoundException; +use Example\Domain\Sale\DomainModel\Identity\OrderId; +use Example\Domain\Sale\DomainModel\Order; +use Example\Domain\Sale\DomainModel\OrderRepository; + +final class OrderCollection implements OrderRepository +{ + /** + * @var Order[] + */ + private $orders = []; + + /** + * @param Order $order + */ + public function saveOrder(Order $order) + { + $this->orders[$order->getIdentity()->id()] = $order; + } + + /** + * @param OrderId $id + * + * @return Order + * @throws EntityNotFoundException + */ + public function orderWithId(OrderId $id) + { + if (! isset($this->orders[$id->id()])) { + throw EntityNotFoundException::entityWithIdentity($id); + } + + return $this->orders[$id->id()]; + } +} diff --git a/tests/Domain/Administration/DomainModel/OwnerTest.php b/tests/Domain/Administration/DomainModel/OwnerTest.php index 0c2a5e6..70d324c 100644 --- a/tests/Domain/Administration/DomainModel/OwnerTest.php +++ b/tests/Domain/Administration/DomainModel/OwnerTest.php @@ -8,9 +8,13 @@ namespace Example\Domain\Administration\DomainModel; use Example\Domain\Administration\DomainModel\Event\CandidateWasHired; +use Example\Domain\Administration\DomainModel\Event\RecipeWasCreated; +use Example\Domain\Administration\DomainModel\Event\RecipeWasReleased; +use Example\Domain\Administration\DomainModel\Event\RecipeWasRetired; use Example\Domain\Administration\DomainModel\Identity\OwnerId; use Example\Domain\Common\DomainModel\FullName; use Example\Domain\Common\DomainModel\JobTitle; +use Example\Domain\Common\DomainModel\Money; final class OwnerTest extends \PHPUnit_Framework_TestCase { @@ -47,23 +51,51 @@ public function test_it_should_hire_candidate() /** * @depends test_it_should_create_a_owner */ - public function test_it_should_create_new_recipe_with_ingredients() + public function test_it_should_create_new_recipe() { - $this->markTestIncomplete('TODO'); - $recipe = $owner->newRecipe(new RecipeId(), RecipeName::fromString('All dress Pizza'), $ingredients = []); + $this->owner->uncommitedEvents(); // reset + $this->owner->newRecipe(RecipeName::fromString('All dress Pizza'), Money::fromInt(1200)); + $events = $this->owner->uncommitedEvents(); + $this->assertCount(1, $events, 'event should be added'); + /** + * @var RecipeWasCreated $event + */ + $event = $events[0]; + $this->assertInstanceOf(RecipeWasCreated::class, $event); + $this->assertEquals($this->owner->getIdentity(), $event->creator(), 'The creator id should be defined'); + $this->assertInstanceOf(RecipeId::class, $event->recipeId(), 'The recipe id should be defined'); + $this->assertEquals(Money::fromInt(1200), $event->price(), 'The price should be defined'); + + $recipe = $this->owner->recipeWithId($event->recipeId()); $this->assertInstanceOf(Recipe::class, $recipe, 'Owner should create recipe'); $this->assertFalse($recipe->isReleased(), 'Recipe should not be released by default'); $this->assertFalse($recipe->isRetired(), 'Recipe should not be retired by default'); + $this->assertEquals(Money::fromInt(1200), $recipe->price()); + $this->assertEquals(RecipeName::fromString('All dress Pizza'), $recipe->name()); } /** - * @depends test_it_should_create_new_recipe_with_ingredients + * @depends test_it_should_create_new_recipe */ public function test_it_should_release_recipe() { - $this->markTestIncomplete('TODO'); - $recipe = $owner->releaseRecipe(new RecipeId()); + $this->owner->uncommitedEvents(); // reset + $this->owner->newRecipe(RecipeName::fromString('All dress Pizza'), Money::fromInt(1000)); + $recipeId = $this->getNewRecipeId(); + $this->owner->releaseRecipe($recipeId); + + $events = $this->owner->uncommitedEvents(); + $this->assertCount(1, $events, 'event should be added'); + /** + * @var RecipeWasReleased $event + */ + $event = $events[0]; + $this->assertInstanceOf(RecipeWasReleased::class, $event); + $this->assertInstanceOf(RecipeId::class, $event->recipeId(), 'The recipe id should be defined'); + + $recipe = $this->owner->recipeWithId($event->recipeId()); + $this->assertTrue($recipe->isReleased(), 'Recipe should be released'); $this->assertFalse($recipe->isRetired(), 'Recipe should not be retired'); } @@ -73,9 +105,79 @@ public function test_it_should_release_recipe() */ public function test_it_should_retire_released_recipe() { - $this->markTestIncomplete('TODO'); - $recipe = $owner->retireRecipe(new RecipeId()); + $this->owner->newRecipe(RecipeName::fromString('Recipe name'), Money::fromInt(1000)); + $recipeId = $this->getNewRecipeId(); + $this->owner->releaseRecipe($recipeId); + $this->owner->uncommitedEvents(); // reset + + $this->owner->retireRecipe($recipeId); + + $events = $this->owner->uncommitedEvents(); + $this->assertCount(1, $events, 'event should be added'); + /** + * @var RecipeWasRetired $event + */ + $event = $events[0]; + $this->assertInstanceOf(RecipeWasRetired::class, $event); + $this->assertInstanceOf(RecipeId::class, $event->recipeId(), 'The recipe id should be defined'); + + $recipe = $this->owner->recipeWithId($recipeId); $this->assertFalse($recipe->isReleased(), 'Recipe should not be released'); $this->assertTrue($recipe->isRetired(), 'Recipe should be retired'); } + + /** + * @depends test_it_should_retire_released_recipe + * @depends test_it_should_create_new_recipe + */ + public function test_it_should_retire_a_pending_recipe() + { + $this->owner->newRecipe(RecipeName::fromString('name'), Money::fromInt(1000)); + $recipeId = $this->getNewRecipeId(); + $recipe = $this->owner->recipeWithId($recipeId); + + $this->assertFalse($recipe->isRetired()); + $this->owner->retireRecipe($recipeId); + $recipe = $this->owner->recipeWithId($recipeId); + $this->assertTrue($recipe->isRetired()); + } + + /** + * @expectedException \Example\Domain\Administration\Exception\RecipeTransitionException + * @expectedExceptionMessage The recipe transition from Retired to Retired is invalid. + * @depends test_it_should_retire_released_recipe + */ + public function test_it_should_throw_exception_when_releasing_a_retired_recipe() + { + $this->owner->newRecipe(RecipeName::fromString('name'), Money::fromInt(1000)); + $id = $this->getNewRecipeId(); + $this->owner->releaseRecipe($id); + $this->owner->retireRecipe($id); + $this->owner->retireRecipe($id); + } + + /** + * @expectedException \Example\Domain\Common\Exception\EntityNotFoundException + * @expectedExceptionMessage The entity of type 'Example\Domain\Administration\DomainModel\Recipe' with identity + */ + public function test_it_should_throw_exception_when_recipe_not_found() + { + $this->owner->recipeWithId(new RecipeId(RecipeName::fromString('recipe'))); + } + + /** + * @return RecipeId + */ + private function getNewRecipeId() + { + $events = $this->owner->uncommitedEvents(); + $this->assertCount(1, $events, 'event should be added'); + /** + * @var RecipeWasCreated $event + */ + $event = $events[0]; + $this->assertInstanceOf(RecipeWasCreated::class, $event); + + return $event->recipeId(); + } } diff --git a/tests/Domain/Administration/DomainModel/RecipeNameTest.php b/tests/Domain/Administration/DomainModel/RecipeNameTest.php new file mode 100644 index 0000000..f526846 --- /dev/null +++ b/tests/Domain/Administration/DomainModel/RecipeNameTest.php @@ -0,0 +1,25 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Administration\DomainModel; + +final class RecipeNameTest extends \PHPUnit_Framework_TestCase +{ + public function test_it_should_return_string_representation() + { + $this->assertSame('my name', RecipeName::fromString('my name')->toString()); + } + + /** + * @expectedException \Example\Domain\Common\Exception\InvalidArgumentException + * @expectedExceptionMessage The recipe name cannot be empty. + */ + public function test_it_should_not_allow_empty_string() + { + RecipeName::fromString(''); + } +} diff --git a/tests/Domain/Sale/DomainModel/BuyerTest.php b/tests/Domain/Sale/DomainModel/BuyerTest.php new file mode 100644 index 0000000..1650db8 --- /dev/null +++ b/tests/Domain/Sale/DomainModel/BuyerTest.php @@ -0,0 +1,23 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Sale\DomainModel; + +final class BuyerTest extends \PHPUnit_Framework_TestCase +{ + public function test_it_should_be_created_from_phone() + { + $buyer = new PhoneBuyer( + PhoneNumber::fromString('5555555', 'CA'), + Address::fromString('1 main street') + ); + + $this->assertInstanceOf(BuyerId::class, $buyer->getIdentity()); + $this->assertSame('555-5555', $buyer->getIdentity()->id()); + $this->assertSame(Buyer::class, $buyer->getIdentity()->getEntityClass()); + } +} diff --git a/tests/Domain/Sale/DomainModel/CashierTest.php b/tests/Domain/Sale/DomainModel/CashierTest.php index 63be939..f6a1190 100644 --- a/tests/Domain/Sale/DomainModel/CashierTest.php +++ b/tests/Domain/Sale/DomainModel/CashierTest.php @@ -48,4 +48,10 @@ public function test_it_should_generate_an_event_on_creation() $this->assertEquals(new EmployeeId(123), $event->employeeId()); $this->assertEquals(FullName::fromString('Joe', 'Blow'), $event->name()); } + + public function test_should_take_phone_order() + { + $this->cashier->takePhoneOrder(); + + } } diff --git a/tests/Domain/Sale/DomainModel/OrderTest.php b/tests/Domain/Sale/DomainModel/OrderTest.php new file mode 100644 index 0000000..6ec5bfb --- /dev/null +++ b/tests/Domain/Sale/DomainModel/OrderTest.php @@ -0,0 +1,112 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Sale\DomainModel; + +use Example\Domain\Sale\DomainModel\Event\OrderWasConfirmed; +use Example\Domain\Sale\DomainModel\Event\OrderWasCreated; +use Example\Domain\Sale\DomainModel\Identity\EmployeeId; +use Example\Domain\Sale\DomainModel\Identity\MealId; +use Example\Domain\Sale\DomainModel\Identity\OrderId; + +final class OrderTest extends \PHPUnit_Framework_TestCase +{ + public function test_it_should_create_a_pending_order_with_id() + { + $order = Order::PendingOrder( + new OrderId(123), + new EmployeeId('employee'), + $buyer = new NullBuyer() + ); + $this->assertInstanceOf(Order::class, $order); + $events = $order->uncommitedEvents(); + $this->assertFalse($order->isConfirmed()); + + $this->assertCount(1, $events); + + /** + * @var OrderWasCreated $event + */ + $event = $events[0]; + $this->assertInstanceOf(OrderWasCreated::class, $event); + $this->assertSame(123, $event->orderId()->id()); + $this->assertSame(Order::class, $event->orderId()->getEntityClass()); + $this->assertSame('employee', $event->employeeId()->id()); + $this->assertSame($buyer->getIdentity(), $event->buyerId()); + } + + public function test_it_should_create_a_confirmed_order() + { + $order = Order::ConfirmedOrder( + $orderId = new OrderId(1), $employee = new EmployeeId(1), $buyer = new NullBuyer() + ); + $this->assertInstanceOf(Order::class, $order); + $this->assertTrue($order->isConfirmed()); + $this->assertEquals($buyer->getIdentity(), $order->buyerId()); + $this->assertEquals($employee, $order->takenBy()); + $this->assertEquals($orderId, $order->getIdentity()); + + $events = $order->uncommitedEvents(); + $this->assertInstanceOf(OrderWasCreated::class, $events[0]); + $this->assertInstanceOf(OrderWasConfirmed::class, $events[1]); + } + + public function test_it_should_add_meal_to_order() + { + $order = Order::PendingOrder(new OrderId(1), new EmployeeId(1), new NullBuyer()); + $this->assertEquals([], $order->orderedMeals()); + $order->orderMeal(new Meal($orderOne = new MealId('meal 1'), 'Sandwich')); + $this->assertEquals([$orderOne], $order->orderedMeals()); + $order->orderMeal(new Meal($orderTwo = new MealId('meal 2'), 'Burger')); + $this->assertEquals([$orderOne, $orderTwo], $order->orderedMeals()); + } + + /** + * @expectedException \Example\Domain\Sale\DomainModel\OrderException + * @expectedExceptionMessage Cannot confirm an order twice. + */ + public function test_it_should_not_allow_to_confirm_order_twice() + { + Order::fromStream( + [ + new OrderWasConfirmed(new OrderId(1)), + new OrderWasConfirmed(new OrderId(1)), + ] + ); + } + + public function test_confirming_the_order_should_start_preparation_of_order() + { + $order = Order::PendingOrder(new OrderId(1), new EmployeeId(1), new NullBuyer()); + $this->assertFalse($order->isConfirmed()); + $scheduledOrder = $order->confirm(new \DateTime('2010-01-01 10:00:55')); + + $this->assertInstanceOf(ScheduleForCookingOrder::class, $scheduledOrder); + $this->assertTrue($order->isConfirmed()); + } +} + +final class NullBuyer implements Buyer +{ + /** + * @var BuyerId + */ + private $id; + + public function __construct() + { + $this->id = BuyerId::fromString(uniqid('buyer-')); + } + + /** + * @return BuyerId + */ + public function getIdentity() + { + return $this->id; + } +} diff --git a/tests/Domain/Sale/DomainModel/PhoneNumberFormatTest.php b/tests/Domain/Sale/DomainModel/PhoneNumberFormatTest.php new file mode 100644 index 0000000..b347bdc --- /dev/null +++ b/tests/Domain/Sale/DomainModel/PhoneNumberFormatTest.php @@ -0,0 +1,74 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Sale\DomainModel; + +use Example\Domain\Sale\Exception\PhoneNumberException; + +final class PhoneNumberFormatTest extends \PHPUnit_Framework_TestCase +{ + /** + * @param $string + * + * @dataProvider provideValidFormat + */ + public function test_it_should_create_format($string) + { + $format = PhoneNumberFormat::fromString($string); + $this->assertInstanceOf(PhoneNumberFormat::class, $format); + } + + public static function provideValidFormat() + { + return [ + ['CA'], + ['ca'], + ]; + } + + /** + * @param $invalid + * @param $value + * @dataProvider provideUnsupportedFormat + */ + public function test_it_should_throw_exception_when_invalid_format($invalid, $value) + { + $this->setExpectedException( + PhoneNumberException::class, "The phone number format '{$value}' is not a recognized format." + ); + + PhoneNumberFormat::fromString($invalid); + } + + public static function provideUnsupportedFormat() + { + return [ + 'Should not support object' => [new \stdClass(), 'Object'], + 'Should not support array' => [[], 'Array'], + 'Should not support empty string' => ['', ''], + ]; + } + + /** + * @param $countryCode + * @param $string + * @param $expected + * + * @dataProvider provideValidConversionNumber + */ + public function test_should_convert_number_to_correct_format($countryCode, $string, $expected) + { + $this->assertSame($expected, PhoneNumberFormat::fromString($countryCode)->convert($string)); + } + + public static function provideValidConversionNumber() + { + return [ + ['CA', '1234567', '123-4567'], + ]; + } +} diff --git a/tests/Domain/Sale/DomainModel/PhoneNumberTest.php b/tests/Domain/Sale/DomainModel/PhoneNumberTest.php new file mode 100644 index 0000000..9123d8f --- /dev/null +++ b/tests/Domain/Sale/DomainModel/PhoneNumberTest.php @@ -0,0 +1,45 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Sale\DomainModel; + +use Example\Domain\Sale\Exception\PhoneNumberException; + +final class PhoneNumberTest extends \PHPUnit_Framework_TestCase +{ + public function test_it_should_be_created_without_slashes() + { + $number = PhoneNumber::fromString('1234567', 'CA'); + $this->assertSame('123-4567', $number->toString()); + } + + public function test_it_should_be_created_with_slashes() + { + $number = PhoneNumber::fromString('123-4567', 'CA'); + $this->assertSame('123-4567', $number->toString()); + } + + /** + * @param $invalid + * @dataProvider provideInvalidFormat + */ + public function test_it_should_throw_exception_when_invalid_format($invalid) + { + $this->setExpectedException( + PhoneNumberException::class, + "The phone number '{$invalid}' is not a valid format for country 'CA'." + ); + PhoneNumber::fromString($invalid, 'CA'); + } + + public static function provideInvalidFormat() + { + return [ + 'Should not contain alphabetic char' => ['e2121'], + ]; + } +} diff --git a/tests/Infrastructure/ForgedIdGenerator.php b/tests/Infrastructure/ForgedIdGenerator.php new file mode 100644 index 0000000..0286a17 --- /dev/null +++ b/tests/Infrastructure/ForgedIdGenerator.php @@ -0,0 +1,35 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Infrastructure; + +use Example\Domain\Sale\DomainModel\ConfirmationNumberGenerator; +use Example\Domain\Sale\DomainModel\Identity\OrderId; + +final class ForgedIdGenerator implements ConfirmationNumberGenerator +{ + /** + * @var OrderId|null + */ + private $orderId; + + /** + * @param OrderId $id + */ + public function returnsOrderIdOnNextCall(OrderId $id) + { + $this->orderId = $id; + } + + /** + * @return OrderId + */ + public function generateOrderId() + { + return $this->orderId; + } +} diff --git a/tests/Infrastructure/InMemory/OwnerCollectionTest.php b/tests/Infrastructure/InMemory/OwnerCollectionTest.php index 918930f..5e0a5c4 100644 --- a/tests/Infrastructure/InMemory/OwnerCollectionTest.php +++ b/tests/Infrastructure/InMemory/OwnerCollectionTest.php @@ -25,7 +25,7 @@ public function setUp() public function test_it_should_return_owner_with_id() { - $owner = Owner::fakeWithId('id'); + $owner = new Owner(new OwnerId(FullName::fromSingleString('id'))); $this->collection->saveOwner($owner); $this->assertCount(1, $this->collection);