Browse Source

add properly divorced bank + account type logic

Grega Bremec 2 years ago
parent
commit
9912264c1e

+ 136 - 0
red-hat-bank/bank-intranet/pom.xml

@@ -0,0 +1,136 @@
+<?xml version="1.0"?>
+<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>com.redhat.training</groupId>
+  <artifactId>bank-intranet</artifactId>
+  <version>1.0.0-SNAPSHOT</version>
+  <properties>
+    <compiler-plugin.version>3.8.1</compiler-plugin.version>
+    <maven.compiler.parameters>true</maven.compiler.parameters>
+    <maven.compiler.source>11</maven.compiler.source>
+    <maven.compiler.target>11</maven.compiler.target>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+    <quarkus.platform.artifact-id>quarkus-universe-bom</quarkus.platform.artifact-id>
+    <quarkus.platform.group-id>io.quarkus</quarkus.platform.group-id>
+    <quarkus.platform.version>2.1.4.Final</quarkus.platform.version>
+    <surefire-plugin.version>3.0.0-M5</surefire-plugin.version>
+  </properties>
+  <dependencyManagement>
+    <dependencies>
+      <dependency>
+        <groupId>${quarkus.platform.group-id}</groupId>
+        <artifactId>${quarkus.platform.artifact-id}</artifactId>
+        <version>${quarkus.platform.version}</version>
+        <type>pom</type>
+        <scope>import</scope>
+      </dependency>
+    </dependencies>
+  </dependencyManagement>
+  <dependencies>
+    <dependency>
+      <groupId>io.quarkus</groupId>
+      <artifactId>quarkus-apicurio-registry-avro</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.quarkus</groupId>
+      <artifactId>quarkus-smallrye-reactive-messaging-kafka</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.quarkus</groupId>
+      <artifactId>quarkus-arc</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.quarkus</groupId>
+      <artifactId>quarkus-hibernate-orm-panache</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.quarkus</groupId>
+      <artifactId>quarkus-jdbc-h2</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.quarkus</groupId>
+      <artifactId>quarkus-resteasy-jackson</artifactId>
+    </dependency>
+  </dependencies>
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>${quarkus.platform.group-id}</groupId>
+        <artifactId>quarkus-maven-plugin</artifactId>
+        <version>${quarkus.platform.version}</version>
+        <extensions>true</extensions>
+        <executions>
+          <execution>
+            <goals>
+              <goal>build</goal>
+              <goal>generate-code</goal>
+              <goal>generate-code-tests</goal>
+            </goals>
+          </execution>
+        </executions>
+        <dependencies>
+          <dependency>
+            <groupId>jline</groupId>
+            <artifactId>jline</artifactId>
+            <version>2.14.6</version>
+          </dependency>
+        </dependencies>
+      </plugin>
+      <plugin>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <version>${compiler-plugin.version}</version>
+        <configuration>
+          <parameters>${maven.compiler.parameters}</parameters>
+        </configuration>
+      </plugin>
+      <plugin>
+        <artifactId>maven-surefire-plugin</artifactId>
+        <version>${surefire-plugin.version}</version>
+        <configuration>
+          <systemPropertyVariables>
+            <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
+            <maven.home>${maven.home}</maven.home>
+          </systemPropertyVariables>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+  <profiles>
+    <profile>
+      <id>native</id>
+      <activation>
+        <property>
+          <name>native</name>
+        </property>
+      </activation>
+      <build>
+        <plugins>
+          <plugin>
+            <artifactId>maven-failsafe-plugin</artifactId>
+            <version>${surefire-plugin.version}</version>
+            <executions>
+              <execution>
+                <goals>
+                  <goal>integration-test</goal>
+                  <goal>verify</goal>
+                </goals>
+                <configuration>
+                  <systemPropertyVariables>
+                    <native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
+                    <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
+                    <maven.home>${maven.home}</maven.home>
+                  </systemPropertyVariables>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+      <properties>
+        <quarkus.package.type>native</quarkus.package.type>
+      </properties>
+    </profile>
+  </profiles>
+</project>

+ 20 - 0
red-hat-bank/bank-intranet/src/main/avro/new-bank-account.avsc

@@ -0,0 +1,20 @@
+{
+  "namespace": "com.redhat.training.bank.message",
+  "type": "record",
+  "name": "NewBankAccount",
+  "fields": [
+    {
+      "name": "id",
+      "type": "long"
+    },
+    {
+      "name": "balance",
+      "type": "long"
+    },
+    {
+      "name": "type",
+      "type": ["null", "string"],
+      "default": null
+    }
+  ]
+}

+ 14 - 0
red-hat-bank/bank-intranet/src/main/java/com/redhat/training/bank/command/DepositAmountInBankAccount.java

@@ -0,0 +1,14 @@
+package com.redhat.training.bank.command;
+
+public class DepositAmountInBankAccount {
+    public Long id;
+    public int amount;
+
+    public DepositAmountInBankAccount() {
+    }
+
+    public DepositAmountInBankAccount(Long id, int amount) {
+        this.id     = id;
+        this.amount = amount;
+    }
+}

+ 14 - 0
red-hat-bank/bank-intranet/src/main/java/com/redhat/training/bank/command/WithdrawAmountFromBankAccount.java

@@ -0,0 +1,14 @@
+package com.redhat.training.bank.command;
+
+public class WithdrawAmountFromBankAccount {
+    public Long id;
+    public int amount;
+
+    public WithdrawAmountFromBankAccount() {
+    }
+
+    public WithdrawAmountFromBankAccount(Long id, int amount) {
+        this.id     = id;
+        this.amount = amount;
+    }
+}

+ 34 - 0
red-hat-bank/bank-intranet/src/main/java/com/redhat/training/bank/consumer/UpdatedAccountConsumer.java

@@ -0,0 +1,34 @@
+package com.redhat.training.bank.consumer;
+
+import com.redhat.training.bank.message.NewBankAccount;
+import com.redhat.training.bank.model.BankAccount;
+import io.smallrye.common.annotation.Blocking;
+import org.eclipse.microprofile.reactive.messaging.Incoming;
+import org.jboss.logging.Logger;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.transaction.Transactional;
+
+@ApplicationScoped
+public class UpdatedAccountConsumer {
+    private static final Logger LOGGER = Logger.getLogger(UpdatedAccountConsumer.class);
+
+    @Incoming("account-update")
+    @Blocking
+    @Transactional
+    public void processMessage(NewBankAccount message) {
+
+        BankAccount entity = BankAccount.findById(message.getId());
+
+        if (entity != null) {
+            entity.profile = message.getType();
+            LOGGER.info(
+                    "Received Updated Bank Account - ID: "
+                    + entity.id + " - Type: " + entity.profile
+            );
+            entity.persist();
+        } else {
+            LOGGER.info("Bank Account not found!");
+        }
+    }
+}

+ 23 - 0
red-hat-bank/bank-intranet/src/main/java/com/redhat/training/bank/model/BankAccount.java

@@ -0,0 +1,23 @@
+package com.redhat.training.bank.model;
+
+import io.quarkus.hibernate.orm.panache.PanacheEntity;
+
+import javax.persistence.Cacheable;
+import javax.persistence.Entity;
+
+@Entity
+@Cacheable
+public class BankAccount extends PanacheEntity {
+
+    public Long balance;
+
+    public String profile;
+
+    public BankAccount() {
+    }
+
+    public BankAccount(Long balance, String profile) {
+        this.balance = balance;
+        this.profile = profile;
+    }
+}

+ 228 - 0
red-hat-bank/bank-intranet/src/main/java/com/redhat/training/bank/resource/AccountResource.java

@@ -0,0 +1,228 @@
+package com.redhat.training.bank.resource;
+
+import java.util.List;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.inject.Inject;
+import javax.transaction.*;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import com.redhat.training.bank.message.NewBankAccount;
+import com.redhat.training.bank.model.BankAccount;
+import com.redhat.training.bank.command.DepositAmountInBankAccount;
+import com.redhat.training.bank.command.WithdrawAmountFromBankAccount;
+import org.jboss.logging.Logger;
+import org.eclipse.microprofile.reactive.messaging.Channel;
+import org.eclipse.microprofile.reactive.messaging.Emitter;
+import org.jboss.resteasy.annotations.jaxrs.PathParam;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+import io.quarkus.panache.common.Sort;
+
+@Path("accounts")
+@ApplicationScoped
+@Produces("application/json")
+@Consumes("application/json")
+public class AccountResource {
+
+    private static final Logger LOGGER = Logger.getLogger(AccountResource.class.getName());
+
+    @Inject @Channel("new-bank-account-out")
+    Emitter<NewBankAccount> emitter;
+
+    @GET
+    public List<BankAccount> get() {
+        return BankAccount.listAll(Sort.by("id"));
+    }
+
+    @GET
+    @Path("{id}")
+    public BankAccount getSingle(@PathParam Long id) {
+        BankAccount entity = BankAccount.findById(id);
+
+        if (entity == null) {
+            throw new WebApplicationException(
+                    "Bank Account with id of " + id + " does not exist.",
+                    Response.Status.NOT_FOUND
+            );
+        }
+
+        return entity;
+    }
+
+    @POST
+    public Response create(BankAccount bankAccount) {
+
+        createBankAccount(bankAccount);
+        sendMessageAboutNewBankAccount(bankAccount);
+
+        return Response.ok(bankAccount).status(Response.Status.CREATED).build();
+    }
+
+    @PUT
+    @Path("{id}/deposit")
+    @Transactional
+    public BankAccount deposit(@PathParam Long id, DepositAmountInBankAccount deposit) {
+
+        if (deposit.id == null) {
+            throw new WebApplicationException(
+                    "Bank Account ID was not set on request.",
+                    Response.Status.BAD_REQUEST
+                    );
+        }
+
+        if (deposit.amount <= 0) {
+            throw new WebApplicationException(
+                    "Unprocessable amount on request.",
+                    Response.Status.BAD_REQUEST
+            );
+        }
+
+        BankAccount entity = BankAccount.findById(id);
+
+        if (entity == null) {
+            throw new WebApplicationException(
+                    "Bank Account with id of " + id + " does not exist.",
+                    Response.Status.NOT_FOUND
+            );
+        }
+
+        entity.balance = entity.balance + deposit.amount;
+
+        return entity;
+    }
+
+    @PUT
+    @Path("{id}/withdraw")
+    @Transactional
+    public BankAccount withdraw(@PathParam Long id, WithdrawAmountFromBankAccount withdraw) {
+
+        if (withdraw.id == null) {
+            throw new WebApplicationException(
+                    "Bank Account ID was not set on request.",
+                    Response.Status.BAD_REQUEST
+            );
+        }
+
+        if (withdraw.amount <= 0) {
+            throw new WebApplicationException(
+                    "Unprocessable amount on request.",
+                    Response.Status.BAD_REQUEST
+            );
+        }
+
+        BankAccount entity = BankAccount.findById(id);
+
+        if (entity == null) {
+            throw new WebApplicationException(
+                    "Bank Account with id of " + id + " does not exist.",
+                    Response.Status.NOT_FOUND
+            );
+        }
+
+        if (entity.balance < withdraw.amount) {
+            throw new WebApplicationException(
+                    "Insufficient funds for withdraw.",
+                    Response.Status.CONFLICT
+            );
+        }
+
+        entity.balance = entity.balance - withdraw.amount;
+
+        return entity;
+    }
+
+    @DELETE
+    @Path("{id}")
+    @Transactional
+    public Response delete(@PathParam Long id) {
+        BankAccount entity = BankAccount.findById(id);
+
+        if (entity == null) {
+            throw new WebApplicationException(
+                    "Bank Account with id of " + id + " does not exist.",
+                    Response.Status.NOT_FOUND
+            );
+        }
+
+        entity.delete();
+        LOGGER.info("Deleted bank account - ID: " + id);
+
+        return Response.status(Response.Status.NO_CONTENT).build();
+    }
+
+    @Provider
+    public static class ErrorMapper implements ExceptionMapper<Exception> {
+
+        @Inject
+        ObjectMapper objectMapper;
+
+        @Override
+        public Response toResponse(Exception exception) {
+            int code = 500;
+            if (exception instanceof WebApplicationException) {
+                code = ((WebApplicationException) exception).getResponse().getStatus();
+            }
+
+            ObjectNode exceptionJson = objectMapper.createObjectNode();
+            exceptionJson.put("exceptionType", exception.getClass().getName());
+            exceptionJson.put("code", code);
+
+            if (exception.getMessage() != null) {
+                exceptionJson.put("error", exception.getMessage());
+            }
+
+            return Response.status(code)
+                    .entity(exceptionJson)
+                    .build();
+        }
+    }
+
+    @Transactional
+    public void createBankAccount(BankAccount bankAccount) {
+        if (bankAccount.balance == null || bankAccount.balance < 0) {
+            throw new WebApplicationException(
+                    "Invalid amount to open a bank account.",
+                    Response.Status.BAD_REQUEST
+            );
+        }
+
+        if (bankAccount.id != null) {
+            throw new WebApplicationException(
+                    "Id was invalidly set on request.",
+                    Response.Status.BAD_REQUEST
+            );
+        }
+
+        bankAccount.persist();
+    }
+
+    private void sendMessageAboutNewBankAccount(BankAccount bankAccount)
+    {
+        LOGGER.info(
+                "New Bank Account - ID: " + bankAccount.id
+                + ", Balance: " + bankAccount.balance
+        );
+
+        // TODO: Send a message about the new bank account
+        emitter.send(
+                new NewBankAccount(
+                        bankAccount.id,
+                        bankAccount.balance,
+                        null
+                )
+        );
+    }
+}

+ 106 - 0
red-hat-bank/bank-intranet/src/main/resources/META-INF/resources/css/style.css

@@ -0,0 +1,106 @@
+body {
+    background: #f5f5f5;
+    color: #202020;
+}
+
+.container {
+    max-width: 800px;
+}
+
+.nav {
+    margin-top: 5rem;
+}
+
+.nav-item a {
+    color: #797979;
+    text-decoration: none;
+    font-family: "Consolas", "Monaco", "Menlo", monospace;
+}
+
+.nav-item a:hover {
+    cursor: pointer;
+    color: #4d4a4a;
+    border: none;
+}
+
+.hero {
+    color: #363636;
+    height: 80vh;
+}
+
+.hero div {
+    margin-bottom: 5rem;
+}
+
+.hero h3 {
+    font-family: "Edelsans", sans-serif;
+    font-size: 5rem;
+    margin-bottom: 0;
+}
+
+.hero h5 {
+    margin-top: 1rem;
+    margin-bottom: 0;
+    font-weight: 200;
+}
+
+.grid-demo-col {
+    background: #e4e4e4;
+}
+
+.header {
+    font-weight: 200;
+}
+
+.content {
+    font-size: 1.8rem;
+}
+
+.card {
+    background: #FAFAFA;
+}
+
+.string {
+    color: #02C1A7;
+}
+.special {
+    color: #AC2EC5;
+}
+.global {
+    color: #db9114;
+}
+.method {
+    color: #00A2CD;
+}
+.comment {
+    color: #989898;
+}
+
+/* === Footer === */
+footer {
+    margin: 50px;
+}
+
+#footer-link {
+    font-size: 16px;
+    color: #111111;
+    text-decoration: none;
+}
+
+#footer-made, #footer-name, #footer-logo {
+    margin-top: 0;
+    margin-bottom: 0;
+    vertical-align: middle;
+}
+
+#footer-name {
+    font-weight: 700;
+}
+
+#footer-logo {
+    height: 25px;
+    width: 25px;
+    margin-left: 10px;
+    margin-right: 10px;
+    border-radius: 3px;
+}

+ 544 - 0
red-hat-bank/bank-intranet/src/main/resources/META-INF/resources/css/wing.css

@@ -0,0 +1,544 @@
+/*
+* Wing 1.0.0-beta
+* Copyright 2016, Kabir Shah
+* http://usewing.ml/
+* Free to use under the MIT license.
+* https://kingpixil.github.io/license
+*/
+
+/*------------------------------------------------------------
+  Base Style
+------------------------------------------------------------*/
+
+html {
+    box-sizing: border-box;
+    font-size: 62.5%;
+    margin: 0;
+    padding: 0;
+}
+
+body {
+    letter-spacing: 0.01em;
+    line-height: 1.6;
+    font-size: 1.5em;
+    font-weight: 400;
+    margin: 0;
+    font-family: -apple-system, BlinkMacSystemFont, Avenir, "Avenir Next", "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
+}
+
+/*------------------------------------------------------------
+  Typography
+------------------------------------------------------------*/
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+    font-weight: 400;
+    font-family: -apple-system, BlinkMacSystemFont, Avenir, "Avenir Next", "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
+}
+
+h1, h2, h3 {
+    letter-spacing: -.1rem;
+}
+
+h1 {
+    font-size: 4.0rem;
+    line-height: 1.2;
+}
+
+h2 {
+    font-size: 3.6rem;
+    line-height: 1.25;
+}
+
+h3 {
+    font-size: 3.0rem;
+    line-height: 1.3;
+}
+
+h4 {
+    font-size: 2.4rem;
+    line-height: 1.35;
+    letter-spacing: -.08rem;
+}
+
+h5 {
+    font-size: 1.8rem;
+    line-height: 1.5;
+    letter-spacing: -.05rem;
+}
+
+h6 {
+    font-size: 1.5rem;
+    line-height: 1.6;
+    letter-spacing: 0;
+}
+
+@media (min-width: 550px) {
+    h1 {
+        font-size: 5.0rem;
+    }
+    h2 {
+        font-size: 4.2rem;
+    }
+    h3 {
+        font-size: 3.6rem;
+    }
+    h4 {
+        font-size: 3.0rem;
+    }
+    h5 {
+        font-size: 2.4rem;
+    }
+    h6 {
+        font-size: 1.5rem;
+    }
+}
+
+/*------------------------------------------------------------
+  Links
+------------------------------------------------------------*/
+a {
+    color: #104cfb;
+    transition: all .1s ease;
+}
+
+a:hover {
+    cursor: pointer;
+    color: #222222;
+}
+
+/*------------------------------------------------------------
+  Buttons
+------------------------------------------------------------*/
+
+button, [type=submit] {
+    padding: 1.1rem 3.5rem;
+    margin: 1rem 0;
+    background: #111111;
+    color: #f5f5f5;
+    border-radius: 2px;
+    border: none;
+    font-size: 1.3rem;
+    transition: all .2s ease;
+}
+
+button.outline, [type=submit].outline {
+    padding: 1.1rem 3.5rem;
+    background: none;
+    color: #111111;
+    border: 1.5px solid #111111;
+}
+
+button:hover, [type=submit]:hover {
+    background: #222222;
+}
+
+button.outline:hover, [type=submit].outline:hover {
+    background: none;
+    border: 1.5px solid #444444;
+    color: #444444;
+}
+
+button:focus, [type=submit]:focus {
+    outline: none;
+}
+
+button:active, [type=submit]:active {
+    transform: scale(.99);
+}
+
+/*------------------------------------------------------------
+  Forms
+------------------------------------------------------------*/
+
+input[type=text], input[type=password], input[type=email], input[type=search], input[type=number], input[type=file], input[type=tel], select, textarea, textarea[type=text] {
+    width: 100%;
+    height: 45px;
+    padding: 10px 10px;
+    margin-top: 1rem;
+    margin-bottom: 1rem;
+    background: #FFFFFF;
+    border-radius: 2px;
+    border: 1px solid #a4a4a4;
+    font-size: 1.3rem;
+    box-sizing: border-box;
+    transition: all .2s ease;
+}
+
+input[type=text]:hover, input[type=password]:hover, input[type=email]:hover, input[type=search]:hover, input[type=number]:hover, input[type=file], input[type=tel], select:hover, textarea:hover, textarea[type=text]:hover {
+    border: 1px solid #111111;
+}
+
+input[type=text]:focus, input[type=password]:focus, input[type=email]:focus, input[type=search]:focus, input[type=number], input[type=file], input[type=tel], select:focus, textarea:focus, textarea[type=text]:focus {
+    outline: none;
+    border: 1px solid #104cfb;
+}
+
+textarea, textarea[type=text] {
+    min-height: 7rem;
+}
+
+.container {
+    max-width: 960px;
+    margin: 0 auto;
+    width: 80%;
+}
+
+.row {
+    display: flex;
+    flex-flow: row wrap;
+    justify-content: space-between;
+    margin-top: 1rem;
+    margin-bottom: 1rem;
+}
+
+.row > :first-child {
+    margin-left: 0;
+}
+
+.col {
+    -webkit-box-flex: 1;
+    -moz-box-flex: 1;
+    -webkit-flex: 1;
+    -ms-flex: 1;
+    flex: 1;
+}
+
+.col, [class^='col-'], [class*=" col-"] {
+    margin-left: 4%;
+}
+
+.col-1 {
+    flex: 1;
+}
+
+.col-2 {
+    flex: 2;
+}
+
+.col-3 {
+    flex: 3;
+}
+
+.col-4 {
+    flex: 4;
+}
+
+.col-5 {
+    flex: 5;
+}
+
+.col-6 {
+    flex: 6;
+}
+
+.col-7 {
+    flex: 7;
+}
+
+.col-8 {
+    flex: 8;
+}
+
+.col-9 {
+    flex: 9;
+}
+
+.col-10 {
+    flex: 10;
+}
+
+.col-11 {
+    flex: 11;
+}
+
+.col-12 {
+    flex: 12;
+}
+
+@media screen and (max-width: 768px) {
+    .col, [class^='col-'], [class*=" col-"] {
+        margin: 1rem 0;
+        flex: 0 0 100%;
+    }
+}
+
+/*------------------------------------------------------------
+  Lists
+------------------------------------------------------------*/
+
+ul, ol {
+    padding-left: 0;
+    margin-top: 0;
+}
+
+ul ul, ul ol, ol ol, ol ul {
+    margin: 1rem 0 1rem 2rem;
+    font-size: 95%;
+}
+
+ul {
+    list-style: circle inside;
+}
+
+ol {
+    list-style: decimal inside;
+}
+
+li {
+    margin-bottom: 1rem;
+}
+
+/*------------------------------------------------------------
+  Tables
+  ------------------------------------------------------------*/
+
+.table {
+    width: 100%;
+    border: none;
+    border-collapse: collapse;
+    border-spacing: 0;
+    text-align: left;
+}
+
+.table th, .table td {
+    vertical-align: middle;
+    padding: 12px 4px;
+}
+
+.table thead {
+    border-bottom: 2px solid #333030;
+}
+
+/* responsive tables */
+@media screen and (max-width: 768px) {
+    .table.responsive {
+        position: relative;
+        display: block;
+    }
+    .table.responsive th, .table.responsive td {
+        margin: 0;
+    }
+    .table.responsive thead {
+        display: block;
+        float: left;
+        border: 0;
+    }
+    .table.responsive thead tr {
+        display: block;
+        padding: 0 10px 0 0;
+        border-right: 2px solid #333030;
+    }
+    .table.responsive th {
+        display: block;
+        text-align: right;
+    }
+    .table.responsive tbody {
+        display: block;
+        overflow-x: auto;
+        white-space: nowrap;
+    }
+    .table.responsive tbody tr {
+        display: inline-block;
+    }
+    .table.responsive td {
+        display: block;
+        min-height: 16px;
+        text-align: left;
+    }
+    .table.responsive tr {
+        padding: 0 10px;
+    }
+}
+
+/*------------------------------------------------------------
+  Utilities
+------------------------------------------------------------*/
+
+.pull-right {
+    float: right;
+}
+
+.pull-left {
+    float: left;
+}
+
+.text-center {
+    text-align: center;
+}
+
+.text-left {
+    text-align: left;
+}
+
+.text-right {
+    text-align: right;
+}
+
+.full-screen {
+    width: 100%;
+    min-height: 100vh;
+}
+
+.full-width {
+    width: 100%;
+}
+
+.vertical-align {
+    display: flex;
+    align-items: center;
+}
+
+.horizontal-align {
+    display: flex;
+    justify-content: center;
+}
+
+.center {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-direction: column;
+}
+
+.right {
+    display: flex;
+    align-items: center;
+    justify-content: flex-end;
+}
+
+.left {
+    display: flex;
+    align-items: center;
+    justify-content: flex-start;
+}
+
+.fixed {
+    position: fixed;
+    width: 100%;
+}
+
+@media screen and (max-width: 400px) {
+    .hide-phone { display: none; }
+}
+
+@media screen and (max-width: 768px) {
+    .hide-tablet { display: none; }
+}
+
+/*------------------------------------------------------------
+  Misc
+------------------------------------------------------------*/
+
+code {
+    padding: 0.2rem 0.5rem;
+    margin: 0 0.2rem;
+    font-size: 90%;
+    white-space: nowrap;
+    background: #F1F1F1;
+    border: 1px solid #E1E1E1;
+    border-radius: 4px;
+    font-family: "Consolas", "Monaco", "Menlo", monospace;
+}
+
+pre > code {
+    display: block;
+    padding: 1rem 1.5rem;
+    white-space: pre-wrap;
+    white-space: -moz-pre-wrap;
+    white-space: -pre-wrap;
+    white-space: -o-pre-wrap;
+    word-wrap: break-word;
+}
+
+/*------------------------------------------------------------
+  Navigation
+  ------------------------------------------------------------*/
+
+.nav {
+    position: relative;
+    display: flex;
+    flex-wrap: wrap;
+    align-items: center;
+    padding: 1rem;
+}
+
+.nav-menu,
+.nav-brand {
+    display: flex;
+}
+
+.nav-menu {
+    flex-flow: row;
+    flex: 1 0 auto;
+}
+
+.nav-item {
+    padding: 1rem 2rem;
+}
+
+.nav-logo {
+    font-weight: bolder;
+    font-size: 2rem;
+    line-height: 0;
+    padding-right: 2rem;
+}
+
+.cards {
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: space-between;
+    overflow: hidden;
+    margin-bottom: 2rem
+}
+
+@media (max-width: 675px) {
+    .cards > .card {
+        flex: auto;
+    }
+}
+
+.card {
+    flex-direction: column;
+    overflow: hidden;
+    flex: 0 1 calc(50% - 0.5rem);
+    box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
+    margin-bottom: 2rem;
+}
+
+.card-header {
+    font-weight: 600;
+    margin: 0;
+    padding: 2rem 3rem 1rem;
+}
+
+.card-body {
+    padding: 0 3rem 2rem 3rem;
+    min-height: 100px;
+}
+
+.card-footer {
+    display: flex;
+    align-items: stretch;
+    border-top: 1px solid #dbdbdb;
+    flex: 1;
+}
+
+.card-footer .card-footer-item {
+    display: flex;
+    flex-basis: 0;
+    flex-grow: 1;
+    flex-shrink: 0;
+    align-items: center;
+    justify-content: center;
+    margin: 0;
+    padding: 1rem;
+}
+
+.card-footer-item:not(:first-child) {
+    border-left: 1px solid #dbdbdb;
+}

+ 154 - 0
red-hat-bank/bank-intranet/src/main/resources/META-INF/resources/index.html

@@ -0,0 +1,154 @@
+<!doctype html>
+<html>
+<head>
+    <meta charset="utf-8"/>
+    <title>Red Hat Bank - Intranet (v1.0)</title>
+    <link rel="stylesheet" href="/css/wing.css"/>
+    <link rel="stylesheet" href="/css/style.css"/>
+    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
+    <style>
+        .card .row input[type=number] {
+            width: 90%;
+        }
+    </style>
+    <script type="text/javascript">
+        var app = angular.module("BankAccountManagement", []);
+
+        app.controller("BankAccountManagementController", function ($scope, $http) {
+
+            $scope.bankAccounts = [];
+
+            $scope.form = {
+                id: -1,
+                balance: ""
+            };
+
+            _refreshPageData();
+
+            $scope.create = function () {
+                var data = {};
+                data.balance = $scope.form.balance;
+
+                $http({
+                    method: 'POST',
+                    url: '/accounts',
+                    data: angular.toJson(data),
+                    headers: {
+                        'Content-Type': 'application/json'
+                    }
+                }).then(_success, _error);
+            };
+
+            $scope.remove = function (bankAccount) {
+                $http({
+                    method: 'DELETE',
+                    url: '/accounts/' + bankAccount.id
+                }).then(_success, _error);
+            };
+
+            $scope.deposit = function (bankAccount) {
+                var data = {}
+                data.id = bankAccount.id;
+                data.amount = bankAccount.amount;
+
+                $http({
+                    method: 'PUT',
+                    url: '/accounts/' + bankAccount.id + '/deposit',
+                    data: angular.toJson(data),
+                    headers: {
+                        'Content-Type': 'application/json'
+                    }
+                }).then(_success, _error);
+            };
+
+            $scope.withdraw = function (bankAccount) {
+                var data = {}
+                data.id = bankAccount.id;
+                data.amount = bankAccount.amount;
+
+                $http({
+                    method: 'PUT',
+                    url: '/accounts/' + bankAccount.id + '/withdraw',
+                    data: angular.toJson(data),
+                    headers: {
+                        'Content-Type': 'application/json'
+                    }
+                }).then(_success, _error);
+            };
+            function _refreshPageData() {
+                $http({
+                    method: 'GET',
+                    url: '/accounts'
+                }).then(function successCallback(response) {
+                    $scope.bankAccounts = response.data;
+                }, function errorCallback(response) {
+                    console.log(response.statusText);
+                });
+            }
+
+        function _success(response) {
+          _refreshPageData();
+          _clearForm()
+        }
+
+        function _error(response) {
+          alert(response.data.error || response.statusText);
+        }
+
+        function _clearForm() {
+            $scope.form.id = -1;
+            $scope.form.balance = "";
+        }
+      });
+    </script>
+</head>
+<body ng-app="BankAccountManagement" ng-controller="BankAccountManagementController">
+
+    <div class="container">
+        <h1>Red Hat Training Bank - Intranet</h1>
+
+        <h3>Create a Bank Account</h3>
+        <form ng-submit="create()">
+            <div class="row">
+                <div class="col-6">
+                    <input type="number" min="1" placeholder="Initial Balance" ng-model="form.balance" />
+                </div>
+                <div class="col-5">
+                    <input type="submit" value="Create"/>
+                </div>
+            </div>
+        </form>
+
+        <h3>Customer Bank Accounts</h3>
+        <div class="cards">
+            <div class="card" ng-repeat="account in bankAccounts">
+                <h5 class="card-header">Account #{{ account.id }}</h5>
+                <p class="card-body">
+                    Current Balance: {{ account.balance }} <br>
+                    Type: {{ account.profile }}
+                </p>
+                <div class="card-footer center text-center">
+                    <div class="row">
+                        <div class="col">
+                            <input type="number" min="1" placeholder="Amount" ng-model="account.amount" />
+                        </div>
+                    </div>
+                    <div class="row">
+                        <div class="col">
+                            <button ng-click="deposit( account )">Deposit</button>
+                        </div>
+                        <div class="col">
+                            <button ng-click="withdraw( account )">Withdraw</button>
+                        </div>
+                    </div>
+                </div>
+                <div class="card-footer center text-center">
+                    <p>
+                        <button ng-click="remove( account )" class='outline'>Remove</button>
+                    </p>
+                </div>
+            </div>
+        </div>
+    </div>
+</body>
+</html>

+ 27 - 0
red-hat-bank/bank-intranet/src/main/resources/application.properties

@@ -0,0 +1,27 @@
+quarkus.datasource.db-kind = h2
+quarkus.datasource.jdbc.url = jdbc:h2:mem:default;DB_CLOSE_DELAY=-1
+quarkus.datasource.jdbc.max-size = 8
+quarkus.datasource.jdbc.min-size = 2
+quarkus.hibernate-orm.dialect = org.hibernate.dialect.H2Dialect
+quarkus.hibernate-orm.database.generation = drop-and-create
+quarkus.hibernate-orm.log.sql = false
+quarkus.hibernate-orm.sql-load-script = import.sql
+
+kafka.security.protocol = SSL
+kafka.bootstrap.servers = my-kafka-cluster:port
+kafka.ssl.truststore.location = path/to/truststore.jks
+kafka.ssl.truststore.password = truststorepass
+
+mp.messaging.connector.smallrye-kafka.apicurio.registry.url = http://service-registry/apis/registry/v2
+
+mp.messaging.outgoing.new-bank-account-out.connector = smallrye-kafka
+mp.messaging.outgoing.new-bank-account-out.topic = new-bank-account
+mp.messaging.outgoing.new-bank-account-out.apicurio.registry.auto-register = true
+
+mp.messaging.incoming.account-update.connector = smallrye-kafka
+mp.messaging.incoming.account-update.topic = updated-bank-account
+mp.messaging.incoming.account-update.enable.auto.commit = true
+mp.messaging.incoming.account-update.auto.commmit.offset = 500
+mp.messaging.incoming.account-update.auto.offset.reset = earliest
+mp.messaging.incoming.account-update.apicurio.registry.use-specific-avro-reader = true
+

+ 2 - 0
red-hat-bank/bank-intranet/src/main/resources/import.sql

@@ -0,0 +1,2 @@
+INSERT INTO bankaccount(id, balance, profile) VALUES (nextval('hibernate_sequence'), 1000, 'regular');
+INSERT INTO bankaccount(id, balance, profile) VALUES (nextval('hibernate_sequence'), 2000, 'regular');

+ 134 - 0
red-hat-bank/new-account-type/pom.xml

@@ -0,0 +1,134 @@
+<?xml version="1.0"?>
+<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>com.redhat.training</groupId>
+  <artifactId>new-account-type</artifactId>
+  <version>1.0.0-SNAPSHOT</version>
+  <properties>
+    <compiler-plugin.version>3.8.1</compiler-plugin.version>
+    <maven.compiler.parameters>true</maven.compiler.parameters>
+    <maven.compiler.source>11</maven.compiler.source>
+    <maven.compiler.target>11</maven.compiler.target>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+    <quarkus.platform.artifact-id>quarkus-universe-bom</quarkus.platform.artifact-id>
+    <quarkus.platform.group-id>io.quarkus</quarkus.platform.group-id>
+    <quarkus.platform.version>2.1.4.Final</quarkus.platform.version>
+    <surefire-plugin.version>3.0.0-M5</surefire-plugin.version>
+  </properties>
+  <dependencyManagement>
+    <dependencies>
+      <dependency>
+        <groupId>${quarkus.platform.group-id}</groupId>
+        <artifactId>${quarkus.platform.artifact-id}</artifactId>
+        <version>${quarkus.platform.version}</version>
+        <type>pom</type>
+        <scope>import</scope>
+      </dependency>
+    </dependencies>
+  </dependencyManagement>
+  <dependencies>
+    <dependency>
+      <groupId>io.quarkus</groupId>
+      <artifactId>quarkus-apicurio-registry-avro</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.quarkus</groupId>
+      <artifactId>quarkus-smallrye-reactive-messaging-kafka</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.quarkus</groupId>
+      <artifactId>quarkus-arc</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.quarkus</groupId>
+      <artifactId>quarkus-junit5</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>io.rest-assured</groupId>
+      <artifactId>rest-assured</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>${quarkus.platform.group-id}</groupId>
+        <artifactId>quarkus-maven-plugin</artifactId>
+        <version>${quarkus.platform.version}</version>
+        <extensions>true</extensions>
+        <executions>
+          <execution>
+            <goals>
+              <goal>build</goal>
+              <goal>generate-code</goal>
+              <goal>generate-code-tests</goal>
+            </goals>
+          </execution>
+        </executions>
+        <dependencies>
+          <dependency>
+            <groupId>jline</groupId>
+            <artifactId>jline</artifactId>
+            <version>2.14.6</version>
+          </dependency>
+        </dependencies>
+      </plugin>
+      <plugin>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <version>${compiler-plugin.version}</version>
+        <configuration>
+          <parameters>${maven.compiler.parameters}</parameters>
+        </configuration>
+      </plugin>
+      <plugin>
+        <artifactId>maven-surefire-plugin</artifactId>
+        <version>${surefire-plugin.version}</version>
+        <configuration>
+          <systemPropertyVariables>
+            <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
+            <maven.home>${maven.home}</maven.home>
+          </systemPropertyVariables>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+  <profiles>
+    <profile>
+      <id>native</id>
+      <activation>
+        <property>
+          <name>native</name>
+        </property>
+      </activation>
+      <build>
+        <plugins>
+          <plugin>
+            <artifactId>maven-failsafe-plugin</artifactId>
+            <version>${surefire-plugin.version}</version>
+            <executions>
+              <execution>
+                <goals>
+                  <goal>integration-test</goal>
+                  <goal>verify</goal>
+                </goals>
+                <configuration>
+                  <systemPropertyVariables>
+                    <native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
+                    <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
+                    <maven.home>${maven.home}</maven.home>
+                  </systemPropertyVariables>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+      <properties>
+        <quarkus.package.type>native</quarkus.package.type>
+      </properties>
+    </profile>
+  </profiles>
+</project>

+ 20 - 0
red-hat-bank/new-account-type/src/main/avro/new-bank-account.avsc

@@ -0,0 +1,20 @@
+{
+  "namespace": "com.redhat.training.bank.message",
+  "type": "record",
+  "name": "NewBankAccount",
+  "fields": [
+    {
+      "name": "id",
+      "type": "long"
+    },
+    {
+      "name": "balance",
+      "type": "long"
+    },
+    {
+      "name": "type",
+      "type": ["null", "string"],
+      "default": null
+    }
+  ]
+}

+ 50 - 0
red-hat-bank/new-account-type/src/main/java/com/redhat/training/bank/consumer/NewBankAccountConsumer.java

@@ -0,0 +1,50 @@
+package com.redhat.training.bank.consumer;
+
+import com.redhat.training.bank.message.NewBankAccount;
+import io.smallrye.common.annotation.Blocking;
+
+import org.eclipse.microprofile.reactive.messaging.Channel;
+import org.eclipse.microprofile.reactive.messaging.Emitter;
+import org.eclipse.microprofile.reactive.messaging.Incoming;
+import org.eclipse.microprofile.reactive.messaging.Message;
+import org.jboss.logging.Logger;
+
+import java.util.concurrent.CompletableFuture;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.inject.Inject;
+import javax.transaction.Transactional;
+
+@ApplicationScoped
+public class NewBankAccountConsumer {
+    private static final Logger LOGGER = Logger.getLogger(NewBankAccountConsumer.class);
+
+    @Inject
+    @Channel("account-update")
+    Emitter<NewBankAccount> e;
+
+    @Incoming("new-bank-account-in")
+    @Blocking
+    @Transactional
+    public void processMessage(NewBankAccount message) {
+        if (message == null) {
+            LOGGER.error("Received NULL message!");
+            return;
+        }
+
+        if (message.getBalance() < 100000) {
+            message.setType("regular");
+        } else {
+            message.setType("premium");
+        }
+
+        LOGGER.info(
+                "Got bank account w/ balance of " + message.getBalance() + "; " +
+                "ID: " + message.getId() + "; setting type to \"" + message.getType() + "\""
+        );
+
+        e.send(Message.of(message)
+            .withAck(() -> { LOGGER.info("Message sent: " + message.toString()); return CompletableFuture.completedFuture(null); })
+            .withNack(thr -> { LOGGER.error(thr); return CompletableFuture.completedFuture(null); }));
+    }
+}

+ 17 - 0
red-hat-bank/new-account-type/src/main/resources/application.properties

@@ -0,0 +1,17 @@
+kafka.security.protocol = SSL
+kafka.bootstrap.servers = my-kafka-cluster:port
+kafka.ssl.truststore.location = path/to/truststore.jks
+kafka.ssl.truststore.password = truststorepass
+
+mp.messaging.connector.smallrye-kafka.apicurio.registry.url = http://service-registry/apis/registry/v2
+
+mp.messaging.incoming.new-bank-account-in.connector = smallrye-kafka
+mp.messaging.incoming.new-bank-account-in.topic = new-bank-account
+mp.messaging.incoming.new-bank-account-in.enable.auto.commit = false
+mp.messaging.incoming.new-bank-account-in.auto.offset.reset = earliest
+mp.messaging.incoming.new-bank-account-in.apicurio.registry.use-specific-avro-reader = true
+
+mp.messaging.outgoing.account-update.connector = smallrye-kafka
+mp.messaging.outgoing.account-update.topic = updated-bank-account
+mp.messaging.outgoing.account-update.apicurio.registry.auto-register = true
+