Document the Rhythm service viz beats and clockoffsets
diff --git a/.gitignore b/.gitignore
index f9d7cba..cc730d5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,10 @@
 .idea
 build/
 target/
+api/out/
+component-test/out/
+service/out/
+spi/out/
 
 # Ignore Gradle GUI config
 gradle-app.setting
diff --git a/component-test/build.gradle b/component-test/build.gradle
index 0e9f6be..0f34ef6 100644
--- a/component-test/build.gradle
+++ b/component-test/build.gradle
@@ -26,6 +26,7 @@
 
     dependencies {
         classpath ("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
+        classpath("org.asciidoctor:asciidoctor-gradle-plugin:1.5.3")
     }
 }
 
@@ -34,6 +35,7 @@
     id("org.nosphere.apache.rat") version "0.3.1"
 }
 apply from: '../shared.gradle'
+apply plugin: 'org.asciidoctor.convert'
 
 dependencies {
     compile(
@@ -43,10 +45,19 @@
             [group: 'org.apache.fineract.cn', name: 'api', version: versions.frameworkapi],
             [group: 'org.apache.fineract.cn', name: 'test', version: versions.frameworktest],
             [group: 'org.apache.fineract.cn', name: 'lang', version: versions.frameworklang],
-            [group: 'org.springframework.boot', name: 'spring-boot-starter-test']
+            [group: 'org.springframework.boot', name: 'spring-boot-starter-test'],
+            [group: 'org.springframework.restdocs', name: 'spring-restdocs-mockmvc'],
+            [group: 'junit', name: 'junit', version: '4.12']
     )
 }
 
+asciidoctor {
+    sourceDir 'build/doc/asciidoc/'
+    outputDir 'build/doc/html5'
+    options backend: "html", doctype: "book"
+    attributes "source-highlighter": "highlightjs", 'snippets': file('build/doc/generated-snippets/')
+}
+
 publishing {
     publications {
         mavenJava(MavenPublication) {
diff --git a/component-test/src/main/java/org/apache/fineract/cn/rhythm/RhythmApiDocumentation.java b/component-test/src/main/java/org/apache/fineract/cn/rhythm/RhythmApiDocumentation.java
new file mode 100644
index 0000000..810d376
--- /dev/null
+++ b/component-test/src/main/java/org/apache/fineract/cn/rhythm/RhythmApiDocumentation.java
@@ -0,0 +1,292 @@
+/*
+ * 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 agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.cn.rhythm;
+
+import com.google.gson.Gson;
+import org.apache.fineract.cn.rhythm.api.v1.domain.Beat;
+import org.apache.fineract.cn.rhythm.api.v1.domain.ClockOffset;
+import org.apache.fineract.cn.rhythm.api.v1.events.EventConstants;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Matchers;
+import org.mockito.Mockito;
+import org.mockito.internal.stubbing.answers.Returns;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.restdocs.JUnitRestDocumentation;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.context.WebApplicationContext;
+
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.util.List;
+
+import static org.apache.fineract.cn.lang.config.TenantHeaderFilter.TENANT_HEADER;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
+import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post;
+import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.put;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete;
+import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;
+import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
+import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
+import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
+
+public class RhythmApiDocumentation extends AbstractRhythmTest {
+  @Rule
+  public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/doc/generated-snippets/test-rhythm");
+
+  @Autowired
+  private WebApplicationContext context;
+
+  private MockMvc mockMvc;
+
+  @Before
+  public void setUp ( ) {
+    this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
+            .apply(documentationConfiguration(this.restDocumentation))
+            .build();
+  }
+
+  @Test
+  public void documentCreateBeat ( ) throws Exception {
+    final String applicationIdentifier = "funnybusiness-v1";
+    final String oldBeatId = "oldBeatIdentifier123";
+
+    final LocalDateTime now = LocalDateTime.now(ZoneId.of("UTC"));
+    int alignmentHour = now.getHour();
+    final LocalDateTime expectedBeatTimestamp = getExpectedBeatTimestamp(now, alignmentHour);
+
+    Mockito.doAnswer(new Returns(true)).when(super.beatPublisherServiceMock).publishBeat(
+            Matchers.eq(oldBeatId),
+            Matchers.eq(tenantDataStoreContext.getTenantName()),
+            Matchers.eq(applicationIdentifier),
+            Matchers.eq(expectedBeatTimestamp));
+
+    Beat newBeat = new Beat();
+    newBeat.setIdentifier(oldBeatId);
+    newBeat.setAlignmentHour(expectedBeatTimestamp.getHour());
+
+    Gson gson = new Gson();
+    this.mockMvc.perform(post("/applications/" + applicationIdentifier + "/beats")
+            .accept(MediaType.APPLICATION_JSON_VALUE)
+            .contentType(MediaType.APPLICATION_JSON_VALUE)
+            .header(TENANT_HEADER, tenantDataStoreContext.getTenantName())
+            .content(gson.toJson(newBeat)))
+            .andExpect(status().isAccepted())
+            .andDo(document(
+                    "document-create-beat", preprocessRequest(prettyPrint()),
+                    requestFields(
+                            fieldWithPath("identifier").description("Beat Identifier"),
+                            fieldWithPath("alignmentHour").description("Beat Alignment Hour")
+                    )
+            ));
+  }
+
+  @Test
+  public void documentGetBeat ( ) throws InterruptedException {
+    final String applicationIdentifier = "funnybusiness-v2";
+    final String oldBeatId = "oldBeatIdentifier789";
+
+    final LocalDateTime now = LocalDateTime.now(ZoneId.of("UTC"));
+    int alignmentHour = now.getHour();
+    final LocalDateTime expectedBeatTimestamp = getExpectedBeatTimestamp(now, alignmentHour);
+
+    Mockito.doAnswer(new Returns(true)).when(super.beatPublisherServiceMock).publishBeat(
+            Matchers.eq(oldBeatId),
+            Matchers.eq(tenantDataStoreContext.getTenantName()),
+            Matchers.eq(applicationIdentifier),
+            Matchers.eq(expectedBeatTimestamp));
+
+    final Beat createdBeat = createBeat(applicationIdentifier, oldBeatId, alignmentHour, expectedBeatTimestamp);
+
+    try {
+      this.mockMvc.perform(get("/applications/" + applicationIdentifier + "/beats/" + createdBeat.getIdentifier())
+              .accept(MediaType.APPLICATION_JSON_VALUE)
+              .contentType(MediaType.APPLICATION_JSON_VALUE)
+              .header(TENANT_HEADER, tenantDataStoreContext.getTenantName()))
+              .andExpect(status().isOk())
+              .andDo(document(
+                      "document-get-beat", preprocessRequest(prettyPrint()),
+                      responseFields(
+                              fieldWithPath("identifier").description("Beat Identifier"),
+                              fieldWithPath("alignmentHour").description("Beat Alignment Hour")
+                      )
+              ));
+    } catch (Exception e) {
+      e.printStackTrace();
+    }
+  }
+
+  @Test
+  public void documentGetBeatsForApplication ( ) throws InterruptedException {
+    final String applicationIdentifier = "interestingbusiness-v1";
+    final String oldBeatId = "oldBeatIdentifier789";
+    final String newBeatId = "newBeatIdentifier456";
+
+    final LocalDateTime nowOne = LocalDateTime.now(ZoneId.of("UTC"));
+    int alignmentHourOne = nowOne.getHour();
+    final LocalDateTime expectedBeatTimestamp = getExpectedBeatTimestamp(nowOne, alignmentHourOne);
+
+    final LocalDateTime nowTwo = LocalDateTime.now(ZoneOffset.ofHours(2));
+    int alignmentHourTwo = nowTwo.getHour();
+    final LocalDateTime beatTimestamp = getExpectedBeatTimestamp(nowTwo, alignmentHourTwo);
+
+    Mockito.doAnswer(new Returns(true)).when(super.beatPublisherServiceMock).publishBeat(
+            Matchers.eq(oldBeatId),
+            Matchers.eq(tenantDataStoreContext.getTenantName()),
+            Matchers.eq(applicationIdentifier),
+            Matchers.eq(expectedBeatTimestamp));
+
+    final Beat firstBeat = createBeat(applicationIdentifier, oldBeatId, alignmentHourOne, expectedBeatTimestamp);
+    final Beat secondBeat = createBeat(applicationIdentifier, newBeatId, alignmentHourTwo, beatTimestamp);
+
+    try {
+      this.mockMvc.perform(get("/applications/" + applicationIdentifier + "/beats/")
+              .accept(MediaType.APPLICATION_JSON_VALUE)
+              .contentType(MediaType.APPLICATION_JSON_VALUE)
+              .header(TENANT_HEADER, tenantDataStoreContext.getTenantName()))
+              .andExpect(status().isOk())
+              .andDo(document(
+                      "document-get-beats", preprocessRequest(prettyPrint()),
+                      responseFields(
+                              fieldWithPath("[].identifier").description("First Beat's Identifier"),
+                              fieldWithPath("[].alignmentHour").description("First Beat's Alignment Hour"),
+                              fieldWithPath("[1].identifier").description("Second Beat's Identifier"),
+                              fieldWithPath("[1].alignmentHour").description("Second Beat's Alignment Hour")
+                      )
+              ));
+    } catch (Exception e) {
+      e.printStackTrace();
+    }
+  }
+
+  @Test
+  public void documentDeleteBeat ( ) throws InterruptedException {
+    final String applicationIdentifier = "interestingbusiness-v2";
+    final String oldBeatId = "beatIdentifier789";
+
+    final LocalDateTime now = LocalDateTime.now(ZoneId.of("UTC"));
+    int alignmentHour = now.getHour();
+    final LocalDateTime expectedBeatTimestamp = getExpectedBeatTimestamp(now, alignmentHour);
+
+    Mockito.doAnswer(new Returns(true)).when(super.beatPublisherServiceMock).publishBeat(
+            Matchers.eq(oldBeatId),
+            Matchers.eq(tenantDataStoreContext.getTenantName()),
+            Matchers.eq(applicationIdentifier),
+            Matchers.eq(expectedBeatTimestamp));
+
+    final Beat createdBeat = createBeat(applicationIdentifier, oldBeatId, alignmentHour, expectedBeatTimestamp);
+
+    final List <Beat> allEntities = super.testSubject.getAllBeatsForApplication(applicationIdentifier);
+    Assert.assertTrue(allEntities.contains(createdBeat));
+
+    try {
+      this.mockMvc.perform(delete("/applications/" + applicationIdentifier + "/beats/" + createdBeat.getIdentifier())
+              .accept(MediaType.ALL_VALUE)
+              .contentType(MediaType.ALL_VALUE)
+              .header(TENANT_HEADER, tenantDataStoreContext.getTenantName()))
+              .andExpect(status().isAccepted())
+              .andDo(document("document-delete-beat"));
+    } catch (Exception e) {
+      e.printStackTrace();
+    }
+  }
+
+  @Test
+  public void documentDeleteApplication ( ) throws InterruptedException {
+    final String applicationIdentifier = "goodbusiness-v1";
+    super.createBeatForThisHour(applicationIdentifier, "beatDefender");
+
+    List <Beat> allEntities = this.testSubject.getAllBeatsForApplication(applicationIdentifier);
+    Assert.assertTrue(allEntities.size() == 1);
+
+    try {
+      this.mockMvc.perform(delete("/applications/" + applicationIdentifier)
+              .accept(MediaType.ALL_VALUE)
+              .contentType(MediaType.ALL_VALUE)
+              .header(TENANT_HEADER, tenantDataStoreContext.getTenantName()))
+              .andExpect(status().isAccepted())
+              .andDo(document("document-delete-application"));
+    } catch (Exception e) {
+      e.printStackTrace();
+    }
+
+    this.testSubject.deleteApplication(applicationIdentifier);
+    Assert.assertTrue(this.eventRecorder.wait(EventConstants.DELETE_APPLICATION, applicationIdentifier));
+
+    allEntities = this.testSubject.getAllBeatsForApplication(applicationIdentifier);
+    Assert.assertTrue(allEntities.isEmpty());
+  }
+
+  @Test
+  public void documentSetClockOffset ( ) throws InterruptedException {
+    final LocalDateTime now = LocalDateTime.now(ZoneOffset.ofHours(6));
+    ClockOffset clockOffset = new ClockOffset(now.getHour(), now.getMinute(), now.getSecond());
+
+    Gson gson = new Gson();
+    try {
+      this.mockMvc.perform(put("/clockoffset")
+              .accept(MediaType.ALL_VALUE)
+              .contentType(MediaType.APPLICATION_JSON_VALUE)
+              .content(gson.toJson(clockOffset))
+              .header(TENANT_HEADER, tenantDataStoreContext.getTenantName()))
+              .andExpect(status().isAccepted())
+              .andDo(document(
+                      "document-set-clockoffset", preprocessRequest(prettyPrint()),
+                      requestFields(
+                              fieldWithPath("hours").type("Integer").description("Clock Offset Hour"),
+                              fieldWithPath("minutes").type("Integer").description("Clock Offset Minutes"),
+                              fieldWithPath("seconds").type("Integer").description("Clock Offset Seconds")
+                      )
+              ));
+    } catch (Exception e) {
+      e.printStackTrace();
+    }
+  }
+
+  @Test
+  public void documentGetClockOffset ( ) {
+    try {
+      this.mockMvc.perform(get("/clockoffset")
+              .accept(MediaType.ALL_VALUE)
+              .contentType(MediaType.APPLICATION_JSON_VALUE)
+              .header(TENANT_HEADER, tenantDataStoreContext.getTenantName()))
+              .andExpect(status().isOk())
+              .andDo(document(
+                      "document-get-clockoffset", preprocessResponse(prettyPrint()),
+                      responseFields(
+                              fieldWithPath("hours").description("Clock Offset Hour"),
+                              fieldWithPath("minutes").description("Clock Offset Minutes"),
+                              fieldWithPath("seconds").description("Clock Offset Seconds")
+                      )
+              ));
+    } catch (Exception e) {
+      e.printStackTrace();
+    }
+  }
+}