You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by da...@apache.org on 2021/10/07 22:39:34 UTC

[isis] branch 2.0.0-M6 updated: ISIS-2873: removes reference to incomplete exercises

This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch 2.0.0-M6
in repository https://gitbox.apache.org/repos/asf/isis.git


The following commit(s) were added to refs/heads/2.0.0-M6 by this push:
     new b29f2d9  ISIS-2873: removes reference to incomplete exercises
b29f2d9 is described below

commit b29f2d96159a6469e5a94be3b03b2bd0d1e89ff9
Author: danhaywood <da...@haywood-associates.co.uk>
AuthorDate: Thu Oct 7 23:39:13 2021 +0100

    ISIS-2873: removes reference to incomplete exercises
    
    and fixes typo, makes title shorter
---
 .../tutorials/modules/petclinic/nav.adoc           |  20 +-
 .../petclinic/pages/010-getting-started.adoc       |  10 +-
 .../petclinic/pages/020-the-petclinic-domain.adoc  |   2 +-
 .../petclinic/pages/030-petowner-entity.adoc       |  22 +-
 .../modules/petclinic/pages/040-pet-entity.adoc    |  20 +-
 .../modules/petclinic/pages/050-visit-entity.adoc  |   6 +-
 .../modules/petclinic/pages/060-unit-testing.adoc  |   2 +-
 .../modules/petclinic/pages/070-modularity.adoc    |   2 +-
 .../modules/petclinic/pages/080-view-models.adoc   |   6 +-
 .../petclinic/pages/090-integration-testing.adoc   |   3 +-
 ...ing-further-business-logic-worked-examples.adoc | 707 --------------------
 .../petclinic/pages/architecture-rules.adoc        |   4 +-
 ...cture-rules.adoc => commands-and-auditing.adoc} |   4 +-
 .../further-business-logic-worked-examples.adoc    | 708 +++++++++++++++++++++
 .../tutorials/modules/petclinic/pages/i18n.adoc    |   2 +-
 .../{architecture-rules.adoc => restful-api.adoc}  |   4 +-
 16 files changed, 756 insertions(+), 766 deletions(-)

diff --git a/antora/components/tutorials/modules/petclinic/nav.adoc b/antora/components/tutorials/modules/petclinic/nav.adoc
index f5996b2..aefe758 100644
--- a/antora/components/tutorials/modules/petclinic/nav.adoc
+++ b/antora/components/tutorials/modules/petclinic/nav.adoc
@@ -55,21 +55,9 @@
 ** xref:080-view-models.adoc#exercise-8-3-using-a-view-model-as-a-projection-of-an-entity[image:hand.png[] *8.3*: Using a view model as a projection of an entity]
 
 
-* xref:090-integration-testing.adoc[(Integration) Testing]
-** xref:090-integration-testing.adoc#_an_improved_fixture_script[image:hand.png[] *270*: An improved Fixture Script]
-** xref:090-integration-testing.adoc#_writing_integration_tests[image:hand.png[] *280*: Writing Integration Tests]
-** xref:090-integration-testing.adoc#_factor_out_abstract_integration_test[image:hand.png[] *290*: Factor out abstract integration test]
-** xref:090-integration-testing.adoc#_move_teardowns_to_modules[image:hand.png[] *300*: Move teardowns to modules]
-** xref:090-integration-testing.adoc#_fake_data_service[image:hand.png[] *310*: Fake Data Service]
-** xref:090-integration-testing.adoc#_extend_the_fixture_script_to_set_up_visits[image:hand.png[] *320*: Extend the Fixture script to set up visits]
-
-* xref:110-adding-further-business-logic-worked-examples.adoc[Further business logic]
-** xref:110-adding-further-business-logic-worked-examples.adoc#_enter_an_outcome[image:hand.png[] *330*: Enter an outcome]
-** xref:110-adding-further-business-logic-worked-examples.adoc#_pay_for_a_visit[image:hand.png[] *340*: Pay for a visit]
-** xref:110-adding-further-business-logic-worked-examples.adoc#_prevent_payment_for_a_visit_twice[image:hand.png[] *350*: Prevent payment for a visit twice ("see it? use it? do it?")]
-** xref:110-adding-further-business-logic-worked-examples.adoc#_find_code_visit_code_s_not_yet_paid_and_overdue[image:hand.png[] *360*: Find Visits not yet paid and overdue]
-** xref:110-adding-further-business-logic-worked-examples.adoc#_digression_hiding_columns_in_tables[image:hand.png[] *370*: Digression: Hiding Columns in Tables]
-** xref:110-adding-further-business-logic-worked-examples.adoc#_another_digression_icons_and_css[image:hand.png[] *380*: Another Digression: Icons and CSS]
-** xref:110-adding-further-business-logic-worked-examples.adoc#_delete_an_code_owner_code_provided_no_unpaid_code_visit_code_s[image:hand.png[] *390*: Delete an Owner provided no unpaid Visits]
+* xref:090-integration-testing.adoc[Integration Testing]
+** xref:090-integration-testing.adoc#exercise-9-1-testing-bookvisit-using-an-integtest[image:hand.png[] *9.1*: Testing bookVisit using an integtest]
+
+
 
 //* xref:i18n.adoc[i18n]
diff --git a/antora/components/tutorials/modules/petclinic/pages/010-getting-started.adoc b/antora/components/tutorials/modules/petclinic/pages/010-getting-started.adoc
index b84deea..7f3fe66 100644
--- a/antora/components/tutorials/modules/petclinic/pages/010-getting-started.adoc
+++ b/antora/components/tutorials/modules/petclinic/pages/010-getting-started.adoc
@@ -30,7 +30,7 @@ For this tutorial, make sure that your IDE is configured to support Lombok.
 
 
 [#exercise-1-1-starter-apps-clone-the-repo]
-== Exercise 1.1: Starter apps / clone the repo
+== Ex 1.1: Starter apps / clone the repo
 
 Apache Isis provides a two starter apps, xref:docs:starters:helloworld.adoc[HelloWorld] and xref:docs:starters:simpleapp.adoc[SimpleApp].
 These are identical in terms of functionality, but the simpleapp provides more structure and includes example tests.
@@ -71,7 +71,7 @@ mvn -pl webapp spring-boot:run
 
 
 [#exercise-1-2-explore-the-simple-app]
-== Exercise 1.2: Explore the Simple App
+== Ex 1.2: Explore the Simple App
 
 Although we'll be refactoring the codebase in the next exercise, take a few minutes to familiarize yourself with the functionality of the simpleapp.
 
@@ -87,7 +87,7 @@ This will create some sample data.
 
 
 [#exercise-1-3-running-from-the-ide]
-== Exercise 1.3: Running from the IDE
+== Ex 1.3: Running from the IDE
 
 Running from the command line isn't ideal, so
 
@@ -104,7 +104,7 @@ If you want to go deeper, open up the xref:docs:starters:simpleapp.adoc[page des
 
 
 [#exercise-1-4-naked-objects-pattern]
-== Exercise 1.4: Naked Objects pattern
+== Ex 1.4: Naked Objects pattern
 
 Apache Isis is an implementation of the _naked objects pattern_, which means that entities (and later, as we'll see view models) are automatically exposed in the UI.
 
@@ -140,7 +140,7 @@ See the xref:refguide:applib-svc:about.adoc[Reference Guide: Domain Services] do
 
 
 [#exercise-1-5-ui-hints]
-== Exercise 1.5: UI Hints
+== Ex 1.5: UI Hints
 
 The framework derives as much of the UI as possible from the domain objects' intrinsic structure and behaviour, but there are some supporting structures and conventions that are there primarily to improve the UI.
 
diff --git a/antora/components/tutorials/modules/petclinic/pages/020-the-petclinic-domain.adoc b/antora/components/tutorials/modules/petclinic/pages/020-the-petclinic-domain.adoc
index 388a185..cc89c84 100644
--- a/antora/components/tutorials/modules/petclinic/pages/020-the-petclinic-domain.adoc
+++ b/antora/components/tutorials/modules/petclinic/pages/020-the-petclinic-domain.adoc
@@ -29,7 +29,7 @@ Some of the use cases we might want to support include:
 This tutorial has worked solutions for all of these.
 
 [#exercise-2-1-refactor-simpleobject-to-petowner]
-== Exercise 2.1: Refactor `SimpleObject` to `PetOwner`
+== Ex 2.1: Refactor `SimpleObject` to `PetOwner`
 
 To start with, let's rename the `SimpleObject` entity to `PetOwner`
 
diff --git a/antora/components/tutorials/modules/petclinic/pages/030-petowner-entity.adoc b/antora/components/tutorials/modules/petclinic/pages/030-petowner-entity.adoc
index 06e438c..4274c61 100644
--- a/antora/components/tutorials/modules/petclinic/pages/030-petowner-entity.adoc
+++ b/antora/components/tutorials/modules/petclinic/pages/030-petowner-entity.adoc
@@ -6,7 +6,7 @@
 In this set of exercises we'll just focus on the `PetOwner` entity.
 
 [#exercise-3-1-rename-petowners-name-property]
-== Exercise 3.1: Rename PetOwner's name property
+== Ex 3.1: Rename PetOwner's name property
 
 In the domain we are working on, `PetOwner` has a `firstName` and a `lastName` property, not a single `name` property.
 
@@ -57,7 +57,7 @@ Build and run the application to make sure it still runs fine.
 
 
 [#exercise-3-2-add-petowners-firstname-property]
-== Exercise 3.2: Add PetOwner's firstName property
+== Ex 3.2: Add PetOwner's firstName property
 
 Now that `PetOwner` has a `lastName` property, let's also add a `firstName` property.
 We'll also update our fixture script (which sets up ``PetOwner``s) so that it is more descriptive.
@@ -141,7 +141,7 @@ Learn more about fixture scripts xref:testing:fixtures:about.adoc[here].
 
 
 [#exercise-3-3-modify-petowners-updatename-action]
-== Exercise 3.3: Modify PetOwner's updateName action
+== Ex 3.3: Modify PetOwner's updateName action
 
 Although we've added a `firstName` property, currently it can't be edited.
 In this exercise we'll modify the `updateName` action to also allow the `firstName` to be changed.
@@ -197,7 +197,7 @@ The "default" supporting methods are called when the action prompt is rendered,
 
 
 [#exercise-3-4-modify-the-menu-action-to-create-petowners]
-== Exercise 3.4: Modify the menu action to create PetOwners
+== Ex 3.4: Modify the menu action to create PetOwners
 
 If we want to create a new `PetOwner` and provide their `firstName`, at the moment it's a two stage process: create the `PetOwner` (using `PetOwners#create` action from the menu), then update their name (using the `updateName` action that we just looked at).
 
@@ -240,7 +240,7 @@ Or, even better would be to introduce some sort of "customerNumber" and use this
 
 
 [#exercise-3-5-initial-fixture-script]
-== Exercise 3.5: Initial Fixture Script
+== Ex 3.5: Initial Fixture Script
 
 As we prototype with an in-memory database, it means that we need to setup the database each time we restart the application.
 Using the menu:Prototyping[Fixture Scripts] menu to setup data saves some time, but it would nicer still if that script could be run automatically.
@@ -285,7 +285,7 @@ When you run the application you should now find that there are 10 `PetOwner` ob
 
 
 [#exercise-3-6-prompt-styles]
-== Exercise 3.6: Prompt styles
+== Ex 3.6: Prompt styles
 
 The framework provides many ways to customise the UI, either through the layout files or using the `@XxxLayout` annotations.
 Default UI conventions can also be specified using the `application.yml` configuration file.
@@ -367,7 +367,7 @@ isis:
 
 
 [#exercise-3-7-derived-name-property]
-== Exercise 3.7: Derived name property
+== Ex 3.7: Derived name property
 
 The ``PetOwner``'s `firstName` and `lastName` properties are updated using the `updateName` action, but when the action's button is invoked, it only "replaces" the `lastName` property:
 
@@ -553,7 +553,7 @@ assertThat(wrap(fred).getName()).isEqualTo("Freddy"); // <.>
 
 
 [#exercise-3-8-add-other-properties-for-petowner]
-== Exercise 3.8: Add other properties for PetOwner
+== Ex 3.8: Add other properties for PetOwner
 
 Let's add the two remaining properties for `PetOwner`:
 
@@ -651,7 +651,7 @@ private String emailAddress;
 
 
 [#exercise-3-9-validation]
-== Exercise 3.9: Validation
+== Ex 3.9: Validation
 
 At the moment there are no constraints for the format of `phoneNumber` or `emailAddress` properties.
 We can fix this by adding rules to their respective meta-annotations.
@@ -783,7 +783,7 @@ As the logic is shared, create a new meta-(meta-)annotation called `@Name`, move
 
 
 [#exercise-3-10-field-layout]
-== Exercise 3.10: Field layout
+== Ex 3.10: Field layout
 
 At the moment all the properties of `PetOwner` are grouped into a single fieldset.
 The UI would be improved by grouping properties according to their nature, for example the "phoneNumber" and "emailAddress" in a "Contact Details" fieldset.
@@ -887,7 +887,7 @@ It really is a matter of personal preference which approach you use.
 
 
 [#exercise-3-11-column-orders]
-== Exercise 3.11: Column Orders
+== Ex 3.11: Column Orders
 
 The home page of the webapp shows a list of all `PetOwner`s (inherited from the original simple app).
 We also see a list of `PetOwner`s if we invoke menu:Pet Owners[List All].
diff --git a/antora/components/tutorials/modules/petclinic/pages/040-pet-entity.adoc b/antora/components/tutorials/modules/petclinic/pages/040-pet-entity.adoc
index bcb09ad..5278215 100644
--- a/antora/components/tutorials/modules/petclinic/pages/040-pet-entity.adoc
+++ b/antora/components/tutorials/modules/petclinic/pages/040-pet-entity.adoc
@@ -12,7 +12,7 @@ In this set of exercises we'll focus on the `Pet` entity and its relationship wi
 Each `PetOwner` will hold a collection of their ``Pet``s, with actions to add or remove `Pet` instances for that collection.
 
 [#exercise-4-1-pet-entitys-key-properties]
-== Exercise 4.1: Pet entity's key properties
+== Ex 4.1: Pet entity's key properties
 
 In this exercise we'll just create the outline of the `Pet` entity, and ensure it is mapped to the database correctly.
 
@@ -114,7 +114,7 @@ Run the application, and confirm that the table is created correctly using menu:
 
 
 [#exercise-4-2-add-petrepository]
-== Exercise 4.2: Add PetRepository
+== Ex 4.2: Add PetRepository
 
 We will need to find the ``Pet``s belonging to a `PetOwner`.
 We do this by introducing a `PetRepository`, implemented as a link:https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.definition[Spring Data repository].
@@ -150,7 +150,7 @@ Confirm the application still runs
 
 
 [#exercise-4-3-add-petowners-collection-of-pets]
-== Exercise 4.3: Add PetOwner's collection of Pets
+== Ex 4.3: Add PetOwner's collection of Pets
 
 In this next exercise we'll add the ``PetOwner``'s collection of ``Pet``s, using a xref:userguide:fun:mixins.adoc[mixin].
 
@@ -275,7 +275,7 @@ Run the application (or just reload the changed classes) and confirm the columns
 
 
 [#exercise-4-4-add-pets-remaining-properties]
-== Exercise 4.4: Add Pet's remaining properties
+== Ex 4.4: Add Pet's remaining properties
 
 In this exercise we'll add the remaining properties for `Pet`.
 
@@ -377,7 +377,7 @@ Run the application and use menu:Prototyping[H2 Console] to confirm the database
 
 
 [#exercise-4-5-digression-clean-up-casing-of-database-schema]
-== Exercise 4.5: Digression: clean-up casing of database schema
+== Ex 4.5: Digression: clean-up casing of database schema
 
 Reviewing the tables in the database we can see that we have a mix between lower- and upper-case table and column names.
 In this exercise we'll take a timeout to make everything consistent.
@@ -404,7 +404,7 @@ mvn -pl spring-boot:run
 
 
 [#exercise-4-6-add-petowner-action-to-add-pets]
-== Exercise 4.6: Add PetOwner action to add Pets
+== Ex 4.6: Add PetOwner action to add Pets
 
 In this exercise we'll bring in the capability to add ``Pet``s, as a responsibility of `PetOwner`.
 We'll use an mixin action to implement this.
@@ -500,7 +500,7 @@ Run the application once more to test.
 
 
 [#exercise-4-7-add-pets-ui-customisation]
-== Exercise 4.7: Add Pet's UI customisation
+== Ex 4.7: Add Pet's UI customisation
 
 If we run the application and create a `Pet`, then the framework will render a page but the layout could be improved.
 So in this exercise we'll add a layout file for `Pet` and other UI files.
@@ -650,7 +650,7 @@ image::04-07/download-layout-xml.png[width=400]
 
 
 [#exercise-4-8-update-fixture-script-using-pet-personas]
-== Exercise 4.8: Update fixture script using Pet personas
+== Ex 4.8: Update fixture script using Pet personas
 
 By now you are probably tiring of continually creating a Pet in order to perform your tests.
 So let's take some time out to extend our fixture so that each `PetOwner` also has some ``Pet``s.
@@ -807,7 +807,7 @@ public class PetClinicDemo extends FixtureScript {
 
 
 [#exercise-4-9-add-petowner-action-to-delete-a-pet]
-== Exercise 4.9: Add PetOwner action to delete a Pet
+== Ex 4.9: Add PetOwner action to delete a Pet
 
 We will probably also need to delete an action to delete a `Pet` (though once there are associated ``Visit``s for a `Pet`, we'll need to disable this action).
 
@@ -947,7 +947,7 @@ public class PetOwner_removePets {                      // <.>
 
 
 [#exercise-4-10-cleanup]
-== Exercise 4.10: Cleanup
+== Ex 4.10: Cleanup
 
 Reviewing the contents of the `pets` module, we can see (in the solutions provided at least) that there are a few thing that still need some attention:
 
diff --git a/antora/components/tutorials/modules/petclinic/pages/050-visit-entity.adoc b/antora/components/tutorials/modules/petclinic/pages/050-visit-entity.adoc
index 06687d1a..eb088b3 100644
--- a/antora/components/tutorials/modules/petclinic/pages/050-visit-entity.adoc
+++ b/antora/components/tutorials/modules/petclinic/pages/050-visit-entity.adoc
@@ -13,7 +13,7 @@ We'll look at the important topic of modularity in later exercises.
 
 
 [#exercise-5-1-the-visits-module]
-== Exercise 5.1: The visits module
+== Ex 5.1: The visits module
 
 In this exercise we'll just create an empty visits module.
 
@@ -72,7 +72,7 @@ It still depends upon `PetsModule`, but now as a transitive dependency.
 
 
 [#exercise-5-2-visit-entitys-key-properties]
-== Exercise 5.2: Visit entity's key properties
+== Ex 5.2: Visit entity's key properties
 
 Now we have a visits module, we can now add in the `Visit` entity.
 We'll start just with the key properties.
@@ -168,7 +168,7 @@ Run the application, and confirm that the table is created correctly using menu:
 
 
 [#exercise-5-3-book-visit-action]
-== Exercise 5.3: "Book Visit" action
+== Ex 5.3: "Book Visit" action
 
 In addition to the key properties, the `Visit` has one further mandatory property, `reason`.
 This is required to be specified when a `Visit` is created ("what is the purpose of this visit?")
diff --git a/antora/components/tutorials/modules/petclinic/pages/060-unit-testing.adoc b/antora/components/tutorials/modules/petclinic/pages/060-unit-testing.adoc
index 96f4c6c..c1c718f 100644
--- a/antora/components/tutorials/modules/petclinic/pages/060-unit-testing.adoc
+++ b/antora/components/tutorials/modules/petclinic/pages/060-unit-testing.adoc
@@ -8,7 +8,7 @@ In this part of the tutorial we'll cover unit testing, later on we'll look at in
 
 
 [#exercise-6-1-unit-test-the-default-time-when-booking-visits]
-== Exercise 6.1: Unit test the default time when booking visits
+== Ex 6.1: Unit test the default time when booking visits
 
 The xref:050-visit-entity.adoc#exercise-5-3-book-visit-action["Book Visit"] action has a default time of 9am the next morning.
 In this section we'll write a unit test to verify this logic, using Mockito to "mock the clock".
diff --git a/antora/components/tutorials/modules/petclinic/pages/070-modularity.adoc b/antora/components/tutorials/modules/petclinic/pages/070-modularity.adoc
index 932d6e0..a62595f 100644
--- a/antora/components/tutorials/modules/petclinic/pages/070-modularity.adoc
+++ b/antora/components/tutorials/modules/petclinic/pages/070-modularity.adoc
@@ -22,7 +22,7 @@ In this part of the tutorial we'll look at domain events.
 
 
 [#exercise-7-1-refactor-petowners-delete-action]
-== Exercise 7.1: refactor PetOwner's delete action
+== Ex 7.1: refactor PetOwner's delete action
 
 Currently the `delete` action for `PetOwner` is implemented as a mixin within the `Pet` package.
 That's a nice place for that functionality, because it can delete any `Pet`s for the `PetOwner` if any exist.
diff --git a/antora/components/tutorials/modules/petclinic/pages/080-view-models.adoc b/antora/components/tutorials/modules/petclinic/pages/080-view-models.adoc
index 4c0bd8f..0085cb6 100644
--- a/antora/components/tutorials/modules/petclinic/pages/080-view-models.adoc
+++ b/antora/components/tutorials/modules/petclinic/pages/080-view-models.adoc
@@ -11,7 +11,7 @@ Some real-world examples include importing/exporting spreadsheets periodically (
 
 
 [#exercise-8-1-extend-the-home-page]
-== Exercise 8.1: Extend the Home Page.
+== Ex 8.1: Extend the Home Page.
 
 In this exercise we'll extend the home page by displaying additional data in new collections.
 
@@ -98,7 +98,7 @@ public class HomePageViewModel {
 
 
 [#exercise-8-2-add-a-convenience-action]
-== Exercise 8.2: Add a convenience action
+== Ex 8.2: Add a convenience action
 
 View models can have behaviour (actions), the same as entities.
 In this exercise we'll extend the home page by providing a convenience action to book a `Visit` for any `Pet` of any `PetOwner`.
@@ -183,7 +183,7 @@ If additional business rules were added to that delegate, then the mistake would
 
 
 [#exercise-8-3-using-a-view-model-as-a-projection-of-an-entity]
-== Exercise 8.3: Using a view model as a projection of an entity
+== Ex 8.3: Using a view model as a projection of an entity
 
 In the home page, the ``Visit`` instances show the `Pet` but they do not show the `PetOwner`.
 One option (probably the correct one in this case) would be to extend `Visit` itself and show this derived information:
diff --git a/antora/components/tutorials/modules/petclinic/pages/090-integration-testing.adoc b/antora/components/tutorials/modules/petclinic/pages/090-integration-testing.adoc
index aad77d1..4bbb772 100644
--- a/antora/components/tutorials/modules/petclinic/pages/090-integration-testing.adoc
+++ b/antora/components/tutorials/modules/petclinic/pages/090-integration-testing.adoc
@@ -8,7 +8,8 @@ Integration tests are _not_ written using Selenium or similar, so avoid the frag
 Instead, the framework provides the xref:refguide:applib:index/services/wrapper/WrapperFactory.adoc[WrapperFactory] domain service which simulates the user interface in a type-safe way.
 
 
-== Exercuse 9.1: Testing bookVisit using an integtest
+[#exercise-9-1-testing-bookvisit-using-an-integtest]
+== Ex 9.1: Testing bookVisit using an integtest
 
 In this exercise we'll test the `bookVisit` mixin action (on `Pet` entity).
 
diff --git a/antora/components/tutorials/modules/petclinic/pages/110-adding-further-business-logic-worked-examples.adoc b/antora/components/tutorials/modules/petclinic/pages/110-adding-further-business-logic-worked-examples.adoc
deleted file mode 100644
index 2065caa..0000000
--- a/antora/components/tutorials/modules/petclinic/pages/110-adding-further-business-logic-worked-examples.adoc
+++ /dev/null
@@ -1,707 +0,0 @@
-= Adding further business logic - Worked Examples
-
-:Notice: Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at. http://www.apache.org/licenses/LICENSE-2.0 . Unless required by applicable law or ag [...]
-
-
-Let's remind ourselves of the original use cases we identified; some of these have been implemented already (admittedly, not all with tests around them):
-
-* create an `PetOwner` : yes, implemented
-
-* add and remove ``Pet``s for said `PetOwner` : yes, implemented.
-
-* book a `Pet` in for a `Visit`: yes, implemented.
-
-* enter an `outcome` and `cost` of a `Visit`: not yet
-
-* allow an `PetOwner` to pay for a `Visit`: not yet
-
-* find ``Visit``s not yet paid and overdue (more than 28 days old): not yet
-
-* delete an `PetOwner` and its ``Pet``s and ``Visit``s, so long as there are no unpaid ``Visit``s: partly.
-We currently just delete everything.
-
-In this section we'll implement the missing functionality, along with unit or integration tests as necessary.
-
-
-== Enter an outcome
-
-An outcome for a `Visit` consists of a diagnosis, and also the cost to be paid by the ``Pet``'s `PetOwner`.
-
-image::Visit-enterOutcome.png[width="800px",link="_images/Visit-enterOutcome.png"]
-
-=== Solution
-
-[source,bash]
-----
-git checkout tags/330-enter-an-outcome
-mvn clean package jetty:run
-----
-
-
-=== Exercise
-
-* add a new integration test, `Visit_enterOutcome_IntegTest`,
-+
-[source,java]
-----
-public class Visit_enterOutcome_IntegTest extends PetClinicModuleIntegTestAbstract {
-
-    Visit visit;
-
-    @Before
-    public void setup() {
-        // given
-        Owner owner = runBuilderScript(Owner_enum.JOHN_SMITH);
-        Pet pet = owner.getPets().first();
-        visit = wrap(mixin(Pet_visits.class, pet)).coll().iterator().next();
-    }
-
-    @Test
-    public void happy_case() {
-
-        // when
-        String diagnosis = someRandomDiagnosis();
-        BigDecimal cost = someRandomCost();
-
-        wrap(visit).enterOutcome(diagnosis, cost);
-
-        // then
-        assertThat(visit.getDiagnosis()).isEqualTo(diagnosis);
-        assertThat(visit.getCost()).isEqualTo(cost);
-    }
-
-    private BigDecimal someRandomCost() {
-        return new BigDecimal(20.00 + fakeDataService.doubles().upTo(30.00d));
-    }
-
-    private String someRandomDiagnosis() {
-        return fakeDataService.lorem().paragraph(3);
-    }
-
-    @Inject
-    FakeDataService fakeDataService;
-}
-----
-
-* in `Visit`, add in the two new properties and action.
-+
-[source,java]
-----
-@Action(semantics = SemanticsOf.IDEMPOTENT)
-public Visit enterOutcome(
-        @Parameter(maxLength = 4000)
-        @ParameterLayout(multiLine = 5)
-        final String diagnosis,
-        final BigDecimal cost) {
-    this.diagnosis = diagnosis;
-    this.cost = cost;
-    return this;
-}
-
-@javax.jdo.annotations.Column(allowsNull = "true", length = 4000)
-@Property(editing = Editing.DISABLED, editingDisabledReason = "Use 'enter outcome' action")
-@PropertyLayout(multiLine = 5)
-@Getter @Setter
-private String diagnosis;
-
-@javax.jdo.annotations.Column(allowsNull = "true", length = 6, scale = 2)
-@Property(editing = Editing.DISABLED, editingDisabledReason = "Use 'enter outcome' action")
-@Getter @Setter
-private BigDecimal cost;
-
-----
-
-* update `Visit.layout.xml` for the two new properties and action.
-
-* add in some further integration tests to ensure that the properties cannot be edited directly:
-+
-[source,java]
-----
-@Test
-public void cannot_edit_outcome_directly() {
-
-    // expecting
-    expectedExceptions.expect(DisabledException.class);
-    expectedExceptions.expectMessage("Use 'enter outcome' action");
-
-    // when
-    String diagnosis = someRandomDiagnosis();
-    wrap(visit).setDiagnosis(diagnosis);
-}
-
-@Test
-public void cannot_edit_cost_directly() {
-
-    // expecting
-    expectedExceptions.expect(DisabledException.class);
-    expectedExceptions.expectMessage("Use 'enter outcome' action");
-
-    // when
-    BigDecimal cost = someRandomCost();
-
-    wrap(visit).setCost(cost);
-}
-----
-
-
-== Pay for a visit
-
-We'll support this use case through a new action "paid", on the `Visit` domain entity.
-
-To support the testing (and with half an eye to a future use case) we'll also implement a "findNotPaid" query on the `Visits` repository domain service.
-
-=== Solution
-
-[source,bash]
-----
-git checkout tags/340-pay-for-a-visit
-mvn clean package jetty:run
-----
-
-
-=== Exercise
-
-Let's first work on the happy case:
-
-* Update `Visit` with a new `paid()` action and `paidOn` property.
-Also inject `ClockService`:
-+
-[source,java]
-----
-@Action(semantics = SemanticsOf.IDEMPOTENT)
-public Visit paid() {
-    paidOn = clockService.now();
-    return this;
-}
-
-@javax.jdo.annotations.Column(allowsNull = "true")
-@Property(editing = Editing.DISABLED, editingDisabledReason = "Use 'paid' action")
-@Getter @Setter
-private LocalDate paidOn;
-
-...
-
-@Inject
-ClockService clockService;
-----
-
-* Update the `Visits` domain service repository to find ``Visit``s that haven't been paid:
-+
-[source,java]
-----
-@Programmatic
-public java.util.List<Visit> findNotPaid() {
-    TypesafeQuery<Visit> q = isisJdoSupport.newTypesafeQuery(Visit.class);
-    final QVisit cand = QVisit.candidate();
-    q = q.filter(
-            cand.paidOn.eq(q.parameter("paidOn", LocalDateTime.class)
-        )
-    );
-    return q.setParameter("paidOn", null)
-            .executeList();
-}
-----
-
-* Extend `PetOwnerBuilderScript` so that all but the last `Visit` for each ``PetOwner``'s ``Pet``s has been paid.
-+
-Add some further supporting methods:
-+
-[source,java]
-----
-private String someDiagnosis() {
-    return fakeDataService.lorem().paragraph(fakeDataService.ints().between(1, 3));
-}
-
-private BigDecimal someCost() {
-    return new BigDecimal(20.00 + fakeDataService.doubles().upTo(30.00d));
-}
-----
-+
-In the `execute(...)`, update the `for` loop so that all ``Visit``s have an outcome and all but the last (for each ``PetOwner``) has been paid:
-+
-[source,java]
-----
-for (int i = 0; i < petDatum.numberOfVisits; i++) {
-    ...
-    LocalDateTime someTimeInPast = ...
-    Visit visit = ...
-    wrap(visit).enterOutcome(someDiagnosis(), someCost());
-    if(i != petDatum.numberOfVisits - 1) {
-        setTimeTo(ec, someTimeInPast.plusDays(fakeDataService.ints().between(10,30)));
-        wrap(visit).paid();
-    }
-}
-----
-
-
-== Prevent payment for a visit twice
-
-We've already seen that it's possible to validate arguments to actions; for example that a `Visit` can only be booked in the future.
-But if a `Visit` has already been paid for, then we don't want the user to be able to even attempt to invoke the action.
-
-The framework provides three different types of pre-condition checks:
-
-* "See it?" - should the action/property be visible at all, or has it been hidden?
-
-* "Use it" - if visible, then can the action/property be used or has it been disabled (greyed out)
-
-* "Do it" - if the action/property is ok to be used (action invoked/property edited) then are the proposed action arguments or new property value valid, or are they invalid?
-
-Or in other words, "see it, use it, do it".
-
-As with validation, disablement can be defined either declaratively (annotations) or imperatively (supporting methods).
-Let's see how an imperative supporting method can be used to implement this particular requirement (that a visit can't be paid for twice).
-
-=== Solution
-
-[source,bash]
-----
-git checkout tags/350-prevent-payment-for-a-visit-twice
-mvn clean package jetty:run
-----
-
-
-=== Exercise
-
-* update `Visit_pay_IntegTest` to ensure cannot enter into the `paidOn` property directly:
-+
-[source,java]
-----
-@Test
-public void cannot_edit_paidOn_directly() {
-
-    // expecting
-    expectedExceptions.expect(DisabledException.class);
-    expectedExceptions.expectMessage("Use 'paid on' action");
-
-    // when
-    wrap(visit).setPaidOn(clockService.now());
-}
-----
-
-* now, add in the test that asserts that a `Visit` cannot be paid more than once:
-+
-[source,java]
-----
-@Test
-public void cannot_pay_more_than_once() {
-
-    // given
-    wrap(visit).paid();
-    assertThat(visits.findNotPaid()).asList().doesNotContain(visit);
-
-    // expecting
-    expectedExceptions.expect(DisabledException.class);
-    expectedExceptions.expectMessage("Already paid");
-
-    // when
-    wrap(visit).paid();
-}
-----
-
-* and finally update `Visit`.
-This is done using a supporting method.
-+
-[source,java]
-----
-public String disablePaid() {
-    return getPaidOn() != null ? "Already paid": null;
-}
-----
-
-
-== Find ``Visit``s not yet paid and overdue
-
-In the previous scenario we implemented `Visits#findNotPaid()`.
-Since this is pretty important information, let's surface that to the end-user by adding it to the home page dashboard.
-
-We could also go a little further by allowing the user to use the dashboard to update visits that have been paid.
-This is a good example of how a view model can support specific business processes, in this case saving the end-user from having to navigate down to each and every one of the ``Visit``s.
-
-=== Solution
-
-[source,bash]
-----
-git checkout tags/360-find-visits-not-yet-paid-and-overdue
-mvn clean package jetty:run
-----
-
-image::Dashboard-overdue.png[width="800px",link="_images/Dashboard-overdue.png"]
-
-=== Exercise
-
-
-* update `Dashboard`:
-+
-[source,java]
-----
-@CollectionLayout(defaultView = "table")
-public List<Visit> getOverdue() {
-    List<Visit> notPaid = visits.findNotPaid();
-    LocalDateTime thirtyDaysAgo = clockService.nowAsLocalDateTime().minusDays(30);
-    return notPaid.stream()
-            .filter(x -> x.getVisitAt().isBefore(thirtyDaysAgo))        // <1>
-            .collect(Collectors.toList());
-}
-
-@Action(semantics = SemanticsOf.IDEMPOTENT, associateWith = "overdue")  // <2>
-public Dashboard paid(List<Visit> visits) {
-    for (Visit visit : visits) {
-        if(visit.getPaidOn() == null) {
-            visit.paid();
-        }
-    }
-    return this;
-}
-
-@javax.inject.Inject
-Visits visits;
-
-@javax.inject.Inject
-ClockService clockService;
-----
-<1> An alternative (better?) design would have been to add a new query method in `Visits` to find those overdue, avoiding the client-side filtering that we see above.
-<2> The "associateWith" annotation results in checkboxes alongside the "overdue" collection, with the collection providing the set of values for the parameter.
-
-
-* update `Dashboard.layout.xml` also
-
-* write a new `Dashboard_paid_IntegTest` integration test:
-+
-[source,java]
-----
-public class Dashboard_paid_IntegTest extends PetClinicModuleIntegTestAbstract {
-
-    Dashboard dashboard;
-
-    @Before
-    public void setup() {
-        // given
-        runFixtureScript(new PersonaEnumPersistAll<>(Owner_enum.class));
-        dashboard = homePageProvider.dashboard();
-    }
-
-    @Test
-    public void happy_case() {
-
-        // given
-        List<Visit> overdue = dashboard.getOverdue();
-        assertThat(overdue).isNotEmpty();
-
-        // when
-        wrap(dashboard).paid(overdue);
-
-        // then
-        List<Visit> overdueAfter = dashboard.getOverdue();
-        assertThat(overdueAfter).isEmpty();
-
-        for (Visit visit : overdue) {
-            assertThat(visit.getDiagnosis()).isNotNull();
-            assertThat(visit.getPaidOn()).isNotNull();
-        }
-    }
-
-    @Inject
-    HomePageProvider homePageProvider;
-}
-----
-
-* Running the integration test at this point will produce a null pointer exception.
-That's because the framework has had no opportunity to inject any domain services into the `Dashboard`.
-+
-Under normal runtime cases this doesn't matter because the only caller of the method is the framework itself, and when the domain object is rendered the framework will automatically ensure that any domain sevices are injected.
-+
-In an integration test this doesn't occur, and so we need to manually inject the services.
-It makes most sense to do this in `HomePageProvider`; we use the framework-provided `ServiceRegistry2` domain service:
-+
-[source,java]
-----
-@HomePage
-public Dashboard dashboard() {
-    return serviceRegistry2.injectServicesInto(new Dashboard());
-}
-@Inject
-ServiceRegistry2 serviceRegistry2;
-----
-
-
-
-== Digression: Hiding Columns in Tables
-
-We could improve the dashboard a little.
-After all, in the "overdue" collection there's no point in showing the "paidOn"; the value will always be null.
-Also, the "reason" column is also somewhat superfluous (as, arguably, is the "diagnosis" column):
-
-image::Dashboard-overdue-ui-hints.png[width="800px",link="_images/Dashboard-overdue-ui-hints.png"]
-
-The framework offers two different ways to address this, so we'll show both.
-
-=== Solution
-
-[source,bash]
-----
-git checkout tags/370-digression-hiding-columns-in-tables
-mvn clean package jetty:run
-----
-
-
-=== Exercise
-
-* The first technique is within the Java code; one could think of this as an implication within the "application layer".
-+
-We use a domain service that implements `TableColumnOrderService` as an SPI to "advise" the framework on how to render the collection.
-Traditionally such classes are implemented as a nested static class, in this case of `Dashboard`:
-+
-[source,java]
-----
-@DomainService(nature = NatureOfService.DOMAIN)
-public static class RemovePaidOnFromOverdue extends TableColumnOrderService.Default {
-    @Override
-    public List<String> orderParented(
-            final Object parent,
-            final String collectionId,
-            final Class<?> collectionType,
-            final List<String> propertyIds) {
-        if (parent instanceof Dashboard && "overdue".equalsIgnoreCase(collectionId)) {
-            propertyIds.remove("paidOn");
-        }
-        return propertyIds;
-    }
-}
-----
-+
-The above code removes the "paidOn" column.
-
-* The second technique is to exploit the fact that the HTML generated by the framework is liberally annotated with domain class identifiers.
-The column can therefore be removed by supplying the appropriate CSS.
-We could think of this as an implementation within the presentation layer.
-+
-In the `src/main/webapp/css/application.css` file, add:
-+
-[source,css]
-----
-.domainapp-modules-impl-dashboard-Dashboard .entityCollection .overdue .Visit-reason {
-    display: none;
-}
-----
-
-
-
-== Another Digression: Icons and CSS
-
-In the same way that titles can be specified imperatively, so too can icons, using the `iconName()` method.
-One use case is for a domain object that has several states: the `iconName()` defines a suffix which is used to lookup different icons (eg "ToDoItem-notDone.png" and "ToDoItem-done.png").
-
-Similarly, it's possible to specify CSS hints imperatively using the `cssClass()`.
-This returns a simple string that is added as a CSS class wherever the object is rendered in the UI.
-
-In this exercise we'll use a different icon for the various species of `Pet`:
-
-image::Pet-icons.png[width="800px",link="_images/Pet-icons.png"]
-
-Let's also use a strike-through text for all ``Visit``s that are paid when rendered within a collection:
-
-image::Visits-paid-strikethrough.png[width="800px",link="_images/Visits-paid-strikethrough.png"]
-
-
-
-
-=== Solution
-
-[source,bash]
-----
-git checkout tags/380-another-digression-icons-and-css
-mvn clean package jetty:run
-----
-
-
-
-=== Exercise
-
-For the icons:
-
-* add new icons for each of the pet species: `Pet-dog.png`, `Pet-cat.png`, `Pet-hamster.dog` and `Pet-budgerigar.png`
-
-* add an `iconName()` method to `Pet`:
-+
-[source,java]
-----
-public String iconName() {
-    return getPetSpecies().name().toLowerCase();
-}
-----
-
-
-For the CSS class:
-
-* add a `cssClass()` method to `Visit`:
-+
-[source,java]
-----
-public String cssClass() {
-    boolean isPaid = getPaidOn() != null;
-    return isPaid ? "paid": null;
-}
-----
-
-
-* update `application.css`:
-
-[source,css]
-----
-.entityCollection .domainapp-modules-impl-visits-dom-Visit .paid {
-    text-decoration: line-through;
-    color: lightgrey;
-}
-----
-
-
-== Delete an `PetOwner` provided no unpaid ``Visit``s
-
-=== Solution
-
-[source,bash]
-----
-git checkout tags/390-delete-an-owner-provided-no-unpaid-visits
-mvn clean package jetty:run
-----
-
-
-=== Exercise
-
-We don't want `PetOwner` (in the `pets` module) to check for unpaid ``Visit``s, because that would create a cyclic dependency between modules.
-Instead, we'll use a subscriber in the `visits` module which can veto any attempt to delete an owner if there are unpaid visits.
-
-For this, we arrange for the `PetOwner` to emit an action domain event when its `delete()` action is invoked.
-In fact, the event will be emitted by the framework up to five times: to check if the action is visible, if it is disabled, if it's valid, pre-execute and post-execute.
-The subscriber in the ``visits`` module will therefore potentially veto on the disable phase.
-
-* in the `Visits` repository, add `findNotPaidBy` method to find any unpaid ``Visit``s for an `PetOwner`:
-+
-[source,java]
-----
-@Programmatic
-public java.util.List<Visit> findNotPaidBy(Owner owner) {
-    TypesafeQuery<Visit> q = isisJdoSupport.newTypesafeQuery(Visit.class);
-    final QVisit cand = QVisit.candidate();
-    q = q.filter(
-            cand.paidOn.eq(q.parameter("paidOn", LocalDateTime.class)
-        ).and(
-                cand.pet.owner.eq(q.parameter("owner", Owner.class))
-            )
-    );
-    return q.setParameter("paidOn", null)
-            .setParameter("owner", owner)
-            .executeList();
-}
-----
-
-* update `PetOwner`'s `delete()` action so that it emits an action domain event.
-+
-[source,java]
-----
-import org.apache.isis.applib.services.eventbus.ActionDomainEvent;
-...
-public static class Delete extends ActionDomainEvent<Owner> {}  // <1>
-@Action(
-        domainEvent = Delete.class                              // <2>
-        semantics = SemanticsOf.NON_IDEMPOTENT                  // <3>
-)
-public void delete() {
-    final String title = titleService.titleOf(this);
-    messageService.informUser(String.format("'%s' deleted", title));
-    repositoryService.removeAndFlush(this);
-}
-----
-<1> declare the event, and
-<2> emit it
-<3> change from `NON_IDEMPOTENT_ARE_YOU_SURE` (due to a bug in the framework).
-
-* add a new integration test:
-+
-[source,java]
-----
-public class Owner_delete_IntegTest extends PetClinicModuleIntegTestAbstract {
-
-    @Test
-    public void can_delete_if_there_are_no_unpaid_visits() {
-
-        // given
-        runFixtureScript(Owner_enum.FRED_HUGHES.builder());
-
-        Owner owner = Owner_enum.FRED_HUGHES.findUsing(serviceRegistry);
-        List<Visit> any = visits.findNotPaidBy(owner);
-        assertThat(any).isEmpty();
-
-        // when
-        wrap(owner).delete();
-
-        // then
-        Owner ownerAfter = Owner_enum.FRED_HUGHES.findUsing(serviceRegistry);
-        assertThat(ownerAfter).isNull();
-    }
-
-    @Test
-    public void cannot_delete_with_unpaid_visits() {
-
-        // given
-        runFixtureScript(Owner_enum.MARY_JONES.builder());
-
-        Owner owner = Owner_enum.MARY_JONES.findUsing(serviceRegistry);
-        List<Visit> any = visits.findNotPaidBy(owner);
-        assertThat(any).isNotEmpty();
-
-        // expect
-        expectedExceptions.expect(DisabledException.class);
-        expectedExceptions.expectMessage("This owner still has unpaid visit(s)");
-
-        // when
-        wrap(owner).delete();
-    }
-
-    @Inject
-    Visits visits;
-}
-----
-
-* add the subscriber to veto the action if required:
-+
-[source,java]
-----
-@DomainService(nature = NatureOfService.DOMAIN)
-public class VetoDeleteOfOwnerWithUnpaidVisits
-        extends org.apache.isis.applib.AbstractSubscriber {
-
-    @org.axonframework.eventhandling.annotation.EventHandler
-    public void on(Owner.Delete ev) {
-
-        switch (ev.getEventPhase()) {
-        case DISABLE:
-            Collection<Visit> visitsForPet = visits.findNotPaidBy(ev.getSource());
-            if (!visitsForPet.isEmpty()) {
-                ev.veto("This owner still has unpaid visit(s)");
-            }
-            break;
-        }
-    }
-
-    @javax.inject.Inject
-    Visits visits;
-}
-----
-
-* finally, in `PetClinicModuleIntegTestAbstract`, we need to make a small adjustment to use the same event bus implementation as the production app:
-+
-[source,java]
-----
-super(new PetClinicModule()
-    .withAdditionalServices(DeploymentCategoryProviderForTesting.class)
-    .withConfigurationProperty("isis.services.eventbus.implementation","axon")      // <1>
-    .withConfigurationProperty(TranslationServicePo.KEY_PO_MODE, "write")
-);
-----
-<1> specify Axon as the event bus implementation
-
-
-
-
diff --git a/antora/components/tutorials/modules/petclinic/pages/architecture-rules.adoc b/antora/components/tutorials/modules/petclinic/pages/architecture-rules.adoc
index 257a961..4db485f 100644
--- a/antora/components/tutorials/modules/petclinic/pages/architecture-rules.adoc
+++ b/antora/components/tutorials/modules/petclinic/pages/architecture-rules.adoc
@@ -1,6 +1,6 @@
-= architecture rules
+= Architecture rules
 
 :Notice: Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at. http://www.apache.org/licenses/LICENSE-2.0 . Unless required by applicable law or ag [...]
 
-TODO.
+CAUTION: TODO.
 
diff --git a/antora/components/tutorials/modules/petclinic/pages/architecture-rules.adoc b/antora/components/tutorials/modules/petclinic/pages/commands-and-auditing.adoc
similarity index 95%
copy from antora/components/tutorials/modules/petclinic/pages/architecture-rules.adoc
copy to antora/components/tutorials/modules/petclinic/pages/commands-and-auditing.adoc
index 257a961..5531e2b 100644
--- a/antora/components/tutorials/modules/petclinic/pages/architecture-rules.adoc
+++ b/antora/components/tutorials/modules/petclinic/pages/commands-and-auditing.adoc
@@ -1,6 +1,6 @@
-= architecture rules
+= Commands and Auditing
 
 :Notice: Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at. http://www.apache.org/licenses/LICENSE-2.0 . Unless required by applicable law or ag [...]
 
-TODO.
+CAUTION: TODO.
 
diff --git a/antora/components/tutorials/modules/petclinic/pages/further-business-logic-worked-examples.adoc b/antora/components/tutorials/modules/petclinic/pages/further-business-logic-worked-examples.adoc
new file mode 100644
index 0000000..258ddc0
--- /dev/null
+++ b/antora/components/tutorials/modules/petclinic/pages/further-business-logic-worked-examples.adoc
@@ -0,0 +1,708 @@
+= Adding further business logic - Worked Examples
+
+:Notice: Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at. http://www.apache.org/licenses/LICENSE-2.0 . Unless required by applicable law or ag [...]
+
+CAUTION: TODO
+
+//Let's remind ourselves of the original use cases we identified; some of these have been implemented already (admittedly, not all with tests around them):
+//
+//* create an `PetOwner` : yes, implemented
+//
+//* add and remove ``Pet``s for said `PetOwner` : yes, implemented.
+//
+//* book a `Pet` in for a `Visit`: yes, implemented.
+//
+//* enter an `outcome` and `cost` of a `Visit`: not yet
+//
+//* allow an `PetOwner` to pay for a `Visit`: not yet
+//
+//* find ``Visit``s not yet paid and overdue (more than 28 days old): not yet
+//
+//* delete an `PetOwner` and its ``Pet``s and ``Visit``s, so long as there are no unpaid ``Visit``s: partly.
+//We currently just delete everything.
+//
+//In this section we'll implement the missing functionality, along with unit or integration tests as necessary.
+//
+//
+//== Enter an outcome
+//
+//An outcome for a `Visit` consists of a diagnosis, and also the cost to be paid by the ``Pet``'s `PetOwner`.
+//
+//image::Visit-enterOutcome.png[width="800px",link="_images/Visit-enterOutcome.png"]
+//
+//=== Solution
+//
+//[source,bash]
+//----
+//git checkout tags/330-enter-an-outcome
+//mvn clean package jetty:run
+//----
+//
+//
+//=== Exercise
+//
+//* add a new integration test, `Visit_enterOutcome_IntegTest`,
+//+
+//[source,java]
+//----
+//public class Visit_enterOutcome_IntegTest extends PetClinicModuleIntegTestAbstract {
+//
+//    Visit visit;
+//
+//    @Before
+//    public void setup() {
+//        // given
+//        Owner owner = runBuilderScript(Owner_enum.JOHN_SMITH);
+//        Pet pet = owner.getPets().first();
+//        visit = wrap(mixin(Pet_visits.class, pet)).coll().iterator().next();
+//    }
+//
+//    @Test
+//    public void happy_case() {
+//
+//        // when
+//        String diagnosis = someRandomDiagnosis();
+//        BigDecimal cost = someRandomCost();
+//
+//        wrap(visit).enterOutcome(diagnosis, cost);
+//
+//        // then
+//        assertThat(visit.getDiagnosis()).isEqualTo(diagnosis);
+//        assertThat(visit.getCost()).isEqualTo(cost);
+//    }
+//
+//    private BigDecimal someRandomCost() {
+//        return new BigDecimal(20.00 + fakeDataService.doubles().upTo(30.00d));
+//    }
+//
+//    private String someRandomDiagnosis() {
+//        return fakeDataService.lorem().paragraph(3);
+//    }
+//
+//    @Inject
+//    FakeDataService fakeDataService;
+//}
+//----
+//
+//* in `Visit`, add in the two new properties and action.
+//+
+//[source,java]
+//----
+//@Action(semantics = SemanticsOf.IDEMPOTENT)
+//public Visit enterOutcome(
+//        @Parameter(maxLength = 4000)
+//        @ParameterLayout(multiLine = 5)
+//        final String diagnosis,
+//        final BigDecimal cost) {
+//    this.diagnosis = diagnosis;
+//    this.cost = cost;
+//    return this;
+//}
+//
+//@javax.jdo.annotations.Column(allowsNull = "true", length = 4000)
+//@Property(editing = Editing.DISABLED, editingDisabledReason = "Use 'enter outcome' action")
+//@PropertyLayout(multiLine = 5)
+//@Getter @Setter
+//private String diagnosis;
+//
+//@javax.jdo.annotations.Column(allowsNull = "true", length = 6, scale = 2)
+//@Property(editing = Editing.DISABLED, editingDisabledReason = "Use 'enter outcome' action")
+//@Getter @Setter
+//private BigDecimal cost;
+//
+//----
+//
+//* update `Visit.layout.xml` for the two new properties and action.
+//
+//* add in some further integration tests to ensure that the properties cannot be edited directly:
+//+
+//[source,java]
+//----
+//@Test
+//public void cannot_edit_outcome_directly() {
+//
+//    // expecting
+//    expectedExceptions.expect(DisabledException.class);
+//    expectedExceptions.expectMessage("Use 'enter outcome' action");
+//
+//    // when
+//    String diagnosis = someRandomDiagnosis();
+//    wrap(visit).setDiagnosis(diagnosis);
+//}
+//
+//@Test
+//public void cannot_edit_cost_directly() {
+//
+//    // expecting
+//    expectedExceptions.expect(DisabledException.class);
+//    expectedExceptions.expectMessage("Use 'enter outcome' action");
+//
+//    // when
+//    BigDecimal cost = someRandomCost();
+//
+//    wrap(visit).setCost(cost);
+//}
+//----
+//
+//
+//== Pay for a visit
+//
+//We'll support this use case through a new action "paid", on the `Visit` domain entity.
+//
+//To support the testing (and with half an eye to a future use case) we'll also implement a "findNotPaid" query on the `Visits` repository domain service.
+//
+//=== Solution
+//
+//[source,bash]
+//----
+//git checkout tags/340-pay-for-a-visit
+//mvn clean package jetty:run
+//----
+//
+//
+//=== Exercise
+//
+//Let's first work on the happy case:
+//
+//* Update `Visit` with a new `paid()` action and `paidOn` property.
+//Also inject `ClockService`:
+//+
+//[source,java]
+//----
+//@Action(semantics = SemanticsOf.IDEMPOTENT)
+//public Visit paid() {
+//    paidOn = clockService.now();
+//    return this;
+//}
+//
+//@javax.jdo.annotations.Column(allowsNull = "true")
+//@Property(editing = Editing.DISABLED, editingDisabledReason = "Use 'paid' action")
+//@Getter @Setter
+//private LocalDate paidOn;
+//
+//...
+//
+//@Inject
+//ClockService clockService;
+//----
+//
+//* Update the `Visits` domain service repository to find ``Visit``s that haven't been paid:
+//+
+//[source,java]
+//----
+//@Programmatic
+//public java.util.List<Visit> findNotPaid() {
+//    TypesafeQuery<Visit> q = isisJdoSupport.newTypesafeQuery(Visit.class);
+//    final QVisit cand = QVisit.candidate();
+//    q = q.filter(
+//            cand.paidOn.eq(q.parameter("paidOn", LocalDateTime.class)
+//        )
+//    );
+//    return q.setParameter("paidOn", null)
+//            .executeList();
+//}
+//----
+//
+//* Extend `PetOwnerBuilderScript` so that all but the last `Visit` for each ``PetOwner``'s ``Pet``s has been paid.
+//+
+//Add some further supporting methods:
+//+
+//[source,java]
+//----
+//private String someDiagnosis() {
+//    return fakeDataService.lorem().paragraph(fakeDataService.ints().between(1, 3));
+//}
+//
+//private BigDecimal someCost() {
+//    return new BigDecimal(20.00 + fakeDataService.doubles().upTo(30.00d));
+//}
+//----
+//+
+//In the `execute(...)`, update the `for` loop so that all ``Visit``s have an outcome and all but the last (for each ``PetOwner``) has been paid:
+//+
+//[source,java]
+//----
+//for (int i = 0; i < petDatum.numberOfVisits; i++) {
+//    ...
+//    LocalDateTime someTimeInPast = ...
+//    Visit visit = ...
+//    wrap(visit).enterOutcome(someDiagnosis(), someCost());
+//    if(i != petDatum.numberOfVisits - 1) {
+//        setTimeTo(ec, someTimeInPast.plusDays(fakeDataService.ints().between(10,30)));
+//        wrap(visit).paid();
+//    }
+//}
+//----
+//
+//
+//== Prevent payment for a visit twice
+//
+//We've already seen that it's possible to validate arguments to actions; for example that a `Visit` can only be booked in the future.
+//But if a `Visit` has already been paid for, then we don't want the user to be able to even attempt to invoke the action.
+//
+//The framework provides three different types of pre-condition checks:
+//
+//* "See it?" - should the action/property be visible at all, or has it been hidden?
+//
+//* "Use it" - if visible, then can the action/property be used or has it been disabled (greyed out)
+//
+//* "Do it" - if the action/property is ok to be used (action invoked/property edited) then are the proposed action arguments or new property value valid, or are they invalid?
+//
+//Or in other words, "see it, use it, do it".
+//
+//As with validation, disablement can be defined either declaratively (annotations) or imperatively (supporting methods).
+//Let's see how an imperative supporting method can be used to implement this particular requirement (that a visit can't be paid for twice).
+//
+//=== Solution
+//
+//[source,bash]
+//----
+//git checkout tags/350-prevent-payment-for-a-visit-twice
+//mvn clean package jetty:run
+//----
+//
+//
+//=== Exercise
+//
+//* update `Visit_pay_IntegTest` to ensure cannot enter into the `paidOn` property directly:
+//+
+//[source,java]
+//----
+//@Test
+//public void cannot_edit_paidOn_directly() {
+//
+//    // expecting
+//    expectedExceptions.expect(DisabledException.class);
+//    expectedExceptions.expectMessage("Use 'paid on' action");
+//
+//    // when
+//    wrap(visit).setPaidOn(clockService.now());
+//}
+//----
+//
+//* now, add in the test that asserts that a `Visit` cannot be paid more than once:
+//+
+//[source,java]
+//----
+//@Test
+//public void cannot_pay_more_than_once() {
+//
+//    // given
+//    wrap(visit).paid();
+//    assertThat(visits.findNotPaid()).asList().doesNotContain(visit);
+//
+//    // expecting
+//    expectedExceptions.expect(DisabledException.class);
+//    expectedExceptions.expectMessage("Already paid");
+//
+//    // when
+//    wrap(visit).paid();
+//}
+//----
+//
+//* and finally update `Visit`.
+//This is done using a supporting method.
+//+
+//[source,java]
+//----
+//public String disablePaid() {
+//    return getPaidOn() != null ? "Already paid": null;
+//}
+//----
+//
+//
+//== Find ``Visit``s not yet paid and overdue
+//
+//In the previous scenario we implemented `Visits#findNotPaid()`.
+//Since this is pretty important information, let's surface that to the end-user by adding it to the home page dashboard.
+//
+//We could also go a little further by allowing the user to use the dashboard to update visits that have been paid.
+//This is a good example of how a view model can support specific business processes, in this case saving the end-user from having to navigate down to each and every one of the ``Visit``s.
+//
+//=== Solution
+//
+//[source,bash]
+//----
+//git checkout tags/360-find-visits-not-yet-paid-and-overdue
+//mvn clean package jetty:run
+//----
+//
+//image::Dashboard-overdue.png[width="800px",link="_images/Dashboard-overdue.png"]
+//
+//=== Exercise
+//
+//
+//* update `Dashboard`:
+//+
+//[source,java]
+//----
+//@CollectionLayout(defaultView = "table")
+//public List<Visit> getOverdue() {
+//    List<Visit> notPaid = visits.findNotPaid();
+//    LocalDateTime thirtyDaysAgo = clockService.nowAsLocalDateTime().minusDays(30);
+//    return notPaid.stream()
+//            .filter(x -> x.getVisitAt().isBefore(thirtyDaysAgo))        // <1>
+//            .collect(Collectors.toList());
+//}
+//
+//@Action(semantics = SemanticsOf.IDEMPOTENT, associateWith = "overdue")  // <2>
+//public Dashboard paid(List<Visit> visits) {
+//    for (Visit visit : visits) {
+//        if(visit.getPaidOn() == null) {
+//            visit.paid();
+//        }
+//    }
+//    return this;
+//}
+//
+//@javax.inject.Inject
+//Visits visits;
+//
+//@javax.inject.Inject
+//ClockService clockService;
+//----
+//<1> An alternative (better?) design would have been to add a new query method in `Visits` to find those overdue, avoiding the client-side filtering that we see above.
+//<2> The "associateWith" annotation results in checkboxes alongside the "overdue" collection, with the collection providing the set of values for the parameter.
+//
+//
+//* update `Dashboard.layout.xml` also
+//
+//* write a new `Dashboard_paid_IntegTest` integration test:
+//+
+//[source,java]
+//----
+//public class Dashboard_paid_IntegTest extends PetClinicModuleIntegTestAbstract {
+//
+//    Dashboard dashboard;
+//
+//    @Before
+//    public void setup() {
+//        // given
+//        runFixtureScript(new PersonaEnumPersistAll<>(Owner_enum.class));
+//        dashboard = homePageProvider.dashboard();
+//    }
+//
+//    @Test
+//    public void happy_case() {
+//
+//        // given
+//        List<Visit> overdue = dashboard.getOverdue();
+//        assertThat(overdue).isNotEmpty();
+//
+//        // when
+//        wrap(dashboard).paid(overdue);
+//
+//        // then
+//        List<Visit> overdueAfter = dashboard.getOverdue();
+//        assertThat(overdueAfter).isEmpty();
+//
+//        for (Visit visit : overdue) {
+//            assertThat(visit.getDiagnosis()).isNotNull();
+//            assertThat(visit.getPaidOn()).isNotNull();
+//        }
+//    }
+//
+//    @Inject
+//    HomePageProvider homePageProvider;
+//}
+//----
+//
+//* Running the integration test at this point will produce a null pointer exception.
+//That's because the framework has had no opportunity to inject any domain services into the `Dashboard`.
+//+
+//Under normal runtime cases this doesn't matter because the only caller of the method is the framework itself, and when the domain object is rendered the framework will automatically ensure that any domain sevices are injected.
+//+
+//In an integration test this doesn't occur, and so we need to manually inject the services.
+//It makes most sense to do this in `HomePageProvider`; we use the framework-provided `ServiceRegistry2` domain service:
+//+
+//[source,java]
+//----
+//@HomePage
+//public Dashboard dashboard() {
+//    return serviceRegistry2.injectServicesInto(new Dashboard());
+//}
+//@Inject
+//ServiceRegistry2 serviceRegistry2;
+//----
+//
+//
+//
+//== Digression: Hiding Columns in Tables
+//
+//We could improve the dashboard a little.
+//After all, in the "overdue" collection there's no point in showing the "paidOn"; the value will always be null.
+//Also, the "reason" column is also somewhat superfluous (as, arguably, is the "diagnosis" column):
+//
+//image::Dashboard-overdue-ui-hints.png[width="800px",link="_images/Dashboard-overdue-ui-hints.png"]
+//
+//The framework offers two different ways to address this, so we'll show both.
+//
+//=== Solution
+//
+//[source,bash]
+//----
+//git checkout tags/370-digression-hiding-columns-in-tables
+//mvn clean package jetty:run
+//----
+//
+//
+//=== Exercise
+//
+//* The first technique is within the Java code; one could think of this as an implication within the "application layer".
+//+
+//We use a domain service that implements `TableColumnOrderService` as an SPI to "advise" the framework on how to render the collection.
+//Traditionally such classes are implemented as a nested static class, in this case of `Dashboard`:
+//+
+//[source,java]
+//----
+//@DomainService(nature = NatureOfService.DOMAIN)
+//public static class RemovePaidOnFromOverdue extends TableColumnOrderService.Default {
+//    @Override
+//    public List<String> orderParented(
+//            final Object parent,
+//            final String collectionId,
+//            final Class<?> collectionType,
+//            final List<String> propertyIds) {
+//        if (parent instanceof Dashboard && "overdue".equalsIgnoreCase(collectionId)) {
+//            propertyIds.remove("paidOn");
+//        }
+//        return propertyIds;
+//    }
+//}
+//----
+//+
+//The above code removes the "paidOn" column.
+//
+//* The second technique is to exploit the fact that the HTML generated by the framework is liberally annotated with domain class identifiers.
+//The column can therefore be removed by supplying the appropriate CSS.
+//We could think of this as an implementation within the presentation layer.
+//+
+//In the `src/main/webapp/css/application.css` file, add:
+//+
+//[source,css]
+//----
+//.domainapp-modules-impl-dashboard-Dashboard .entityCollection .overdue .Visit-reason {
+//    display: none;
+//}
+//----
+//
+//
+//
+//== Another Digression: Icons and CSS
+//
+//In the same way that titles can be specified imperatively, so too can icons, using the `iconName()` method.
+//One use case is for a domain object that has several states: the `iconName()` defines a suffix which is used to lookup different icons (eg "ToDoItem-notDone.png" and "ToDoItem-done.png").
+//
+//Similarly, it's possible to specify CSS hints imperatively using the `cssClass()`.
+//This returns a simple string that is added as a CSS class wherever the object is rendered in the UI.
+//
+//In this exercise we'll use a different icon for the various species of `Pet`:
+//
+//image::Pet-icons.png[width="800px",link="_images/Pet-icons.png"]
+//
+//Let's also use a strike-through text for all ``Visit``s that are paid when rendered within a collection:
+//
+//image::Visits-paid-strikethrough.png[width="800px",link="_images/Visits-paid-strikethrough.png"]
+//
+//
+//
+//
+//=== Solution
+//
+//[source,bash]
+//----
+//git checkout tags/380-another-digression-icons-and-css
+//mvn clean package jetty:run
+//----
+//
+//
+//
+//=== Exercise
+//
+//For the icons:
+//
+//* add new icons for each of the pet species: `Pet-dog.png`, `Pet-cat.png`, `Pet-hamster.dog` and `Pet-budgerigar.png`
+//
+//* add an `iconName()` method to `Pet`:
+//+
+//[source,java]
+//----
+//public String iconName() {
+//    return getPetSpecies().name().toLowerCase();
+//}
+//----
+//
+//
+//For the CSS class:
+//
+//* add a `cssClass()` method to `Visit`:
+//+
+//[source,java]
+//----
+//public String cssClass() {
+//    boolean isPaid = getPaidOn() != null;
+//    return isPaid ? "paid": null;
+//}
+//----
+//
+//
+//* update `application.css`:
+//
+//[source,css]
+//----
+//.entityCollection .domainapp-modules-impl-visits-dom-Visit .paid {
+//    text-decoration: line-through;
+//    color: lightgrey;
+//}
+//----
+//
+//
+//== Delete an `PetOwner` provided no unpaid ``Visit``s
+//
+//=== Solution
+//
+//[source,bash]
+//----
+//git checkout tags/390-delete-an-owner-provided-no-unpaid-visits
+//mvn clean package jetty:run
+//----
+//
+//
+//=== Exercise
+//
+//We don't want `PetOwner` (in the `pets` module) to check for unpaid ``Visit``s, because that would create a cyclic dependency between modules.
+//Instead, we'll use a subscriber in the `visits` module which can veto any attempt to delete an owner if there are unpaid visits.
+//
+//For this, we arrange for the `PetOwner` to emit an action domain event when its `delete()` action is invoked.
+//In fact, the event will be emitted by the framework up to five times: to check if the action is visible, if it is disabled, if it's valid, pre-execute and post-execute.
+//The subscriber in the ``visits`` module will therefore potentially veto on the disable phase.
+//
+//* in the `Visits` repository, add `findNotPaidBy` method to find any unpaid ``Visit``s for an `PetOwner`:
+//+
+//[source,java]
+//----
+//@Programmatic
+//public java.util.List<Visit> findNotPaidBy(Owner owner) {
+//    TypesafeQuery<Visit> q = isisJdoSupport.newTypesafeQuery(Visit.class);
+//    final QVisit cand = QVisit.candidate();
+//    q = q.filter(
+//            cand.paidOn.eq(q.parameter("paidOn", LocalDateTime.class)
+//        ).and(
+//                cand.pet.owner.eq(q.parameter("owner", Owner.class))
+//            )
+//    );
+//    return q.setParameter("paidOn", null)
+//            .setParameter("owner", owner)
+//            .executeList();
+//}
+//----
+//
+//* update `PetOwner`'s `delete()` action so that it emits an action domain event.
+//+
+//[source,java]
+//----
+//import org.apache.isis.applib.services.eventbus.ActionDomainEvent;
+//...
+//public static class Delete extends ActionDomainEvent<Owner> {}  // <1>
+//@Action(
+//        domainEvent = Delete.class                              // <2>
+//        semantics = SemanticsOf.NON_IDEMPOTENT                  // <3>
+//)
+//public void delete() {
+//    final String title = titleService.titleOf(this);
+//    messageService.informUser(String.format("'%s' deleted", title));
+//    repositoryService.removeAndFlush(this);
+//}
+//----
+//<1> declare the event, and
+//<2> emit it
+//<3> change from `NON_IDEMPOTENT_ARE_YOU_SURE` (due to a bug in the framework).
+//
+//* add a new integration test:
+//+
+//[source,java]
+//----
+//public class Owner_delete_IntegTest extends PetClinicModuleIntegTestAbstract {
+//
+//    @Test
+//    public void can_delete_if_there_are_no_unpaid_visits() {
+//
+//        // given
+//        runFixtureScript(Owner_enum.FRED_HUGHES.builder());
+//
+//        Owner owner = Owner_enum.FRED_HUGHES.findUsing(serviceRegistry);
+//        List<Visit> any = visits.findNotPaidBy(owner);
+//        assertThat(any).isEmpty();
+//
+//        // when
+//        wrap(owner).delete();
+//
+//        // then
+//        Owner ownerAfter = Owner_enum.FRED_HUGHES.findUsing(serviceRegistry);
+//        assertThat(ownerAfter).isNull();
+//    }
+//
+//    @Test
+//    public void cannot_delete_with_unpaid_visits() {
+//
+//        // given
+//        runFixtureScript(Owner_enum.MARY_JONES.builder());
+//
+//        Owner owner = Owner_enum.MARY_JONES.findUsing(serviceRegistry);
+//        List<Visit> any = visits.findNotPaidBy(owner);
+//        assertThat(any).isNotEmpty();
+//
+//        // expect
+//        expectedExceptions.expect(DisabledException.class);
+//        expectedExceptions.expectMessage("This owner still has unpaid visit(s)");
+//
+//        // when
+//        wrap(owner).delete();
+//    }
+//
+//    @Inject
+//    Visits visits;
+//}
+//----
+//
+//* add the subscriber to veto the action if required:
+//+
+//[source,java]
+//----
+//@DomainService(nature = NatureOfService.DOMAIN)
+//public class VetoDeleteOfOwnerWithUnpaidVisits
+//        extends org.apache.isis.applib.AbstractSubscriber {
+//
+//    @org.axonframework.eventhandling.annotation.EventHandler
+//    public void on(Owner.Delete ev) {
+//
+//        switch (ev.getEventPhase()) {
+//        case DISABLE:
+//            Collection<Visit> visitsForPet = visits.findNotPaidBy(ev.getSource());
+//            if (!visitsForPet.isEmpty()) {
+//                ev.veto("This owner still has unpaid visit(s)");
+//            }
+//            break;
+//        }
+//    }
+//
+//    @javax.inject.Inject
+//    Visits visits;
+//}
+//----
+//
+//* finally, in `PetClinicModuleIntegTestAbstract`, we need to make a small adjustment to use the same event bus implementation as the production app:
+//+
+//[source,java]
+//----
+//super(new PetClinicModule()
+//    .withAdditionalServices(DeploymentCategoryProviderForTesting.class)
+//    .withConfigurationProperty("isis.services.eventbus.implementation","axon")      // <1>
+//    .withConfigurationProperty(TranslationServicePo.KEY_PO_MODE, "write")
+//);
+//----
+//<1> specify Axon as the event bus implementation
+//
+//
+//
+//
diff --git a/antora/components/tutorials/modules/petclinic/pages/i18n.adoc b/antora/components/tutorials/modules/petclinic/pages/i18n.adoc
index e289832..ef223b4 100644
--- a/antora/components/tutorials/modules/petclinic/pages/i18n.adoc
+++ b/antora/components/tutorials/modules/petclinic/pages/i18n.adoc
@@ -2,5 +2,5 @@
 
 :Notice: Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at. http://www.apache.org/licenses/LICENSE-2.0 . Unless required by applicable law or ag [...]
 
-TODO.
+CAUTION: TODO.
 
diff --git a/antora/components/tutorials/modules/petclinic/pages/architecture-rules.adoc b/antora/components/tutorials/modules/petclinic/pages/restful-api.adoc
similarity index 96%
copy from antora/components/tutorials/modules/petclinic/pages/architecture-rules.adoc
copy to antora/components/tutorials/modules/petclinic/pages/restful-api.adoc
index 257a961..1f74d13 100644
--- a/antora/components/tutorials/modules/petclinic/pages/architecture-rules.adoc
+++ b/antora/components/tutorials/modules/petclinic/pages/restful-api.adoc
@@ -1,6 +1,6 @@
-= architecture rules
+= Restful API
 
 :Notice: Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at. http://www.apache.org/licenses/LICENSE-2.0 . Unless required by applicable law or ag [...]
 
-TODO.
+CAUTION: TODO.