From e3171586095b5d45594ce3f8791eef97a3262fda Mon Sep 17 00:00:00 2001 From: Yannick Voyer Date: Sat, 7 May 2016 10:30:48 -0400 Subject: [PATCH 1/3] Add order by phone * Add basic menu configuration by owner * Add cashier manages the order of the phone customer * On order confirms, the cook can cook the order's meal * On order ready, the delivery boy can deliver meal * When delivered, the money should be registered --- features/bootstrap/ApplicationContext.php | 169 ++++++++++++++++++ features/manage-store.feature | 32 +++- .../Application/MenuService.php | 19 ++ .../Application/RegisterNewMeal.php | 12 ++ .../Application/ReleaseMeal.php | 12 ++ .../Administration/DomainModel/Meal.php | 19 ++ .../Administration/DomainModel/MealId.php | 12 ++ .../DomainModel/MealRepository.php | 20 +++ src/Domain/Common/DomainModel/MealName.php | 16 ++ src/Domain/Common/DomainModel/Money.php | 21 +++ src/Domain/Sale/Application/OrderMeal.php | 12 ++ src/Domain/Sale/Application/OrderService.php | 14 ++ src/Domain/Sale/DomainModel/CustomerName.php | 21 +++ src/Domain/Sale/DomainModel/CustomerType.php | 19 ++ .../InMemory/Sale/MealCollection.php | 25 +++ 15 files changed, 418 insertions(+), 5 deletions(-) create mode 100644 src/Domain/Administration/Application/MenuService.php create mode 100644 src/Domain/Administration/Application/RegisterNewMeal.php create mode 100644 src/Domain/Administration/Application/ReleaseMeal.php create mode 100644 src/Domain/Administration/DomainModel/Meal.php create mode 100644 src/Domain/Administration/DomainModel/MealId.php create mode 100644 src/Domain/Administration/DomainModel/MealRepository.php create mode 100644 src/Domain/Common/DomainModel/MealName.php create mode 100644 src/Domain/Common/DomainModel/Money.php create mode 100644 src/Domain/Sale/Application/OrderMeal.php create mode 100644 src/Domain/Sale/Application/OrderService.php create mode 100644 src/Domain/Sale/DomainModel/CustomerName.php create mode 100644 src/Domain/Sale/DomainModel/CustomerType.php create mode 100644 src/Infrastructure/InMemory/Sale/MealCollection.php diff --git a/features/bootstrap/ApplicationContext.php b/features/bootstrap/ApplicationContext.php index cf24db9..5faa5b4 100644 --- a/features/bootstrap/ApplicationContext.php +++ b/features/bootstrap/ApplicationContext.php @@ -9,15 +9,27 @@ use Behat\Gherkin\Node\TableNode; use Example\Domain\Administration\Application\AdministrationService; use Example\Domain\Administration\Application\HireCandidateCommand; +use Example\Domain\Administration\Application\MenuService; +use Example\Domain\Administration\Application\RegisterNewMeal; +use Example\Domain\Administration\Application\ReleaseMeal; 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\MealName; +use Example\Domain\Common\DomainModel\Money; use Example\Domain\Sale\Application\CashierService; +use Example\Domain\Sale\Application\OrderMeal; +use Example\Domain\Sale\Application\OrderService; use Example\Domain\Sale\Application\WaitressService; +use Example\Domain\Sale\DomainModel\CustomerName; +use Example\Domain\Sale\DomainModel\CustomerType; +use Example\Domain\Sale\DomainModel\Identity\EmployeeId; +use Example\Domain\Sale\DomainModel\Waitress; use Example\Domain\Shipping\Application\CreateDeliveryBoyHandler; use Example\Infrastructure\InMemory\EmployeeCollection; use Example\Infrastructure\InMemory\OwnerCollection; +use Example\Infrastructure\InMemory\Sale\MealCollection; use Example\Infrastructure\Symfony\SymfonyPublisher; use Example\Domain\Sale\Application\CookService; use PHPUnit_Framework_Assert as Assert; @@ -37,11 +49,33 @@ class ApplicationContext implements Context, SnippetAcceptingContext */ private $owners; + /** + * @var MealCollection + */ + private $meals; + /** * @var AdministrationService */ private $administrationService; + /** + * @var MenuService + */ + private $menuService; + + /** + * @var OrderService + */ + private $orderService; + + /** + * The current waitress on post + * + * @var Waitress|null + */ + private $waitress; + /** * Initializes context. * @@ -52,16 +86,19 @@ class ApplicationContext implements Context, SnippetAcceptingContext public function __construct() { $publisher = new SymfonyPublisher(); + $this->meals = new MealCollection(); // Administration context $this->owners = new OwnerCollection(); $this->administrationService = new AdministrationService($this->owners, $publisher); + $this->menuService = new MenuService(); // Sale context $this->employees = new EmployeeCollection(); new CookService($this->employees, $publisher); new CashierService($this->employees, $publisher); new WaitressService($this->employees, $publisher); + $this->orderService = new OrderService(); // Shipping context new CreateDeliveryBoyHandler($this->employees, $publisher); @@ -86,6 +123,33 @@ public function alreadyEmploysAs($ownerName, $employeeName, $role) ->hire(FullName::fromSingleString($employeeName), JobTitle::fromString($role)); } + /** + * @Given :owner has created the meal :mealName with price of :mealPrice each + */ + public function hasCreatedTheMealWithPriceOfEach($owner, $mealName, $mealPrice) + { + $this->menuService->registerMeal( + new RegisterNewMeal( + new OwnerId(FullName::fromSingleString($owner)), + MealName::fromString($mealName), + Money::fromString($mealPrice) + ) + ); + } + + /** + * @Given :owner has released the meal :mealName + */ + public function hasReleasedTheMeal($owner, $mealName) + { + $this->menuService->releaseMeal( + new ReleaseMeal( + new OwnerId(FullName::fromSingleString($owner)), + MealName::fromString($mealName) + ) + ); + } + /** * @Given :applicantName postulate on a job offering */ @@ -93,6 +157,47 @@ public function postulateOnAJobOffering($applicantName) { } + /** + * @Given :waitressName is taking phone call orders + */ + public function isTakingPhoneCallOrders($waitressName) + { + $this->waitress = $this->employees->employeeWithIdentity(new EmployeeId($waitressName)); + } + + /** + * @Given :customerName calls the shop to order the meal :mealName + */ + public function callsTheShopToOrderTheMeal($customerName, $mealName) + { + $meal = $this->meals->mealWithName(MealName::fromString($mealName)); + + $this->orderService->orderMeal( + new OrderMeal( + $this->waitress->getIdentity(), + CustomerType::PhoneCustomer(), + $meal->getIdentity(), + CustomerName::fromString($customerName) + ) + ); + } + + /** + * @Given :customerName phone number is :phoneNumber + */ + public function phoneNumberIs($customerName, $phoneNumber) + { + throw new PendingException(); + } + + /** + * @Given :customerName home address is :address + */ + public function homeAddressIs($customerName, $address) + { + throw new PendingException(); + } + /** * @When :ownerName hires :applicantName as the new :role */ @@ -107,6 +212,38 @@ public function hiresAsTheNew($ownerName, $applicantName, $role) ); } + /** + * @When :waitressName confirms the order with id :orderId at :time + */ + public function confirmsTheOrderWithIdAt($waitressName, $orderId, $time) + { + throw new PendingException(); + } + + /** + * @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 +263,36 @@ 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(); + } } diff --git a/features/manage-store.feature b/features/manage-store.feature index 0f947ce..ac58483 100644 --- a/features/manage-store.feature +++ b/features/manage-store.feature @@ -8,11 +8,14 @@ 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 meal 'All dressed pizza' with price of '10.00$' each + And 'John' has released the meal 'All dressed pizza' Scenario: Hiring new cashier to help customer pay for meals 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 @@ -20,15 +23,15 @@ Feature: Scenario: Hiring new cook to cook meals 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 @@ -36,7 +39,26 @@ Feature: Scenario: Hiring new delivery boy to deliver meals 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 Should be in shipping context + + Scenario: Cashier serves a phone customer who wishes its order to be delivered + Given 'Mark' is taking phone call orders + And 'Billy' calls the shop to order the meal 'All dressed pizza' + And 'Billy' phone number is '555-5555' + # todo address must be in range of delivery + And 'Billy' home address is '1 main street' + When 'Mark' confirms the order with id '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 '10.00$' + And The order '333' should registers a tip of '2.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/MenuService.php b/src/Domain/Administration/Application/MenuService.php new file mode 100644 index 0000000..04b98ca --- /dev/null +++ b/src/Domain/Administration/Application/MenuService.php @@ -0,0 +1,19 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Administration\Application; + +final class MenuService +{ + public function registerMeal(RegisterNewMeal $command) + { + } + + public function releaseMeal(ReleaseMeal $command) + { + } +} diff --git a/src/Domain/Administration/Application/RegisterNewMeal.php b/src/Domain/Administration/Application/RegisterNewMeal.php new file mode 100644 index 0000000..c2543f9 --- /dev/null +++ b/src/Domain/Administration/Application/RegisterNewMeal.php @@ -0,0 +1,12 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Administration\Application; + +final class RegisterNewMeal +{ +} diff --git a/src/Domain/Administration/Application/ReleaseMeal.php b/src/Domain/Administration/Application/ReleaseMeal.php new file mode 100644 index 0000000..1ec0b6e --- /dev/null +++ b/src/Domain/Administration/Application/ReleaseMeal.php @@ -0,0 +1,12 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Administration\Application; + +final class ReleaseMeal +{ +} diff --git a/src/Domain/Administration/DomainModel/Meal.php b/src/Domain/Administration/DomainModel/Meal.php new file mode 100644 index 0000000..8c769a4 --- /dev/null +++ b/src/Domain/Administration/DomainModel/Meal.php @@ -0,0 +1,19 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Administration\DomainModel; + +final class Meal +{ + /** + * @return MealId + */ + public function getIdentity() + { + return new MealId(); + } +} diff --git a/src/Domain/Administration/DomainModel/MealId.php b/src/Domain/Administration/DomainModel/MealId.php new file mode 100644 index 0000000..00dcf65 --- /dev/null +++ b/src/Domain/Administration/DomainModel/MealId.php @@ -0,0 +1,12 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Administration\DomainModel; + +final class MealId +{ +} diff --git a/src/Domain/Administration/DomainModel/MealRepository.php b/src/Domain/Administration/DomainModel/MealRepository.php new file mode 100644 index 0000000..85ae533 --- /dev/null +++ b/src/Domain/Administration/DomainModel/MealRepository.php @@ -0,0 +1,20 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Administration\DomainModel; + +use Example\Domain\Common\DomainModel\MealName; + +interface MealRepository +{ + /** + * @param MealName $name + * + * @return Meal + */ + public function mealWithName(MealName $name); +} diff --git a/src/Domain/Common/DomainModel/MealName.php b/src/Domain/Common/DomainModel/MealName.php new file mode 100644 index 0000000..1835742 --- /dev/null +++ b/src/Domain/Common/DomainModel/MealName.php @@ -0,0 +1,16 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Common\DomainModel; + +final class MealName +{ + public static function fromString($name) + { + return new self(); + } +} diff --git a/src/Domain/Common/DomainModel/Money.php b/src/Domain/Common/DomainModel/Money.php new file mode 100644 index 0000000..ee48de2 --- /dev/null +++ b/src/Domain/Common/DomainModel/Money.php @@ -0,0 +1,21 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Common\DomainModel; + +final class Money +{ + /** + * @param string $string + * + * @return Money + */ + public static function fromString($string) + { + return new self(); + } +} diff --git a/src/Domain/Sale/Application/OrderMeal.php b/src/Domain/Sale/Application/OrderMeal.php new file mode 100644 index 0000000..e54c350 --- /dev/null +++ b/src/Domain/Sale/Application/OrderMeal.php @@ -0,0 +1,12 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Sale\Application; + +final class OrderMeal +{ +} diff --git a/src/Domain/Sale/Application/OrderService.php b/src/Domain/Sale/Application/OrderService.php new file mode 100644 index 0000000..1761ab2 --- /dev/null +++ b/src/Domain/Sale/Application/OrderService.php @@ -0,0 +1,14 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Sale\Application; + +final class OrderService +{ + public function orderMeal(OrderMeal $command) + {} +} 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..89ab91c --- /dev/null +++ b/src/Domain/Sale/DomainModel/CustomerType.php @@ -0,0 +1,19 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Sale\DomainModel; + +final class CustomerType +{ + /** + * @return CustomerType + */ + public static function PhoneCustomer() + { + return new self(); + } +} diff --git a/src/Infrastructure/InMemory/Sale/MealCollection.php b/src/Infrastructure/InMemory/Sale/MealCollection.php new file mode 100644 index 0000000..8783ce1 --- /dev/null +++ b/src/Infrastructure/InMemory/Sale/MealCollection.php @@ -0,0 +1,25 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Infrastructure\InMemory\Sale; + +use Example\Domain\Administration\DomainModel\Meal; +use Example\Domain\Administration\DomainModel\MealRepository; +use Example\Domain\Common\DomainModel\MealName; + +final class MealCollection implements MealRepository +{ + /** + * @param MealName $name + * + * @return Meal + */ + public function mealWithName(MealName $name) + { + throw new \RuntimeException('Method ' . __METHOD__ . ' not implemented yet.'); + } +} From 4bc4d3f7f8eb752b58f381637d6edf7cb3515f68 Mon Sep 17 00:00:00 2001 From: Yannick Voyer Date: Sun, 8 May 2016 12:13:27 -0400 Subject: [PATCH 2/3] Add recipe/meal construction * Add recipe state * Add recipe creation * Add events generation on recipe by Owner * Manage order setup with meals * Create Meal on Recipe released --- features/bootstrap/ApplicationContext.php | 177 ++++++++++++------ features/manage-store.feature | 29 +-- .../Application/AdministrationService.php | 1 - .../Application/MenuService.php | 48 ++++- .../Application/RegisterNewMeal.php | 12 -- .../Application/RegisterNewRecipe.php | 66 +++++++ .../Application/ReleaseMeal.php | 12 -- .../Application/ReleaseRecipe.php | 50 +++++ .../Administration/DomainModel/Candidate.php | 3 + .../DomainModel/Event/RecipeWasCreated.php | 77 ++++++++ .../DomainModel/Event/RecipeWasReleased.php | 50 +++++ .../DomainModel/Event/RecipeWasRetired.php | 32 ++++ .../Administration/DomainModel/MealId.php | 12 -- .../Administration/DomainModel/Owner.php | 89 ++++++++- .../DomainModel/PendingStatus.php | 21 +++ .../Administration/DomainModel/Recipe.php | 127 +++++++++++++ .../{MealRepository.php => RecipeContext.php} | 10 +- .../Administration/DomainModel/RecipeId.php | 43 +++++ .../Administration/DomainModel/RecipeName.php | 49 +++++ .../DomainModel/RecipeStatus.php | 83 ++++++++ .../DomainModel/ReleasedStatus.php | 27 +++ .../{Meal.php => RetiredStatus.php} | 8 +- .../Exception/RecipeTransitionException.php | 24 +++ src/Domain/Common/DomainModel/MealName.php | 16 -- src/Domain/Common/DomainModel/Money.php | 27 ++- src/Domain/Sale/Application/BuyerService.php | 50 +++++ .../Application/CreateMealOnRecipeCreated.php | 49 +++++ src/Domain/Sale/Application/OrderMeal.php | 12 -- src/Domain/Sale/Application/OrderService.php | 97 +++++++++- src/Domain/Sale/DomainModel/Address.php | 21 +++ src/Domain/Sale/DomainModel/Buyer.php | 56 ++++++ src/Domain/Sale/DomainModel/BuyerId.php | 62 ++++++ .../Sale/DomainModel/BuyerRepository.php | 26 +++ src/Domain/Sale/DomainModel/Cashier.php | 5 + .../ConfirmationNumberGenerator.php | 18 ++ src/Domain/Sale/DomainModel/CustomerType.php | 16 +- .../DomainModel/Event/OrderWasCreated.php | 67 +++++++ .../Sale/DomainModel/Identity/MealId.php | 43 +++++ .../Sale/DomainModel/Identity/OrderId.php | 43 +++++ src/Domain/Sale/DomainModel/Meal.php | 42 +++++ .../Sale/DomainModel/MealRepository.php | 27 +++ src/Domain/Sale/DomainModel/Order.php | 86 +++++++++ .../Sale/DomainModel/OrderRepository.php | 27 +++ src/Domain/Sale/DomainModel/PhoneCustomer.php | 26 +++ src/Domain/Sale/DomainModel/PhoneNumber.php | 66 +++++++ .../Sale/DomainModel/PhoneNumberFormat.php | 90 +++++++++ .../Sale/Exception/PhoneNumberException.php | 32 ++++ .../InMemory/Sale/BuyerCollection.php | 44 +++++ .../InMemory/Sale/MealCollection.php | 31 ++- .../InMemory/Sale/OrderCollection.php | 44 +++++ .../Administration/DomainModel/OwnerTest.php | 118 +++++++++++- .../DomainModel/RecipeNameTest.php | 25 +++ tests/Domain/Sale/DomainModel/BuyerTest.php | 23 +++ tests/Domain/Sale/DomainModel/CashierTest.php | 6 + .../DomainModel/PhoneNumberFormatTest.php | 74 ++++++++ .../Sale/DomainModel/PhoneNumberTest.php | 45 +++++ .../Sale/DomainModel/PhoneOrderTest.php | 37 ++++ tests/Infrastructure/ForgedIdGenerator.php | 35 ++++ .../InMemory/OwnerCollectionTest.php | 2 +- 59 files changed, 2359 insertions(+), 179 deletions(-) delete mode 100644 src/Domain/Administration/Application/RegisterNewMeal.php create mode 100644 src/Domain/Administration/Application/RegisterNewRecipe.php delete mode 100644 src/Domain/Administration/Application/ReleaseMeal.php create mode 100644 src/Domain/Administration/Application/ReleaseRecipe.php create mode 100644 src/Domain/Administration/DomainModel/Event/RecipeWasCreated.php create mode 100644 src/Domain/Administration/DomainModel/Event/RecipeWasReleased.php create mode 100644 src/Domain/Administration/DomainModel/Event/RecipeWasRetired.php delete mode 100644 src/Domain/Administration/DomainModel/MealId.php create mode 100644 src/Domain/Administration/DomainModel/PendingStatus.php create mode 100644 src/Domain/Administration/DomainModel/Recipe.php rename src/Domain/Administration/DomainModel/{MealRepository.php => RecipeContext.php} (55%) create mode 100644 src/Domain/Administration/DomainModel/RecipeId.php create mode 100644 src/Domain/Administration/DomainModel/RecipeName.php create mode 100644 src/Domain/Administration/DomainModel/RecipeStatus.php create mode 100644 src/Domain/Administration/DomainModel/ReleasedStatus.php rename src/Domain/Administration/DomainModel/{Meal.php => RetiredStatus.php} (64%) create mode 100644 src/Domain/Administration/Exception/RecipeTransitionException.php delete mode 100644 src/Domain/Common/DomainModel/MealName.php create mode 100644 src/Domain/Sale/Application/BuyerService.php create mode 100644 src/Domain/Sale/Application/CreateMealOnRecipeCreated.php delete mode 100644 src/Domain/Sale/Application/OrderMeal.php create mode 100644 src/Domain/Sale/DomainModel/Address.php create mode 100644 src/Domain/Sale/DomainModel/Buyer.php create mode 100644 src/Domain/Sale/DomainModel/BuyerId.php create mode 100644 src/Domain/Sale/DomainModel/BuyerRepository.php create mode 100644 src/Domain/Sale/DomainModel/ConfirmationNumberGenerator.php create mode 100644 src/Domain/Sale/DomainModel/Event/OrderWasCreated.php create mode 100644 src/Domain/Sale/DomainModel/Identity/MealId.php create mode 100644 src/Domain/Sale/DomainModel/Identity/OrderId.php create mode 100644 src/Domain/Sale/DomainModel/Meal.php create mode 100644 src/Domain/Sale/DomainModel/MealRepository.php create mode 100644 src/Domain/Sale/DomainModel/Order.php create mode 100644 src/Domain/Sale/DomainModel/OrderRepository.php create mode 100644 src/Domain/Sale/DomainModel/PhoneCustomer.php create mode 100644 src/Domain/Sale/DomainModel/PhoneNumber.php create mode 100644 src/Domain/Sale/DomainModel/PhoneNumberFormat.php create mode 100644 src/Domain/Sale/Exception/PhoneNumberException.php create mode 100644 src/Infrastructure/InMemory/Sale/BuyerCollection.php create mode 100644 src/Infrastructure/InMemory/Sale/OrderCollection.php create mode 100644 tests/Domain/Administration/DomainModel/RecipeNameTest.php create mode 100644 tests/Domain/Sale/DomainModel/BuyerTest.php create mode 100644 tests/Domain/Sale/DomainModel/PhoneNumberFormatTest.php create mode 100644 tests/Domain/Sale/DomainModel/PhoneNumberTest.php create mode 100644 tests/Domain/Sale/DomainModel/PhoneOrderTest.php create mode 100644 tests/Infrastructure/ForgedIdGenerator.php diff --git a/features/bootstrap/ApplicationContext.php b/features/bootstrap/ApplicationContext.php index 5faa5b4..fe1bc34 100644 --- a/features/bootstrap/ApplicationContext.php +++ b/features/bootstrap/ApplicationContext.php @@ -7,29 +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\RegisterNewMeal; -use Example\Domain\Administration\Application\ReleaseMeal; +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\MealName; use Example\Domain\Common\DomainModel\Money; +use Example\Domain\Sale\Application\BuyerService; use Example\Domain\Sale\Application\CashierService; -use Example\Domain\Sale\Application\OrderMeal; +use Example\Domain\Sale\Application\CreateMealOnRecipeCreated; use Example\Domain\Sale\Application\OrderService; use Example\Domain\Sale\Application\WaitressService; -use Example\Domain\Sale\DomainModel\CustomerName; +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\Waitress; +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; @@ -50,9 +59,14 @@ class ApplicationContext implements Context, SnippetAcceptingContext private $owners; /** - * @var MealCollection + * @var BuyerRepository */ - private $meals; + private $buyers; + + /** + * @var PhoneNumber[] + */ + private $phones = []; /** * @var AdministrationService @@ -62,7 +76,12 @@ class ApplicationContext implements Context, SnippetAcceptingContext /** * @var MenuService */ - private $menuService; + private $menu; + + /** + * @var BuyerService + */ + private $buyerService; /** * @var OrderService @@ -70,11 +89,19 @@ class ApplicationContext implements Context, SnippetAcceptingContext private $orderService; /** - * The current waitress on post - * - * @var Waitress|null + * @var OrderCollection + */ + private $orders; + + /** + * @var MealCollection */ - private $waitress; + private $meals; + + /** + * @var ForgedIdGenerator + */ + private $fakeIdGenerator; /** * Initializes context. @@ -85,20 +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->menuService = new MenuService(); + $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->orderService = new OrderService( + $this->orders, $publisher, $this->buyers, $this->meals, $this->fakeIdGenerator + ); + new CreateMealOnRecipeCreated($this->meals, $publisher); // Shipping context new CreateDeliveryBoyHandler($this->employees, $publisher); @@ -119,33 +154,38 @@ 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 meal :mealName with price of :mealPrice each + * @Given :owner has created the recipe :recipeName with price of :recipePrice each */ - public function hasCreatedTheMealWithPriceOfEach($owner, $mealName, $mealPrice) + public function hasCreatedTheRecipeWithPriceOfEach($owner, $recipeName, $recipePrice) { - $this->menuService->registerMeal( - new RegisterNewMeal( + $this->menu->registerRecipe( + new RegisterNewRecipe( new OwnerId(FullName::fromSingleString($owner)), - MealName::fromString($mealName), - Money::fromString($mealPrice) + RecipeName::fromString($recipeName), + Money::fromInt($recipePrice) ) ); } /** - * @Given :owner has released the meal :mealName + * @Given :owner has released the recipe :recipeName */ - public function hasReleasedTheMeal($owner, $mealName) + public function hasReleasedTheRecipe($owner, $recipeName) { - $this->menuService->releaseMeal( - new ReleaseMeal( + $this->menu->releaseRecipe( + new ReleaseRecipe( new OwnerId(FullName::fromSingleString($owner)), - MealName::fromString($mealName) + new RecipeId(RecipeName::fromString($recipeName)) ) ); } @@ -158,66 +198,69 @@ public function postulateOnAJobOffering($applicantName) } /** - * @Given :waitressName is taking phone call orders + * @Given :customerName never ordered meals to the shop */ - public function isTakingPhoneCallOrders($waitressName) + public function neverOrderedMealsToTheShop($customerName) { - $this->waitress = $this->employees->employeeWithIdentity(new EmployeeId($waitressName)); } /** - * @Given :customerName calls the shop to order the meal :mealName + * @Given :customerName gives his phone number :phoneNumber and home address :address */ - public function callsTheShopToOrderTheMeal($customerName, $mealName) + public function givesHisPhoneNumberAndHomeAddress($customerName, $phoneNumber, $address) { - $meal = $this->meals->mealWithName(MealName::fromString($mealName)); - - $this->orderService->orderMeal( - new OrderMeal( - $this->waitress->getIdentity(), - CustomerType::PhoneCustomer(), - $meal->getIdentity(), - CustomerName::fromString($customerName) - ) - ); + $this->phones[$customerName] = PhoneNumber::fromString($phoneNumber, 'CA'); + $this->buyerService->registerPhoneBuyer($phoneNumber, $address); } /** - * @Given :customerName phone number is :phoneNumber + * @When :ownerName hires :applicantName as the new :role */ - public function phoneNumberIs($customerName, $phoneNumber) + public function hiresAsTheNew($ownerName, $applicantName, $role) { - throw new PendingException(); + $this->administrationService->hireCandidate( + new HireCandidateCommand( + new OwnerId(FullName::fromSingleString($ownerName)), + FullName::fromSingleString($applicantName), + JobTitle::fromString($role) + ) + ); } /** - * @Given :customerName home address is :address + * @When :waitressName starts the order :orderId of :customerName */ - public function homeAddressIs($customerName, $address) + public function startsTheOrderOf($waitressName, $orderId, $customerName) { - throw new PendingException(); + $this->fakeIdGenerator->returnsOrderIdOnNextCall(new OrderId($orderId)); + $this->orderService->startOrder( + EmployeeId::fromName(FullName::fromSingleString($waitressName)), + CustomerType::PhoneCustomer(), + BuyerId::phoneBuyer($this->getPhoneNumberOfCustomer($customerName)) + ); } /** - * @When :ownerName hires :applicantName as the new :role + * @When :customerName order :quantity meal :mealName on order :orderId */ - public function hiresAsTheNew($ownerName, $applicantName, $role) + public function orderMealOnOrder($customerName, $quantity, $mealName, $orderId) { - $this->administrationService->hireCandidate( - new HireCandidateCommand( - new OwnerId(FullName::fromSingleString($ownerName)), - FullName::fromSingleString($applicantName), - JobTitle::fromString($role) - ) + $this->orderService->orderMeal( + new OrderId($orderId), + $quantity, + new MealId(Transliterator::transliterate($mealName)) ); } /** - * @When :waitressName confirms the order with id :orderId at :time + * @When :waitressName confirms that :customerName order confirmation number is :orderId at :time */ - public function confirmsTheOrderWithIdAt($waitressName, $orderId, $time) + public function confirmsThatOrderConfirmationNumberIsAt($waitressName, $customerName, $orderId, $time) { - throw new PendingException(); + $this->orderService->confirmOrder( + new OrderId($orderId), + new \DateTime($time) + ); } /** @@ -295,4 +338,14 @@ 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 ac58483..dbc376a 100644 --- a/features/manage-store.feature +++ b/features/manage-store.feature @@ -9,10 +9,12 @@ Feature: 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 meal 'All dressed pizza' with price of '10.00$' each - And 'John' has released the meal 'All dressed pizza' + 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 5 employees @@ -20,7 +22,7 @@ Feature: # 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 5 employees @@ -36,27 +38,28 @@ Feature: # 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 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 - Given 'Mark' is taking phone call orders - And 'Billy' calls the shop to order the meal 'All dressed pizza' - And 'Billy' phone number is '555-5555' # todo address must be in range of delivery - And 'Billy' home address is '1 main street' - When 'Mark' confirms the order with id '333' at '18:00:00' + 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 '10.00$' - And The order '333' should registers a tip of '2.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 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 index 04b98ca..2a1e447 100644 --- a/src/Domain/Administration/Application/MenuService.php +++ b/src/Domain/Administration/Application/MenuService.php @@ -7,13 +7,57 @@ 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 { - public function registerMeal(RegisterNewMeal $command) + /** + * @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()); } - public function releaseMeal(ReleaseMeal $command) + /** + * @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/RegisterNewMeal.php b/src/Domain/Administration/Application/RegisterNewMeal.php deleted file mode 100644 index c2543f9..0000000 --- a/src/Domain/Administration/Application/RegisterNewMeal.php +++ /dev/null @@ -1,12 +0,0 @@ - (http://github.com/yvoyer) - */ - -namespace Example\Domain\Administration\Application; - -final class RegisterNewMeal -{ -} 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/ReleaseMeal.php b/src/Domain/Administration/Application/ReleaseMeal.php deleted file mode 100644 index 1ec0b6e..0000000 --- a/src/Domain/Administration/Application/ReleaseMeal.php +++ /dev/null @@ -1,12 +0,0 @@ - (http://github.com/yvoyer) - */ - -namespace Example\Domain\Administration\Application; - -final class ReleaseMeal -{ -} 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/MealId.php b/src/Domain/Administration/DomainModel/MealId.php deleted file mode 100644 index 00dcf65..0000000 --- a/src/Domain/Administration/DomainModel/MealId.php +++ /dev/null @@ -1,12 +0,0 @@ - (http://github.com/yvoyer) - */ - -namespace Example\Domain\Administration\DomainModel; - -final class MealId -{ -} 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/MealRepository.php b/src/Domain/Administration/DomainModel/RecipeContext.php similarity index 55% rename from src/Domain/Administration/DomainModel/MealRepository.php rename to src/Domain/Administration/DomainModel/RecipeContext.php index 85ae533..9c2fb35 100644 --- a/src/Domain/Administration/DomainModel/MealRepository.php +++ b/src/Domain/Administration/DomainModel/RecipeContext.php @@ -7,14 +7,12 @@ namespace Example\Domain\Administration\DomainModel; -use Example\Domain\Common\DomainModel\MealName; - -interface MealRepository +interface RecipeContext { /** - * @param MealName $name + * @param RecipeStatus $status * - * @return Meal + * @internal Used by state machine only */ - public function mealWithName(MealName $name); + 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/Meal.php b/src/Domain/Administration/DomainModel/RetiredStatus.php similarity index 64% rename from src/Domain/Administration/DomainModel/Meal.php rename to src/Domain/Administration/DomainModel/RetiredStatus.php index 8c769a4..245c68c 100644 --- a/src/Domain/Administration/DomainModel/Meal.php +++ b/src/Domain/Administration/DomainModel/RetiredStatus.php @@ -7,13 +7,13 @@ namespace Example\Domain\Administration\DomainModel; -final class Meal +final class RetiredStatus extends RecipeStatus { /** - * @return MealId + * @return bool */ - public function getIdentity() + public function isRetired() { - return new MealId(); + 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/MealName.php b/src/Domain/Common/DomainModel/MealName.php deleted file mode 100644 index 1835742..0000000 --- a/src/Domain/Common/DomainModel/MealName.php +++ /dev/null @@ -1,16 +0,0 @@ - (http://github.com/yvoyer) - */ - -namespace Example\Domain\Common\DomainModel; - -final class MealName -{ - public static function fromString($name) - { - return new self(); - } -} diff --git a/src/Domain/Common/DomainModel/Money.php b/src/Domain/Common/DomainModel/Money.php index ee48de2..68cc891 100644 --- a/src/Domain/Common/DomainModel/Money.php +++ b/src/Domain/Common/DomainModel/Money.php @@ -10,12 +10,33 @@ final class Money { /** - * @param string $string + * @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 fromString($string) + public static function fromInt($int) { - return new self(); + 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..6a08523 --- /dev/null +++ b/src/Domain/Sale/Application/BuyerService.php @@ -0,0 +1,50 @@ + (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\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 = Buyer::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/OrderMeal.php b/src/Domain/Sale/Application/OrderMeal.php deleted file mode 100644 index e54c350..0000000 --- a/src/Domain/Sale/Application/OrderMeal.php +++ /dev/null @@ -1,12 +0,0 @@ - (http://github.com/yvoyer) - */ - -namespace Example\Domain\Sale\Application; - -final class OrderMeal -{ -} diff --git a/src/Domain/Sale/Application/OrderService.php b/src/Domain/Sale/Application/OrderService.php index 1761ab2..484e338 100644 --- a/src/Domain/Sale/Application/OrderService.php +++ b/src/Domain/Sale/Application/OrderService.php @@ -7,8 +7,101 @@ 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 { - public function orderMeal(OrderMeal $command) - {} + /** + * @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()); + } + + public function confirmOrder(OrderId $orderId, \DateTime $time) + { + throw new \RuntimeException(__METHOD__ . ' is not implemented yet.'); + } } 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..a9e2112 --- /dev/null +++ b/src/Domain/Sale/DomainModel/Buyer.php @@ -0,0 +1,56 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Sale\DomainModel; + +final class Buyer +{ + /** + * @var string + */ + private $phone_number; + + /** + * @var string + */ + private $phone_country; + + /** + * @var string + */ + private $address; + + /** + * @param PhoneNumber $number + * @param Address $address + */ + private 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)); + } + + /** + * @param PhoneNumber $number + * @param Address $address + * + * @return Buyer + */ + public static function PhoneBuyer(PhoneNumber $number, Address $address) + { + return new self($number, $address); + } +} 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/CustomerType.php b/src/Domain/Sale/DomainModel/CustomerType.php index 89ab91c..39a9727 100644 --- a/src/Domain/Sale/DomainModel/CustomerType.php +++ b/src/Domain/Sale/DomainModel/CustomerType.php @@ -7,13 +7,25 @@ namespace Example\Domain\Sale\DomainModel; -final class CustomerType +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 self(); + return new PhoneCustomer(); } } diff --git a/src/Domain/Sale/DomainModel/Event/OrderWasCreated.php b/src/Domain/Sale/DomainModel/Event/OrderWasCreated.php new file mode 100644 index 0000000..bc41955 --- /dev/null +++ b/src/Domain/Sale/DomainModel/Event/OrderWasCreated.php @@ -0,0 +1,67 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Sale\DomainModel\Event; + +use Example\Domain\Common\DomainModel\Event\DomainEvent; +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 BuyerId + */ + private $buyerId; + + /** + * @param OrderId $id + * @param EmployeeId $employeeId + * @param BuyerId $buyerId + */ + public function __construct(OrderId $id, EmployeeId $employeeId, BuyerId $buyerId) + { + $this->orderId = $id; + $this->employeeId = $employeeId; + $this->buyerId = $buyerId; + } + + /** + * @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->buyerId; + } +} 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..4cec35b --- /dev/null +++ b/src/Domain/Sale/DomainModel/Order.php @@ -0,0 +1,86 @@ + (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\OrderWasCreated; +use Example\Domain\Sale\DomainModel\Identity\EmployeeId; +use Example\Domain\Sale\DomainModel\Identity\OrderId; + +final class Order extends AggregateRoot +{ + /** + * @var mixed + */ + private $id; + + /** + * @var mixed + */ + private $employeeId; + + /** + * @var Buyer + */ + private $buyer; + + /** + * @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); + } + + /** + * @param MEal $meal + */ + public function orderMeal(MEal $meal) + { + + } + + /** + * @param OrderWasCreated $event + */ + protected function onOrderWasCreated(OrderWasCreated $event) + { + $this->id = $event->orderId()->id(); + $this->employeeId = $event->employeeId()->id(); + } + + /** + * @param OrderId $id + * @param EmployeeId $employeeId + * @param Buyer $buyer + * + * @return Order + */ + public static function PhoneOrder(OrderId $id, EmployeeId $employeeId, Buyer $buyer) + { + $order = new self( + [ + new OrderWasCreated($id, $employeeId, $buyer->getIdentity()), + ] + ); + $order->buyer = $buyer; + + return $order; + } +} 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/PhoneCustomer.php b/src/Domain/Sale/DomainModel/PhoneCustomer.php new file mode 100644 index 0000000..88df9a3 --- /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::PhoneOrder($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 index 8783ce1..fae8b26 100644 --- a/src/Infrastructure/InMemory/Sale/MealCollection.php +++ b/src/Infrastructure/InMemory/Sale/MealCollection.php @@ -7,19 +7,38 @@ namespace Example\Infrastructure\InMemory\Sale; -use Example\Domain\Administration\DomainModel\Meal; -use Example\Domain\Administration\DomainModel\MealRepository; -use Example\Domain\Common\DomainModel\MealName; +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 { /** - * @param MealName $name + * @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 mealWithName(MealName $name) + public function activeMeal(MealId $id) { - throw new \RuntimeException('Method ' . __METHOD__ . ' not implemented yet.'); + 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..1d853b3 --- /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 = Buyer::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/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/Domain/Sale/DomainModel/PhoneOrderTest.php b/tests/Domain/Sale/DomainModel/PhoneOrderTest.php new file mode 100644 index 0000000..46d792d --- /dev/null +++ b/tests/Domain/Sale/DomainModel/PhoneOrderTest.php @@ -0,0 +1,37 @@ + (http://github.com/yvoyer) + */ + +namespace Example\Domain\Sale\DomainModel; + +use Example\Domain\Sale\DomainModel\Event\OrderWasCreated; +use Example\Domain\Sale\DomainModel\Identity\EmployeeId; +use Example\Domain\Sale\DomainModel\Identity\OrderId; + +final class PhoneOrderTest extends \PHPUnit_Framework_TestCase +{ + public function test_it_should_create_a_phone_order_with_id() + { + $order = Order::PhoneOrder( + new OrderId(123), + new EmployeeId('employee'), + Buyer::PhoneBuyer(PhoneNumber::fromString('555-5555', 'CA'), Address::fromString('')) + ); + $this->assertInstanceOf(Order::class, $order); + $events = $order->uncommitedEvents(); + $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('555-5555', $event->buyerId()->id()); + } +} 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); From 765ded98ba38e175d3d39882f181549abcebbd94 Mon Sep 17 00:00:00 2001 From: Yannick Voyer Date: Wed, 25 Jan 2017 22:49:19 -0500 Subject: [PATCH 3/3] * Rework Buyer to use interface * Add confirmation process on Order --- src/Domain/Sale/Application/BuyerService.php | 3 +- src/Domain/Sale/Application/OrderService.php | 11 ++ src/Domain/Sale/DomainModel/Buyer.php | 44 +------ .../Sale/DomainModel/Event/MealWasOrdered.php | 60 ++++++++++ .../DomainModel/Event/OrderWasConfirmed.php | 35 ++++++ .../DomainModel/Event/OrderWasCreated.php | 23 +++- .../Sale/DomainModel/Identity/EmployeeId.php | 10 ++ src/Domain/Sale/DomainModel/Order.php | 108 +++++++++++++++-- .../Sale/DomainModel/OrderException.php | 12 ++ src/Domain/Sale/DomainModel/PhoneBuyer.php | 45 +++++++ src/Domain/Sale/DomainModel/PhoneCustomer.php | 2 +- tests/Domain/Sale/DomainModel/BuyerTest.php | 2 +- tests/Domain/Sale/DomainModel/OrderTest.php | 112 ++++++++++++++++++ .../Sale/DomainModel/PhoneOrderTest.php | 37 ------ 14 files changed, 409 insertions(+), 95 deletions(-) create mode 100644 src/Domain/Sale/DomainModel/Event/MealWasOrdered.php create mode 100644 src/Domain/Sale/DomainModel/Event/OrderWasConfirmed.php create mode 100644 src/Domain/Sale/DomainModel/OrderException.php create mode 100644 src/Domain/Sale/DomainModel/PhoneBuyer.php create mode 100644 tests/Domain/Sale/DomainModel/OrderTest.php delete mode 100644 tests/Domain/Sale/DomainModel/PhoneOrderTest.php diff --git a/src/Domain/Sale/Application/BuyerService.php b/src/Domain/Sale/Application/BuyerService.php index 6a08523..8aa9216 100644 --- a/src/Domain/Sale/Application/BuyerService.php +++ b/src/Domain/Sale/Application/BuyerService.php @@ -10,6 +10,7 @@ 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 @@ -40,7 +41,7 @@ public function __construct(BuyerRepository $buyers, $countryCode) */ public function registerPhoneBuyer($phoneNumber, $address) { - $buyer = Buyer::PhoneBuyer( + $buyer = new PhoneBuyer( PhoneNumber::fromString($phoneNumber, $this->countryCode), Address::fromString($address) ); diff --git a/src/Domain/Sale/Application/OrderService.php b/src/Domain/Sale/Application/OrderService.php index 484e338..124d02b 100644 --- a/src/Domain/Sale/Application/OrderService.php +++ b/src/Domain/Sale/Application/OrderService.php @@ -100,8 +100,19 @@ public function orderMeal(OrderId $orderId, $quantity, MealId $mealId) $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/Buyer.php b/src/Domain/Sale/DomainModel/Buyer.php index a9e2112..1fbf088 100644 --- a/src/Domain/Sale/DomainModel/Buyer.php +++ b/src/Domain/Sale/DomainModel/Buyer.php @@ -7,50 +7,10 @@ namespace Example\Domain\Sale\DomainModel; -final class Buyer +interface Buyer { - /** - * @var string - */ - private $phone_number; - - /** - * @var string - */ - private $phone_country; - - /** - * @var string - */ - private $address; - - /** - * @param PhoneNumber $number - * @param Address $address - */ - private 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)); - } - - /** - * @param PhoneNumber $number - * @param Address $address - * - * @return Buyer - */ - public static function PhoneBuyer(PhoneNumber $number, Address $address) - { - return new self($number, $address); - } + public function getIdentity(); } 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 index bc41955..d638665 100644 --- a/src/Domain/Sale/DomainModel/Event/OrderWasCreated.php +++ b/src/Domain/Sale/DomainModel/Event/OrderWasCreated.php @@ -8,6 +8,7 @@ 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; @@ -25,20 +26,20 @@ final class OrderWasCreated implements DomainEvent private $employeeId; /** - * @var BuyerId + * @var Buyer */ - private $buyerId; + private $buyer; /** * @param OrderId $id * @param EmployeeId $employeeId - * @param BuyerId $buyerId + * @param Buyer $buyer */ - public function __construct(OrderId $id, EmployeeId $employeeId, BuyerId $buyerId) + public function __construct(OrderId $id, EmployeeId $employeeId, Buyer $buyer) { $this->orderId = $id; $this->employeeId = $employeeId; - $this->buyerId = $buyerId; + $this->buyer = $buyer; } /** @@ -62,6 +63,16 @@ public function employeeId() */ public function buyerId() { - return $this->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/Order.php b/src/Domain/Sale/DomainModel/Order.php index 4cec35b..0c5acc1 100644 --- a/src/Domain/Sale/DomainModel/Order.php +++ b/src/Domain/Sale/DomainModel/Order.php @@ -9,8 +9,11 @@ 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 @@ -30,6 +33,16 @@ final class Order extends AggregateRoot */ private $buyer; + /** + * @var Meal[] + */ + private $meals = []; + + /** + * @var bool + */ + private $isConfirmed = false; + /** * @param DomainEvent[] $events */ @@ -49,11 +62,56 @@ public function getIdentity() } /** - * @param MEal $meal + * @return BuyerId + */ + public function buyerId() + { + return $this->buyer->getIdentity(); + } + + /** + * @return EmployeeId */ - public function orderMeal(MEal $meal) + 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(); } /** @@ -63,24 +121,60 @@ 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 $employeeId + * @param EmployeeId $takenBy * @param Buyer $buyer * * @return Order */ - public static function PhoneOrder(OrderId $id, EmployeeId $employeeId, Buyer $buyer) + public static function PendingOrder(OrderId $id, EmployeeId $takenBy, Buyer $buyer) { - $order = new self( + return new self( [ - new OrderWasCreated($id, $employeeId, $buyer->getIdentity()), + new OrderWasCreated($id, $takenBy, $buyer), ] ); - $order->buyer = $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/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 index 88df9a3..e7371ac 100644 --- a/src/Domain/Sale/DomainModel/PhoneCustomer.php +++ b/src/Domain/Sale/DomainModel/PhoneCustomer.php @@ -21,6 +21,6 @@ final class PhoneCustomer extends CustomerType */ public function startOrder(OrderId $orderId, EmployeeId $employeeId, Buyer $buyer) { - return Order::PhoneOrder($orderId, $employeeId, $buyer); + return Order::PendingOrder($orderId, $employeeId, $buyer); } } diff --git a/tests/Domain/Sale/DomainModel/BuyerTest.php b/tests/Domain/Sale/DomainModel/BuyerTest.php index 1d853b3..1650db8 100644 --- a/tests/Domain/Sale/DomainModel/BuyerTest.php +++ b/tests/Domain/Sale/DomainModel/BuyerTest.php @@ -11,7 +11,7 @@ final class BuyerTest extends \PHPUnit_Framework_TestCase { public function test_it_should_be_created_from_phone() { - $buyer = Buyer::PhoneBuyer( + $buyer = new PhoneBuyer( PhoneNumber::fromString('5555555', 'CA'), Address::fromString('1 main street') ); 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/PhoneOrderTest.php b/tests/Domain/Sale/DomainModel/PhoneOrderTest.php deleted file mode 100644 index 46d792d..0000000 --- a/tests/Domain/Sale/DomainModel/PhoneOrderTest.php +++ /dev/null @@ -1,37 +0,0 @@ - (http://github.com/yvoyer) - */ - -namespace Example\Domain\Sale\DomainModel; - -use Example\Domain\Sale\DomainModel\Event\OrderWasCreated; -use Example\Domain\Sale\DomainModel\Identity\EmployeeId; -use Example\Domain\Sale\DomainModel\Identity\OrderId; - -final class PhoneOrderTest extends \PHPUnit_Framework_TestCase -{ - public function test_it_should_create_a_phone_order_with_id() - { - $order = Order::PhoneOrder( - new OrderId(123), - new EmployeeId('employee'), - Buyer::PhoneBuyer(PhoneNumber::fromString('555-5555', 'CA'), Address::fromString('')) - ); - $this->assertInstanceOf(Order::class, $order); - $events = $order->uncommitedEvents(); - $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('555-5555', $event->buyerId()->id()); - } -}